泛型概述
泛型的本质是为了将类型参数化, 也就是说在泛型使用过程中,数据类型被设置为一个参数,在使用时再从外部传入一个数据类型;而一旦传入了具体的数据类型后,传入变量(实参)的数据类型如果不匹配,编译器就会直接报错。这种参数化类型
可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
泛型方法、泛型类、泛型接口
1. 泛型方法
当在一个方法签名中的返回值前面声明了一个 < T > 时,该方法就被声明为一个泛型方法
。< T >表明该方法声明了一个类型参数 T,并且这个类型参数 T 只能在该方法中使用。
注意1:只有在方法签名中声明了< T >的方法才是泛型方法,仅使用了泛型类定义的类型参数的方法并不是泛型方法。
注意2:泛型类中定义的类型参数和泛型方法中定义的类型参数是相互独立的。
注意3:为了避免混淆,如果在一个泛型类中存在泛型方法,那么两者的类型参数最好不要同名。
注意4:在静态成员中不能使用泛型类定义的类型参数,但我们可以将静态成员方法定义为一个泛型方法,这样方法中便可以使用其声明的类型参数了。例如:
// 该方法只是使用了泛型类定义的类型参数,不是泛型方法
public void testMethod(T t){
System.out.println(t);
}
// <T> 真正声明了下面的方法是一个泛型方法
public <T> T testMethod1(T t){
return t;
}
泛型方法,是在调用方法的时候再确定类型参数的具体类型,这一点很好理解,并且其签名中声明的类型参数只能在该方法里使用。
在调用泛型方法的时候,可以显式地指定类型参数,也可以不指定。
- 当泛型方法的形参列表中有多个类型参数时,在不指定类型参数的情况下,方法中声明的的类型参数为泛型方法中的几种类型参数的共同父类的最小级,直到 Object。
- 在指定了类型参数的时候,传入泛型方法中的实参的数据类型必须为指定数据类型或者其子类。
public class Test {
// 这是一个简单的泛型方法
public static <T> T add(T x, T y) {
return y;
}public static void main(String[] args) {
// 一、不显式地指定类型参数
//(1)传入的两个实参都是 Integer,所以泛型方法中的<T> == <Integer>
int i = Test.add(1, 2);
//(2)传入的两个实参一个是 Integer,另一个是 Float,
// 所以<T>取共同父类的最小级,<T> == <Number>
Number f = Test.add(1, 1.2);
// (3)传入的两个实参一个是 Integer,另一个是 String,
// 所以<T>取共同父类的最小级,<T> == <Object>
Object o = Test.add(1, "asd");
// 二、显式地指定类型参数
//(1)指定了<T> = <Integer>,所以传入的实参只能为 Integer 对象
int a = Test.<Integer>add(1, 2);
//(2)指定了<T> = <Integer>,所以不能传入 Float 对象
int b = Test.<Integer>add(1, 2.2);// 编译错误
//(3)指定<T> = <Number>,所以可以传入 Number 对象
// Integer 和 Float 都是 Number 的子类,因此可以传入两者的对象
Number c = Test.<Number>add(1, 2.2);
}
}
2. 泛型类
在泛型类中,类型参数定义的位置有三处,分别为:
1.非静态的成员属性类型
2.非静态方法的形参类型(包括非静态成员方法和构造器)
3.非静态的成员方法的返回值类型
泛型类中的静态方法和静态变量不可以使用泛型类所声明的类型参数
public class Test<T> {
public static T one; // 编译错误
public static T show(T one){ // 编译错误
return null;
}
}
-
泛型类中的类型参数的确定是在创建泛型类对象的时候(例如 ArrayList< Integer >)。
-
而静态变量和静态方法在类加载时已经初始化,直接使用类名调用;在泛型类的类型参数未确定时,静态成员有可能被调用,因此泛型类的类型参数是不能在静态成员中使用的。
泛型类不只可以接受一个类型参数,它还可以接受多个类型参数。
public class MultiType <E,T> {
E value1;
T value2;
public E getValue1(){
return value1;
}
public T getValue2(){
return value2;
}
}
3.泛型接口
泛型接口中的类型参数,在该接口被继承或者被实现时确定。
在泛型接口中,静态成员也不能使用泛型接口定义的类型参数。
public interface Test<T> {
public abstract void testInterface(T t) ;
}
在上述代码中,声明的 E field 属性会报错,因为在接口中的属性默认都是静态的,因此不能直接使用类型参数声明。
注意:定义一个接口 SonB 继承了 泛型接口 FatherA,在 接口 IA 定义时必须确定泛型接口 FatherA 中的类型参数。
interface SonB extends FatherA<String, String> {
}
注意:定义一个类 ImpA 实现了 泛型接口 FatherA,在 类 ImpA 定义时需要确定泛型接口 FatherA中的类型参数。(如果没有确定具体的类型参数,则默认是Object)
class ImpA implements FatherA<String, String>{
@Override
public String function1(String s) {
return null;
}
}
类型擦除
1. 什么是类型擦除
泛型的本质是将数据类型进行参数化
,而类型擦除就是编译器会在编译期间擦除
代码中的所有泛型语法并相应的做出一些类型转换动作。换言之,泛型信息只存在于代码编译阶段,在代码编译结束后,与泛型相关的信息会被擦除掉。也就是说,成功编译过后的 class 文件中不包含任何泛型信息。
例如分别定义一个Integer 和一个 String 类型的 List 集合,比较它们的类信息。
@Test
public void testTypeErasure() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
List<Integer> integerList = new ArrayList<Integer>();
List<String> stringList = new ArrayList<String>();
integerList.add(1);
stringList.add("temp");
System.out.println(stringList.getClass() == integerList.getClass());
integerList.getClass().getMethod("add", Object.class).invoke(integerList,"zhangsan");
for (int i = 0; i < integerList.size(); i++) {
System.out.println(integerList.get(i));
}
}
true
1
zhangsan
我们发现两者的类信息是相同的,这是因为,在编译期间,所有的泛型信息都会被擦除, ArrayList< Integer > 和 ArrayList< String >类型,在编译后都会变成ArrayList< Object >类型。
public class Test<T> {
private T num;
}
public class Test<T extends Number> {
private T num;
}
通过反编译可以发现编译器擦除了 Test 类后面的泛型标识 < T >,将 num 的数据类型替换为 Object 类型。
但是第二个我们使用<T extends Number>代替<T>,结果是参数 T 被擦除后会替换为 Number 而不再是 Object。这是因为这里使用了extends 上界通配符,extends 限定了 T 只能是 Number 或者是 Number 的子类。
2. 类型擦除的原理
再谈论类型擦除的原理之前,很多小伙伴肯定有这么一个疑问:既然泛型的信息被擦除了,那么是怎么保证我们在集合中只添加指定的数据类型的对象呢?
原因:其实 JAVA 在创建一个泛型类的对象时, Java 编译器是先检查代码中传入 < T > 的数据类型,并记录下来,然后再对代码进行编译,在编译的过程中再进行擦除的工作。
ArrayList<Integer> arrayInteger = new ArrayList<Integer>();
arrayInteger.add(1);
Integer num = arrayInteger.get(0);
System.out.println(num);
Java 编译器因为把 ArrayList< Integer > 的泛型信息擦除了,所以get方法返回的其实是Object 类型的值,但是Java 编译器会帮我们完成Object 到 Integer 的类型转换。
- 对原始方法 get() 的调用,返回的是 Object 类型;
- 将返回的 Object 类型强制转换为 Integer 类型;
Integer num = arrayInteger.get(0);
// 可以理解为java的编译器帮我们作了两步操作
// get方法的返回值返回的是Object
Object object = arrayInteger.get(0);
//编译器自动进行Integer的强制类型转换
Integer num = (Integer) object;
在代码成功编译后,所有的泛型信息都会被擦除,并且编译器还会把相关的泛型信息存储下来,并且类型参数 T 会被统一替换为原始数据类型。
3. 类型擦除可能会带来的问题
问题1:类在编译之后所有泛型都会被Object或者泛型上界代替,会导致 泛型类 中无法获取原本的泛型信息,如果我有一个需求,想要能够获得原本的泛型信息,该怎么办?
这里转载博主的解答:【Java】 泛型擦除-CSDN博客
比如:要实现以下需求
有两种方式实现该需求,这两种方式都是借助反射实现该需求的:
方式一:将MyTest的class对象当做参数传入即可。但是这样需要修改Stream类的代码,也就是说要修改源码
方式二:使用匿名内部类。这样不用修改Stream类的代码,也就是说不需要修改源码,只要在使用该类的地方做修改即可
问题2:对于函数式接口,如果使用匿名内部类创建该接口对象的话,一般都会使用lambda表达式来代替匿名内部类。但是,在某些情况下匿名内部类不会报错,而使用lambda表达式会报错
有两种方式实现该需求,这两种方式都是借助反射实现该需求的:
方式一:将MyTest的class对象当做参数传入即可。但是这样需要修改FlatMapFunc类的代码,也就是说要修改源码
方式二:使用匿名内部类。这样不用修改Stream类的代码,也就是说不需要修改源码,只要在使用该类的地方做修改即可
泛型通配符
在 Java 的多态中,我们知道可以将一个子类对象赋值给其父类的引用,这叫做 向上转型。例如,我们在定义一个list集合的时候:
public class GenericTest {
public static void main(String[] args) {
List list = new ArrayList();
}
}
而我们进入ArrayList的源码,可以清楚地看到ArrayList<E>实现了List<E>接口:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
那如果我们在 ArrayList< T > 泛型集合中,当传入 < T > 中的数据类型相同时,是否还能将一个 ArrayList< T > 对象赋值给其父类的引用 List< T > , 例如下面代码所示:
答案是:可以的!
public class GenericTest {
public static void main(String[] args) {
List<Integer> list = new ArrayList<Integer>();
}
}
但是,再考虑到另外一个情况:已知 Integer 类是 Number 类的 子 类,如果想将 ArrayList< Integer > 对象赋值给 List< Number > 的引用,是否被允许呢?
@Test
public void testIntegerToNumber() {
List<Integer> integerList = new ArrayList<Number>();
List<Number> numberList = new ArrayList<Integer>();
}
可以看到在IDEA编译器中会有报错:
这就证明了:在 JAVA 中不能把 ArrayList< Integer > 对象赋值给 List< Number >的引用。这也说明了在一般泛型中,不能向上转型
。同时,也不能把ArrayList< Number > 对象赋值给 List< Integer >的引用,这也说明了在一般泛型中,不能向下转型.
现在有这么个需求,假设我们定义了一个 Fruit< T >类,如下:
@Data
class Fruit<T> {
private T field1;
private T field2;
}
这里有个calc方法,需要计算Fruit两个属性的值之和,入参为 Fruit<Number> 类型的对象。当在test3方法中进行创建调用时,代码是能够正确运行的。
@Test
public void test3() {
Fruit<Number> numberFruit = new Fruit<>(1, 2);
System.out.println(calc(numberFruit));
}
public int calc(Fruit<Number> fruit){
return fruit.getField1().intValue()+ fruit.getField2().intValue();
}
但是我们传入的实参 (1, 2) 实际上是 Integer 类型;那是否可以直接创建一个 Fruit< Integer > 对象呢?例如
Fruit<Integer> fruitInteger = new Fruit<>(123, 456);
代码运行:编译器会直接报错,这是因为calc方法的形参数据类型为 Fruit < Number > ,而Fruit < Integer > 对象不能传给 calc() 方法。
如果我们想实现这种功能是不是只能再另外写一个接受参数为Fruit < Integer >的方法呢,这种方式会大大增加代码的冗余量。那能不能有一种方式能够让我们可以自由选择传Fruit < Number >、Fruit < Integer >、或者其他呢,能不能自由灵活点?
换句话说:我们需要一个在逻辑上可以表示为 Fruit < Integer > 和 Fruit < Number > 这两者的父类引用类型,这就是——泛型通配符。
1. 上界通配符 <? extends T>
上界通配符 < ? extends T >:T 代表了类型参数的上界,表示了类型参数的范围是 T 和 T 的子类。
注意点:ArrayList<? extends Number> 可以代表 ArrayList< Integer >、ArrayList< Float >、… 、ArrayList< Number >中的某一个集合
,但我们不能指定 ArrayList<? extends Number> 的数据类型。例如,下面这个例子中,不能直接向list集合中添加Integer、Float对象
ArrayList<? extends Number> list = new ArrayList<>();
list.add(new Integer(1));// 编译错误
list.add(new Float(1.0));// 编译错误
原因是 ArrayList<? extends Number> 的类型是未知的表,它可以代表 ArrayList< Integer >、ArrayList< Float > 等集合,但却不能确定它具体是哪一种类型的集合。
有了上界通配符之后,我们就可以完善上述例子的代码了:
public static void main(String[] args) {
Fruit<Integer> numberFruit = new Fruit<>(1, 2);
System.out.println(TestGenerics.calc(numberFruit));
}
public static int calc(Fruit< ? extends Number> fruit){
Number field1 = fruit.getField1();
Number field2 = fruit.getField2();
return field1.intValue()+ field2.intValue();
}
2. 下界通配符 <? super T>
下界通配符 <? super T>:T 代表了类型参数的下界,表示了类型参数的范围是 T 和 T 的超类,直至 Object。
ArrayList<Integer> list01 = new ArrayList<Number>();// 编译错误 ArrayList<? super Integer> list02 = new ArrayList<Number>();// 编译正确
ArrayList<? super Integer> 在逻辑上表示为 Integer 类以及 Integer 类的所有父类,它可以代表 ArrayList< Integer>、ArrayList< Number >、 ArrayList< Object >中的某一个集合
,但实质上它们之间没有继承关系。
同样:ArrayList<? super Integer> 只能表示指定类型参数范围中的某一个集合
,但我们不能指定 ArrayList<? super Integer> 的数据类型
与带有上界通配符的集合 ArrayList< ? extends T >的用法不同,带有下界通配符的集合ArrayList< ? super T >中可以添加 Number 类及其子类的对象;ArrayList< ? super Number >的下界就是ArrayList<Number>
集合,因此,其中必然可以添加 Number 类及其子类的对象;但不能添加 Number 类的父类对象(不包括 Number 类)。
以下代码能够成功运行:
public class Test {
public static void main(String[] args) {
ArrayList<Number> list = new ArrayList();
list.add(new Integer(1));
list.add(new Float(1.1));
fillNumList(list);
System.out.println(list);
}
public static void fillNumList(ArrayList<? super Number> list) {
list.add(new Integer(0));
list.add(new Float(1.0));
}
}
3. 无限定通配符 <?>
无界通配符<?>:? 代表了任何一种数据类型,能代表任何一种数据类型的只有 null。需要注意的是:<?>也是一个数据类型实参,它和 Number、String、Integer 一样都是一种实际的数据类型。
注意:Object 本身也算是一种数据类型,但却不能代表任何一种数据类型,所以 ArrayList< Object > 和 ArrayList<?> 的含义是不同的,前者类型是 Object,也就是继承树的最高父类,而后者的类型完全是未知的;ArrayList<?> 是 ArrayList< Object > 逻辑上的父类
@Test
public void testIntegerToNumber() {
ArrayList<Integer> arrayList = new ArrayList<>();
ArrayList<?> list = arrayList; // 成功
list.add(1); // 编译错误
}
查看Collections 中的 copy 方法可以知道,copy 方法使用了一个 for 循环,在 for 循环中,对于 <? extends T> 集合 src,我们可以安全地获取类型参数T
的引用(即变量 t),而对于 <? super T> 的集合 dest,我们可以安全地传入类型参数T
的引用。
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}
那我们在使用泛型的时候如何去判断该使用 extends,还是 super 通配符呢?
这里就要提到一个PECS原则。 Producer Extends Consumer Super。换句话说,如果参数化类型表示一个生产者,就使用<? extends T>;如果它表示一个消费者,就使用<? super T>
即:如果需要返回 T,则它是生产者(Producer),要使用 extends 通配符;如果需要写入T,则它是消费者(Consumer),要使用 super 通配符。
Java 中常用的通配符有:T,E,K,V,?。这些数字本质上没啥区别,只不过是编码时的一种约定俗成的东西。比如上述代码中的 T ,我们可以换成 A-Z 之间的任何一个 字母都可以。通常情况下,T,E,K,V,? 是这样约定的:
- ? 表示不确定的 java 类型
- T (type) 表示具体的一个java类型
- K V (key value) 分别代表java键值中的Key Value
- E (element) 代表Element