天天看点

泛型详解

今天跟着frank大神的blog ​​传送门​​学习泛型的知识。

首先看一下下面代码

List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
    
System.out.println(l1.getClass() == l2.getClass());      

上次在讲解反射机制的时候通过反射举了个例子和这个是有关的。

上面结果输出是true,而造成输出结果的原因是泛型擦除。

泛型的概念

泛型就是一句话:​

​将类型参数化​

​​。

那什么又是类型参数化呢?

举一个例子:

public class Cache {
  Object value;

  public Object getValue() {
    return value;
  }

  public void setValue(Object value) {
    this.value = value;
  }
  
}      

假设这个Cache可以存取任何的值,那我们可以这样利用它:

Cache cache = new Cache();
cache.setValue(134);
int value = (int) cache.getValue();
cache.setValue("hello");
String value1 = (String) cache.getValue();      

使用起来很简单,我们只需要在get的时候强转就行了。

而泛型可以做到相同的功能,却带来了另外一种体验:

public class Cache<T> {
  T value;

  public Object getValue() {
    return value;
  }

  public void setValue(T value) {
    this.value = value;
  }
  
}      

这里就是体现了泛型的作用,它将value这个类型参数化了。再来看看它的使用方法:

Cache<String> cache1 = new Cache<String>();
cache1.setValue("123");
String value2 = cache1.getValue();
    
Cache<Integer> cache2 = new Cache<Integer>();
cache2.setValue(456);
int value3 = cache2.getValue();      

看来和上面的相比,它的好处就是不用强制转换类型了。

而当一个new出一个并确定好泛型后,就不能再放置别的类型了,比如上面的cache2,当我们对其setValue(“hello”)的时候,编译都不能通过。

从上可以总结:

  • 相比于直接用Object这样代替一切类型而言,泛型更加符合面向抽象开发的软件编程宗旨,因为泛型可以使具体的数据类别都传入进去。
  • 当泛型的具体类型确定以后,泛型提供了类型检测的机制,只有相匹配的数据才能正常的赋值,否则编译无法通过。
  • 提高了代码可读性

泛型的使用

泛型根据使用情况可以分为三种方法:

  1. 泛型类
  2. 泛型方法
  3. 泛型接口

泛型类

我们可以这样定义一个泛型类:

public class Test<T> {
  T field1;
}      

​<>​

​尖括号中的T被称作是类型参数,用于指代任意类型,事实上,T只是一种习惯性的写法,我们甚至可以把T换成任意的字母,不过出于规范我们把单个大写字母来表示一些参数类型:

  1. T:表示任何类
  2. E:表示Element,或者Expection
  3. K:代表key
  4. V:代表value
  5. S:代表SubType,后面会讲到

泛型的使用,最前面已经讲解过一个Cache的例子了。

当然泛型不止接受一个类型参数,他还可以接受多个类型参数:

public class MultiType <E,T>{
  E value1;
  T value2;
  
  public E getValue1(){
    return value1;
  }
  
  public T getValue2(){
    return value2;
  }
}      

泛型方法

public class Test1 {

  public <T> void testMethod(T t){
    
  }
}      

泛型方法和泛型类不同之处在于,泛型方法中<>是写在返回值前的,而< T>中的T不是运行时真正的参数。

当然,声明的类型参数,也可以当成返回值类型返回的。

泛型方法和泛型类共存现象:

public class Test1<T>{

  public  void testMethod(T t){
    System.out.println(t.getClass().getName());
  }
  public  <T> T testMethod1(T t){
    return t;
  }
}      

有人会问,泛型类中的参数T和泛型方法中的参数T有没有联系?

上述代码中 testMethod只是Test1中的一个普通方法,而testMethod1则是一个泛型方法。

泛型类的类型参数和泛型方法的类型参数没有任何的联系,泛型方法只以自己定义的类型参数为标准。

针对上面,可以写出这样的代码:

Test1<String> t = new Test1();
t.testMethod("generic");
Integer i = t.testMethod1(new Integer(1));      

就是我们定义了一个泛型为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;
  }
}      

泛型接口

和泛型类差不多:

public interface InterfaceTest<T> {
}      

通配符

除了​

​<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>的子类呀。

实际开发中我们有时候会有这样的需求,而通配符的出现就是为了解决这个问题的。

通配符的出现是为了指定泛型中的类型范围。

通配符有3中类型:

  1. ​<?>​

    ​被称作无限定的通配符
  2. ​< ? extends T>​

    ​被称为有上限的通配符
  3. ​< ? super T>​

    ​被称为有下限的通配符

无限定通配符<?>

无限定通配符经常与容器类 配合使用,它其中的?代表的是未知类型,所以涉及到了?操作时,一定与具体的类型无关。

public void testWildCards(Collection<?> collection){
}      

这里代码表示,testWildCards无需关注collection的真实类型,因为其类型参数是位置的,所以你只能调用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>

这个和前面的相对应,代表?是T或者T的超类(即父类)。

神奇的在于,它有一定的写操作能力。

public void testSuper(Collection<? super Sub> para){ 

             para.add(new Sub());//编译通过
             para.add(new Base());//编译不通过 
 }      

通配符与类型参数的区别 一般而言,通配符能干的事情都可以用类型参数替换。

比如

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;
}      

E 类型是 T 类型的子类,显然这种情况类型参数更适合。

有一种情况是,通配符和类型参数一起使用:

public <T> void test(T t,Collection<? extends T> collection){
}      

类型擦除

之前在讲反射的时候提过一句话:

泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。

List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
    
System.out.println(l1.getClass() == l2.getClass());      

最后得到的结果都是是 List.class,他们的String、Integer都被擦除了。

那Stirng、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.test.Erasure      

Class的类型仍然是Erasure而不是Erasure< T>,那我们再看看泛型类中T在运行时是什么类型:

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>​

​,则类型参数就被替换成类型上限。

类型擦除带来的局限性

类型擦除,是泛型能够与之前的 java 版本代码兼容共存的原因。但也因为类型擦除,它会抹掉很多继承相关的特性,这是它带来的局限性。

泛型详解

上述就是局限性的体现。

但是我们可以通过反射机制来解决这个问题。

泛型值得注意的地方

  1. 泛型不接受8中基础数据类型
  2. Java 不能创建具体类型的泛型数组

    比如:

List<Integer>[] li2 = new ArrayList<Integer>[];
List<Boolean> li3 = new ArrayList<Boolean>[];      
List<?>[] li3 = new ArrayList<?>[10];
li3[1] = new ArrayList<String>();
List<?> v = li3[1];      

继续阅读