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