泛型基础

定义:类定义和方法定义的时候不指定具体类型,把具体类型声明的工作推迟到创建实例或调用方法的时候才去明确的特殊的类型。

参数多态:类定义和方法定义时不指定具体参数类型,把类型作为参数使用,创建实例或调用方法的时候可以指定不同的具体类型。因此泛型(Generics)也叫参数化类型(ParameterizedType)

Java在JDK5中引入泛型特性,本质是一个语法糖,编译阶段会进行脱糖处理。

泛型使用

定义泛型

定义一个泛型:使用尖括号包裹,多个泛型类型用逗号隔开,可以起任意名字。如<T><Key, Value><Afauria>。类似定义一个新类型,但是无法被初始化。

  • 在类或接口定义的时候定义泛型:class 类名<T>,作用范围是当前类或接口
  • 在方法定义的时候定义泛型:<T> 返回值类型 方法名(),作用范围是当前方法
  • 定义的时候可以指定泛型是哪个类的子类,如<T extends View>。默认是Object类的子类,即可以表示任意类型

使用泛型

  • 原始类型(Raw Type,裸类型):如List
  • 带参数类型:如List<String>。可以把带参数类型当作不同的类来理解和使用。
  • 通配符类型:如List<?>List<? extends Object>List<? super String>

原始类型List和带参数类型List<Object>区别?

  1. 原始类型编译时不会进行安全检查,带参数类型编译时会进行安全检查
  2. 不同的带参数类型都可以赋值给原始类型,但是不同的带参数类型没法互相赋值,即使参数类型是父子类关系,如下
List list1 = new ArrayList<String>(); //编译正常
List<Object> list2 = new ArrayList<String>(); //编译错误
List<String> list3 = new ArrayList<Object>(); //编译错误

泛型不具备继承关系:List<Object>List<String>可以看作两种无关的类型

为什么需要泛型?

早期Java是使用Object来代表任意类型的,可以接收不同类型的对象,会自动向上转型为Object,但是取出的时候向下转型需要进行强转,程序不安全。

使用泛型可以在编译期检查对象是否合法,并且实现参数多态。

泛型好处

  • 提高代码的复用性:使用集合类存储对象的时候不需要为每个类型建一个类。如StringList,IntList等
  • 代码更加简洁:不用强制转换
  • 程序更加健壮:编译时进行安全检查,只要编译时期没有警告,那么运行时期就不会出现ClassCastException异常
  • 可读性和稳定性:在编写集合的时候,就限定了类型

泛型作用于编译期还是运行期,为什么?

  • 只在编译期报错,在运行时不会报错:例如构建 ArrayList 实例时指定具体类型,编译器将标记该实例并关注该实例后续所有方法的调用,每次调用前都进行安全检查,非指定类型的对象或方法会编译报错。
  • 运行的时候通过反射获取到的类型名不是泛型名称,而是真实类型
  • List<Integer>源码阶段无法add其他类型的值。但是编译之后泛型被擦除为原始类型List,因此在运行的时候能够通过反射添加其他类型的值。
  • 运行时无法new创建泛型的实例

泛型擦除

编译器对泛型有两种处理方式

  1. Code specialization:实例化一个泛型类的时候都生成一份目标代码。
    1. 运行期仍然存在该类型,如List<String>List<Integer>是两个不同的类型。称为真实泛型。
    2. 这种方式会导致代码膨胀,浪费内存空间。
    3. C++C#
  2. Code sharing:每个泛型类只生成一份目标代码,该泛型类的所有实例都映射到这份目标代码上。并在需要的时候进行类型检查和转换。
    1. 只在源码中存在,在编译时会被替换为原始类型(类型擦除),并且在相应的位置插入强制转换代码。称为伪泛型。
    2. Java,本质上是Java语言的语法糖

Java编译时会发生类型擦除:即编译后不存在泛型,会转换成Object类型,或者extend的类型。

  1. 移除所有类型参数
List<String> list = new ArrayList<>();
list.add("Hello World");
//反编译后结果如下:被替换为原始类型
ArrayList var1 = new ArrayList();
var1.add("Hello World");
  1. 将泛型参数替换为其最顶级的父类(上界),默认上界为Object。
public static <T> void foo(T a) {}
public static <T extends String> void foo(T a) {}

使用javap -s Main查看汇编代码:-s可以显示方法内部签名

Compiled from "Main.java"
public class Main {
  #...
  public <T> void foo(T);
    descriptor: (Ljava/lang/Object;)V #第一个方法签名变为Object

  public <T extends java.lang.String> void foo(T);
    descriptor: (Ljava/lang/String;)V #第二个方法签名被换为String
}

限定通配符

  • 非限定通配符:<T>,不对类型进行限制,可以替代任意类型
  • 限定通配符(Wildcard):会对泛型参数进行检查
    • 无限定通配符:<?>,表示未知类型。
    • 上限通配符(上界):<? extends A>,类型必须为A类型或者A子类
    • 下限通配符(下界):<? super B>,类型必须为B类型或者B的父类

限定通配符只在类型声明的时候使用,不能在定义泛型、或者初始化对象的时候使用。如new ArrayList<?>();class B<? extends A><? extends A> void foo()会编译错误

上限通配符<? extends A>和定义泛型的时候使用的<T extends A>要区分开来

限定通配符遵循PECS原则(Producer Extends Consumer Super):

  • 只读取不写入该数据结构(该数据结构是生产者),使用<? extends A>通配符;(Producer Extends)
  • 只写入不读取该数据结构(该数据结构是消费者),使用<? super B>通配符;(Consumer Super)
  • 如果既要存又要取,那么就不要使用任何通配符。

PECS原则基于这样一个事实:“向上转型可以自动转换,向下转型需要强制转换”。

//定义A、B、C类。C继承B,B继承A。
List<? extends B> list1 = new ArrayList<>(); //元素是B的子类,B的子类可能有多个
list1.add(new A()); //编译错误:由于无法自动向下转型,编译器不知道应该转为哪个子类
list1.add(new B()); //编译错误
list1.add(new C()); //编译错误
A obj1 = list1.get(0); //编译通过。元素是B的子类,get的时候可以自动向上转型为A类或B类
B obj2 = list1.get(0); //编译通过。
C obj3 = list1.get(0); //编译错误。B的子类可能有多个,不一定是C类

List<? super B> list2 = new ArrayList<>(); //元素是B的父类,B的父类可能有多个
list2.add(new A()); //编译错误。B的父类可能有多个,不一定是A类
list2.add(new B()); //编译正常。添加B和B的子类的时候,可以自动向上转型,转为对应的父类
list2.add(new C()); //编译正常
Object obj = list2.get(0); //编译正常。
A obj4 = list2.get(0); //编译错误。无法确定返回哪个父类,因此只能返回Object类型
B obj5 = list2.get(0); //编译错误
C obj6 = list2.get(0); //编译错误

可以发现上限通配符add的操作都会失败。下限通配符get的操作都会失败。

原始类型List和通配符类型List<?>区别?

  1. 都可以被带参数类型赋值
  2. 都可以读取为Object类型,因为向上转型不需要安全检查
  3. 原始类型可以写入任意类型,但是会警告未进行安全检查。List<?>不可以写入,会编译错误。<?>使用场景:不关心容器内类型,只关心元素数量和是否为空
List list1 = new ArrayList<String>();
List<?> list2 = new ArrayList<String>();
list1.add(new Object()); //警告未进行安全检查
list2.add(new Object()); //编译错误,具体类型无法写入未知类型。不限制的话容易出现异常
//都可以读取为Object类型
Object obj1 = list1.get(0);
Object obj2 = list2.get(0);

其他细节

泛型参数不接收基本数据类型

类型会被擦除为Object类,而基本数据类型不是Object的子类。需要使用包装类

泛型和instanceof

泛型无法和instanceof使用,会编译错误。因为类型擦除之后<String>就不在了

List<String> list = new ArrayList<>();
if(list instanceof ArrayList<String>) {} //编译错误
if(list instanceof ArrayList) {} //编译正常

泛型和static

  • 类中定义的泛型,无法被静态方法和静态变量使用。因为泛型参数的实例化是在定义对象的时候指定的,静态方法属于类,不属于对象,因此不能明确类型。
  • 静态方法中定义的泛型,可以被自身使用
class A<T> {
  T b; //编译正常
  static T a; //编译错误
  static void foo(T a) {} //编译错误

  static <E> void foo2(E a) {} //编译正常,静态方法自己定义的泛型
}

泛型和重载

//编译错误,无法重载:编译时会被擦除成Object,不管起什么名字
public static <T> void foo(T obj) {}
public static <E> void foo(E obj) {}

//编译错误,无法重载:编译时会被擦除成原始类型,即List,
public static void foo(List<String> list) {}
public static void foo(List<Integer> list) {}

//可以重载:第一个方法被擦除为Object,第二个方法被擦除成View
public static <T> void foo(T obj) {}
public static <T extends View> void foo(T obj) {}

泛型和重写

class A<T> {
    public void setValue(T a){
    }
}
class B extends A<String>{
    @Override
    public void setValue(String a) {
        super.setValue(a);
    }
}

如上,预期是B类重写A类的setValue方法,@Override注解也没有提示错误。但是实际上使用javap -c -v B.class反编译出来的结果如下:

Compiled from "Main.java"
class B extends A<java.lang.String> {
   #...
   public void setValue(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: invokespecial #2                  // Method A.setValue:(Ljava/lang/Object;)V
         5: return
      LineNumberTable:
        line 22: 0
        line 23: 5

  public void setValue(java.lang.Object);
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC # 多了ACC_BRIDGE、ACC_SYNTHETIC标记
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: checkcast     #3                  // class java/lang/String,强制转换
         5: invokevirtual #4                  // Method setValue:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 19: 0
}

可以看到有两个setValue方法,相当于进行了重载。

为什么会多生成一个方法呢?

编译时对A进行了类型擦除,方法变为setValue(Object)。而B类擦除之后变为了setValue(String)。不满足重写的条件。(子类参数类型不能比父类更严格)

因此虚拟机生成了一个桥方法,解决类型擦除和多态冲突的问题。可以看到桥方法内部进行了强制转换,并且调用了setValue(String)方法

真的是重载吗?

如果是重载的话对象应该可以调用两个setValue方法。但是实际上调用setValue(Object)方法会编译失败,如下

B obj = new B();
obj.setValue("");  //编译通过
obj.setValue(new Object()); //编译失败

为什么无法调用setValue(Object)方法?

  • 桥方法是编译阶段生成的,源码阶段无法直接调用。
  • 开发者的意图是重写,因此编译器对桥方法做了特殊处理,使得源码阶段看起来像是重写。
  • 桥方法内部有强制转换,如果接收Object对象,必然会出现cast异常,为了避免这个问题,干脆在源码阶段就限制调用。我们使用反射验证,如下
public static void main(String[] args) {
    B obj = new B();
    try {
        Method strMethod = B.class.getMethod("setValue", String.class);  //可以查找到方法
        strMethod.invoke(obj, "Hello World"); //可以调用
        Method objMethod = B.class.getMethod("setValue", Object.class); //可以查找到方法
        objMethod.invoke(obj, "Hello World"); //可以调用
        objMethod.invoke(obj, new Object());  //可以调用,但是会抛出cast异常
    } catch (Exception e) {
        e.printStackTrace();
    }
}

results matching ""

    No results matching ""