Java之泛型浅析(generic type)

以下都是个人理解,若有错误,请多多批评

1. 例子

先定义如下继承关系

class C{}
class A {
        public void say() {
            System.out.println("I am A");
        }
}

class B extends A {
        public void say() {
            System.out.println("I am B");
        }
}

早期版本的Java代码(1):

List list = new ArrayList();
list.add(123);
list.add("abc");
//o到底是什么类型?不清楚....可能我们本意是放入数字,但不知被谁放入了字符串,容易引发bug
Object o = list.get(0); 

之后引入了泛型,代码(2)变成了这样:

List<Integer> list1 = new ArrayList();
list1.add(123);
list1.add("abc"); // error ! error !
//此时o是Integer
Integer o = list1.get(0);

很多时候我们想定义一种List,这种List里只能包含一个继承体系内的对象,比如只能包含Number,代码(3)如下:

//list3中只能放Number的子类或Number本身
List<? extends Number> list3 = new ArrayList();
//下面这句为何会报错?Integer是Number的子类呀。
//List<? extends Number>实际上声明了一个list,这个list中可以放入Number的某一个子类型,Integer或Long或其他均可;但是并不能直接向里面add;
//试想,如果add(Integer)可以,那么add(Long)肯定也行,这样和早期没有泛型的代码有何区别?
//换句话说,此时list里的类型并不能确定,所以不能add具体的类型进去.
list3.add(new Integer(1)); //error !  
//get()是可以的,因为里面的类型至少能确定是Number的。
Number number = list3.get(0);

List<? super B> list4 = new ArrayList();
//放入B肯定是正确的
list4.add(new B());
//此时的list4可以放入B的某一个父类,但不能确定是A
list4.add(new A()); //error !
Object object = list4.get(0);//类型又不确定了

如上所述,List<? extends Number>List<? super B>都只是对类型范围进行了限定,list中具体是哪种类型是不确定的。在java中,若想自由的add和get,list中的类型必须如代码(2)所示的那样,确定类型。因此,这种用法一般会在方法中来限定方法参数,代码(4)如下:

//定义一个方法, 这个方法第一个参数接收一个list,这个list里要么是T,要么是T的一个父类型
private <T> void addElement(List<? super T> list, T t){
        list.add(t);
}
List<A> list2 = new ArrayList();//包含具体类型的list
//可以直接list2.add(new A())和list2.add(new B())
//确定类型的list传入方法,
addElement(list2, new A());
addElement(list2, new B());
list2.get(0).say(); //I am A
list2.get(1).say(); //I am B

List<C> list3 = new ArrayList();
addElement(list3, new C());

2. 协变和逆变

简单得说,协变是把子(窄)类型赋值给父(宽)类型,逆变则相反。

List list1 = new ArrayList(); // ok,多态的实质:父类的引用指向具体的一个子类实例
List<Number> list2 = new ArrayList(); // ok,此时左侧可以有泛型,右侧没有泛型
List<Number> list21 = new ArrayList<Number>(); // ok

List<Number> list3 = new ArrayList<Integer>(); // error,泛型不支持协变
List<? extends Number> list4 = new ArrayList<Integer>(); // ok,<? extends >使泛型也可以进行协变

List<Integer> list5 = new ArrayList<Number>(); // error,泛型不支持逆变
List<? super Integer> list6 = new ArrayList<Number>(); // ok,<? super >使泛型支持了逆变
list6.add(new Integer(1)); // ok
list6.add(new Long(1)); // error
List<? super Number> list61 = new ArrayList<Number>(); // ok
list61.add(new Integer(1)); // ok
list61.add(new Float(1.2f)); // ok 
//?super AAA,只要是在AAA的继承图谱中,子类或者父类都可以add

3. 泛型数组

首先数组是协变的

Number[] numbers = new Integer[1]; // ok
List<String>[] list = new ArrayList[1]; // ok

但是,不能创建泛型数组

new ArrayList<String>[1] // error
List<?>[] lsa = new List<?>[10] // ok, 这时相当于没有泛型

Example
下面的代码编译没问题,但是运行期报错了

//List<String>[] lsa = new List<String>[10]; // error.
List<String>[] lsa = new List[10]; // ok,这不是泛型数组
Object[] oa = lsa; //移花接木,运用协变性,将lsa赋给父类型
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3)); // list中放入了Integer
oa[1] = li;
String s = lsa[1].get(0); // error: ClassCastException.

上面代码的本意是创建一个只能装入String的数组,但我们使用一点技术手段将Integer装入,运行时就会出错,这就是不能创建泛型数组的最主要原因。

采用通配符的方式是允许的。虽然这种方式是可以的,但是需要显示转换类型,违背了泛型设计的初衷:添加泛型的一个重要目的就是消除这种显示的转换,而这种代码又必须添加。

List<?>[] lsa = new List<?>[10]; // ok
Object[] oa = lsa;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; 
Integer i = (Integer) lsa[1].get(0); // ok,需要强制转换
System.out.println(i);

List<String> list = new ArrayList<>();
list.add("hahaha");
oa[2] = list;
String o1 = (String) lsa[2].get(0); 
System.out.println(o1);

4. ?和T的区别

逻辑上List<?>可以看成是所有List<Integer>,List<String>等的父类。T代表某种具体类型。很多时候二者是等价的。

//下述两个方法是相同的,不能同时出现在一个类中
//这种方式定义方法可以消除重复性
public void test(List<?> list){} 
public <T> void test(List<T> list){}

5. instanceof

list instanceof ArrayList<Number> // error. 这样使用时,可能是想判断list是否是盛着Number的Arraylist,但是Number会被擦除掉,因此不能对确切的泛型使用
list instanceof ArrayList<?> // ok,相当与list instanceof ArrayList。其目的应该是判断list是否是Arraylist

6. 类型擦除

我们都知道,编译后泛型会被擦除,那我们怎样在运行期获取这些泛型信息呢?
Java 引入泛型擦除的原因是避免因为引入泛型而导致运行时创建不必要的类。

(1)如下例子,两个class相同,泛型的不同并没有导致生成不同的Class类。

Class c1 = new ArrayList<Integer>().getClass();
Class c2 = new ArrayList<String>().getClass();
System.out.println(c1 == c2); //true

(2)定义如下类,查看一下字节码

class Generic_2<T>{
   List<String> list = new ArrayList<String>();  //1
   List<T> list2 = new ArrayList<T>();  //2

   public void test(T t) {
       List<Integer> list3 = new ArrayList<Integer>();    //3
       list3.add(1); //4
   }
}

运行命令:java -v Generic_2.class,下面为部分信息

java.util.List<java.lang.String> list;  //1 
    descriptor: Ljava/util/List;
    flags:
    Signature: #13                          // Ljava/util/List<Ljava/lang/String;>;  //有泛型信息

  java.util.List<T> list2;  //2
    descriptor: Ljava/util/List;
    flags:
    Signature: #15                          // Ljava/util/List<TT;>;  //有泛型信息

  public void test(T);  //test方法
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=2
         0: new           #2                  // class java/util/ArrayList
         3: dup
         4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
         7: astore_2
         8: aload_2
         9: iconst_1
        10: invokestatic  #6                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        13: invokeinterface #7,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z  //没有泛型信息了
        18: pop
        19: return
      LineNumberTable:
        line 34: 0
        line 35: 8
        line 36: 19
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      20     0  this   Lcom/young/generic/Generic_2;
            0      20     1     t   Ljava/lang/Object;
            8      12     2 list3   Ljava/util/List;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0      20     0  this   Lcom/young/generic/Generic_2<TT;>;
            0      20     1     t   TT;
            8      12     2 list3   Ljava/util/List<Ljava/lang/Integer;>;
    Signature: #32                          // (TT;)V  //有泛型信息
}

上述字节码其实说明了泛型擦除后,其实只是擦除了Code属性里字节码指令相关的泛型信息,Signature这些元数据信息中其实依然保留了泛型信息,这就是我们可以通过反射获取泛型信息的根本原因

(3)定义了几个类来获取泛型信息:

class A<K,V>{}
class B extends A<String, Integer>{}

interface C<T>{}
class D implements C<String>{}

public static void main(String[] args){
        //extends
        Type type = B.class.getGenericSuperclass();
        ParameterizedType parameterizedType = (ParameterizedType) type;
        for (Type type1 : parameterizedType.getActualTypeArguments()) {
            System.out.println(type1);
        }
        
        //implements
        ParameterizedType parameterizedType1 = (ParameterizedType) D.class.getGenericInterfaces()[0];//0代表只有一个接口
        for (Type type1 : parameterizedType1.getActualTypeArguments()) {
            System.out.println(type1);
        }

        //getTypeParameters()只能获取声明时的泛型名字
        TypeVariable<? extends Class<? extends A>>[] typeParameters = new A().getClass().getTypeParameters();
        for (TypeVariable<? extends Class<? extends A>> typeParameter : typeParameters) {
            System.out.println(typeParameter.getTypeName());
        }
}
/**output
class java.lang.String
class java.lang.Integer

class java.lang.String

K
V
**/

7. 几个例子

Collections.java

// sort方法是既要从list中取,也要把结果放入list,这种即get又set的方法,必须用确定的类型T
public static <T extends Comparable<? super T>> void sort(List<T> list){...};

//fill方法只会set而不会get,所以要用super
public static <T> void fill(List<? super T> list, T obj) {...}

//这个方法只会get而不会set,所以要用extends
public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) {...}

本文参考了
http://swiftlet.net/archives/1950
https://www.cnblogs.com/lzq198754/p/5780426.html
https://my.oschina.net/lifany/blog/875769

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容