前言
整理一下Java泛型的相关知识,算是比较基础的,希望大家一起学习进步。
一、什么是Java泛型
Java 泛型(generics)是 JDK 5 中引入的一个新特性,其本质是参数化类型,解决不确定具体对象类型的问题。其所操作的数据类型被指定为一个参数(type parameter)这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
泛型类
泛型类(generic class) 就是具有一个或多个类型变量的类。一个泛型类的简单例子如下:
1. //常见的如T、E、K、V等形式的参数常用于表示泛型,编译时无法知道它们类型,实例化时需要指定。
2. public class Pair <K,V>{
3. private K first;
4. private V second;
5.
6. public Pair(K first, V second) {
7. this.first = first;
8. this.second = second;
9. }
10.
11. public K getFirst() {
12. return first;
13. }
14.
15. public void setFirst(K first) {
16. this.first = first;
17. }
18.
19. public V getSecond() {
20. return second;
21. }
22.
23. public void setSecond(V second) {
24. this.second = second;
25. }
26.
27. public static void main(String[] args) {
28. // 此处K传入了Integer,V传入String类型
29. Pair<Integer,String> pairInteger = new Pair<>(1, "第二");
30. System.out.println("泛型测试,first is " + pairInteger.getFirst()
31. + " ,second is " + pairInteger.getSecond());
32. }
33. }
运行结果如下:
1. 泛型测试,first is 1 ,second is 第二
泛型接口
泛型也可以应用于接口。
1. public interface Generator<T> {
2. T next();
3. }
实现类去实现这个接口的时候,可以指定泛型T的具体类型。
指定具体类型为Integer的实现类:
1. public class NumberGenerator implements Generator<Integer> {
2.
3. @Override
4. public Integer next() {
5. return new Random().nextInt();
6. }
7. }
指定具体类型为String的实现类:
1. public class StringGenerator implements Generator<String> {
2.
3. @Override
4. public String next() {
5. return "测试泛型接口";
6. }
7. }
泛型方法
具有一个或多个类型变量的方法,称之为泛型方法。
1. public class GenericMethods {
2.
3. public <T> void f(T x){
4. System.out.println(x.getClass().getName());
5. }
6.
7. public static void main(String[] args) {
8. GenericMethods gm = new GenericMethods();
9. gm.f("字符串");
10. gm.f(666);
11. }
12. }
运行结果:
1. java.lang.String
2. java.lang.Integer
二、泛型的好处
Java语言引入泛型的好处是安全简单。泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。
我们先来看看一个只能持有单个对象的类。
1. public class Holder1 {
2. private Automobile a;
3.
4. public Holder1(Automobile a) {
5. this.a = a;
6. }
7.
8. public Automobile getA() {
9. return a;
10. }
11. }
我们可以发现,这个类的重用性不怎样。要使它持有其他类型的任何对象,在jdk1.5泛型之前,可以把类型设置为Object,如下:
1. public class Holder2 {
2. private Object a;
3.
4. public Holder2(Object a) {
5. this.a = a;
6. }
7.
8. public Object getA() {
9. return a;
10. }
11.
12. public void setA(Object a) {
13. this.a = a;
14. }
15.
16. public static void main(String[] args) {
17. Holder2 holder2 = new Holder2(new Automobile());
18. //强制转换
19. Automobile automobile = (Automobile) holder2.getA();
20. holder2.setA("测试泛型");
21. String s = (String) holder2.getA();
22. }
23. }
我们引入泛型,实现功能那个跟Holder2类一致的Holder3,如下:
1. public class Holder3<T> {
2.
3. private T a;
4.
5. public T getA() {
6. return a;
7. }
8.
9. public void setA(T a) {
10. this.a = a;
11. }
12.
13. public Holder3(T a) {
14. this.a = a;
15. }
16.
17. public static void main(String[] args) {
18. Holder3<Automobile> holder3 = new Holder3<>(new Automobile());
19. holder3.setA("测试泛型");
20. Automobile automobile = holder3.getA();
21. }
22. }
因此,泛型的好处很明显了:
- 不用强制转换,因此代码比较简洁;(简洁性)
- 代替Object来表示其他类型对象,与ClassCastException异常划清界限。(安全性)
- 泛型使代码可读性增强。(可读性)
三、泛型通配符
我们定义泛型时,经常碰见T,E,K,V,?等通配符。本质上这些都是通配符,是编码时一种约定俗成的东西。当然,你换个A-Z中另一个字母表示没有关系,但是为了可读性,一般有以下定义:
- ? 表示不确定的 java 类型
- T (type) 表示具体的一个java类型
- K V (key value) 分别代表java键值中的Key Value
- E (element) 代表Element
为什么需要引入通配符呢,我们先来看一个例子:
1. class Fruit{
2. public int getWeigth(){
3. return 0;
4. }
5. }
6. //Apple是水果Fruit类的子类
7. class Apple extends Fruit {
8. public int getWeigth(){
9. return 5;
10. }
11. }
12.
13. public class GenericTest {
14. //数组的传参
15. static int sumWeigth(Fruit[] fruits) {
16. int weight = 0;
17. for (Fruit fruit : fruits) {
18. weight += fruit.getWeigth();
19. }
20. return weight;
21. }
22.
23. static int sumWeight1(List<? extends Fruit> fruits) {
24. int weight = 0;
25. for (Fruit fruit : fruits) {
26. weight += fruit.getWeigth();
27. }
28. return weight;
29. }
30. static int sumWeigth2(List<Fruit> fruits){
31. int weight = 0;
32. for (Fruit fruit : fruits) {
33. weight += fruit.getWeigth();
34. }
35. return weight;
36. }
37.
38. public static void main(String[] args) {
39. Fruit[] fruits = new Apple[10];
40. sumWeigth(fruits);
41. List<Apple> apples = new ArrayList<>();
42. sumWeight1(apples);
43. //报错
44. sumWeigth2(apples);
45. }
46. }
我们可以发现,Fruit[]与Apple[]是兼容的。 List<Fruit>
与 List<Apple>
不兼容的,集合List是不能协变的,会报错,而 List<Fruit>
与 List<?extendsFruits>
是OK的,这就是通配符的魅力所在。通配符通常分三类:
- 无边界通配符,如
<?>
- 上边界限定通配符,如
<?extendsE>
; - 下边界通配符,如
<?superE>
;
?无边界通配符
无边界通配符,它的使用形式是一个单独的问号: List<?>
,也就是没有任何限定。
看个例子:
1. public class GenericTest {
2.
3. public static void printList(List<?> list) {
4. for (Object object : list) {
5. System.out.println(object);
6. }
7. }
8.
9. public static void main(String[] args) {
10. List<String> list1 = new ArrayList<>();
11. list1.add("A");
12. list1.add("B");
13. List<Integer> list2 = new ArrayList<>();
14. list2.add(100);
15. list2.add(666);
16. //报错,List<?>不能添加任何类型
17. List<?> list3 = new ArrayList<>();
18. list3.add(666);
19. }
20. }
无界通配符 (<?>)
可以适配任何引用类型,看起来与原生类型等价,但与原生类型还是有区别,使用无界通配符则表明在使用泛型 。同时, List<?>list
不可以添加任何类型,因为并不知道实际是哪种类型。但是List list因为持有的是Object类型对象,所以可以add任何类型的对象。
上边界限定通配符 < ? extends E>
使用 <?extendsFruit>
形式的通配符,就是上边界限定通配符。 extends关键字表示这个泛型中的参数必须是 E 或者 E 的子类,请看demo:
1. class apple extends Fruit{}
2. static int sumWeight1(List<? extends Fruit> fruits) {
3. int weight = 0;
4. for (Fruit fruit : fruits) {
5. weight += fruit.getWeigth();
6. }
7. return weight;
8. }
9. public static void main(String[] args) {
10. List<Apple> apples = new ArrayList<>();
11. sumWeight1(apples);
12. }
但是,以下这段代码是不可行的:
1. static int sumWeight1(List<? extends Fruit> fruits){
2. //报错
3. fruits.add(new Fruit());
4. //报错
5. fruits.add(new Apple());
6. }
- 在
List<Fruit>
里只能添加Fruit类对象及其子类对象(如Apple对象,Oragne对象),在 List<Apple>
里只能添加Apple类和其子类对象。 - 我们知道
List<Fruit>、List<Apple>
等都是List<? extends Fruit>的子类型。假设一开始传参是 List<Fruit>list
,两个添加没问题,那如果传来 List<Apple>list
,添加就失败了,编译器为了保护自己,直接禁用添加功能了。 - 实际上,不能往
List<?extendsE>
添加任意对象,除了null。
下边界限定通配符 < ? super E>
使用 <?superE>
形式的通配符,就是下边界限定通配符。 super关键字表示这个泛型中的参数必须是所指定的类型E,或者是此类型的父类型,直至 Object。
1. public class GenericTest {
2.
3. private static <T> void test(List<? super T> dst, List<T> src){
4. for (T t : src) {
5. dst.add(t);
6. }
7. }
8.
9. public static void main(String[] args) {
10. List<Apple> apples = new ArrayList<>();
11. List<Fruit> fruits = new ArrayList<>();
12. test(fruits, apples);
13. }
14. }
可以发现, List<?superE>
添加是没有问题的,因为子类是可以指向父类的,它添加并不像 List<?extendsE>
会出现安全性问题,所以可行。
四、泛型擦除
什么是类型擦除
什么是Java泛型擦除呢? 先来看demo:
1. Class c1 = new ArrayList<Integer>().getClass();
2. Class c2 = new ArrayList<String>().getClass();
3. System.out.println(c1 == c2);
4. /* Output
5. true
6. */
日常开发中, ArrayList<Integer>
和 ArrayList<String>
很容易被认为是不同的类型。但是这里输出结果是true,这是因为Java泛型是使用擦除实现的,不管是 ArrayList<Integer>()
还是 newArrayList<String>()
,在编译生成的字节码中都不包含泛型中的类型参数,即都擦除成了ArrayList,也就是被擦除成“原生类型”,这就是泛型擦除。
类型擦除底层
Java泛型在编译期完成,它是依赖编译器实现的。其实,编译器主要做了这些工作:
- set()方法的类型检验
- get()处的类型转换,编译器插入了一个checkcast语句,
再看个例子:
1. public class GenericTest<T> {
2.
3. private T t;
4.
5. public T get() {
6. return t;
7. }
8.
9. public void set(T t) {
10. this.t = t;
11. }
12.
13. public static void main(String[] args) {
14. GenericTest<String> test = new GenericTest<String>();
15. test.set("jay@huaxiao");
16. String s = test.get();
17. System.out.println(s);
18. }
19. }
20. /* Output
21. jay@huaxiao
22. */
javap -c GenericTest.class反编译GenericTest类可得
1. public class generic.GenericTest<T> {
2. public generic.GenericTest();
3. Code:
4. 0: aload_0
5. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
6. 4: return
7.
8. public T get();
9. Code:
10. 0: aload_0
11. 1: getfield #2 // Field t:Ljava/lang/Object;
12. 4: areturn
13.
14. public void set(T);
15. Code:
16. 0: aload_0
17. 1: aload_1
18. 2: putfield #2 // Field t:Ljava/lang/Object;
19. 5: return
20.
21. public static void main(java.lang.String[]);
22. Code:
23. 0: new #3 // class generic/GenericTest
24. 3: dup
25. 4: invokespecial #4 // Method "<init>":()V
26. 7: astore_1
27. 8: aload_1
28. 9: ldc #5 // String jay@huaxiao
29. 11: invokevirtual #6 // Method set:(Ljava/lang/Object;)V
30. 14: aload_1
31. 15: invokevirtual #7 // Method get:()Ljava/lang/Object;
32. 18: checkcast #8 // class java/lang/String
33. 21: astore_2
34. 22: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
35. 25: aload_2
36. 26: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
37. 29: return
38. }
- 看第11,set进去的是原始类型Object(#6);
- 看第15,get方法获得也是Object类型(#7),说明类型被擦出了。
- 再看第18,它做了一个checkcast操作,是一个String类型,强转。
五、泛型的限制与局限
使用Java泛型需要考虑以下一些约束与限制,其实几乎都跟泛型擦除有关。
不能用基本类型实例化类型化参数
不能用类型参数代替基本类型。因此, 没有 Pair<double>
, 只 有 Pair<Double>
。 当然, 其原因是类型擦除。擦除之后, Pair 类含有 Object 类型的域, 而 Object 不能存储 double值。
运行时类型查询只适用于原始类型
如,getClass()方法等只返回原始类型,因为JVM根本就不知道泛型这回事,它只知道原始类型。
1. if(a instanceof Pair<String>) //ERROR,仅测试了a是否是任意类型的一个Pair,会看到编译器ERROR警告
2.
3. if(a instanceof Pair<T>) //ERROR
4.
5. Pair<String> p = (Pair<String>) a;//WARNING,仅测试a是否是一个Pair
6.
7. Pair<String> stringPair = ...;
8. Pair<Employee> employeePair = ...;
9. if(stringPair.getClass() == employeePair.getClass()) //会得到true,因为两次调用getClass都将返回Pair.class
不能创建参数化类型的数组
不能实例化参数化类型的数组, 例如:
1. Pair<String>[] table = new Pair<String>[10]; // Error
不能实例化类型变量
不能使用像 new T(...),newT[...] 或 T.class 这样的表达式中的类型变量。例如, 下面的 Pair<T>
构造器就是非法的:
1. public Pair() { first = new T(); second = new T(); } // Error
使用泛型接口时,需要避免重复实现同一个接口
1. interface Swim<T> {}
2.
3. class Duck implements Swim<Duck> {}
4.
5. class UglyDuck extends Duck implements Swim<UglyDuck> {}
可以消除对受查异常的检查
1. @SuppressWamings("unchecked")
2. public static <T extends Throwable〉void throwAs(Throwable e) throws T { throw (T) e; }
定义API返回报文时,尽量使用泛型;
1. public class Response<T> extends BaseResponse {
2. private static final long serialVersionUID = -xxx;
3.
4. private T data;
5.
6. private String code;
7.
8. public Response() {
9. }
10.
11. public T getData() {
12. return this.data;
13. }
14.
15. public void setData(T data,String code ) {
16. this.data = data;
17. this.code = code;
18. }
19. }
六、Java泛型常见面试题
Java泛型常见几道面试题
- Java中的泛型是什么 ? 使用泛型的好处是什么?(第一,第二小节可答)
- Java的泛型是如何工作的 ? 什么是类型擦除 ? (第四小节可答)
- 什么是泛型中的限定通配符和非限定通配符 ? (第三小节可答)
- List和List 之间有什么区别 ?(第三小节可答)
- 你了解泛型通配符与上下界吗?(第三小节可答)
个人公众号
- 如果你是个爱学习的好孩子,可以关注我公众号,一起学习讨论。
- 如果你觉得本文有哪些不正确的地方,可以评论,也可以关注我公众号,大家一起学习进步哈。