泛型
1. 什么是泛型
- 泛型是JDK5中引入的一种参数化类型的特性
2. 泛型的作用
1. 可以使得代码更加的健壮,将类型检查提前到编译期,便于更早的发现问题
2. 使得代码更加的简洁,使得代码可以更方便的复用
3. 泛型的三种使用场景
-
泛型类
class Generic_class<T>
{ } -
泛型接口
interface Generic_interface<T> { }
-
泛型方法
<T> void Generic(T t) { }
- 注意:
void function(T t) { }
这个函数并非泛型方法,这只是使用了泛型类型作为传参的普通方法;
- 注意:
/* 一、泛型类 */
class Generic_class<K, V> {
public K key;
public V value;
}
/* 二、泛型接口 */
interface Generic_interface<K, V> {
public void setKey(K key);
public V getValue();
}
/* 三、泛型方法 */
<K, V> void Generic_function(K key, V value) {}
static <K, V> void Generic_static_function(K key, V Value) {}
/* 四、使用了泛型类型的普通方法 */
class Generic_class<T> {
//这个方法就不是泛型方法,而只是使用了泛型类型的普通方法
void function(T t);
//原因在于,该方法中的T,并非是可以由function决定的,这个T的类型是由泛型类Generic_class在实例化的时候决定的
}
4. 常见的类型变量名
The most commonly used type parameter names are:
- E - Element (used extensively by the Java Collections Framework)
- K - Key
- N - Number
- T - Type
- V - Value
- S,U,V etc. - 2nd, 3rd, 4th types
- 如上类型变量名,只是一个约定俗成,实际并没有具体的含义
5. 类型参数和类型变量
Type Parameter and Type Argument Terminology: Many developers use the terms “type parameter” and “type argument” interchangeably, but these terms are not the same. When coding, one provides type arguments in order to create a parameterized type. Therefore, the
T
inFoo<T>
is a type parameter and theString
inFoo<String> f
is a type argument. This lesson observes this definition when using these terms.
- 如上定义,通常我们将
Foo<T>
中的 T 称作类型参数,而将Foo<String>
中的 String 称作类型变量 ,亦即 实际类型参数 - 其中
Foo<T>
称作泛型类型,而Foo<String>
称作参数化类型
6. 原始类型
-
定义:缺少实际类型变量的泛型,如下代码中,就定义了一个原始类型
class Box<T> { private T data; public void SetData(T t) { data = t; } public T GetData() { return data; } } public static void main(String[] args) { Box<Integer> box = new Box<>(); //这个就是存在实际类型参数的 Box raw_box = new Box(); //这个就是原始类型 raw_box = box; raw_box.SetData("123"); }
如上代码中的
Box raw_box = box;
就是原始类型,显然,泛型类型可以直接赋给原始类型,但是这样写,实际上是绕过了泛型的类型检查,如上代码,我们限定了 box 是 Integer ,但是通过原始类型 raw_box 将 String 类型赋值进去了,显然这并不是我们要的效果,所以我们应该尽量避免使用原始类型,而如果使用 javac -Xlint:unchecked XXX 进行编译,我们会得到如下警告信息:Generic_demo_01.java:74: 警告: [unchecked] 对作为原始类型Box的成员的SetData(T)的调用未经过检查
raw_box.SetData(“123”);
^
其中, T是类型变量:
T扩展已在类 Box中声明的Object
1 个警告
7. 受限类型
-
类型限制,只能用
T extends XXX
,和通配符上下界不同,限制条件只能用 extends ,而没有 super 的用法! -
受限类型的作用:
-
限定泛型类型,如下例子:
//定义一个泛型方法,限定其传参必须是Number及其子类 static <V extends Number> void setValue(V value) { } //此处传参String类型,就会报错 setValue("123");
-
保证方法调用,如下例子:
static <V extends Number> void setValue(V value) { //业务逻辑 /* 因为限定V是Number及其子类,所以传参value一定拥有Number类的方法,所以可以保证能够调用到其中的intValue等方法 */ value.intValue(); //业务逻辑 } //限制类型继承接口 interface promise_func { public void function(); } static <V extends promise_func> void test(V value) { //业务逻辑 //原因如上 value.function(); //业务逻辑 }
-
-
具有多个限定类型的,顺序是有要求的,且限制类型是类的话只能有一种(受限于JAVA中的单继承)
-
当存在多个限定类型的时候,如果限定类型中有类,则类必须放在最前面!
-
如果是多重限定,代表着对应类型参数必须是包含所有限定条件的实际类型,演示如下:
//接口 interface Generic_interface { public void test(); } //继承接口和Number类的test类 class test extends Number implements Generic_interface { .... } //泛型方法中,多重限定,类型必须是实现Number及接口的类 static <V extends Number & Generic_interface> void setValue(V value) { ... } //实际传入test类满足要求 setValue(new test());
-
tips:
-
根据类型擦除机制,泛型最终都会被擦除,那么如果是多重限定的情况,擦除后泛型会被替换成第一个限定条件,即:
Test<T extends A & B> { public T t; } //那么编译后,T会被替换成第一个限定条件A,即 Test<A> { public A t; } //那么第二个参数有什么用呢?当使用到第二个限定条件的时候,编译器会做一个强制的类型转换,下面的例子是说明泛型真正实现的时候会存在强制类型转换的 public class Theory { public static void main(String[] args) { Map<String, String> map = new HashMap<>(); map.put("1111", "18"); System.out.println(map.get("1111")); } } /* 编译后生成的class文件,使用jd-gui 1.4.1工具解析 */ public class Theory { public static void main(String[] args) { Map<String, String> map = new HashMap(); map.put("1111", "18"); System.out.println((String)map.get("1111")); //这里就是强制转型得到的String } }
注意:需要用旧一点的工具,新的的反编译工具太好了,已经可以把泛型也反编译出来了!
-
-
-
8. 推断算法
- 推断算法尝试找到与所有参数一起使用的最基本的类型
9. 通配符
-
通配符种类:
-
上限通配符:
? extends XXX
-
下限通配符:
? super XXX
-
无限通配符:
?
-
无限通配符的适用场景:
-
如果你正在编写一个可以使用
Object
类中提供的功能实现的方法;源码Class
类中就存在大量这样的应用,比如下面的代码:public boolean isAssignableFrom(Class<?> cls) { if (this == cls) { return true; // Can always assign to things of the same type. } else if (this == Object.class) { return !cls.isPrimitive(); // Can assign any reference to java.lang.Object. } else if (isArray()) { return cls.isArray() && componentType.isAssignableFrom(cls.componentType); } else if (isInterface()) { // Search iftable which has a flattened and uniqued list of interfaces. Object[] iftable = cls.ifTable; if (iftable != null) { for (int i = 0; i < iftable.length; i += 2) { if (iftable[i] == this) { return true; } } } return false; } else { if (!cls.isInterface()) { for (cls = cls.superClass; cls != null; cls = cls.superClass) { if (cls == this) { return true; } } } return false; } }
-
当代码使用通用类中不依赖于类型参数的方法时。例如,
List.size
或List.clear
-
-
-
-
使用范围:
- 参数类型
- 字段类型
- 局部变量类型
- 返回类型(极少用,也尽量避免使用)
-
通配符无法用作泛型方法调用,泛型类实例创建或超类的类型参数,即无法实现如下代码:
/* 用作泛型方法 */ <? extends Number> void test() {} /* 泛型类 */ class <? extends Number> CLASS {}
-
PESC
-
上限的限制(副作用) ------> 生产者:只能获取数据,不能消费数据 PE
- 当使用
List<? extends Number>
后,该List就会变为只读的,即无法调用:List.add(Integer.valueOf(1))
- 不能存放数据的原因:当我们定义了上界之后,首先这个 List 中存放的数据必定是对应上界的子类(或其本身),而向其中存放数据的时候,由于限定的是子类,所以我们无法确定传入的数据和限定的数据其之间的继承关系,而由于 JAVA 只支持 向上转型 所以这种情况下极有可能出现非法的向下转型的情况,显然这是不正确的,所以我们无法向使用了上界通配符的类中传入数据。
- 可以读取数据的原因:与不能存放数据的原因正好相反的,因为定义了上界之后,我们可以确定的是这个List里面存放是必然是限制类型的子类(或其本身),那么由于 向上转型 的存在,我们只要用限制类型去接收List里面的数据,必定是能够成功的,或者限制类型的父类也是可以的。
- 但是可以通过使用反射进行绕过
- 当使用
-
下限的限制(副作用) -----> 消费者:只能消费数据,不能获取数据(确切来说是无法明确知晓获取的数据是什么类型) SC
-
可以往内写数据,但是无法去读取数据
-
可以写入数据的原因:当我们定义了下界之后,首先这个 List 中存放的数据必定是对应下界的父类(或其本身),这个时候向其中传入的数据,我们基本可以确认,会是该限制类型的子类,这是符合 向上转型 的原则,所以是可以向其中传入数据的,同样的,若是我们特意向其中放置限制类型的父类,同样是会报错的。
/* 举例下界设值的限制 */ class A {} class B extends A {} class C extends B {} class Test { public static void main(String[] args) { List<? super B> list = new ArrayList<>(); list.add(new C()); list.add(new B()); //报错,因为这样会存在可能为:B b = a的向下转型的不安全情况 list.add(new A()); } }
-
不可以读取数据的原因:与可以写入数据的原因正好相反,因为定义了下界,我们无法确定其中存放的数据类型和限制类型之间的继承关系,而这时候如果擅自采用某一种类型去接收List中的数据,那么极有可能出现 向下转型 的情况,显然这是不符合要求的,所以我们无法去获取数据,但是因为所有的类都是Object的子类,所以我们如果用Object类进行数据接收,是没有问题的。
-
-
-
无限通配符的限制(副作用)
- 类型退化,如有
List<?>
,不能(再往其中添加)使用任何带有 ?(T) 的类型的参数,只能往其中添加null
- 类型退化,如有
-
10. 类型擦除
- JAVA是一种伪泛型,因为虚拟机并不支持泛型;作用是为了兼容低版本;JDK5才有的泛型;
- 在代码编译时,编译器会将所有泛型类型擦除,这样就不存在新的类型到字节码,所有的泛型最终实际都变成了原始类型(或者限制类型),而这样就使得JAVA在运行时不存在泛型信息做到了兼容
- JAVA编译器擦除泛型的具体做法:
- 检查泛型类型,获取目标类型
- 擦除泛型信息,替换为限定类型
- 如果没有限定信息,就替换为 Object
- 在必要的时候插入类型转换以保持类型安全
- 生成桥方法以在拓展时保持多态性
11. 桥接
-
因为JAVA的泛型属于伪泛型,实际是进行了类型擦除,即有类型限制时,会擦除类型成限制类型,而如果没有限制,则都会擦除成
object
,而为了维持 多态,如下代码就会有桥接函数出现class Mint<T> { private T m_data; public void SetData(T t) { m_data = t; } } class Beef extends Mint<Integer> { private Integer m_data; public void SetData(Integer t) { m_data = t; } }
- 由于类型擦除的存在,父类 Mint 中的类型会被擦除为 Object ,即父类的方法变为 SetData(Object t) 但是子类限定了类型为 Integer ,而为了维持多态性,此时就会生成一个桥接方法,保证类型不变,如下为解析出来的class文件:
Compiled from "Beef.java" class demo_0.Beef extends demo_0.Mint<java.lang.Integer> { demo_0.Beef(); Code: 0: aload_0 1: invokespecial #1 // Method demo_0/Mint."<init>":()V 4: return public void SetData(java.lang.Integer); Code: 0: aload_0 1: aload_1 2: putfield #2 // Field m_data:Ljava/lang/Integer; 5: return public void SetData(java.lang.Object); -----> 这个就是桥接方法 Code: 0: aload_0 1: aload_1 2: checkcast #3 // class java/lang/Integer ---->这里有一个类型强转 5: invokevirtual #4 // Method SetData:(Ljava/lang/Integer;)V 8: return }
12. 使用泛型的七个限制
- 泛型变量不能是基本数据类型
- 限制于类型擦除,当不存在限制条件时,所有的泛型都会被擦除为object,但是基本数据类型并不是object的子类,所以无法将泛型变量设置为基本数据类型
- 无法创建类型参数的实例
- 因为类型参数是不确定的,所以无法进行实例化
- 无法声明类型参数为静态变量
- 因为类型参数都是在进行实例化时进行指定具体类型的,而静态变量无需实例化就可以使用,这就导致存在无法获知其具体类型的情况
- 无法对泛型使用Casts和instanceof
- 因为类型擦除的存在,实际运行时,泛型类型都是被擦除的,而这样也就无法判断对象和类之间的归属了
- 无法创建泛型数组
- 协变:所谓的协变即,B继承于A,那么B[]也是继承于A[]
- 而在泛型数据中, B继承于A,*Plant并不是继承于Plant*的,即泛型是不支持协变的,这也就导致无法创建泛型数组
- 无法创建、捕获、抛出参数化类型的对象
- 实例化对象的时候,都必须是指定类型参数的,而未知的参数化类型在实例化的时候是不被允许
- 无法重载具有泛型的函数
- 类型擦除
13. 通配符的捕获和帮助方法
-
所谓通配符的捕获:在某些场景下,编译器能够自己推断出通配符的类型,类似类型推断,例如:
class Wildcard_catch<T> { private T data1; private T data2; public Wildcard_catch(T t1, T t2) { data1 = t1; data2 = t2; } public void SetData(T t1) { data1 = t1; } public T GetNumber() { return data2; } public static void Catch(Wildcard_catch<? extends Number> tmp) { System.out.print("class = " + tmp.GetNumber().getClass().getName() + "\n"); } public static void main(String[] args) { Wildcard_catch<Integer> tmp = new Wildcard_catch<>(10, 20); Generic_demo_01.Catch(tmp); } }
- 如上代码,
Catch()
函数使用了上界通配符,我们传入参数时,并未进行类型参数的指定,但是编译器根据我们传入的参数类型,进行推断,得出了当前Wildcard_catch
类中的类型参数。
- 如上代码,
-
编译器能够自行捕获通配符当然是我们最希望看到的情况,但是编译器并没有我们想象中的那么灵活,很多场景下,它无法正确捕获到通配符的类型,而在这种场景下,就需要我们编写相关的帮助函数去帮助编译器识别具体类型,如下例子:
class Wildcard_catch<T> { private T data1; private T data2; public Wildcard_catch(T t1, T t2) { data1 = t1; data2 = t2; } public void SetData(T t1) { data1 = t1; } public T GetNumber() { return data2; } public static void Catch(Wildcard_catch<? extends Number> tmp) { System.out.print("class = " + tmp.GetNumber().getClass().getName() + "\n"); tmp.SetData(tmp.GetNumber()); //这里就会出错 } public static void main(String[] args) { Wildcard_catch<Integer> tmp = new Wildcard_catch<>(10, 20); Generic_demo_01.Catch(tmp); } }
-
如上例子中,使用
javac
进行编译会报错,报错信息如下:Generic_demo_01.java:52: 错误: 无法将类 Wildcard_catch中的方法 SetData应用到给定类型;
tmp.SetData(tmp.GetNumber());
^
需要: CAP#1
找到: CAP#2
原因: 参数不匹配; Number无法转换为CAP#1
其中, T是类型变量:
T扩展已在类 Wildcard_catch中声明的Object
其中, CAP#1,CAP#2是新类型变量:
CAP#1从? extends Number的捕获扩展Number
CAP#2从? extends Number的捕获扩展Number
1 个错误如上错误信息,原因是类型变量匹配不上,这是因为泛型的类型擦除,实际上,擦除类型后,都是使用一个标志代表对应的类型,可以看到 SetData(T t) 中的 T 擦除后标志是 CAP#1 ,而 tmp.GetNumber() 对应的 T 会被标志为 CAP#2 ,而显然,在编译器看来,这两个标志是没有关联的,因此就需要借助辅助方法进行类型的确认
class Wildcard_catch<T> { private T data1; private T data2; public Wildcard_catch(T t1, T t2) { data1 = t1; data2 = t2; } public void SetData(T t1) { data1 = t1; } public T GetNumber() { return data2; } public static <T> void Catchhelp(Wildcard_catch<T> tmp) { tmp.SetData(tmp.GetNumber()); } public static void Catch(Wildcard_catch<? extends Number> tmp) { System.out.print("class = " + tmp.GetNumber().getClass().getName() + "\n"); Generic_demo_01.Catchhelp(tmp); } public static void main(String[] args) { Wildcard_catch<Integer> tmp = new Wildcard_catch<>(10, 20); Generic_demo_01.Catch(tmp); } }
- 借由辅助捕获方法,在调用 tmp 时指定了 T ,此时 tmp 类中的所有 T 都被明确指定,那么对应的 SetData(T t) 和 tmp.GetNumber() 的 T 就建立了联系,即 CAP#1 和 CAP#2 建立了关联,编译器就可以推断出相应的类型
-
14. 泛型类的继承关系
- 用一幅图来说明:
/* 下界的继承关系 */
List<? super Number> list1 = new ArrayList<>();
List<? super Integer> list2 = new ArrayList<>();
list2 = list1;
-
关于这个继承关系,我们把他们类比为集合的概念就会比较好理解:
- 如果存在D继承C,C继承B,B继承A,这样关系的四个类
- 那么:
List<? extends B>
即可以是:List<B>
、List<C>
、List<D>
List<? extends C>
即可以是:List<C>
、List<D>
List<? super B>
既可以是:List<A>
、List<B>
List<? super C>
既可以是:List<A>
、List<B>
、List<C>
- 由以上列举,可以发现:
List<? extends B>
是包含List<? extends C>
;List<? super C>
是包含List<? super B>