Java 基础 - 泛型机制
文章目录
前言
Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。
一、为什么要引入泛型?
1、代码复用
引入泛型的一个意义在于:适用于多种数据类型执行相同的代码,也就是代码复用
比如以下的代码:
public static int add(int a, int b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
public static float add(float a, float b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
public static double add(double a, double b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
如果没有泛型,要实现不同类型的加法,每种类型都需要重载一个add方法;通过泛型,我们可以复用为一个方法:
public static <T extends Number> double add(T a, T b) {
System.out.println(a + "+" + b + "=" + (a.doubleValue() + b.doubleValue()));
return a.doubleValue() + b.doubleValue();
}
2、保证代码类型安全
泛型中的类型在使用时指定,不需要强制类型转换(类型安全,编译器会检查类型)。
List list = new ArrayList();
list.add(111);
list.add("aaa");
list.add(new User());
在上面的代码里,list中的元素都是Object类型,所以在取出集合元素时需要人为的强制类型转化到具体的目标类型,这样很容易出现java.lang.ClassCastException异常。
引入泛型,它将提供类型的约束,提供编译前的检查:
// list中只能放String类型的元素, 不能放其它类型的元素
List<String> list = new ArrayList<String>();
二、泛型的使用
泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法。
1、泛型类
普通泛型
// 此处可以随便写标识符号,T是type的简称
class Test<T>{
// var的类型由T指定,即:由外部指定
private T value;
// 返回值的类型由外部决定
public T getValue(){
return value;
}
// 设置的类型由外部决定
public void setValue(T value){
this.value = value;
}
}
public class GenericsDemo{
public static void main(String args[]){
// value的类型为String类型
Test<String> test = new Test<String>();
// 设置字符串
test.setValue("abc");
// 取得字符串的长度
System.out.println(test.getValue().length());
}
}
多元泛型
// 此处指定了两个泛型类型
class Test<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 GenericsDemo{
public static void main(String args[]){
// 定义两个泛型类型的对象,里面的key为String,value为Integer
Test<String,Integer> t = new Test<String,Integer>() ;
// 设置key的内容
t.setKey("abc") ;
// 设置value的内容
t.setValue(100) ;
// 打印信息
System.out.print("key;" + t.getKey()) ;
System.out.print(",value;" + t.getValue()) ;
}
}
2、泛型接口
// 在接口上定义泛型
interface Info<T>{
// 定义抽象方法,抽象方法的返回值就是泛型类型
public T getValue() ;
}
// 定义泛型接口的子类
class InfoImpl<T> implements Info<T>{
// 定义属性
private T value;
// 通过构造方法设置属性内容
public InfoImpl(T value){
this.setValue(value) ;
}
public void setValue(T value){
this.value = value;
}
@Overrivd
public T getValue(){
return this.value;
}
}
public class GenericsDemo{
public static void main(String arsg[]){
// 声明接口对象,并通过子类实例化对象
Info<String> info = new InfoImpl<String>("abc") ;
System.out.println("内容:" + info.getValue()) ;
}
}
3、泛型方法
定义泛型方法时,必须在返回值前边加一个,来声明这是一个泛型方法,持有一个泛型T,然后才可以用泛型T作为方法的返回值。
三、泛型的上下限
<?> 无限制通配符
<? extends E> extends 关键字声明了类型的上界,表示参数化的类型只能接收该类型及其子类
<? super E> super 关键字声明了类型的下界,表示参数化的类型只能接收该类型及其父类型
1、上限
泛型的上限只能是该类型的类型及其子类
// ArrayList<? extends Animal> list = new ArrayList<Object>();//报错
ArrayList<? extends Animal> list2 = new ArrayList<Animal>();
ArrayList<? extends Animal> list3 = new ArrayList<Dog>();
ArrayList<? extends Animal> list4 = new ArrayList<Cat>();
// 泛型的上限:此时的泛型?,必须是Number类型或者Number类型的子类
public static void getElement1(Collection<? extends Number> coll){}
2、下限
泛型的下限只能是该类型的类型及其父类
ArrayList<? super Animal> list5 = new ArrayList<Object>();
ArrayList<? super Animal> list6 = new ArrayList<Animal>();
// ArrayList<? super Animal> list7 = new ArrayList<Dog>();//报错
// ArrayList<? super Animal> list8 = new ArrayList<Cat>();//报错
// 泛型的下限:此时的泛型?,必须是Number类型或者Number类型的父类
public static void getElement2(Collection<? super Number> coll){}
3、多个限制
使用&符号
public static <T extends Staff & Passenger> void discount(T t){
// .....
}
四、泛型数组
1、创建泛型数组
Java中可以声明带泛型的数组引用,但是不能直接创建带泛型的数组对象,需要通过java.lang.reflect.Array的newInstance(Class, int )创建T[]数组对象。
Dog<String>[] dogs = new Dog<String>[10];
// 如果运行上面的代码在编译期间会报一个错误:Cannot create a generic array of Dog<String>,意思就是不能创建一个泛型数组。
以下为正确创建泛型数组(利用反射):
import java.lang.reflect.Array;
public class Fruit<T> {
private T[] array;
public Fruit(Class<T> clz, int length) {
array = (T[]) Array.newInstance(clz, length);
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) {
return array[index];
}
public T[] getArray() {
return array;
}
}
//...
Fruit<String> fruit= new Fruit<>(String.class, 100);
fruit.put(0, "苹果");
fruit.put(1, "西瓜");
fruit.put(2, "香蕉");
Integer[] array = fruit.getArray();
2、为什么不能直接创建泛型数组
Java中的数组有2个特性:
特性1、数组是协变的
如果A是B的子类,那么A[]将是B[]的子类,例如:
Number[] numbers=new Integer[10];
但是,如果将数组设计为协变,可能会引发一个问题:类型安全性,例如:
Number[] numbers=new Integer[10];
numbers[0]=new Double(3.14);
这时候就需要数组的特性2了。
特性2:数组能够记住元素的类型,并且要进行运行期类型检查
上面的代码执行,将引发System.lang.ArrayStoreException异常。这是因为数组numbers始终记得它的元素类型是Integer,当对数组进行赋值为Double对象时,Java进行类型检查,发现类型不符,抛出异常。
解答:
Java泛型实现是“伪泛型”的策略,在编译阶段会进行类型擦除(后续讲解),泛型类将会被转换为原始类。这里主要解释一个问题,为什么不能有泛型数组?由于数组必须进行运行期类型检查,泛型数组也是数组,也要进行运行期类型检查;而由于类型擦除,造成数组运行期类型检查不能正常进行,破坏了Java数组运行期类型检查的机制,故不能直接创建泛型数组。
五、类型擦除
1、理解什么是类型擦除
Java泛型是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。
2、类型擦除原则
1、消除类型参数声明,即删除<>及其包围的部分。
2、根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。
3、为了保证类型安全,必要时插入强制类型转换代码。
4、自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。
3、证明类型擦除
1、原始类型相同
public class Test {
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
}
}
2、可以通过反射添加其它类型元素
public class Test {
public static void main(String[] args) throws Exception {
ArrayList<Integer> list = new ArrayList<Integer>();
//这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer
list.add(1);
// 通过反射可以放入String类型的数据
list.getClass().getMethod("add", Object.class).invoke(list, "asd");
}
}
4、泛型的编译期检查
问题:
既然说类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢?
解答:
Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。
泛型中的类型检查是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。
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
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); //编译错误
String str2 = new ArrayList<String>().get(0); //返回类型就是String
}
}
问题:
泛型中参数化类型为什么不考虑继承关系?
ArrayList<String> list1 = new ArrayList<Object>(); //编译错误
ArrayList<Object> list2 = new ArrayList<String>(); //编译错误
解答:
上面代码中,list1取出的数据是String类型,而存放的数据可以是Object类型,取出时会出现ClassCastException的异常。list2中取出的是Object类型,存放的是String类型,取出时不会出现ClassCastException,但是这样的设计毫无意义,泛型出现的目的,就是为了解决类型转换的问题,现在取出的值为Object类型,还需要自己做强转,违背了泛型设计的初衷。
5、泛型的多态和桥接方法
类型擦除会造成多态的冲突,而JVM解决方法就是桥接方法。
class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
class DateInter extends Pair<Date> {
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
}
由于类型擦除,实际上父类编译之后是这样的:
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();
}
是不是发现问题了,父类的方法是Object类型,子类的方法是Date类型,这在java中属于是方法重载,而不是方法重写。
测试一下:
public static void main(String[] args) throws ClassNotFoundException {
DateInter dateInter = new DateInter();
dateInter.setValue(new Date());
dateInter.setValue(new Object()); //编译错误
}
如果是重载,那么子类中应该有两个setValue方法,一个是参数Object类型,一个是Date类型,可是经过测试发现,子类根本没有继承自父类的Object类型参数的方法。所以说,确实是方法重写了,而不是方法重载了。
这是为什么呢?
JVM采用了一个特殊的方法,也就是桥方法,来解决了泛型多态和类型擦除的冲突问题。
反编译代码:
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
}
通过反编译可以发现,子类中有4个方法,最后的两个方法,就是编译器自己生成的桥方法。可以看到桥方法的参数类型都是Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而打在我们自己定义的setvalue和getValue方法上面的@Oveerride只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。 所以,虚拟机巧妙的使用了桥方法,来解决了类型擦除和多态的冲突。
6、基本类型不能作为泛型类型
问题:
为什么java中没有ArrayList<int>,只有ArrayList<Integer>
解答:
因为当类型擦除后,ArrayList的原始类型变为Object,但是Object类型不能存储int值,只能引用Integer的值。
另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作。
7、泛型类型不能实例化
T test = new T(); // ERROR
因为在 Java 编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了,此外由于T 被擦除为 Object,如果可以 new T() 则就变成了 new Object(),失去了本意。
8、理解泛型类中的静态方法和静态变量
public class Test<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;
}
}
因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。
Test2是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的 T,而不是泛型类中的T。