十四、泛型
14.1 泛型
泛型:可以在类或方法中预知地使用未知的类型。
一般在创建对象时,将未知的类型确定具体的类型。当没有指定泛型时,默认类型为 Object 类型。
有了泛型之后,尤其是对集合类的使用就变得加规范了。看下面这段代码:
ArrayList<String> list = new ArrayList<String>();
list.add("青花椒");
String str = list.get(0);
在 JDK 1.5 之前,没有泛型的时候,都是怎么办的呢?——使用 Object 数组来设计 ArrayList 类:
class Arraylist {
private Object[] objs;
private int i = 0;
public void add(Object obj) {
objs[i++] = obj;
}
public Object get(int i) {
return objs[i];
}
}
然后,向 ArrayList 中存取数据:
Arraylist list = new Arraylist();
list.add("青花椒");
list.add(new Date());
String str = (String)list.get(0);
由此可以看出来:
- ArrayList 可以存放任何类型的数据(既可以是字符串,也可以混入日期),因为所有类都继承自 Object 类;
- 从 ArrayList 取出数据的时候需要强制类型转换,因为编译器并不能确定你取的是字符串还是日期。
这样对比下来可以明显地感受到泛型的好处:使用类型参数解决了元素的不确定性——参数类型为 String 的集合中是不允许存放其他类型元素的,取出数据的时候也不需要强制类型转换了。
我们按照泛型的标准重新设计一下 ArrayList 类:
class Arraylist<E> {
private Object[] elementData;
private int size = 0;
public Arraylist(int initialCapacity) {
this.elementData = new Object[initialCapacity];
}
public boolean add(E e) {
elementData[size++] = e;
return true;
}
E elementData(int index) {
return (E) elementData[index];
}
}
一个泛型类就是具有一个或多个类型变量的类。
这里引入的类型变量为 E(Element,元素的首字母),使用尖括号 <> 括起来,放在类名的后面。然后可以用具体的类型(比如字符串、日期等)替换类型变量来实例化泛型类:
Arraylist<String> list = new Arraylist<String>();
list.add("青花椒");
String str = list.get(0);
Arraylist<Date> list = new Arraylist<Date>();
list.add(new Date());
Date date = list.get(0);
另外,我们还可以在一个非泛型的类(或者泛型类)中定义泛型方法:
class Arraylist<E> {
public <T> T[] toArray(T[] a) {
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
}
}
但是需要注意:方法返回类型和方法参数类型至少需要一个。
14.2 泛型的基本使用
01、定义和使用含有泛型的类
-
定义格式:
修饰符 class 类名<代表泛型的变量> { } 例如: class ArrayList<E>{ public boolean add(E e){ } public E get(int index){ } .... }
多元泛型
修饰符 class 类名<代表泛型的变量1, 代表泛型的变量2> { } 例如: // 定义了两个泛型类型的类 class Notepad<K,V>{ // 这两个变量的类型由外部决定 private K key ; private V value ; public K getKey(){ return this.key ; } public V getValue(){ return this.value ; } public void setKey(K key){ this.key = key ; } public void setValue(V value){ this.value = value ; } } public class GenericsDemo09{ public static void main(String args[]){ // 定义两个泛型类型的对象 Notepad<String,Integer> t = null ; // 里面的key为String,value为Integer t = new Notepad<String,Integer>() ; // 设置第一个内容 t.setKey("汤姆") ; // 设置第二个内容 t.setValue(20) ; // 取得信息 System.out.print("姓名;" + t.getKey()) ; // 取得信息 System.out.print(",年龄;" + t.getValue()) ; } }
-
使用泛型:即什么时候确定泛型。
在创建对象的时候确定泛型:
// 此时变量E就是String类型。 ArrayList<String> list = new ArrayList<String>();
-
自定义泛型类
public class MyGenericClass<MVP> { // 没有MVP类型,在这里代表 未知的一种数据类型 未来传递什么就是什么类型 private MVP mvp; public void setMVP(MVP mvp) { this.mvp = mvp; } public MVP getMVP() { return mvp; } } public class GenericClassDemo { public static void main(String[] args) { // 创建一个泛型为String的类 MyGenericClass<String> my = new MyGenericClass<String>(); // 调用setMVP my.setMVP("大胡子登登"); // 调用getMVP String mvp = my.getMVP(); System.out.println(mvp); // 创建一个泛型为Integer的类 MyGenericClass<Integer> my2 = new MyGenericClass<Integer>(); my2.setMVP(123); Integer mvp2 = my2.getMVP(); } }
02、定义和使用含有泛型的方法
泛型方法是在调用方法的时候指明泛型的具体类型。
-
定义格式:
修饰符 <代表泛型的变量> 返回值类型 方法名(参数){ } 例如: // 定义一个返回值类型是T、参数是泛型T对象的方法 public static <T> T show(T t){ System.out.println(t.getClass()); }
-
使用格式:调用方法时,确定泛型的类型。
public class fanxing { public static <T> T show(T t){ System.out.println(t.getClass()); } public static void main(String[] args) { String name = "张三"; int age = 21; // 利用 Class.forName 来指定泛型的具体类型 Object obj = this.show(Class.forName("com.qhj.test.User")); // 也可以直接使用官方定义的类对象 show(name);//class java.lang.String show(age);//class java.lang.Integer } }
定义泛型方法时,必须在返回值前边加一个 < T >,来声明这是一个泛型方法,持有一个泛型 T,然后才可以用泛型 T 作为方法的返回值。
Class< T > 的作用就是指明泛型的具体类型,而 Class < T > 类型的变量 t,可以用来创建泛型类的对象。
为什么要用变量 t 来创建对象呢?既然是泛型类型,就代表着我们不知道具体的类型是什么、也不知道构造方法如何,因此没有办法去 new 一个对象,但是可以利用变量 t 的 newInstance 方法去创建对象,也就是利用反射创建对象。
泛型方法要求的参数是 Class< T >,而 Class.forName() 方法的返回值也是 Class< T >,因此可以用 Class.forName() 作为参数。其中,forName() 方法中的参数是何种类型,返回的 Class< T > 就是何种类型。
在上述例子中,forName() 方法中传入的是 User 类的完整路径,因此返回的是 Class< User > 类型的对象。所以在调用泛型方法时,变量 t 的类型就是 Class< User >,因此泛型方法中的泛型 T 就被指明为 User,因此变量 obj 的类型就是 User。
当然,泛型方法不是仅仅可以有一个参数 Class< T >,可以根据需要添加其他参数。
由此可见,使用泛型方法有这么个好处:泛型类要在实例化的时候就指明类型,如果想换一种类型,就不得不重新 new 一次,不够灵活。而泛型方法可以在调用的时候指明类型,更加灵活。
03、定义和使用含有泛型的接口
-
定义格式:
修饰符 interface 接口名<代表泛型的变量> { } 例如: public interface USB<E> { public void service(E e); public E doService(); }
-
使用格式:
-
定义类时确定泛型的类型
public class KeyBoard implements USB<String>{ @Override public void service(String s) { } @Override public String doService() { return null; } }
-
始终不确定泛型的类型,直到创建对象时,确定泛型的类型
public class Mouse<E> implements USB<E>{ @Override public void service(E e) { } @Override public E doService() { return null; } } public class test { public static void main(String[] args) { Mouse<String> mouse = new Mouse<>(); mouse.service("张三"); mouse.doService(); } }
-
04、泛型的上下限
看下面这段代码,很明显是会报错的:
class A{}
class B extends A {}
// 如下两个方法不会报错
public static void funA(A a) {
// ...
}
public static void funB(B b) {
funA(b);
// ...
}
// 如下funD方法会报错
public static void funC(List<A> listA) {
// ...
}
public static void funD(List<B> listB) {
funC(listB); // Unresolved compilation problem: The method doPrint(List<A>) in the type test is not applicable for the arguments (List<B>)
// ...
}
那么这个错误是如何解决的呢?
泛型加入了类型参数的上下边界机制。<? extends A> 表示该类型参数可以是 A 及其子类。编译时擦除到类型 A,即用 A 类型代替类型参数。
这种方法可以解决遇到的问题,编译器知道类型参数的范围,如果传入的实体类型 B 是在这个范围内的话,就允许转换,这时只要一次类型转换就可以了,运行时会把对象当做 A 的实例来看待。
public static void funC(List<? extends A> listA) {
// ...
}
public static void funD(List<B> listB) {
funC(listB); // OK
// ...
}
-
泛型的上限:
格式: 类型名称 <? extends 类 > 对象名称
意义: 只能接收该类型及其子类// 此处泛型只能是数字类型 class Info<T extends Number>{ // 定义泛型变量 private T var ; public void setVar(T var){ this.var = var ; } public T getVar(){ return this.var ; } public String toString(){ // 直接打印 return this.var.toString() ; } } public class test{ public static void main(String args[]){ // 声明Integer的泛型对象 Info<Integer> i1 = new Info<Integer>() ; } }
-
泛型的下限:
格式: 类型名称 <? super 类 > 对象名称
意义: 只能接收该类型及其父类型class Info<T>{ // 定义泛型变量 private T var ; public void setVar(T var){ this.var = var ; } public T getVar(){ return this.var ; } public String toString(){ // 直接打印 return this.var.toString() ; } } public class GenericsTest{ public static void main(String args[]){ // 声明String的泛型对象 Info<String> i1 = new Info<String>() ; // 声明Object的泛型对象 Info<Object> i2 = new Info<Object>() ; i1.setVar("hello") ; i2.setVar(new Object()) ; fun(i1) ; fun(i2) ; } // 只能接收String或Object类型的泛型,String类的父类只有Object类 public static void fun(Info<? super String> temp){ System.out.print(temp + ", ") ; } }
-
通配符的一般使用
当使用泛型类或者接口时,传递的数据中,泛型类型不确定,可以通过通配符 <?> 表示。但是一旦使用泛型的通配符后,只能使用 Object 类中的共性方法,集合中元素自身方法无法使用。
泛型的通配符:不知道使用什么类型来接收的时候,此时可以使用 ?,? 表示未知通配符。
此时只能接收数据,不能往该集合中存储数据。
public static void main(String[] args) { Collection<Intger> list1 = new ArrayList<Integer>(); getElement(list1); Collection<String> list2 = new ArrayList<String>(); getElement(list2); } public static void getElement(Collection<?> coll){} //?代表可以接收任意类型
泛型不存在继承关系 Collection list = new ArrayList(); 这种是错误的。
总结:
- <?> 无限制通配符;
- <? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类;
- <? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类。
使用原则《Effictive Java》:为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限。
- 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>;
- 如果它表示一个 T 的消费者,就使用 < ? super T>;
- 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。
举个例子:
private <E extends Comparable<? super E>> E max(List<? extends E> e1) {
if (e1 == null){
return null;
}
// 迭代器返回的元素属于 E 的某个子类型(上限)
Iterator<? extends E> iterator = e1.iterator();
E result = iterator.next();
while (iterator.hasNext()){
E next = iterator.next();
if (next.compareTo(result) > 0){
result = next;
}
}
return result;
}
参数 E 的范围是:<E extends Comparable<? super E>>,我们分步查看:
- 要进行比较,所以 E 需要是可比较的类,因此需要 extends Comparable<…>;
- Comparable<? super E> 要对 E 进行比较,即 E 是消费者,所以需要用 super;
- 而参数 List<? extends E> 表示要操作的数据是 E 的子类的列表,指定上限,这样容器才足够大。
多个限制,可以使用符号 &:
public class Client {
// 工资低于2500元的上班族并且站立的乘客车票打8折
public static <T extends Staff & Passenger> void discount(T t){
if(t.getSalary() < 2500 && t.isStanding()){
System.out.println("恭喜你!您的车票打八折!");
}
}
public static void main(String[] args) {
discount(new Me());
}
}
05、泛型数组
泛型数组相关的声明:
// 编译错误,非法创建
List<String>[] list1 = new ArrayList<String>[10];
// 编译错误,需要强转类型
List<String>[] list2 = new ArrayList<?>[10];
// OK,但是会有警告
List<String>[] list3 = (List<String>[]) new ArrayList<?>[10];
// 编译错误,非法创建
List<?>[] list4 = new ArrayList<String>[10];
// OK
List<?>[] list5 = new ArrayList<?>[10];
// OK,但是会有警告
List<String>[] list6 = new ArrayList[10];
讨巧的使用场景:
public class GenericsTest{
public static void main(String args[]){
// 返回泛型数组
Integer i[] = fun1(1,2,3,4,5,6) ;
fun2(i) ;
}
// 接收可变参数
public static <T> T[] fun1(T...arg){
// 返回泛型数组
return arg ;
}
// 输出
public static <T> void fun2(T param[]){
System.out.print("接收泛型数组:") ;
for(T t : param){
System.out.print(t + "、") ; // 接收泛型数组:1、2、3、4、5、6、
}
}
}
14.3 使用泛型有什么好处?
引入泛型的意义在于:适用于多种数据类型执行相同的代码(代码复用)。
- 将运行时期的 ClassCastException,转移到了编译时期变成了编译失败。
- 泛型中的类型在使用时指定,不需要强制类型转换(类型安全,编译器会检查类型),避免了类型强转的麻烦。
public class fxDemo {
public static void main(String[] args) {
Collection<String> list = new ArrayList<>();
list.add("张三");
list.add("李四");
/**
* 当集合明确类型时,存放类型不一致就会编译报错
* list.add(2);
*/
/**
* 集合已经明确具体存放的元素类型,那么在使用迭代器的时候,
* 迭代器也同样会知道具体遍历元素类型
*/
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
小贴士:
泛型是数据类型的一部分,我们将类名与泛型合并一起看做数据类型。
14.4 深入理解泛型
01、如何理解 Java 中的泛型是伪泛型?
Java 泛型这个特性是从 JDK 1.5 开始加入的,因此为了兼容之前的版本,Java 泛型的实现采取了"伪泛型"
的策略,即 Java 在语法上支持泛型,但是在编译阶段会进行所谓的"类型擦除(Type Erasure)",将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。
泛型的类型擦除原则是:
- 消除类型参数声明,即删除 <> 及其包围的部分;
- 根据类型参数的上下界并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为 Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类);
- 为了保证类型安全,必要时插入强制类型转换代码;
- 自动产生"桥接方法"以保证擦除类型后的代码仍然具有泛型的"多态性"。
02、泛型中如何进行类型擦除?如何证明类型的擦除呢?
那么是如何进行泛型擦除的呢?
-
擦除类定义中的类型参数-无限制类型擦除
当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为 Object,即形如 < T > 和 < ? > 的类型参数都被替换为 Object。
-
擦除类定义中的类型参数-有限制类型擦除
当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如 < T extends Number> 和 < ? extends Number> 的类型参数被替换为 Number,< ? super Number> 被替换为 Object。
-
擦除方法定义中的类型参数-无限制类型擦除
擦除方法定义中的类型参数原则和擦除类定义中的无限制类型参数是一样的。
-
擦除方法定义中的参数类型-有限制类型擦除
擦除方法定义中的类型参数原则和擦除类定义中的有限制类型参数是一样的。
那么是如何证明类型的擦除的呢? -
原始类型相等
/** * @author QHJ * @date 2022/12/1 21:59 * @description: 泛型擦除-原始类型 */ public class Test2 { public static void main(String[] args) { ArrayList<String> list1 = new ArrayList<String>(); list1.add("abc"); ArrayList<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); // true } }
我们定义了两个 ArrayList 数组,一个是 ArrayList< String > 泛型类型的,只能存储字符串;一个是 ArrayList< Integer > 泛型类型的,只能存储整数。
通过 list1 对象和 list2 对象的 getClass() 方法获取他们的类的信息,最后发现结果为true。
说明泛型类型 String 和 Integer 都被擦除掉了,只剩下原始类型。
-
通过反射添加其他类型元素
/** * @author QHJ * @date 2022/12/1 22:02 * @description: 泛型擦除-反射 */ public class Test3 { public static void main(String[] args) throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); // 这样调用 add 方法只能存储整型,因为泛型类型的实例为 Integer list.add(1); list.getClass().getMethod("add", Object.class).invoke(list, "asd"); System.out.println(list.getClass()); // class java.util.ArrayList System.out.println(list.getClass().getMethod("add", Object.class)); // public boolean java.util.ArrayList.add(java.lang.Object) System.out.println(list.getClass().getMethod("add", Object.class).invoke(list, "asd")); // true for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); // 1 asd } } }
在程序中定义了一个 ArrayList 泛型类型实例化为 Integer 对象,如果直接调用 add() 方法,那么只能存储整数数据,不过当我们利用反射调用 add() 方法的时候,却可以存储字符串,这说明了 Integer 泛型实例在编译之后被擦除掉了,只保留了原始类型。
03、如何理解类型擦除后保留的原始类型?
原始类型就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。
无论何时定义一个泛型,相应的原始类型都会被自动提供,然后将类型变量擦除,并使用其限定类型(无限定的变量用 Object)替换。
-
- 原始类型 Object
class Pair<T> { private T value; public T getValue() { return value; } public void setValue(T value) { this.value = value; } }
Pair 的原始类型为:
class Pair { private Object value; public Object getValue() { return value; } public void setValue(Object value) { this.value = value; } }
因为在 Pair< T > 中,T 是一个无限定的类型变量,所以用 Object 替换,其结果就是一个普通的类,如同泛型加入 Java 语言之前已经实现的样子。在程序中可以包含不同类型的 Pair,比如 Pair< String > 或 Pair< Integer >,但是擦除类型后它们就成为原始的 Pair 类型了,原始类型都是 Object。
由此可见,ArrayList 被擦除类型后,原始类型也变为 Object,所以通过反射就可以存储字符串了。
-
- 原始类型-类型变量边界
如果类型变量有限定,那么原始类型就用第一个边界的类型变量类替换。
比如:如果 Pair 这样声明的话,原始类型就是 Comparable:
public class Pair<T extends Comparable> {}
一定要区分原始类型和泛型变量的类型。
在调用泛型方法时,可以指定泛型,也可以不指定泛型:
- 在不指定泛型的情况下,泛型变量的类型为该方法中的几种类型的同一父级的最小级,直到 Object;
- 在指定泛型的情况下,该方法的几种类型必须是该泛型的实例的类型或者其子类。
/**
* @author QHJ
* @date 2022/12/5 20:39
* @description: 原始类型
*/
public class Test4 {
public static void main(String[] args) {
Test4 test4 = new Test4();
// 不指定泛型
test4.add(1, 2); // 这两个参数都是 Integer,所以T为Integer类型
test4.add(1, 1.2); // 这两个参数一个是Integer,一个是Float,所以取同一父级的最小级,T为Number
test4.add(1, "asd"); // 这两个参数一个是Integer,一个是String,所以取同一父级的最小级,T为Object
// 指定泛型
int a = test4.<Integer>add(1, 2); // 指定了Integer,所以只能为Integer类型或者其子类
// int b = test4.<Integer>add(1, 1.2); // 编译错误,指定了Integer,就不能传Float类型的参数
Number c = test4.<Number>add(1, 2.2); // 指定为Number,所以可以为Integer和Float
}
/**
* 定义一个泛型方法
*
* @param x
* @param y
* @param <T>
* @return
*/
public <T> T add(T x, T y) {
return y;
}
}
其实在泛型类中,如果不指定泛型,泛型就是 Object。就像 ArrayList 中,如果不指定泛型,那么这个集合中可以存储任意的对象:
ArrayList list = new ArrayList();
list.add(1);
list.add("asd");
list.add(2.2);
list.add(new Date());
04、如何理解泛型的编译期检查?
既然说类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数时会报错呢:
ArrayList<String> list = new ArrayList<>();
list.add("abc");
list.add(123); // 编译错误
不是说 String 会在编译的时候变为 Object 类型吗?
这是因为 Java 编译器是通过先检查代码中泛型的类型,然后再进行类型擦除、最后再进行编译的。
在上面的三行代码中,使用 add() 方法添加一个整型,在 IDEA 中会直接报错,说明这就是在编译之前检查的。如果是在编译之后检查,类型擦除后,原始类型为 Object,是应该允许任意引用类型添加的。可实际上并不是这样的,这就恰恰说明了关于泛型变量的使用是会在编译之前检查的。
那么,这个类型检查是针对谁的呢?我们先看看参数化类型和原始类型的兼容(以 ArrayList 为例):
// 以前的写法
ArrayList list1 = new ArrayList();
// 现在的写法
ArrayList<String> list2 = new ArrayList<String>();
如果是与以前的代码兼容,各种引用传值之间,必然会出现以下情况:
// 第一种情况
ArrayList<String> list3 = new ArrayList<>();
// 第二种情况
ArrayList list4 = new ArrayList<String>();
这样是没有错误的,不过会有个编译时警告。第一种情况可以实现与完全使用泛型参数一样的效果,第二种则没有。
因为类型检查就是编译时完成的,new ArrayList() 只是在内存中开辟了一个存储空间,可以存储任何类型对象,而真正设计类型检查的是它的引用。因为我们是使用它引用 list3 来调用它的方法,比如说调用 add() 方法,所以 list3 引用能完成泛型类型的检查。而引用 list4 没有使用泛型,所以不行。
/**
* @author QHJ
* @date 2022/12/5 22:14
* @description: 编译期检查
*/
public class Test6 {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<>();
list1.add("1"); // 编译通过
// list1.add(1); // 编译错误
String str1 = list1.get(0); // 返回的类型就是String
ArrayList list2 = new ArrayList<String>();
list2.add("1"); // 编译通过
list2.add(1); // 编译通过
Object object = list2.get(0); // 返回的类型就是Object
new ArrayList<String>().add("1"); // 编译通过
// new ArrayList<String>().add(1); // 编译错误
String str2 = new ArrayList<String>().get(0); // 返回类型就是String
}
}
由此可以看出,类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用的方法进行类型检查,而无关它真正引用的对象。
泛型中参数类型为什么不考虑继承关系?
在 Java 中,像这种形式的引用传递是不允许的:
// 第一种情况
ArrayList<String> list5 = new ArrayList<Object>(); // 编译错误
// 第二种情况
ArrayList<Object> list6 = new ArrayList<String>(); // 编译错误
-
第一种情况
将第一种情况的代码扩展成这种形式:
ArrayList<Object> list5 = new ArrayList<Object>(); list5.add(new Object()); list5.add(new Object()); ArrayList<String> list6 = list5; // 编译错误
显然,在第四行代码中会出现编译错误。我们先来假设它编译没错,那么当我们使用 list6 引用调用 get() 方法取值的时候,返回的都是 String 类型的对象(因为在 list6 引用是 String 类型),可是它里面实际上已经存放的是 Object 对象,这样就会有 ClassCastException 了。所以为了避免这种极易出现的错误,Java 不允许进行这样的引用传递(这也是泛型出现的原因,就是为了解决类型转换的问题,我们不能违背它的初衷)。
-
第二种情况
将第二种情况的代码扩展成这种形式:
ArrayList<String> list7 = new ArrayList<String>(); list5.add(new String()); list5.add(new String()); ArrayList<Object> list8 = list7; // 编译错误
这种情况下在使用 list8 取值的时候不会出现 ClassCastException,因为是从 String 类型转换为 Object。
可是,这样做又有什么意义呢?——泛型的出现,就是为了解决类型转换的问题。
我们使用了泛型,到头来还需要自己进行强转,这就违背了泛型设计的初衷,所以 Java 不允许这么干。另外,如果使用 list8 进行 add() 操作时,取值的时候怎么知道取出来的到底是 String 类型还是 Object 类型呢?
所以,要格外注意泛型中的引用传递的问题。
05、如何理解泛型的多态?泛型的桥接方法?
类型擦除会造成多态的冲突,JVM 解决方法就是桥接方法。
有一个泛型类:
/**
* @author QHJ
* @date 2022/12/16 10:16
* @Description: 泛型类
*/
public class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
再来一个子类继承它:
/**
* @author QHJ
* @date 2022/12/16 10:16
* @Description: 子类
*/
public class DateInter extends Pair<Date> {
@Override
public Date getValue() {
return super.getValue();
}
@Override
public void setValue(Date value) {
super.setValue(value);
}
}
在这个子类中,我们设定父类的泛型类型为 Pair< Date >,然后在子类中覆盖了父类的两个方法。
其实我的原意是这样的:将父类的泛型类型限定为 Date,那么父类里面两个方法的参数类型都是 Date,父类就是这样的:
public Date getValue() {
return value;
}
public void setValue(Date value) {
this.value = value;
}
所以,在子类中重写这两个方法是一点问题都没有的。并且,从 @Override 标签中也可以看到,是没问题的。
但是,实际上,类型擦除后,父类的泛型类型全部变为了原始类型 Object,所以父类编译之后会变成这样:
class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
再看子类重写的两个方法的类型:
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
首先是 setValue() 方法,父类的类型是 Object,而子类的类型是 Date,参数类型不一样。这种情况如果在普通的继承关系中,是不会重写的,而是重载。
但是在测试中会发现,如果是重载,那么一个参数是 Object 类型,一个参数是 Date 类型,但是参数是 Object 类型时会报错:
/**
* @author QHJ
* @date 2022/12/16 10:19
* @Description: 测试类
*/
public class Test7 {
public static void main(String[] args) {
DateInter dateInter = new DateInter();
dateInter.setValue(new Date());
// dateInter.setValue(new Object()); // error
}
}
也就是说,根本没有这样的一个子类继承自父类的 Object 类型参数的方法。所以,确实是重写了,而不是重载。
为什么会这样呢?
原因是这样的,我们传入父类的泛型类型是 Date,Pair< Date >,我们的本意是将泛型类变为这样:
class Pair {
private Date value;
public Date getValue() {
return value;
}
public void setValue(Date value) {
this.value = value;
}
}
然后再在子类中重写参数类型为 Date 的那两个方法,实现继承中的多态。
可是由于种种原因,虚拟机并不能将泛型类型变为 Date,只能将类型擦除掉,变为原始类型 Object。这样的话,就违背了我的本意(我的本意是进行重写、实现多态,这样的话,就变成重载了)。这样,类型擦除就和多态有了冲突。
JVM 知道我的本意吗?它是知道的。那它能直接实现吗?不能。如果真的不能的话,怎么去重写我想要的 Date 类型参数的方法呢?
JVM就采用了一种特殊的方法—桥方法。
首先,我们用 javap -c className 的方式反编译下 DateInter 子类的字节码,结果如下:
class com.tao.test.DateInter extends com.tao.test.Pair<java.util.Date> {
com.tao.test.DateInter();
Code:
0: aload_0
1: invokespecial #8 // Method com/tao/test/Pair."<init>":()V
4: return
public void setValue(java.util.Date); // 我重写的setValue方法
Code:
0: aload_0
1: aload_1
2: invokespecial #16 // Method com/tao/test/Pair.setValue:(Ljava/lang/Object;)V
5: return
public java.util.Date getValue(); //我们重写的getValue方法
Code:
0: aload_0
1: invokespecial #23 // Method com/tao/test/Pair.getValue:()Ljava/lang/Object;
4: checkcast #26 // class java/util/Date
7: areturn
public java.lang.Object getValue(); // 编译时由编译器生成的桥方法
Code:
0: aload_0
1: invokevirtual #28 // Method getValue:()Ljava/util/Date 去调用我重写的getValue方法;
4: areturn
public void setValue(java.lang.Object); // 编译时由编译器生成的桥方法
Code:
0: aload_0
1: aload_1
2: checkcast #26 // class java/util/Date
5: invokevirtual #30 // Method setValue:(Ljava/util/Date; 去调用我们重写的setValue方法)V
8: return
}
从编译的结果来看,我的本意是重写 setValue() 和 getValue() 方法的子类,但是结果中竟然有4个方法!
其实不用惊奇,最后的两个方法,就是编译器自己生成的桥方法。我们可以看到,桥方法的参数类型都是 Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而打在我们自己定义的 setvalue() 和 getValue() 方法上面的 @Override 只不过是假象而已。
而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。
所以,虚拟机巧妙的使用了桥方法,来解决了类型擦除和多态的冲突。
不过,要提到一点,这里面的 setValue() 和 getValue() 这两个桥方法的意义又有不同。setValue() 方法是为了解决类型擦除与多态之间的冲突。
而 getValue() 却有普遍的意义,怎么说呢,如果这是一个普通的继承关系,那么父类的 getValue() 方法如下:
public Object getValue() {
return super.getValue();
}
而子类重写的方法是:
public Date getValue() {
return super.getValue();
}
并且在使用时也不会报错:
// 父类
Object object = dateInter.getValue();
// 子类
Date value = dateInter.getValue();
其实这在普通的类继承中也是普遍存在的重写,这就是协变。
并且,还有一点也许会有疑问,子类中的桥方法 Object getValue() 和 Date getValue() 是同时存在的,可是如果是常规的两个方法,他们的方法签名是一样的,也就是说虚拟机根本不能分别这两个方法。
如果是我们自己编写 Java 代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过参数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情,然后交给虚拟器去区别。
06、如何理解基本类型不能作为泛型类型?
比如:没有 ArrayList< int >,只有 ArrayList< Integer >,这是为什么?
因为当类型擦除后,ArrayList 的原始类型变为 Object,但是 Object 类型不能存储 int 值,只能引用 Integer 的值。
另外需要注意的是,我们能够使用 list.add(1) 是因为 Java 基础类型的自动装箱拆箱操作。
07、如何理解泛型类型不能实例化?
不能实例化泛型类型,这本质上是由于类型擦除决定的:
T test = new T(); // ERROR
看上面一行代码,在编译时会报错。
这是因为在 Java 编译期无法确定泛型参数化类型,也就找不到对应的类字节码文件,所以就报错了。但是,因为 T 被擦除为 Object,如果可以 new T() 的话,就变成了 new Object(),失去了泛型的本质了。
如果我们确实需要实例化一个泛型,应该如何做呢?
可以通过反射来实现:
static <T> T newTclass (Class < T > clazz) throws InstantiationException, IllegalAccessException {
T obj = clazz.newInstance();
return obj;
}
08、泛型数组:能不能采用具体的泛型类型进行初始化?
Oracle 官网提供的:
List<String>[] lsa = new List<String>[10]; // Not really allowed.
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Unsound, but passes run time store check
String s = lsa[1].get(0); // Run-time error ClassCastException.
由于 JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是搬起石头砸自己的脚。
而对于下面的代码来说是成立的:
List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Correct.
Integer i = (Integer) lsa[1].get(0); // OK
所以说采用通配符的方式初始化泛型数组是允许的
,因为对于通配符的方式最后取出数据是要做显式类型转换的,符合预期逻辑。
综上所述:Java 的泛型数组初始化时数组类型不能是具体的泛型类型,只能是通配符的形式
,因为具体类型会导致可存入任意类型对象,在取出时会发生类型转换异常,会与泛型的设计思想冲突,而通配符形式本来就需要自己强转,符合预期。
更进一步地:
List<String>[] list11 = new ArrayList<String>[10]; //编译错误,非法创建
List<String>[] list12 = new ArrayList<?>[10]; //编译错误,需要强转类型
List<String>[] list13 = (List<String>[]) new ArrayList<?>[10]; //OK,但是会有警告
List<?>[] list14 = new ArrayList<String>[10]; //编译错误,非法创建
List<?>[] list15 = new ArrayList<?>[10]; //OK
List<String>[] list6 = new ArrayList[10]; //OK,但是会有警告
因为在 Java 中是不能创建一个确切的泛型类型的数组的,除非是采用通配符的方式且要做显式类型转换才可以。
09、泛型数组:如何正确的初始化泛型数组实例?
我们无论通过 new ArrayList[10] 的形式还是通过泛型通配符的形式初始化泛型数组实例都是存在警告的,也就是说仅仅语法合格,运行时潜在的风险需要我们自己来承担,因此那些方式初始化泛型数组都不是最优雅的方式。
我们在使用到泛型数组的场景下应该尽量使用列表集合替换,此外也可以通过使用 java.lang.reflect.Array.newInstance(Class<T> componentType, int length)
方法来创建一个具有指定类型和维度的数组:
public class ArrayWithTypeToken<T> {
private T[] array;
public ArrayWithTypeToken(Class<T> type, int size) {
array = (T[]) Array.newInstance(type, size);
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) {
return array[index];
}
public T[] create() {
return array;
}
}
//...
ArrayWithTypeToken<Integer> arrayToken = new ArrayWithTypeToken<Integer>(Integer.class, 100);
Integer[] array = arrayToken.create();
所以使用反射来初始化泛型数组算是最优雅实现,因为泛型类型 T 在运行时才能被确定下来,我们能创建泛型数组也必然是在 Java 运行时想办法,而运行时能起作用的技术最好的就是反射了。
10、如何理解泛型类中的静态方法和静态变量?
泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数的。
public class Test2<T> {
public static T one; // 编译错误
public static T show(T one){ // 编译错误
return null;
}
}
因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型呢?所以是错误的。
但是要区分这种情况:
public class Test2<T> {
public static <T>T show(T one){ // 这是正确的
return null;
}
}
因为这是一个泛型方法,在泛型方法中使用的 T 是自己在方法中定义的 T,而不是泛型类中的 T。
11、如何理解异常中使用泛型?
不能抛出也不能捕获泛型类的对象。
事实上,泛型类扩展 Throwable 都不合法:
public class Problem<T> extends Exception {
}
为什么不能扩展 Throwable,因为异常都是在运行时捕获和抛出的,而在编译的时候,泛型信息全部会被擦除掉。那么,假设上面的编译可行,看下面的定义:
try{
} catch(Problem<Integer> e1) {
} catch(Problem<Number> e2) {
}
这个是不行的。
12、如何获取泛型的参数类型?
既然类型被擦除了,那么如何获取泛型的参数类型呢?可以通过反射(java.lang.reflect.Type)获取泛型。
java.lang.reflect.Type
是 Java 中所有类型的公共高级接口,代表了 Java 中的所有类型,Type 体系中的类型包括:数组类型(GenericArrayType)、参数化类型(ParameterizedType)、类型变量(TypeVariable)、通配符类型(WildcardType)、原始类型(Class)、基本类型(Class),这些类型都实现 Type 类型。
public class GenericType<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public static void main(String[] args) {
GenericType<String> genericType = new GenericType<String>() {};
Type superclass = genericType.getClass().getGenericSuperclass();
System.out.println(superclass); // class java.lang.Object
// getActualTypeArguments 返回确切的泛型参数, 如Map<String, Integer>返回[String, Integer]
Type type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
System.out.println(type);// class java.lang.String
}
}
其中,ParameterizedType:
public interface ParameterizedType extends Type {
// 返回确切的泛型参数,比如Map<String, Integer>返回[String, Integer]
Type[] getActualTypeArguments();
// 返回当前class或interface声明的类型,比如List<?>返回List
Type getRawType();
// 返回所属类型,比如:当前类型为O<T>.I<S>, 则返回O<T>,顶级类型将返回null
Type getOwnerType();
}