1. 什么是泛型
Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。
2. 泛型的作用
在泛型出现以前,我们考虑这么一个场景,当我们使用List只是存储字符串数据的时候,在泛型没有出现之前,则对应的代码如下:
List data = new ArrayList(); data.add("233"); data.add(123); // 加入整型数据 String s = (String) data.get(1);
在上面中,通过ArrayList创建了一个集合,这种声明方式主要有以下缺点:
- 存储数据的时候无法做数据类型校验
- 取数据的时候需要判断数据类型,以及需要做数据类型的转换
- 在实际使用过程中,易出错
基于以上的问题,当我们需要共用代码的时候,并且对输入数据类型做校验时,就引入了泛型。
3. 泛型的使用方式
泛型的基本使用主要包含了三种方式:泛型类,泛型接口,泛型方法
3.1 泛型类
下面通过一个简单的实例查看泛型类的使用:
public class GenericType<T>{ private T value; public GenericType(T value) { this.value = value; } public T getValue() { return this.value; } public void setValue(T t) { this.value = t; } }
在这个实例中,主要包含了以下几个部分:
- T只是标识,代表了具体的类型
- 类中包含了T的成员变量,该成员变量的类型是在泛型类定义的时候确定
- 在方法中可以根据泛型类型设置值和取值,也是根据泛型类定义的时候确认
通过以上的类型定义,我们具体的使用方式为:
public static void main(String[] args) { GenericType<Long> type = new GenericType<Long>(2L); type.setValue(123L); type.setValue("2323"); // 类型校验异常,报错 System.out.println(type.getValue()); // 123 }
通过泛型的定义,我们通过<>
的方式为泛型指定具体的类型,因此我们在使用setValue()
方法可以帮助我们对类型进行校验,当我们设置setValue("2323")
时,将会导致编译错误。
在泛型类型的时候,我们也可以同时为设置多个泛型标记,例如:
public class MultiGenericType<K, V, T> { private K key; private V val; private T t; public MultiGenericType(K key, V val, T t) { this.key = key; this.val = val; this.t = t; } public K getKey() { return key; } public void setKey(K key) { this.key = key; } public V getVal() { return val; } public void setVal(V val) { this.val = val; } public T getT() { return t; } public void setT(T t) { this.t = t; } }
则使用方式和单个泛型的使用方式一样,对应的使用方式为:
public static void main(String[] args) { MultiGenericType<String, String, Integer> type = new MultiGenericType<>("2", "6", 3); type.setKey("234"); type.setT(23); type.setVal("34"); }
3.2 泛型接口
其实接口也是一个类型,所以泛型接口的使用方式和类型的使用方式是一样的,使用实例如下:
public interface GenericInterface<T> { T getVal(); }
则在使用该泛型接口的时候,可以传递具体类型或者也可以传递泛型标识,例如:
public class GenericInterfaceImpl implements GenericInterface<Integer> { @Override public Integer getVal() { return null; } } class GenericInterfaceImpl2<T> implements GenericInterface<T> { @Override public T getVal() { return null; } }
3.3 泛型方法
泛型方法与类中的泛型声明是独立的体系,在实体方法或者静态方法上都可以声明泛型,例如:
public class GenericType<T>{ private T value; public GenericType(T value) { this.value = value; } public GenericType() {} public T getValue() { return this.value; } public void setValue(T t) { this.value = t; } /** * * @param clazz 泛型T的class对象 * @return T 确定了返回值为泛型类型 * @param <T> 声明泛型类型 */ public static <T> T getObject(Class<T> clazz) { try { return clazz.newInstance(); } catch (InstantiationException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } /** * * @param clazz 泛型T的class对象 * @return T 确定了返回值为泛型类型 * @param <T> 声明泛型类型 */ public <T> T get(Class<T> clazz) { return getObject(clazz); } }
为了证明泛型方法和泛型类的关系,在上面的实例中,在泛型类中定义了两个泛型方法:
- <T>表明了对应的方法为泛型方法,当在方法上声明就是泛型方法,在类型上声明就是泛型类
- 方法返回值T, 表明了方法的返回值为T,并且根据定义的实际调用的类型返回
- 方法参数Class<T>: 这里定义了T的具体的class对象,泛型T是无法被实例化的,因此需要具体的class对象创建T的具体实例
4. 泛型擦除
在泛型被编译之后,实际在编译后的字节码中是看不到泛型泛型信息,例如我们有如下泛型类声明:
public class Reference<T> { private T value; public void setValue(T value) { this.value = value; } public T getValue() { return value; } }
我们通过查看字节码信息“javap -c Reference.class“, 有如下信息:
以上字节码编译过后可以看到,最终在字节码中存储的是Object的类型。
在上面泛型方法中,也可以通过javap -c GenericType.class
的命令查看编译后的字节码信息:
通过以上的两个字节码的查看,证实了泛型在编译后被擦除的事实,实际上存储的是Object对象。但是在具有上边界和下边界上,却有些不同的地方,将在下面中介绍。
泛型擦除本身具有写局限性:
- <T>不能是基本类型,因为实际类型是Object, int无法转换为Object类型,只能使用Integer
- 无法获取带有泛型的Class对象
- 无法判断带泛型的类型。例如:t instanceof Pair<String>. 这种写法是不被允许的
- 不能实例化泛型标识T
- 不恰当的覆写方法。例如:
public boolean equals(T t)
; 这个方法是不被允许的,因为T最终会被编译成为Object对象,而equals方法来自于Object对象,因此这种覆写不会被允许。
5. 泛型的继承和子类型
在Java中,只要类型存在继承或者实现关系,则可以将子类分配给父类使用。这也是多态使用的一种方式,在泛型中,这种多态也是支持。例如定义一下泛型类:
package com.java.demo.generic; /** * @author xianglujun * @date 2023/2/27 16:39 */ public class GenericSubTypeDemo<T> { private T val; public GenericSubTypeDemo() {} public GenericSubTypeDemo(T val) { this.val = val; } public T getVal() { return val; } public void setVal(T val) { this.val = val; } public static void main(String[] args) { GenericSubTypeDemo<Number> demo = new GenericSubTypeDemo<>(); demo.setVal(12); // 设置Integer demo.setVal(12L); // 设置Long demo.setVal(123.3D); // 设置Double GenericSubTypeDemo<Integer> intDemo = new GenericSubTypeDemo<>(); demo = intDemo; // 编译错误 } }
因为Integer, Double, Long都是Number的子类,因为向Number中设置值都是正常的。但是GenericSubTypeDemo<Integer>与GenericSubTypeDemo<Number>之间并不存在继承关系,因此不能直接赋值。
6. 泛型类及其子类
在上面的实例中,泛型类或者泛型接口是可以被继承或者实现的,我们以JDK框架中的Collection为例:
public static void main(String[] args) { ArrayList<String> arrayList = new ArrayList<>(); List<String> list = arrayList; Collection<String> collection = list; }
在泛型类继承和实现上,只要他们的泛型类型一致,则本身的继承关系没有发生改变。则对应关系为:
7. 泛型上下边界
考虑一下类型,当我们实现两个数相加时,具体实现如下:
public class NumberCountUtil { public static double add(Pair<Number> pair) { return pair.getFirst().doubleValue() + pair.getLast().doubleValue(); } public static class Pair<T> { private T first; private T last; public Pair(T first, T last) { this.first = first; this.last = last; } public T getFirst() { return first; } public T getLast() { return last; } } }
在实际调用的时候,通过Pair<Number>是可以正常编译和运行的:
double sum = add(new Pair<Number>(1,3));
但是当将参数替换为Pair<Integer>时,则会编译出错:
double sum = add(new Pair<Integer>(1,3));
编译时提示:
java: 不兼容的类型: com.java.demo.generic.NumberCountUtil.Pair<java.lang.Integer>无法转换为com.java.demo.generic.NumberCountUtil.Pair<java.lang.Number>
这是因为,我们在上面讨论继承关系时,Pair<Integer>并不是Pair<Number>的子类,因此不能够直接传入参数。
7.1 泛型上边界extends
为了解决以上的问题,我们可以通过<? extends Number>
方式来确定泛型的上边界,该边界使得接收的参数类型变得更加广泛,可以接收:
- Number本身作为参数
- Number的子类作为参数
我们将上面add方法进行改造,让add方法能够接收所有数字类型的参数,并进行计算:
public static double add(Pair<? extends Number> pair) { return pair.getFirst().doubleValue() + pair.getLast().doubleValue(); }
则我们使用时,则Pair<Integer>也可以作为参数传递:
double sum = add(new Pair<Integer>(1,3));
我们再将add方法做些改变,修改对应的声明如下:
public static double add(Pair<? extends Number> pair) { pair.setFirst(new Integer(pair.getFirst().intValue() + 100)); // 编译报错 return pair.getFirst().doubleValue() + pair.getLast().doubleValue(); }
这里会导致在编译的时候无法通过,主要原因:
- 当在调用add方法的时候传入Pair<Double>时,这时是满足上限的约定的
- 但是在方法中继续调用setFirst方法的时候,Pair<Double>和Pair<Integer>两者类型不匹配会导致异常
这里就引出了泛型上边界的一个限制
方法参数签名
setFirst(? extends Number)
无法传递任何Number
的子类型给setFirst(? extends Number)
。
通过上面的分析,我们的到泛型边界的一个特别重要的作用:
在传递的方法参数的时候,可以防止方法向集合中新增元素,或者修改同样具有泛型边界定义的元素的值,这是对对象的数据具有一定的保护作用。
当我们对上面的代码进行反编译如下:
可以看出,泛型上边界<? extends Number>使用的时候,在编译时,使用的是Number类型,因此和不指定上边界时,使用Object存在一定的差别。
7.2 泛型下边界super
在泛型上边界中,我们知道Pair<Integer>并不是Pair<Number>的子类,因此在如下方法中:
public static void set(Pair<Integer> pair, Integer first, Integer last) { pair.setFirst(first); pair.setLast(last); }
在使用set方法的时候,我们不能传入Pair<Number>的定义,然后正确执行该方法。
在java中可以使用泛型下边界super来实现, 这样我们就可以传入Pair<Number>, Pair<Object>, Pair<Integer>这样的参数。我们使用super改造上面的方法:
public static void set(Pair<? super Integer> pair, Integer first, Integer last) { pair.setFirst(first); pair.setLast(last); }
通过super改造后的方法,则表示了可以接收Integer以及Integer的父类声明的Pair类型
则我们可以正常使用一下代码:
public static void main(String[] args) { Pair<Integer> pair = new Pair<Integer>(1,3); double sum = add(pair); set(pair, 1, 3); Pair<Number> numberPair = new Pair<>(23, 14); set(numberPair, 12, 16); }
这里重点关注下Pair中的方法,在上面的声明中,实际上对应的setFirst的方法为:
public void setFirst(? super Integer)
因此,这个时候我们传入Integer类型的参数进入是没有问题,当我们尝试在执行中加入一下代码时:
Integer l = pair.getLast();
将会造成编译报错,报错信息为:
java: 不兼容的类型: capture#1, 共 ? super java.lang.Integer无法转换为java.lang.Integer
这里主要原因在于super的用法,我们考虑在向setFirst设置参数时,是可以设置Number, Integer, Object类型的参数,我们设置Number的参数的时候,然后getFirst用Integer接收,将导致类型的转换异常,因此这里不能直接使用Integer来接收结果值。但是可以通过Object来接收结果。
因此,以上报错的地方,可以修改为:
Object l = pair.getLast();
因此,这里将不会产生编译异常。这主要是考虑到了类型的转换带来的隐藏的问题。
因此,我们可以得出结论,在<? super Integer>声明的泛型,在方法内部只能够写,不能够读。当然这里要除开Object读的情况。
我们还是将字节码文件进行反编译,看下编译器如何处理super这种下边界的限制:
下边界编译的处理,最终是处理成为了Object类型,这里也可以解释为什么getLast方法不能直接使用Integer来接收了。
7.3 对比extends和super通配符
<? extends T>
类型和<? super T>
类型的区别在于:
<? extends T>
允许调用读方法T get()
获取T
的引用,但不允许调用写方法set(T)
传入T
的引用(传入null
除外);<? super T>
允许调用写方法set(T)
传入T
的引用,但不允许调用读方法T get()
获取T
的引用(获取Object
除外)。
一个是允许读不允许写,另一个是允许写不允许读。
7.4 PECS原则
在具体使用场景中,extends和super两者该如何选择呢?主要遵循:Producer Extends Consumer Super:
- 如果需要返回泛型标识T,则为生产者,这个时候就需要使用extends进行声明
- 如果需要写入标识T,则为消费者,这时就需要使用super声明
可以查看下Collections#copy方法:
public static <T> void copy(List<? super T> dest, List<? extends T> src) { .. }
在这个实例中:
- 需要返回T的src是生产者,因此使用extends声明
- 需要写入T的dest是消费者,因此使用super声明
7.4 无限通配符
无限通配符是对泛型没有做限定,例如声明List<?>, Pair<?> 这种就是没有限定的通配符。这种通配符在没有指定extends或者super的时候,同事具有两者的缺点:
- 不允许调用
set(T)
方法并传入引用(null
除外); - 不允许调用
T get()
方法并获取T
引用(只能获取Object
引用)。
public static void set(Pair<?> pair, Integer obj) { pair.setLast(obj); // 编译异常 pair.setLast(null); // 编译通过 Object r = pair.getLast(); // 正常 Integer i = pair.getLast(); // 编译异常 }
因此这种实现,既不能实现写入,也不能读。这可以通过这种方式判断值是否为null。
在大多数情况下,<?>可以使用<T>来进行替换。
但是<?>有个最大的特点,
Pair<?>
是所有Pair<T>
的超类:也就是说,所有的Pair泛型都可以赋值给Pair<?>
8. 泛型的多态
多态为java的特性,泛型的多态说的就是在类中声明了泛型,然后子类继承或者实现泛型类。我们定义泛型类如下:
public class ObjReference<T> { private T val; public void setVal(T val) { this.val = val; } public T getVal() { return this.val; } }
定义一个子类:
public class IntegerReference extends ObjReference<Integer> { @Override public void setVal(Integer val) { super.setVal(val); } @Override public Integer getVal() { return super.getVal(); } }
从上面可以看出,在泛型多态上面,其实是方法的重载,而不是重写。如果是方法的重新,那么根据泛型的擦除,那么父类中的方法定义为:
public void setVal(Object obj) {}
那么,我们可以写一个类,看能否调用到父类方法:
public static void main(String[] args) { IntegerReference reference = new IntegerReference(); reference.setVal(1); reference.setVal(new Object()); // 编译报错 }
从调用方法可以知道,直接调用父类的Object方法会导致编译报错,那么可以确定确实是方法重载。那么jvm是如何解决这样的事情的呢?答案就是桥接方法
我们通过javap的命令,IntegerReference类型的字节码反编译,得到以下的信息:
在上面的字节码反编译后,我们可以看到setVal和getVal方法分别有两个。首先我们来看setVal方法
setVal(Integer)和setVal(Object)两个方法,而setVal(Object)方法是由编译器生成,在指令中,可以看到做了这么几件事情:
- 类型的检查,判断传入的类型是否为Integer类型
- 类型检查通过后,调用setVal(Integer)方法执行
这就是桥接方法的意义,主要作用就在于最终调用实现类的重载方法,这也是JVM为了解决泛型方法重载所采用的策略。
这里我们主要关注下getVal方法,可以看到getVal方法的定义其实很像,唯一不同在于其返回值:
public Integer getVal() {} public Object getVal() {}
在我们平常的开发中,这样的方法定义其实会导致编译不通过的,但是JVM在为了解决这种重载策略时,却能够使用这种定义,这主要是因为JVM中对方法唯一性定义是方法名称+参数+返回值,因此这种编译器加入的代码,在JVM也是能够通过的。
以上就是关于泛型知识的内容,后面将主要介绍泛型和反射的知识,主要讲解集中Type的使用和如何获取到反正真正的类型。
参考文章
- https://www.liaoxuefeng.com/wiki/1252599548343744/1265105920586976
- https://waylau.gitbooks.io/essential-java/content/docs/generics.html
- https://pdai.tech/md/java/basic/java-basic-x-generic.html