【读书笔记】泛型深究

【读书笔记】泛型深究

笔记链接

引子

  • 一道经典的测试题
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();

System.out.println(l1.getClass() == l2.getClass());
  • 不了解泛型的和很熟悉泛型的同学应该能够答出来,而对泛型有所了解,但是了解不深入的同学可能会答错。正确答案是 true。上面的代码中涉及到了泛型,而输出的结果缘由是类型擦除

泛型类与泛型方法的共存现象

public class Test1<T>{

   public  void testMethod(T t){
       System.out.println(t.getClass().getName());
   }
   public  <T> T testMethod1(T t){
       return t;
   }
}
  • 上面代码中,Test1 是泛型类,testMethod 是泛型类中的普通方法,而 testMethod1 是一个泛型方法。而泛型类中的类型参数与泛型方法中的类型参数是没有相应的联系的,泛型方法始终以自己定义的类型参数为准。
  • 测试代码
Test1<String> t = new Test1();
t.testMethod("generic");//必须填字符串
Integer i = t.testMethod1(new Integer(1));//字符串 or 数字等 均可
  • 泛型类的实际类型参数是 String,而传递给泛型方法的类型参数是 Integer,两者不相干。
  • 为了避免混淆,代码可以更改为这样
public class Test1<T>{
   public  void testMethod(T t){
       System.out.println(t.getClass().getName());
   }
   public  <E> E testMethod1(E e){
       return e;
   }
}

T 与 ?

  • 已经有了 <T> 的形式了,为什么还要引进 <?> 这样的概念呢?
class Base{}

class Sub extends Base{}

Sub sub = new Sub();
Base base = sub;
  • Base 是 Sub 的父类,它们之间是继承关系,所以 Sub 的实例可以给一个 Base 引用赋值,那么
List<Sub> lsub = new ArrayList<>();
List<Base> lbase = lsub;
  • 最后一行代码,编译器不会让它通过的。Sub 是 Base 的子类,不代表 List<Sub> 和 List<Base> 有继承关系。
  • 但是,在现实编码中,确实有这样的需求,希望泛型能够处理某一范围内的数据类型,比如某个类和它的子类,对此 Java 引入了通配符?这个概念。
  • 所以,通配符的出现是为了指定泛型中的类型范围
    • <?> 被称作无限定的通配符。
    • <? extends T> 被称作有上限的通配符。
    • <? super T> 被称作有下限的通配符。

<?>

public void testWildCards(Collection<?> collection){
}
  • 上面的代码隐略地表达了一个意图: testWidlCards() 这个方法内部无需关注 Collection 中的真实类型,因为它是未知的。所以,你只能调用 Collection 中与类型无关的方法。
  • 当 <?> 存在时,Collection 对象丧失了 add() 方法的功能,编译器不通过。
List<?> wildlist = new ArrayList<String>();
wildlist.add(123);// 编译不通过
  • <?> 提供了只读的功能,也就是它删减了增加具体类型元素的能力,只保留与具体类型无关的功能。它不管装载在这个容器内的元素是什么类型,它只关心元素的数量、容器是否为空。这种需求还是很常见的。

<? extends T>

  • <?> 代表着类型未知,但是我们的确需要对于类型的描述再精确一点,我们希望在一个范围内确定类别,比如类型 A 及 类型 A 的子类都可以。
public void testSub(Collection<? extends Base> para){

}
  • 上面代码中,para 这个 Collection 接受 Base 及 Base 的子类的类型。
  • 但是,它仍然丧失了写操作的能力。也就是说
para.add(new Sub());// 编译不通过
para.add(new Base());// 编译不通过
  • 没有关系,我们不知道具体类型,但是我们至少清楚了类型的范围。

<? super T>

  • 这个和 <? extends T> 相对应,代表 T 及 T 的超类。
public void testSuper(Collection<? super Sub> para){
}
  • <? super T> 神奇的地方在于,它拥有一定程度的写操作的能力
public void testSuper(Collection<? super Sub> para){
   para.add(new Sub());//编译通过
   para.add(new Base());//编译不通过
}

类型参数T与通配符?的区别

  • 一般而言,? 能干的事情都可以用T替换。
public void testWildCards(Collection<?> collection){}

可以替换为

public <T> void test(Collection<T> collection){}
  • 值得注意的是,如果用泛型方法来取代通配符,那么上面代码中 collection 是能够进行写操作的。只不过要进行强制转换。
public <T> void test(Collection<T> collection){
   collection.add((T)new Integer(12));
   collection.add((T)"123");
}
  • 需要特别注意的是,类型参数适用于参数之间的类别依赖关系,举例说明。
public class Test2 <T,E extends T>{
   T value1;
   E value2;
}
public <D,S extends D> void test(D d,S s){

}
  • E 类型是 T 类型的子类,显然这种情况类型参数更适合。
  • 有一种情况是,通配符和类型参数一起使用。
public <T> void test(T t,Collection<? extends T> collection){

}
  • 如果一个方法的返回类型依赖于参数的类型,那么通配符也无能为力。
public T test1(T t){
   return value1;
}

类型擦除

  • 泛型是 Java 1.5 版本才引进的概念,在这之前是没有泛型的概念的,但显然,泛型代码能够很好地和之前版本的代码很好地兼容。 这是因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除
  • 通俗地讲,泛型类和普通类在 java 虚拟机内是没有什么特别的地方。
  • 回顾文章开始时的那段代码
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();

System.out.println(l1.getClass() == l2.getClass());

  • 打印的结果为 true 是因为 List 和 List 在 jvm 中的 Class 都是 List.class。泛型信息被擦除了。
  • 可能同学会问,那么类型 String 和 Integer 怎么办?答案是泛型转译。
public class Erasure <T>{
   T object;
   public Erasure(T object) {
       this.object = object;
   }
}
  • Erasure 是一个泛型类,我们查看它在运行时的状态信息可以通过反射。
Erasure<String> erasure = new Erasure<String>("hello");
Class eclz = erasure.getClass();
System.out.println("erasure class is:"+eclz.getName());
  • 打印结果:erasure class is:com.frank.test.Erasure
  • Class 的类型仍然是 Erasure 并不是 Erasure<T> 这种形式,那我们再看看泛型类中 T 的类型在 jvm 中是什么具体类型。
Field[] fs = eclz.getDeclaredFields();
for ( Field f:fs) {
   System.out.println("Field name "+f.getName()+" type:"+f.getType().getName());
}
  • 打印结果:Field name object type:java.lang.Object
  • 那我们可不可以说,泛型类被类型擦除后,相应的类型就被替换成 Object 类型呢?这种说法,不完全正确。
  • 我们更改一下代码
public class Erasure <T extends String>{
//  public class Erasure <T>{
   T object;

   public Erasure(T object) {
       this.object = object;
   }

}
  • 再看测试结果:Field name object type:java.lang.String
  • 现在可以下结论了:在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 <T> 则会被转译成普通的 Object 类型,如果指定了上限如 <T extends String> 则类型参数就被替换成类型上限。
  • 所以,在反射中。
public class Erasure <T>{
   T object;

   public Erasure(T object) {
       this.object = object;
   }

   public void add(T object){

   }

}
  • add() 这个方法对应的 Method 的签名应该是 Object.class
Erasure<String> erasure = new Erasure<String>("hello");
Class eclz = erasure.getClass();
System.out.println("erasure class is:"+eclz.getName());

Method[] methods = eclz.getDeclaredMethods();
for ( Method m:methods ){
   System.out.println(" method:"+m.toString());
}
  • 打印结果:method:public void com.frank.test.Erasure.add(java.lang.Object)
  • 也就是说,如果你要在反射中找到 add 对应的 Method,你应该调用 getDeclaredMethod(“add”,Object.class) 否则程序会报错,提示没有这么一个方法,原因就是类型擦除的时候,T 被替换成 Object 类型了。

类型擦除带来的局限性

  • 类型擦除,是泛型能够与之前的 java 版本代码兼容共存的原因。但也因为类型擦除,它会抹掉很多继承相关的特性,这是它带来的局限性。
  • 理解类型擦除有利于我们绕过开发当中可能遇到的雷区,同样理解类型擦除也能让我们绕过泛型本身的一些限制。比如
List<Integer> list = new ArrayList<>();
list.add(1);
list.add("test");//编译不过
  • 正常情况下,因为泛型的限制,编译器不让最后一行代码编译通过,因为类似不匹配,但是,基于对类型擦除的了解,利用反射,我们可以绕过这个限制。
  • List源码如下
public interface List<E> extends Collection<E>{

    boolean add(E e);
}
  • 因为 E 代表任意的类型,所以类型擦除时,add 方法其实等同于
boolean add(Object obj);
  • 那么,利用反射,我们绕过编译器去调用 add 方法。
List<Integer> list = new ArrayList<>();
list.add(1);
//list.add("test");//编译不过
//list.add(42.9f);//编译不过
try {
  Method method = list.getClass().getDeclaredMethod("add",Object.class);
  method.invoke(list,"test");
  method.invoke(list,42.9f);
} catch (NoSuchMethodException e) {
  e.printStackTrace();
} catch (IllegalAccessException e) {
  e.printStackTrace();
} catch (InvocationTargetException e) {
  e.printStackTrace();
}

for(Object o:list){//Object o
  System.out.println(o);
}
  • 可以看到,利用类型擦除的原理,用反射的手段就绕过了正常开发中编译器不允许的操作限制

不能创建具体类型的泛型数组

List<Integer>[] li1 = new ArrayList<Integer>[10];//编译不过,提示generic array creation
List<Boolean>[] li2 = new ArrayList<Boolean>[10];//编译不过,提示generic array creation
//替代方案 创建 ArrayList<ArrayList<String>>
//List<String>[] li3 = new ArrayList[10];//编译通过why?
  • 原因还是类型擦除带来的影响。
  • List<Integer> 和 List<Boolean> 在 jvm 中等同于List<Object> ,所有的类型信息都被擦除,程序也无法分辨一个数组中的元素类型具体是 List<Integer>类型还是 List<Boolean> 类型。
  • 但是下面代码可以编译通过
List<?>[] li3 = new ArrayList<?>[10];
li3[1] = new ArrayList<String>();
List<?> v = li3[1];
  • 借助于无限定通配符却可以,前面讲过 ? 代表未知类型,所以它涉及的操作都基本上与类型无关,因此 jvm 不需要针对它对类型作判断,因此它能编译通过,但是,只提供了数组中的元素因为通配符原因,它只能读,不能写。比如,上面的 v 这个局部变量,它只能进行 get() 操作,不能进行 add() 操作。
  • 在java中,不能通过直接通过T[] tarr=new T[10]的方式来创建数组,最简单的方式便是通过Array.newInstance(Classtype,int size)的方式来创建数组

猜你喜欢

转载自blog.csdn.net/lbbandcheng/article/details/83024479