聊聊Java中的泛型
文章目录
参考资料
- 秦小波老师的《编写高质量Java代码》
- 泛型就这么简单
- java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一
- Java泛型类型擦除以及类型擦除带来的问题
下文如有错漏之处,敬请指正
一、概述
1. 泛型的定义
1.1 定义
官方:Java泛型是Jdk1.5中引入的一个新特性,其本质是参数化类型,也就是所操作的数据类型被指定为一个参数(type parameter),这种参数类型可以用在类、接口和方法的创建,分别称为泛型类、泛型接口和泛型方法。
所谓泛型指的就是在类定义的时候不设置类中属性或方法(返回值及其参数)的类型,使用泛型参数进行替代,当类使用的时候再定义具体类型。
泛型参数:T,其中T可以是(写)任意字符(a-ZA-Z_)
泛型的出现是为了解决类型转换的问题。
1.2 常见形式
-
泛型类
// 定义 public class Message<T> { private T type; } // 使用 public static void main(String[] args) { // 泛型参数的类型为 String Message<String> stringMessage=new Message<>(); // 泛型参数的类型为 Integer Message<Integer>integerMessage=new Message<>(); }
-
泛型接口
// 定义 interface IMessage<T>{ public void send(T t); } // 使用 class MessageImpl implements IMessage<String>{ @Override public void pring(String t) { System.out.println(t); } } public static void main(String[] args) { IMessage message=new MessageImpl(); message.print("hello world"); }
-
泛型方法
// 定义 class Message<T> { private T type; public Message(T type) { this.type = type; } // 泛型方法 // <T> 声明泛型参数 // T 返回的类型 // 这里的T与Message<T>的T没有联系,这里的T也可以用其他字符(如E)来替代: // private <E> E sendMessage(E content){ // return (E) (type+":"+content); // } private <T> T sendMessage(T content){ return (T) (type+":"+content); } // 使用 public static void main(String[] args) { Message<String> message=new Message<>("String"); System.out.println(message.sendMessage("Hello World!")); // 输出结果:String:Hello World! }
2. 为什么需要泛型
如果没有泛型:
-
不能限制元素类型
期望
Collection
集合存放的全部是A对象
,但实际存放B对象
也不会有任何语法错误,因为Collection
集合对元素的类型是没有任何限制的。 -
读元素时要向下转型
由于
Collection
集合不知道元素的实际类型是什么,仅仅知道是Object
。在执行写入操作时需要向上转型,在读取数据后需要向下转型。
向上转型:子类转为父类 Parent parent=new Son(); (向上转型可以不用显式转型)
向下转型:父类转为子类 Son son=(Son) parent; (向下转型存在风险,只有父类是由子类向上转型后再向下转型是安全的)
有了泛型以后:
- 代码更加简洁【不用强制类型转换】
- 程序更加健壮【只要编译时期没有警告,那么运行时期就不会出现ClassCastException异常】
- 可读性和稳定性【在编写集合的时候,限定了元素的类型】
3. 泛型的优点
-
减少强制类型转换
-
规范集合的元素类型,提高代码的安全性和可读性。
4. 泛型的使用
4.1 泛型类
class Car {
private String brand;
public Car(String brand) {
this.brand = brand;
}
@Override
public String toString() {
return "Car{" +
"brand='" + brand + '\'' +
'}';
}
}
class Plane {
private String brand;
public Plane(String brand) {
this.brand = brand;
}
@Override
public String toString() {
return "Plane{" +
"brand='" + brand + '\'' +
'}';
}
}
// 使用泛型定义交通工具的类型为泛型参数T
public class Vehicle<T> {
private List<T> vehicleLists;
public Vehicle() {
this.vehicleLists = new ArrayList<>();
}
public void add(T vehicle) {
vehicleLists.add(vehicle);
}
public void showAll() {
for (T t : vehicleLists) {
System.out.println(t);
}
}
public static void main(String[] args) {
// 定义交通工具的实际类型为汽车
Vehicle<Car> carVehicle = new Vehicle<>();
// 增加交通工具
carVehicle.add(new Car("Benz"));
carVehicle.add(new Car("Tesla"));
// 如果传入的参数不是Car类型,则会编译错误
// carVehicle.add(new Plane("Virgin"));
// 显示已有的交通工具
carVehicle.showAll();
/**
* 输出结果:
* Car{brand='Benz'}
* Car{brand='Tesla'}
*/
}
}
泛型类的实际类型在类实例化的时候确定。
4.2 泛型接口
interface IMessage<T>{
public void send(T t);
}
对于泛型接口的子类有两种实现方法:
-
在子类定义的时候继续使用泛型,在使用时设置实际类型
class MessageImpl<T> implements IMessage<T> { @Override public void send(T t) { System.out.println(t); } } public static void main(String[] args) { // 子类定义时设置实际类型 IMessage<String> iMessage=new MessageImpl<>(); iMessage.send("Hello World"); // Hello World }
-
在子类定义的时候明确类型
class MessageImpl implements IMessage<String>{ @Override public void send(String t) { System.out.println(t); } } public static void main(String[] args) { IMessage iMessage = new MessageImpl(); iMessage.send("Hello World"); // Hello World }
泛型接口的实际类型在子类定义时确定或是在子类实例化时确定。
4.3 泛型方法
public class Demo {
/**
* 泛型方法
*
* @param args 形参
* @param <T> 声明泛型参数
* @return T 返回类型
*/
public static <T> T[] func(T... args) {
return args;
}
public static void main(String[] args) {
// 设置泛型方法的形参和返回类型为 Integer
Integer[] data = func(1, 2, 3, 4);
for (int i : data) {
System.out.print(i + " ");
}
/**
* 输出结果:
* 1 2 3 4
*/
System.out.println();
// 设置泛型方法的形参和返回类型为 String
String[] data2 = func("Hello", "World", "!");
for (String i : data2) {
System.out.print(i + " ");
}
/**
* 输出结果:
* Hello World !
*/
}
}
泛型方法的实际类型在方法运行时确定。
4.4 泛型实际类型的确定时机
-
泛型类的实际类型在类实例化的时候确定。
-
泛型接口的实际类型在子类定义时确定或是在子类实例化时确定。
-
泛型方法的实际类型在方法运行时确定。
二、泛型的扩展
2.1 Java泛型的擦除与补偿
1、泛型参数的擦除与补偿
擦除:
带有泛型参数的JAVA
程序被编译时,会按照泛型参数的擦除规则将泛型参数擦除为原始类型。
如List<String>
会被擦除为原始类型List
。
补偿:
JAVA
程序运行时,当获取某个带有泛型参数的变量,JVM会将该变量的原始类型转为其实际类型,即实现自动类型转换。
例如:
Pair.java
import java.io.Serializable;
public class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
// 泛型方法
public static <E> E getE(E e){
return e;
}
public static void main(String[] args) {
Pair<String> pair=new Pair<>();
pair.setValue("10");
String a=pair.getValue();
int b=getE(10);
}
}
反编译Pair.class:
通过反编译字节码文件,我们可以得出带有泛型参数的变量会被擦除成原始类型,并且会检查变量的实际类型用来判断是否可以进行强制类型转换(即类型安全),之后程序在运行时,JVM
会自动给带有泛型参数的变量增加类型转换的指令,将原始类型转为实际类型。
2、泛型参数的擦除规则
- 若参数类型无限定
<T>
,那么原始类型就是Object
,即所有出现T
的地方都用Object
替换。- T obj 擦除后的原始类型为Object obj
- List、List、List擦除后的原始类型为List
- List[]擦除后的原始类型为List[]。
- 若参数类型有限定
<T extends E >
、<T super E >
,那么原始类型就是E
,即所有出现T
的地方都用E
替换.- List擦除后的原始类型为List。
- List擦除后的原始类型为List。
- 若参数类型有多个限定List< T exnteds XClass1 & XClass2 >,那么使用第一个边界类型XClass1作为原始类型。
例子1:
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0);
}
经过编译器的擦除后,上面的代码与下面的代码是一致的
public static void main(String[] args) {
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
}
例子2:
public class Example{
// listMethod接收List参数,并进行重载
public void listMethod(List<String> strList){} // 擦除后的类型是 listMethod(List strList)
public void listMethod(List<Integer> intList){} // 擦除后的类型是 listMethod(List intList)
}
如上程序无法编译,编译时报错信息如下:
java: name clash: listMethod(java.util.List<java.lang.Integer>) and listMethod(java.util.List<java.lang.String>) have the same erasure
此错误的意思是说listMethod(List<Integer>)
方法在编译时泛型参数擦除后的方法是listMethod(List intList)
,它与另外一个方法参数类型重复,不符合重载,通俗地说就是方法签名重复了。
3、泛型参数擦除的原因
Java
程序经过编译后的字节码文件中已经没有泛型的任何信息了,也就是说一个泛型类和一个普通类在经过编译后都指向了同一字节码。比如Foo<T>
类,经过编译后将只有一份Foo.class
类,不管是Foo<String>
还是Foo<Integer>
引用的都是同一字节码。
Java
之所以如此处理,有如下原因:
-
避免类膨胀
C++
的泛型实现采用不同的实现类,而这会造成类膨胀问题,为此Java
采用泛型参数擦除来解决此问题。 -
避免JVM的大换血
C++
的泛型在运行期也有效,而Java
的泛型在编译期就被擦除了,如果JVM
也把泛型延续到运行期,那么JVM
就需要进行大量的重构工作了。 -
版本兼容
在编译期擦除可以更好地支持原生类型(RawType),
JDK1.5
提出了泛型这个概念,但是JDK1.5
以前是没有泛型的,也就是泛型是需要兼容JDK1.5
以下的版本。
2.2 泛型参数擦除所带来的问题
因为种种原因,Java
不能实现真正的泛型,只能使用泛型参数擦除来实现伪泛型,这样虽然不会有类型膨胀问题(真泛型实现是具有不同的实现类),但是也引起来许多新问题,所以,SUN公司对这些问题做出了种种限制,避免我们发生各种错误。
1、强制类型转换问题
Q:既然所有的泛型参数在编译后都会被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢?
A:在编译器编译完程序后,当程序运行时,JVM
会自动给带有泛型参数的变量增加类型转换的指令,将原始类型转为实际类型。
例如:
一个泛型类Human
:
class Human<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
泛型参数擦除后,Human
的原始类型:
class Human {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
假设value
实际类型是Date
,在程序运行时期,当value
被获取时,JVM
就会把getValue()
的return value;
转换成return (Date) value
,同理,也会把this.value = value;
转换为this.value = (Date)value;
2、引用传递的问题
Q1:既然说泛型参数在编译的时候会被擦除掉,那如何确保我们之前定义的泛型参数被使用(即限定泛型的类型)了呢?
A1:Java
编译器通过先检查代码中泛型参数的实际类型(字节码指令是checkcast
)即类型安全检查,如果类型安全就进行泛型参数擦除,否则编译错误。
例如:
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("123");
list.add(123);// 编译错误
}
在上面的程序中,使用add
方法添加一个整型,在IDE中,直接会报错,说明泛型参数在擦除前会先进行类型安全检查。
Q2:类型检查是针对谁的呢?
A2:类型检查就是针对引用的,而无关它真正引用的实例。
例如:
public class Test {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList();
list1.add("1"); // 编译通过
list1.add(1); // 编译错误
String str1 = list1.get(0); // 返回类型是String (get()的返回值会被补偿,即由JVM进行强制类型转换)
ArrayList list2 = new ArrayList<String>();
list2.add("1"); // 编译通过
list2.add(1); // 编译通过
Object object = list2.get(0); // 返回类型是Object
new ArrayList<String>().add("11"); // 编译通过
new ArrayList<String>().add(22); // 编译错误
}
}
在Java
中,像下面形式的引用传递是不允许的:
ArrayList<String> list1 = new ArrayList<Object>(); // 编译错误
ArrayList<Object> list2 = new ArrayList<String>(); // 编译错误
我们先看第一种情况,将第一种情况拓展成下面的形式:
ArrayList<Object> list1 = new ArrayList<Object>();
list1.add(new Object());
list1.add(new Object());
ArrayList<String> list2 = list1; // 编译错误
实际上,在第4行代码的时候,就会有编译错误。那么,我们先假设它编译没错。当我们使用list2
引用用get()
方法取值的时候,返回的都是String
类型的对象,可是它里面实际上已经被我们存放了Object
类型的对象,这样就会有ClassCastException
了。所以为了避免这种极易出现的错误,Java不允许进行这样的引用传递。(泛型的出现就是为了解决类型转换的问题)。
再看第二种情况,将第二种情况拓展成下面的形式:
ArrayList<String> list1 = new ArrayList<String>();
list1.add(new String());
list1.add(new String());
ArrayList<Object> list2 = list1; // 编译错误
没错,这样的情况比第一种情况好的多,最起码,在我们用list2
取值的时候不会出现ClassCastException
,因为是从String
转换为Object
,Object
是一切类的父类。可是,这样做有什么意义呢?泛型的出现就是为了解决类型转换的问题。我们使用了泛型,到头来,进行读写操作还是要自己强转,违背了泛型设计的初衷。所以Java
也不允许这么干。再说,如果往list2
往里面add()
新的对象,那么到时候取得时候,你怎么知道取出来的到底是String
类型,还是Object
类型的呢?
所以,要格外注意泛型中的引用传递的问题,Java
为了保证运行期的安全性,必须保证泛型参数是固定的,所以它不允许一个泛型参数可以同时包含两种类型,即使是父子类关系也不行。
3、继承中方法重写的问题
Q:子类继承了泛型类且同时指定了泛型参数的具体类型,然后重写了含有泛型参数的方法。由于编译时泛型参数被擦除,导致原本的重写变成了重载,那么在运行时编译器就无法确定执行哪一个方法,如何解决多态冲突呢?
A:编译器生成了桥接方法来避免泛型参数擦除与多态发生冲突。
JAVA重写和重载的区别:
重写 | 重载 | |
---|---|---|
实现方式 | 子类对父类允许的方法进行重新编写 | 对一个类里对同名的方法进行重新编写 |
多态确定 | 运行时期 | 编译时期 |
参数列表 | 必须相同 | 必须不同 |
返回值类型 | 父类或其子类 | 没有要求 |
- 重写是子类对父类允许的方法进行重新编写,重载是对一个类里对同名的方法进行重新编写
- 重写实现的是运行时的多态,而重载实现的是编译时的多态。
- 重写的方法参数列表必须相同;而重载的方法参数列表必须不同。
- 重写的方法返回值类型只能是父类类型或其子类类型,而重载的方法对返回值类型没有要求。
例子:
一个泛型类:
public class Utility<T>{
public void doThings(T utility){
System.out.println(utility.getClass()+"---do……");
}
}
编译后(即泛型参数被擦除后)的泛型类:
public class Utility{
public void doThings(Object utility){
System.out.println(utility.getClass()+"---do……");
}
}
一个类继承了泛型类,并重写了doThings()
方法:
public class StringUtility extends Utility<String> {
@Override
public void doThings(String utility){
System.out.println(utility.getClass()+"---do……");
}
}
// 父类的方法:
public void doThings(Object utility){
System.out.println(utility.getClass()+"---do……");
}
// 子类重写父类的方法:
@Override
public void doThings(String utility){
System.out.println(utility.getClass()+"---do……");
}
观察上面的代码,我们发现子类方法的重写不符合方法重写的规则(重写规则要求方法参数列表必须相同),父类是Object
,子类是String
,自然是不符合重写,但是符合重载的规则,如果是重载的话,那么子类就存在两个doThings
方法,一个参数是Object
,一个参数是String
,我们来验证一下。
public static void main(String[] args) {
StringUtility stringUtility=new StringUtility();
stringUtility.doThings(new String()); // 编译通过
stringUtility.doThings(new Date()); // 编译错误
}
结果不是重载,竟然是重写!可实际又不符合重写的规则,那究竟是怎样实现的呢?
答案就是:Java
编译器自动生成了一个桥接方法来确保子类型按预期工作。
public class StringUtility extends Utility<String> {
/**
* 编译器自动生成的桥接方法,通过javap -c StringUtility.class 就能看到
* public void doThings(java.lang.Object);
* Code:
* 0: aload_0
* 1: aload_1
* 2: checkcast #11 // class java/lang/String
* 5: invokevirtual #12 // Method doThings:(Ljava/lang/String;)V
* 8: return
*/
// 等同于
// public void doThings(Object utility){
// doThings((String) utility)
// }
@Override
public void doThings(String utility){
System.out.println(utility.getClass()+"---do……");
}
}
这个桥接方法实际上就是对父类中dothings(Object utility)
的重写,并委托给原始dothings(String utility)
方法,以此来避免泛型参数擦除与多态发生冲突。
4、不能用instanceof判断泛型参数
List<String> list=new ArrayList<String>();
System.out.println(list instanceof List<String>);// 编译错误
以上代码不能通过编译,因为泛型参数擦除后,List<String>
不存在泛型信息String
了。
5、泛型类中static关键字的问题
Q:为什么泛型类中的静态方法和静态属性不可以使用泛型参数?
A:因为泛型类的实际类型是在类实例化的时候才确定的,而用static
修饰的变量是在类加载后就被初始化了,不需要使用实例对象来调用,那么连对象都没有创建,那又如何确定这个泛型类的实际类型呢??
public class Test<T> {
private static T one; //编译错误
private static T show(T one){ //编译错误
return null;
}
}
静态泛型方法:
public class Test<T> {
public static <E> E show(E one){ // 编译正确
return null;
}
}
这是一个静态泛型方法,该泛型方法中的<E>
是声明泛型参数,而后面的不带尖括号的 E
是方法的返回值类型。
泛型方法的实际类型由方法运行时决定,因此编译正确。
6、不能在泛型类中初始化运行期不安全的泛型变量
public class Foo<T> {
public T t = new T();// 编译错误
public T[] tArray = new T[5];// 编译错误
public List<T> tList = new ArrayList<>();// 编译通过
}
上面代码中t
、tArray
变量经过泛型参数擦除后原始类型都是Object
,按理来说不应该编译错误,但由于Java
语言是一个强类型、编译型的安全语言,要确保运行期的稳定性和安全性。
T[] tArray = new T()
因为不确定运行时期的实际类型是否具有无参构造器,所以编译错误。
T[] tArray = new T[5]
因为Java
数组必须明确知道内部元素的类型,而且会”记住“这个类型,每次往数组里插入新元素都会进行类型安全检查。由于泛型参数擦除,导致数组在运行时期拿不到实际类型(JVM
自动强制类型转(T[])tArray
是错误的),就会抛出java.lang.ArrayStoreException
,因此编译错误。
List<T> tList = new ArrayList<>()
因为ArrayList
源码中elementData
它容纳了ArrayList
的所有元素,其类型是Object
数组,因为Object
是所有类的父类,数组又允许协变(允许父类接收子类),因此elementData
数组可以容纳所有的实例对象。元素加入时向上转型为Object
类型(E
类型转为Object
),取出时向下转型为E
类型(Object
转为E
类型),因此编译通过。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
// 容纳元素的数组
transient Object[] elementData;
// 无参构造器
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 获取一个元素
public E get(int index) {
rangeCheck(index);
// 返回前进行强制类型转换
return (E) elementData[index];
}
}
在某些情况下,我们确实需要使用泛型数组,那该如何处理呢?代码如下:
public class Foo<T> {
// 不在初始化,由构造器初始化
public T t;
public T[] tArray;
public List<T> tList = new ArrayList<>();
// 构造器初始化
public Foo(Class<?> type) {
try {
t=(T)type.newInstance();
tArray=(T[]) Array.newInstance(type,5);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return "Foo{" +
"t=" + t +
", tArray=" + Arrays.toString(tArray) +
", tList=" + tList +
'}';
}
}
public static void main(String[] args) {
Foo<String> foo =new Foo<>(String.class);
foo.t="Hello";
foo.tArray[0]="World";
foo.tList.add("!");
System.out.println(foo);
// 输出结果Foo{t=Hello, tArray=[World, null, null, null, null], tList=[!]}
}
}
2.3 泛型数组
class GenericArray<T> {
T[] tArray = new T[5];// 编译错误
// 模拟JVM自动强制类型转
// public static void main(String[] args) {
// Object[] tArray = new Object[5];
// String [] sArray=(String[])tArray; Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
// }
}
上述创建泛型数组的方式是错误的,因为Java
数组必须明确知道内部元素的类型,而且会”记住“这个类型,每次往数组里插入新元素都会进行类型检查。由于泛型参数擦除,导致数组在运行时期拿不到实际类型,(JVM
自动强制类型转(T[])tArray
是错误的)就会抛出java.lang.ArrayStoreException
,因此编译错误。
Q:那要如何才能创建一个泛型数组呢?
A:利用反射
public class GenericArray <T> {
T[] tArray;
public GenericArray(Class<?> type) {
tArray=(T[]) Array.newInstance(type,5);
}
public static void main(String[] args) {
GenericArray array=new Foo(String.class);
array.tArray[0]="HelloA";
array.tArray[1]="HelloB";
array.tArray[2]="HelloC";
array.tArray[3]="HelloD";
array.tArray[4]="HelloE";
// array.tArray[5]="HelloF"; java.lang.ArrayIndexOutOfBoundsException
System.out.println(array.tArray[3]);// HelloD
}
}
2.4 泛型通配符
1、定义
Java
泛型支持通配符,可以使用一个?
表示任意类,也可以使用extends
关键字表示某一个类(接口)的子类型,还可以使用super
关键字表示某一个类(接口)的父类型。
2、extends关键字
extends
关键字又称为泛型上界,List<? extends E> list
表示list
集合元素中的类型是E
类型(或是其子类型),通常在泛型结构只参与读操作则限定上界。
public static <E> void read(List<? extends E> list){
for (E e : list) {
System.out.println(e);
}
}
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("A");
arrayList.add("B");
arrayList.add("C");
read(arrayList);
/**
* 输出结果:
* A
* B
* C
*/
}
那可以使用List<? super E> list
吗?
不能,我们不知道list
到底存放的是什么元素,只能推断出是E
类型的父类(或是E
类型,下同,不再赘述),但问题是E
类型的父类是什么呢?只有运行时才知道,那么编码期无法推断,也就完全无法操作了。当然,可以把它当作是Object
类来处理,需要时再转换成E
类型,但这完全违背了泛型的初衷。
那可以使用List<E> list
吗?
可以,但不建议这样做,因为译器会推断出类型是Object
,编译时就不会给出unchecked
警告。
3、super关键字
super
关键字又称为泛型下界,List<? super E> list
表示list
集合元素中的类型是E(或其父类),通常在泛型结构只参与写操作则限定下界。
public static void write(List<? super Number> list){
list.add(123);
list.add(1.2f);
list.add(1.2d);
}
那可以使用List<? extends Number> list
吗?
public static void write(List<? extends Number> list){
// 编译失败
list.add(123);
}
不能,编译失败。
? extends Number
的意思是,允许Number
所有的子类(包括自身)作为泛型参数。
list.add(123)
编译错误是因为泛型参数擦除后,list
集合元素中的类型是Number
(与实际引用new ArrayList<Integer>()
无关),而编译器不知道123
是Integer
类型,或是Double类型
,还是Number
类型等,由于Java
泛型要求运行期只能是有一个具体类型,因此编译器无法确定泛型的实际类型,编译失败。
在此种情况下,只有一个元素是可以add
进去的:null
值,这是因为null
是一个万用类型,它可以是所有类的实例对象,所以可以加入到任何列表中。
4、注意
如果一个泛型结构即用作读操作又用作写操作,那该如何进行限定呢?不限定,使用确定的泛型类型即可,如List。
2.4 协变和逆变
1、定义
在Java
中,协变和逆变是指宽类型和窄类型在某种情况下(如参数、泛型、返回值)替换的特性,简单地说,协变是用一个窄类型替换宽类型,而逆变则是用一个宽类型覆盖窄类型。
2、协变
用一个窄类型替换宽类型
class Father {
}
class Son extends Father {
}
public static void main(String[] args) {
Father father = new Son();
}
father
变量发生了协变,father
变量是父类,而其赋值却是子类实例,也就是用窄类型替换了宽类型。这也叫多态,两者同含义,在Java
世界里“重复发明”轮子的事情多了去了。
3、逆变
用一个宽类型覆盖窄类型
class Father {
public void func(Integer a){}
}
class Son extends Father {
public void func(Number a){}
}
子类的func
方法的参数类型比父类要宽,此时就是一个逆变方法,子类方法扩大了父类方法的输入参数。
4、Java泛型不支持协变和逆变
泛型不支持协变
public static void main(String[] args) {
// 数组支持协变
Number[] array=new Integer[10];
// 编译失败,泛型不支持协变
List<Number> list=new ArrayList<Integer>();
}
ArrayList
是List
的子类型,Integer
是Number
的子类型,由于Java
为了保证运行期的安全性,必须保证泛型参数类型是固定的,所以它不允许一个泛型参数可以同时包含两种类型,即使是父子类关系也不行。泛型不支持协变,但可以使用extends
关键字来模拟协变,代码如下所示
public static void main(String[] args) {
// 编译通过,Number的子类型或本身都可以是泛型参数类型
List<? extends Number> list=new ArrayList<Integer>();
// 编译失败
list.add(1);
}
? extends Number
的意思是,允许Number
所有的子类(包括自身)作为泛型参数,这里看着就像是把一个Integer
类型的ArrayList
赋值给了Number
类型的List
,其外观类似于使用一个窄类型替换一个宽类型。
list.add(123)
编译错误是因为泛型参数擦除后,list
的类型是Number
(与实际引用``new ArrayList()无关),而编译器不知道
123是
Integer类型,或是
Double类型,还是
Number类型等,由于
Java`泛型要求运行期只能有一个具体类型,因此编译器无法确定泛型的实际类型,编译失败。
在此种情况下,只有一个元素是可以add
进去的:null
值,这是因为null
是一个万用类型,它可以是所有类的实例对象,所以可以加入到任何列表中。
泛型不支持逆变
Java虽然可以允许逆变存在,但在对类型赋值上是不允许逆变的,你不能把一个父类实例对象赋值给一个子类类型变量,泛型自然也不允许此种情况发生了,但是它可以使用super
关键字来模拟逆变实现,代码如下所示
public static void main(String[] args) {
// 编译通过,Integer的父类型或本身都可以是泛型参数类型
List<? super Integer> list=new ArrayList<Number>();
list.add(1);
}
? super Integer
的意思是可以把所有Integer
父类型(自身、父类)作为泛型参数,这里看着就像是把一个Number
类型的ArrayList
赋值给了Integer
类型的List
,其外观类似于使用一个宽类型覆盖一个窄类型,它模拟了逆变的实现。
5、总结
Java
的泛型是不支持协变和逆变的,只是能够实现协变和逆变。
2.5 泛型的使用顺序
List<T>
、List<?>
、List<Object>
这三者都可以容纳所有的对象,但使用的顺序应该是首选List<T>
,次之List<?>
,最后选择List<Object>
。
List<T>
T
是一个明确的类型,List<T>
表示List集合中的元素都为T
类型,具体类型在运行期决定。List<T>
可以进行读写操作,因为它的类型是固定的T
类型,在编码期不需要进行任何的转型操作。
List<?>
-
?
表示的是任意类型,List<?>
表示List
集合中的元素可以是任意类型。 -
List<?>
是只读类型,可以进行删除操作,因为删除操作与泛型类型无关。不能进行读写操作,因为
List<?>
在编码期间无法确定容纳元素的实际类型是什么,就无法校验类型是否安全了,也就无法进行读写操作。而且List<?>
读取出的元素都是Object
类型的,客户端可以进行手动转型,也因此List<?>
常用于泛型方法的返回值。
List<Object>
Object
表示的是所有类类型,List<Object>
表示List
集合中的元素可以是Object
类型,即可容纳所有类型。List<Object>
可以进行读写操作,但是它执行写入操作时需要向上转型,在读取数据后需要向下转型,而这就失去了泛型存在的意义了。(泛型的出现就是为了解决类型转换的问题)
综上,有固定类型首选<T>
,只进行只读操作就选<?>
,最后才选<Object>
。
2.6 泛型的多重限定边界
1、定义
格式:<T extends XClass1 & XClass2 &XClassN >
使用&符号设定多重边界,指定泛型参数T
必须是XClass1
和XClass2
和XClass……
和XClassN
的共有子类型,此时T
就具有了所有限定的方法和属性。
在Java
的泛型中,可以使用&符号关联多个上界并实现多个边界限定,而且只有上界才有此限定,下界没有多重限定的情况。因为多个下界,编码者可自行推断出具体的类型,无须编译器推断了。
2、例子
模拟一款游戏有VIP玩家和普通玩家,VIP玩家具有出场特效、皮肤加成、武器加成等福利,且对攻击有50%的加成。
interface Show {
public Boolean isShowing();
}
interface Skin {
public Boolean isSKinBonus();
}
interface Weapon {
public Boolean isWeaponBonus();
}
// 模拟普通玩家
class Role1 {
}
// 模拟VIP过期了
class Role2 implements Show, Skin, Weapon {
@Override
public Boolean isShowing() {
return false;
}
@Override
public Boolean isSKinBonus() {
return false;
}
@Override
public Boolean isWeaponBonus() {
return false;
}
}
// 模拟VIP玩家
class Role3 implements Show, Skin, Weapon {
@Override
public Boolean isShowing() {
return true;
}
@Override
public Boolean isSKinBonus() {
return true;
}
@Override
public Boolean isWeaponBonus() {
return true;
}
}
public class Test {
public static <T extends Show & Skin & Weapon> void attackBonus(T t) {
if (t.isShowing() && t.isSKinBonus() && t.isWeaponBonus()) {
System.out.println(t.getClass().getSimpleName() + " 攻击加成50%");
} else {
System.out.println(t.getClass().getSimpleName() + " VIP已过期");
}
}
public static void main(String[] args) {
// 编译错误,不符合泛型多重限定边界
attackBonus(new Role1());
// 编译通过
attackBonus(new Role2());
attackBonus(new Role3());
}
}
三、总结
泛型是JDK1.5
提出的一个新特性,让我们在类定义的时候可以不设置属性或方法参数的类型,当类使用的时候在定义实际类型
。
由于JAVA
为了避免类膨胀、对JVM
进行换血且兼容JDK1.5
以前的版本,采用在程序编译时期对泛型参数进行擦除(擦除:按照泛型参数的擦除规则将泛型参数擦除为原始类型),在程序运行时期对带有泛型参数的变量进行补偿(补偿:JVM
会将该变量的原始类型转为其实际类型)的方式实现。
Java
泛型参数擦除也带来了不少新问题,因此SUN公司对这些问题做出了种种限制,避免我们发生各种错误。例如:
- 泛型参数必须是固定的,不允许一个泛型参数可以同时包含两种类型,即使是父子类关系也不行。
- 编译器生成了桥接方法来避免泛型参数擦除与多态发生冲突。
- 不能用
instanceof
判断泛型参数。 - 泛型类中的静态方法和静态属性不可以使用泛型参数。
- 不能在泛型类中初始化运行期不安全的泛型变量。
Java
的泛型是不支持协变和逆变的,只是能够实现协变和逆变。
泛型的使用顺序是有固定类型首选<T>
,只进行只读操作就选<?>
,最后才选<Object>
。
泛型的作用是解决类型转换的问题,常用于集合类型。