1. 泛型类

class Gen<T> {
private T t; public T get(){
return t;
} public void set(T argt){
t = argt;
}
}

“<>”内的T为类型参数,只能是类名,不能是基本类型(如int , double),泛型类(以及后面讲到的泛型方法)可以有多个类型参数。

class Pair<K,V>{
private K k;
private V v;
……
}

       类型参数可以看做这个泛型类操作的数据类型

泛类型的使用

Gen<String> gs = new Gen<String>();
gs.set("abc");
String str = gs.get();

2. 泛型方法

class GenFun{
public <T> T Mid(T[] a){
return a[a.length/2];
}
}

这是一个普通类,但是具有一个泛型方法,返回T类对象数组的中间位置的元素引用,泛型方法需要在返回值前用“<>”说明类型参数。

(1)如果一个泛型类中有泛型方法,泛型方法的类型参数可以与泛型类的类型参数不同。

(2)若在泛型类中的静态方法要访问泛型参数,必须使它变成泛型方法。

3.类型参数的限定

限定上限关键字 extends

class GenFun{
public <T extends Comparable<T>> T Max(T[] a){
T max = a[0];
for(T t: a){
if(t.compareTo(max) > 0){
max = t;
}
}
return max;
}
}

<T extends A> 这里的extends表示类型参数T必须就是A类或者A的子类,或者表示T需要实现A这个接口。上面的例子就表示T必须实现Comparable接口(注意Comparable接口本身又是个泛型接口)。

现在我们来举一个例子,来说明上限限定符的使用规则,假设E继承 D, D继承C, C继承B, B继承A

class A{};
class B extends A{};
class C extends B{};
class D extends C{};
class E extends D{};
class F{}; class Gen<T extends C> {
private T t; public T get(){
return t;
} public void set(T argt){
t = argt;
}
} public class GenTest {
public static void main(String[] args) {
//----------------------
Gen<F> gf = new Gen<F>();//编译出错 F不是C的子类
Gen<B> ga = new Gen<B>();//编译出错 B是C的父类 //----------------------
Gen<C> gc = new Gen<C>();//编译成功
gc.set(new B());//编译出错 B不是C的子类 Gen<D> gd = new Gen<D>();//编译成功
gd.set(new D());//编译成功
D d0 = gd.get();//编译成功 gd.set(new E());//编译成功,把E的对象当做D对象看待
D d1 = gd.get();//编译成功,注意返回的类型为D
}
}

有多个限定条件可以用“&”连接,多个限定条件中只能有一个类,其它的都为接口

限定下限关键字 super

class Gen<T super D> {//编译错误
……
}

限定条件可用来单独限定方法(即泛型方法),也可以用来限定整个类。含有通配符的限定条件用于创建泛型类的引用。

4. 擦除

都是Object

实际上java语言中并没有泛类型对象,所有对象都是普通对象,也就说泛型仅仅存在于编译阶段。这一点我们可以从Gen<T>的字节码中得到印证。查看字节码我使用的是Bytecode visualizer插件,可以从http://marketplace.eclipse.org/下载到。

class Gen<T> {
/* compiled from GenTest.java *
private T t; Gen() {
/* L18 */
0 aload_0; /* this */
1 invokespecial 12; /* java.lang.Object() */
4 return;
} public java.lang.Object get() {
/* L22 */
0 aload_0; /* this */
1 getfield 23; /* .t */
4 areturn;
} public void set(java.lang.Object arg0) {
/* L26 */
0 aload_0; /* this */
1 aload_1; /* argt */
2 putfield 23; /* .t */
/* L27 */
5 return;
}
}

注意get方法的返回值类型和set方法的参数类型都是Object。可以看出泛型类中,将泛型对象都用Object对象代替。由于get方法应该根据具体的类型参数返回一个对应类型的对象,那么这一点又是如何实现的。不妨先来观察下面的代码。

public class GenTest {
public static void main(String[] args) {
Gen<D> gd = new Gen<D>();
gd.set(new D());
D d0 = gd.get();
}
}

代码的意思很简单就是实例化一个Gen<D>的对象,并调用它的get和set方法。我们再来看看上述代码对应的字节码(省略了不相干的部分)。

public static void main(java.lang.String[] args) {
/* L69 */
0 new 16;
3 dup;
4 invokespecial 18; /* javaleanning.Gen() */
7 astore_1; /* gd */
/* L71 */
8 aload_1; /* gd */
9 new 19;
12 dup;
13 invokespecial 21; /* javaleanning.D() */
16 invokevirtual 22; /* void set(java.lang.Object d0) */
/* L72 */
19 aload_1; /* gd */
20 invokevirtual 26; /* java.lang.Object get() */
23 checkcast 19; /* javaleanning.D */
26 astore_2; /* d0 */
/* L106 */
27 return;
}

第20行是调用get方法,注意23行,它是进行强制类型转换,26行是将结果传递给引用d0。也就是说泛型的实现原理就是在需要返回某个泛型对象之前,进行强制类型转换。在使用泛型类中的方法或泛型方法时,会用实际的类名去替换T,这样一来编译器就知道了强制转换的类型。还有一点要说明,强制转换的代码是编译器在调用get方法时插入的,get方法中并没有进行强制类型转换(它也不知道该转换成什么类型的对象),这样可以使得泛型类和泛型方法的编译不依赖于T的具体类型。

class Gen<T> {
private T t; public T get(){
return t;
} public void set(T argt){
t = argt;
} public void set(Object argt){ //编译出错
t = argt;
}
}

上述泛型类中添加一个public Object get() ,则会出现编译错误,原因是在编译时,set(T argt)会将泛类型擦除,变成set(Object argt),这样一个类中就存在两个完全相同的方法。

静态数据公用

因为擦除效应,类型参数不同的泛型类使用的是同一代码和静态数据。。我们在G<T>中添加一个静态变量n

class Gen<T> {
private T t; public static int n = 0; public T get(){
return t;
} public void set(T argt){
t = argt;
}
}

现在我们做一个测试。

Gen<D> gd = new Gen<D>();
gd.n = 100;
Gen<E> ge = new Gen<E>();
System.out.println(ge.n);

最后的输出结果是100。

5. 覆盖

现在有一个SubGen类继承了Gen<A>,并想覆盖Gen<A>的set方法。

class SubGen extends Gen<A>{
public void set(Object o){ //编译出错
System.out.println("from SubGen set ");
}
}

虽然Gen<A>被擦除后的set方法的原型就是set(Object o),但是想覆盖父类G<A>的set方法,上述写法会出现编译错误。因为编译器以为它成功的欺骗了我们,让我们以为存在泛类型,我们这么写就是告诉它我们识破了骗局,编译器必然不高兴了,编译就不能成功(真正的原因后面会介绍)。所以我们必须假装不知道有擦除这回事,按照泛型的方式来处理。编译器以为我们会认为Gen<A>的set方法的原型是set(A a),所以我们必须这么写代码才能实现子类对(泛型)父类中方法的覆盖。

class SubGen extends Gen<A>{
public void set(A a){
System.out.println("from SubGen set");
}
}

但是现在还有个疑问,明明泛型中的类型参数编译时都替换成了Object,那么子类中的set(A a)就不能覆盖父类中的set(Object argt)方法了(因为这两个方法的参数不同),但是我们运行下面的代码却能得到正确的结果(输出 from SubGen set)。

Gen<A> ga = new SubGen();
ga.set(null);

要解释这个原因,我们可以查看SubGen的字节码。

    public void set(javaleanning.A arg0) {
/* L34 */
0 getstatic 16; /* java.lang.System.out */
3 ldc 22; /* "from SubGen" */
5 invokevirtual 24; /* void println(java.lang.String arg0) */
/* L35 */
8 return;
} /* bridge method generated by the compiler */
public volatile void set(java.lang.Object arg0) {
/* L1 */
0 aload_0;
1 aload_1;
2 checkcast 33; /* javaleanning.A */
5 invokevirtual 35; /* void set(javaleanning.A arg0) */
8 return;
}

可以看到SubGen中有两个set方法,public void set(javaleanning.A arg0)是我们自己编写了,而另一个public volatile void set(java.lang.Object arg0) 是编译器自动帮我们添加的(注意代码的注释bridge method generated by the compiler)。添加的这个方法正好和父类中擦除掉泛型的set方法同名且同参数public volatile void set(java.lang.Object arg0),这就实现了子类对父类方法的覆盖,而set(java.lang.Object arg0)内部就是仅仅调用了我们写的set(javaleanning.A arg0)方法。现在我们回头看看前面我们编译出错的代码,原因就显而易见了。因为编译器帮我们编写了一个set(java.lang.Object arg0)方法,如果我们自己也实现一个set(Object o),那么两个(另一个由编译器自动生成)一样的方法必然会产生冲突,所以会编译失败。

现在SubGen中添加set方法想要覆盖掉父类Gen<A>中的set方法。那么我们可以这么写(代码没有什么具体意义,添加输出语句,只是为了区别子类中的方法和父类中的方法)。

class SubGen extends Gen<A>{
public void set(A a){
System.out.println("from SubGen set");
} public A get(){
System.out.println("from SubGen get");
return new A();
}
}

我们使用下面的代码测试

Gen<A> ga = new SubGen();
ga.set(null);
ga.get();

可以得到正确的结果

from SubGen set

from SubGen get

我们查看字节码可以发现一个有趣的问题

   public javaleanning.A get() {
/* L38 */
0 getstatic 16; /* java.lang.System.out */
3 ldc 22; /* "from SubGen" */
5 invokevirtual 24; /* void println(java.lang.String arg0) */
/* L39 */
8 new 34;
11 dup;
12 invokespecial 36; /* javaleanning.A() */
15 areturn;
} /* bridge method generated by the compiler */
public volatile java.lang.Object get() {
/* L1 */
0 aload_0;
1 invokevirtual 38; /* javaleanning.A get() */
4 areturn;
}

SubGen 中有两个get方法,它们仅仅返回值不同。一般情况下,程序员这样写代码必然会导致编译错误,但是由于是编译器自己添加的一个方法(java.lang.Object get()),所以的确能够编译成功,并且编译器能够通过返回值的不同来区分这两个方法。

6.泛型中的继承

可以使用子类实例

我们仍然假设E继承 D, D继承C, C继承B, B继承A。那么Gen<C> 中的方法可以处理 类C的实例,类D的实例,类E的实例,但不能处理类B的实例和类A的实例。

     Gen<C> gc = new Gen<C>();
gc.set(new C());
C c = gc.get(); gc.set(new D());
c = gc.get();
D d = (D) gc.get(); gc.set(new E());
c = gc.get();
d = (D) gc.get();
E e = (E) gc.get(); gc.set(new A()); //编译错误
gc.set(new B()); //编译错误

继承中的限制

虽然,E继承 D, D继承C, C继承B, B继承A,但是Gen<A>,Gen<B>,Gen<C> ,Gen<D>,     Gen<E> 之间没有任何继承继承关系。因为泛类型的本质就是增强强制转换的安全性,将本来由程序员进行的强制转换工作交给编译器来完成。假设由于B继承了A使得Gen<B>继承Gen<A>,这就可能导致运行时的错误。

       Gen<B> gb = new Gen<B>();
Gen<A> ga = gb; //编译错误
ga.set(new A());
B b = gb.get();

一般来说在Gen<B>放入一个B的实例,但是取出一个A的实例一般没有什么问题,但是如果允许Gen<A> 和 Gen<B> 存在继承关系,按照上如代码,我们就可以让Gen<A>的实例ga和Gen<B>的实例gb指向同一Gen<B>实例,用ga存入一个A类的实例,利用gb的get方法取出一个B类的实例,这就显然会引起运行时的错误。

泛型中的继承的继承情况有很多种,可以继承一个具体的泛型类(像上述的“覆盖”章节中    class SubGen extends Gen<A>),还一个继承一个纯粹的泛型类

class SonGen<U, T> extends Gen<T>{
private U u;//只是定义一个变量而已
public void set(T argt){// 覆盖父类中的方法
System.out.println("I am SonGen");
}
}

SonGen<U, T> 继承了 Gen<T> ,并添加了一个类型类型参数。当然还可以继承具有泛型方法的类,这里就不举例了。

7.通配符及类型参数的限定

泛型中对继承的限制是为了解决类型转换的安全性问题,但是却违背了java中多态的原则。为了同时解决泛型中的安全性和多态原则,java引用通配符。我们继续使用上面的Gen<T>和类A、B、C、D、E作为例子。通配符“ ?”表示任意类型,它的使用有三种形式。

通配符与上限限定符

Gen<? extends B >表示Gen的类型参数是类B或者任何类B的子类。Gen<? extends B >中的两个方法(set 和 get)实际上被通配符和限定符后变成了如下的形式

set( ? extends B )

? extends B get()

我们现在来看一下它的使用

       Gen<? extends B> gec = new Gen<D>(); //编译成功
gec.set(new C()); //编译错误
gec.set(new D()); //编译错误
gec.set(new E()); //编译错误
B b = gec.get(); //编译成功

现在对上述代码进行解释。第一行能编译成功,是因为满足 D 是 B 的子类。

最后一行没有错误,因为编译器只知道gec是对Gen<B>的子类对象的引用,而具体是哪个子类它并不清楚,但是B子类的对象一定可以转换成B类型的对象。

而所有调用的set方法的语句都会出现编译错误,因为编译器只知道gec是对Gen<B>的子类对象的引用,而具体是哪个子类它并不清楚(上述代码中指向了Gen<D>的对象),所以编译器gec把它所指的对象仅当做当做Gen<B>来使用,所以编译器拒绝一切 ? extends B 作为参数,这样做是保障泛型的安全性。比如有个类F,它也继承了B类,而F类和D类不存在继承关系。

       Gen<D> gd = new Gen<D>();
Gen<? extends B> gec = gd;
gec.set(new F());//如果编译成功
D d = gd.get();

若gec.set(new F())编译通过,那么D d = gd.get()必然出现运行错误,因为返回的实际上是个F类型的对象,却被转换成了D类型。

通配符与下限限定符

Gen<? super D>表示Gen的类型参数是类D或者任何类D的父类。Gen<? super D >中的两个方法(set 和 get)实际上被通配符和限定符后变成了如下的形式

set( ? super D )

? super D get()

我们现在来看一下它的使用

       Gen<? super D> supd = new Gen<B>();
supd.set(new A()); //编译错误
A a = supd.get(); //编译错误
        supd.set(new C()); //编译错误
C c = supd.get(); //编译错误 supd.set(new D());
D d = supd.get(); //编译错误
d = (D)supd.get();
supd.set(new E());
Object o = supd.get();

在对上述代码进行解释。第一行能编译成功,是因为满足 B 是 D 的父类。

如果不加强制转换,所有调用的get方法的语句都会出现编译错误,原因其实和上面类似,编译器只知道supD是对Gen<D>的父类对象的引用,而具体是哪个父类它并不清楚,由于get的返回值类型是? super D编译器就没有办法确定应该转换成D类具体的哪种类型。

supd.set(new D())和supd.set(new E())能够编译成功的原因是,虽然编译器只知道supD是对Gen<D>的父类对象的引用,而具体是哪个父类它并不清楚,但是D类的实例和E类的实例一定能够当做D的父类型的实例来对待。

A是D的父类,但是以A类的对象作为参数的get 和 set 方法都会编译失败,这似乎和限定符表示的意思不相符。但是考虑以下的代码:

         Gen<B> gb = new Gen<B>();
Gen<? super D> supC = gb;
supC.set(new A());
B b = gb.get();

如果upC.set(new A())能够编译成功,那么B b = gb.get()一定会出现异常,因为出现了向上转型(由父类A转向了子类B)。

无限定通配符

Gen<?> 等价于Gen<? extends Object> 表示Gen的类型参数是Object或者Object的子类

通配符小节

(1) extends 可用于的返回类型限定,不能用于参数类型限定。

(2) super 可用于参数类型限定,不能用于返回类型限定。

(3) 通配符一般用于创建泛型类的引用

假设X表示一个具体的类,注意区别 ? extends X 和Gen<? extends X> 。Gen<? super X>和   Gen<? extends X>两者可以作为参数类型也可以作为返回值类型,可参见 ArrayList代码。

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}

8. 泛型的局限性

虽然java没有真正的泛型,但是编译器在编译阶段会做一些语法上的限制

(1)判断某个对象是否是泛型类的实例的正确方法

        Gen<B> gb = new Gen<B>();
if(gb instanceof Gen<?>){
System.out.println("only correct way");
}

(2) class实际上是个泛型类 原型为class<T>。A.class 是 Class<A>的唯一一个实例,String.class是Class<Stiring>的唯一一个实例

(3)不能实例化泛型变量,即不能使用
                    new T()
                    T.class
         如果要实例化一个泛型变量,可以在泛型类中添加一个静态方法,并传入class<T>类型的参数

public static <T> T makeTobj(Class<T> cl){
try {
return cl.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
}
return null;
}

(4) 没有泛型数组,如果需要在泛型类中创建一个泛型数组,可以用Object类型的数组代替。我们可以参照ArrayList的源代码。

public class ArrayList<E> extends AbstractList<E> 
  implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
//省略无关代码
transient Object[] elementData;
   // non-private to simplify nested class access

    public E get(int index) {
rangeCheck(index);
return elementData(index);
}
}

并可以直接返回数组中的某个对象,因为编译器会在代用该代码时自动添加强制类型转换。

如果需要返回泛型的数组,我们可以new Object[] 或者利用Array.newInstance类中的方法创建特定类型的数组,然后进行强制类型转换(T[])。参见ArrayList代码。

public static <T,U> T[] copyOf(U[] original, int newLength,
                                  Class<? extends T[]> newType){
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(),
                                  newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
} public <T> T[] toArray(T[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}

虽然不能创建泛型数组,但是可以创建泛型数组的引用 Gen<T>[] genArr;

(5) 没有泛类型的静态数据成员

Java 泛型总结的更多相关文章

  1. Java泛型的历史

    为什么Java泛型会有当前的缺陷? 之前的章节里已经说明了Java泛型擦除会导致的问题,C++和C#的泛型都是在运行时存在的,难道Java天然不支持“真正的泛型”吗? 事实上,在Java1.5在200 ...

  2. 浅析Java 泛型

    泛型是JavaSE5引入的一个新概念,但是这个概念在编程语言中却是很普遍的一个概念.下面,根据以下内容,我们总结下在Java中使用泛型. 泛型使用的意义 什么是泛型 泛型类 泛型方法 泛型接口 泛型擦 ...

  3. Java:泛型基础

    泛型 引入泛型 传统编写的限制: 在Java中一般的类和方法,只能使用具体的类型,要么是基本数据类型,要么是自定义类型.如果要编写可以应用于多种类型的代码,这种刻板的限制就会束缚很多! 解决这种限制的 ...

  4. java泛型基础

    泛型是Java SE 1.5的新特性, 泛型的本质是参数化类型, 也就是说所操作的数据类型被指定为一个参数. 这种参数类型可以用在类.接口和方法的创建中, 分别称为泛型类.泛型接口.泛型方法.  Ja ...

  5. 使用java泛型设计通用方法

    泛型是Java SE 1.5的新特性, 泛型的本质是参数化类型, 也就是说所操作的数据类型被指定为一个参数. 因此我们可以利用泛型和反射来设计一些通用方法. 现在有2张表, 一张user表和一张stu ...

  6. 关于Java泛型的使用

    在目前我遇到的java项目中,泛型应用的最多的就属集合了.当要从数据库取出多个对象或者说是多条记录时,往往都要使用集合,那么为什么这么使用,或者使用时有什么要注意的地方,请关注以下内容. 感谢Wind ...

  7. 初识java泛型

    1 协变数组类型(covariant array type) 数组的协变性: if A IS-A B then A[] IS-A B[] 也就是说,java中的数组兼容,一个类型的数组兼容他的子类类型 ...

  8. 【Java心得总结四】Java泛型下——万恶的擦除

    一.万恶的擦除 我在自己总结的[Java心得总结三]Java泛型上——初识泛型这篇博文中提到了Java中对泛型擦除的问题,考虑下面代码: import java.util.*; public clas ...

  9. 【Java心得总结三】Java泛型上——初识泛型

    一.函数参数与泛型比较 泛型(generics),从字面的意思理解就是泛化的类型,即参数化类型.泛型的作用是什么,这里与函数参数做一个比较: 无参数的函数: public int[] newIntAr ...

  10. 初识Java泛型以及桥接方法

    泛型的由来 在编写程序时,可能会有这样的需求:容器类,比如java中常见的list等.为了使容器可以保存多种类型的数据,需要编写多种容器类,每一个容器类中规定好了可以操作的数据类型.此时可能会有Int ...

随机推荐

  1. Java核心知识点学习----多线程并发之线程间的通信,notify,wait

    1.需求: 子线程循环10次,主线程循环100次,这样间隔循环50次. 2.实现: package com.amos.concurrent; /** * @ClassName: ThreadSynch ...

  2. redis学习(二) Redis Hash

    Redis hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象. Redis 中每个 hash 可以存储 232 - 1 键值对(40多亿). redis ...

  3. 打开office2010里面的access,总是提示要配置Office single image

    刚安装了,office2010,打开里面的access时,总是提示要配置Office single image,但打开word和excel没问题,很是不舒服 在网上找到N种方法,试下来还是不行. 后来 ...

  4. bootstrap的小图标

    bootstrapt的小图标  关于bootstrap的<i>小图标,需要几个要素.<i class="icon-search"></i>形式第 ...

  5. 用Backbone.js创建一个联系人管理系统(一)

    原文 Build a Contacts Manager Using Backbone.js: Part 1 在这个教程里我们将会使用Backbone.js,Underscore.js,JQuery创建 ...

  6. POJ3680_Intervals

    给你若干个区间,每个区间有一个权值,你可以选出某些区间,使得在保证没有任何一段的覆盖次数超过k的前提下,总的权值最大. 这个建模真的十分神奇,赞一个. 对于给出的每一个区间,离散化,最终我们可以知道所 ...

  7. hd2066一个人的旅行

    Problem Description 虽然草儿是个路痴(就是在杭电待了一年多,居然还会在校园里迷路的人,汗~),但是草儿仍然很喜欢旅行,因为在旅途中 会遇见很多人(白马王子,^0^),很多事,还能丰 ...

  8. java和Discuz论坛实现单点登录,通过Ucenter(用户管理中心)

    标题有点问题,没有进行修改. 一 Discuz论坛搭建步骤 1:服务器环境配置 服务器要支持php语言+支持mysql 5.0以上的数据库 + Apache服务器(支持网站的一个服务器,通过域名的能访 ...

  9. Attribute "lazy" with value "true" must have a value from the list "false proxy no-proxy "

    Hibernate 3.2 版本 当设置lazy="true"属性时,会产生该个异常: Attribute "lazy" with value "tr ...

  10. dialogic d300语音卡驱动重装后启动报错问题解决方法

    dialogic d300 驱动重装后 dlstart  报错解决 问题描述:dlstart  后如下报错 [root@BJAPQ091 data]#dlstop Stopping Dialogic ...