泛型
概念与理解
作用
-
引入:在 JDK 5 之前,泛型的功能是通过程序员手动地将比如成员变量的类型声明为
Object
类型,而实际存入具体类型的对象,最后在使用的时候再将Object
类型的引用强制向下转型为那个具体的类型后再使用它。这种做法存在一定的类型安全隐患。
-
在 JDK 5 开始,Java 提供泛型机制:
-
在编程语法上:
参数化类型,将代码中的类型声明处的类型参数化,在声明处暂时不确定具体类型(按
Object
类型),在使用或调用的时候再传入具体的类型。 -
在作用上:
在传入泛型的实参进行使用的时候,本质上就是委托编译器生成将引用变量的向下转型的代码,传什么类型就是委托它转什么类型,同时编译器会检查要进行的向下转型操作是否正确(也就是所谓的类型安全检查机制):
- 检查结果为错误,则编译不通过;
- 检查结果为正确,则编译可以通过,并且由编译器帮生成了向下强转的操作的代码;
要是没有传入泛型实参,那么就相当于没有委托编译器去生成将引用变量的向下转型的代码,从而所有泛型声明处的类型就相当于是
Object
类型。 -
泛型擦除机制:
代码中的泛型只和编译器打交道,在编译完后的字节码中是不存在泛型信息的,是编译器帮生成了向下强转的操作的代码。
-
- 总之,引用变量向下强转操作本身有风险,但很多地方确实要用,通过使用泛型特性,便可以一方面帮我们检查有没有转对,另一方面操作正确的情况下,向下转型的操作由编译器来生成代码。
基本语法
-
声明泛型形参:
- 基本:
<T>
、<V, T>
、…… - 受限泛型:如
<T extends Student>
用于限定使用时的实参只能传入Student
类及其子类。
- 基本:
-
传入泛型实参:
传入具体类的类名,编译器在检查通过后,会把泛型处都按该具体类,从
Object
类型的引用,强转为该类类型的引用,再提供外界使用。
使用场景
泛型类
-
对于整个类都需要使用某个或多个泛型时,用 泛型类,即,在类名处声明的语法格式,这处定义的泛型在整个类中可使用。
修饰符 class 类名<T> { ... } 修饰符 class 类名<T,V> { ... } ...
-
传实参给编译器的方式如:
-
new
类对象的时候; -
声明引用变量(局部/成员)的类型的时候;
-
声明方法的形参或返回值类型的时候;
-
泛型方法
-
若并不需要整个类都使用某个或多个泛型,而只是某个方法需要使用泛型时, 就用 泛型方法,即,方法的形参声明为泛型的语法格式,这处定义的泛型在此方法中可使用(包括方法体内、返回值声明处)。
修饰符 <T> 返回值类型 方法名(T t) { ... } 修饰符 <V, T> 返回值类型 方法名(V v, T t) { ... } ...
-
使用/实参传给编译器的方式:调用方法的地方实际传入了什么类型的对象,那这处调用的泛型类型就被明确成了什么类型。
泛型接口
-
对于整个接口都需要使用某个或多个泛型时,用 泛型接口,即,在接口名处声明的语法格式,这处定义的泛型在整个接口中可使用。
修饰符 interface 接口名<T> { ... } 修饰符 interface 接口名<T,V> { ... } ...
-
在用某个类实现它的时候:
-
要是
implements
后面写接口名但没写泛型信息,则重写它的方法时所有泛型都为Object
类型。 -
要是
impements
后面写接口名写了具体类型,则重新它的方法时所有泛型信息都用此次填写的类型代替。 -
要是
implements
后面写接口名想要继续保留泛型,则实现类自己也要带泛型,要把接口的泛型包括进去,从而等使用这个类的时候,向编译器传入具体类型信息,从类再给到接口(重写的方法)。
-
泛型中的继承关系
机制实测
测试环境
-
将使用泛型的类:
public class Holder<T> { public T val; public T getVal() { return val; } public void setVal(T val) { this.val = val; } }
-
将作为泛型实参的类:
它们都设置了属性:
public String name = ...
,name 为对应的类名,以此来检测引用本身的类型。
测试结果
-
具体类
Apple
作为泛型实参 :-
数据类型使用泛型的引用变量的传参/赋值:
Holder<Apple>
类型的变量只允许被Holder<Apple>
类型的变量传参/赋值。
-
内部以泛型为数据类型的变量的读写:
泛型传入实参后数据类型为
Apple
。-
只有
Apple
及其子类对象引用才可以赋值给类型为Apple
的变量。其实就是面向对象的基本内容。
-
return
它也显然就是按Apple
类型返回。
-
-
具体测试案例:
public static void main(String[] args) { Holder<Apple> h1 = new Holder<>(); // 报错: Holder<Fruit> h2 = h1; // 报错: h1.setVal(new Fruit()); h1.setVal(new Apple()); System.out.println("对象本身:" + h1.getVal() + " -- 引用的类型:" + h1.getVal().name); // 输出: 对象本身:Apple@30f39991 -- 引用的类型:Apple h1.setVal(new RedApple()); System.out.println("对象本身:" + h1.getVal() + " -- 引用的类型:" + h1.getVal().name); // 输出: 对象本身:RedApple@4a574795 -- 引用的类型:Apple h1.setVal(new GreenApple()); System.out.println("对象本身:" + h1.getVal() + " -- 引用的类型:" + h1.getVal().name); // 输出: 对象本身:GreenApple@23fc625e -- 引用的类型:Apple }
-
-
上界通配符
? extends Apple
作为泛型实参:-
数据类型使用泛型的引用变量的传参/赋值:
Holder<? extends Apple>
类型的变量只允许被Holder<Apple>
或Holder<Apple子类>
类型的变量传参/赋值。 -
内部以泛型为数据类型的变量的读写:
泛型传入实参后数据类型为
? extends Apple
。-
所有对类型为
? extends Apple
的变量的赋值操作均不被允许。它代表某个继承
Apple
的具体类,但是是哪个类编译器并不知道,所以拒绝它被赋值,进而避免出现类型转换异常。 -
类型为
? extends Apple
的变量被return
时,会被按Apple
类型转型返回。编译器虽然不知道它是哪个继承
Apple
的具体类,但能通过前一步的编译则一定说明它是Apple
或其子类对象,那么一定可以安全地向上转型为Apple
,所以会被按Apple
类型转型返回。
-
-
具体测试案例:
public static void main(String[] args) { Holder<Apple> h1 = new Holder<>(); h1.setVal(new Apple()); System.out.println("对象本身:" + h1.getVal() + " -- 引用的类型:" + h1.getVal().name); // 输出: 对象本身:Apple@30f39991 -- 引用的类型:Apple // 报错: Holder<? extends RedApple> h2 = h1; Holder<? extends Apple> h3 = h1; System.out.println("对象本身:" + h3.getVal() + " -- 引用的类型:" + h3.getVal().name); // 输出: 对象本身:Apple@30f39991 -- 引用的类型:Apple // 报错: h3.setVal(new RedApple()); // 报错: h3.setVal(new Apple()); // 报错: h3.setVal(new Fruit()); // 报错: h3.setVal(new Food()); // 报错: h3.setVal(new Object()); Holder<? extends Fruit> h4 = h1; System.out.println("对象本身:" + h4.getVal() + " -- 引用的类型:" + h4.getVal().name); // 输出: 对象本身:Apple@30f39991 -- 引用的类型:Fruit // 报错: h2.setVal(new Apple()); // 报错: h2.setVal(new Fruit()); // 报错: h2.setVal(new Food()); // 报错: h2.setVal(new Object()); }
-
-
下界通配符
? super Apple
作为泛型实参:-
数据类型使用泛型的引用变量的传参/赋值:
Holder<? super Apple>
类型的变量只允许被Holder<Apple>
或Holder<Apple父类>
类型的变量传参/赋值。 -
内部以泛型为数据类型的变量的读写:
泛型传入实参后数据类型为
? super Apple
。-
类型
? super Apple
的变量只允许用Apple
及其子类的对象引用赋值。- 尤其要注意,这里并不允许像字面上那样用
Apple
及其父类对象对? super Apple
类型的变量进行传参/赋值,而是只能用Apple
类型的对象(含其子类)。 - 暂时猜测是因为
? super Apple
被设计用来接收一个数据接收者的引用,而“数据接收者”的泛型是Apple
或其父类,接收完毕后要在内部把接收的数据按自己的泛型实参向上转型,因此在数据提供者这一方,限定数据的类型只能是Apple
及其子类,以此来保证这三种类型都在同一继承体系,从而数据提供者那里最终能实现绝对安全的将接收到的数据进行转型。
- 尤其要注意,这里并不允许像字面上那样用
-
类型为
? super Apple
的变量被return
时,会按Object
类型返回。它代表
Apple
的某个具体父类,但是是哪个类编译器并不知道,所以只有用Object
这个顶层父类才是绝对安全的向上转型。
-
-
具体测试案例:
public static void main(String[] args) { // 报错: Holder<? super Apple> h1 = new Holder<RedApple>(); Holder<? super Apple> h2 = new Holder<Apple>(); // 报错: h2.setVal(new Fruit()); h2.setVal(new Apple()); System.out.println("对象本身:" + h2.getVal()); // 输出:对象本身:Apple@1f32e575 h2.setVal(new GreenApple()); System.out.println("对象本身:" + h2.getVal()); // 输出:对象本身:GreenApple@2ff4acd0 // 报错: System.out.println("引用的类型:" + h2.getVal().name); Holder<? super Apple> h3 = new Holder<Fruit>(); // 报错: h3.setVal(new Food()); // 报错: h3.setVal(new Fruit()); h3.setVal(new Apple()); System.out.println("对象本身:" + h3.getVal()); // 输出: 对象本身:Apple@54bedef2 h3.setVal(new RedApple()); System.out.println("对象本身:" + h3.getVal()); // 输出: 对象本身:RedApple@27716f4 // 报错: System.out.println("引用的类型:" + h3.getVal().name); }
-
-
无界通配符
?
作为泛型实参:-
数据类型使用泛型的引用变量的传参/赋值:
Holder<?>
类型的变量允许被Holder<任何类>
类型的变量传参/赋值。 -
内部以泛型为数据类型的变量的读写:
泛型传入实参后数据类型为
?
。- 所有对类型为
?
的变量的赋值操作均不被允许。 - 类型为
?
的变量被return
时,会被按Object
类型返回。
- 所有对类型为
-
具体测试案例:
public static void main(String[] args) { Holder<Apple> h1 = new Holder<>(); h1.setVal(new Apple()); System.out.println("对象本身:" + h1.getVal() + " -- 引用的类型:" + h1.getVal().name); // 输出: 对象本身:Apple@30f39991 -- 引用的类型:Apple Holder<?> h2 = h1; // 报错: h2.setVal(new Object()); // 报错: h2.setVal(new Fruit()); // 报错: h2.setVal(new Apple()); System.out.println("对象本身:" + h2.getVal()); // 输出: 对象本身:Apple@30f39991 // 报错: System.out.println("引用的类型:" + h2.getVal().name); }
-
PCES 应用原则
-
PCES 全称 Producer Extends、Consumer Super。
一个类
class Xxx<E>
对外提供的方法的形参中:-
若传入的是数据生产者(提供者),就用泛型为
? extends E
类型的参数来接收;- 一方面,传入的数据生产者的泛型实参匹配为
E
或其子类,本类Xxx<E>
此时作为数据消费者,会将得到的数据向上转型成自己的泛型实参类型E
来使用(如,用E
类型变量接收? extends E
类型变量)。 - 另一方面,在这场景下,在语法机制上无法将数据传给这场景下的数据生产者,而同时我们也不需要把数据传给数据生产者。
- 一方面,传入的数据生产者的泛型实参匹配为
-
若传入的是数据消费者(接收者),就用泛型为
? super E
类型的参数来接收;- 一方面,传入的数据消费者的泛型实参匹配为
E
或其父类,本类Xxx<E>
此时作为数据生产者,会将内部的类型为自己泛型实参类型E
或其子类类型的数据传给它,而数据消费者一般还会再在自己内部将接收到的数据进一步向上转型为自己的泛型实参类型后再使用。 - 另一方面,在这场景下,在语法机制上让的数据消费者提供的数据只能是
Object
类型引用,而同时我们一般也不需要让数据消费者提供数据。
- 一方面,传入的数据消费者的泛型实参匹配为
-
总的原则:若一个(容器)对象带有具体泛型信息,那么消费它的对象的泛型信息就必须和它相同或者是它父类,以此保证消费方将消费到的数据转型成自己的具体泛型类型再使用时的绝对安全(不会出现类型转换异常)。
也因此,泛型通配符不允许作为直接实例化时的泛型实参,只能像上面这样被动接收别人的赋值再使用。
-
-
应用方式举例:
示例中的“内部数据”的类型关系同前“机制实测”中。
-
例子中的“内部数据”的引用的类型的变化大致如下:
-
例子的具体代码:
import java.util.List; import java.util.Stack; public class MyStack<E> extends Stack<E> { // 需要传入数据提供方 public void pushAll(List<? extends E> list) { for (E e : list) { push(e); } } // 需要传入数据接收方 public void popAll(List<? super E> list) { while (!isEmpty()) { list.add(pop()); } } }
public static void main(String[] args) { ArrayList<Apple> appleList = new ArrayList<>(); appleList.add(new Apple()); appleList.add(new RedApple()); appleList.add(new GreenApple()); MyStack<Fruit> fruitStack = new MyStack<>(); // MyStack 对象从数据提供方接收数据: fruitStack.pushAll(appleList); for (Fruit f : fruitStack) { System.out.println("对象本身:" + f + " -- 引用的类型:" + f.name); } System.out.println("------------"); ArrayList<Food> foodList = new ArrayList<>(); // MyStack 对象提供数据给数据接收方: fruitStack.popAll(foodList); for (Food f : foodList) { System.out.println("对象本身:" + f + " -- 引用的类型:" + f.name); } } /* 输出结果: 对象本身:Apple@3f99bd52 -- 引用的类型:Fruit 对象本身:RedApple@4f023edb -- 引用的类型:Fruit 对象本身:GreenApple@3a71f4dd -- 引用的类型:Fruit ------------ 对象本身:GreenApple@3a71f4dd -- 引用的类型:Food 对象本身:RedApple@4f023edb -- 引用的类型:Food 对象本身:Apple@3f99bd52 -- 引用的类型:Food */
-