Java的泛型
发布日期:2021-10-22 14:27:10 浏览次数:4 分类:技术文章

本文共 9600 字,大约阅读时间需要 32 分钟。

版权声明:本文系博主原创,未经博主许可,禁止转载。保留所有权利。

引用网址:

 

目录

 

1. 泛型

1.1. 解决什么问题

如下的一个类它封装了一个String类型的值。

1 public class StringEntry { 2     private String value; 3  4     public void setValue(String value) { 5         this.value = value; 6     } 7  8     public String getValue() { 9         return value;10     }11 }

现在,我需要再封装一个Integer类型的一个很自然的想法是再定义一个IntegerEntry类。然而此法有不足之处,即如果我需要再封装一些其他类型的值,那么就需要再定义对应的这么多的类,哪怕它们的定义如此类似,即复用性不好。

 

考虑到Object类是所有类的超类可以定义一个类,用它来封装Object类型的值,定义了这个类,就不用再定义上面的许多类了

1 public class ObjectEntry { 2     private Object value; 3  4     public void setValue(Object value) { 5         this.value = value; 6     } 7  8     public Object getValue() { 9         return value;10     }11 }

这样的实现,也是早期Java实现集合框架采用的策略:继承多态

 然而,这个基于Object继承的多态实现有两个问题:一是get方法返回的是Object对象,使用该对象时需要显式地强制转换为我们需要的类型,二是使用set方法即使加入了我们不期望的类型的对象,编译器也不会产生任何的错误提示,我们取出该对象仍按期望的类型转换就会抛异常,即类型不安全

 

C++类型模板特性得到启发,JavaJava 5引入了泛型的概念以解决这个问题。就是在定义一个类时可以加入类型参数,使类成为泛型类

1 public class Entry
{ 2 private T value; 3 4 public void setValue(T value) { 5 this.value = value; 6 } 7 8 public T getValue() { 9 return value;10 }11 }

这样以后,在外部使用get方法时,如果用类型参数的实际类型的变量来接纳它,那么返回的就是我们需要的类型的实例,无须强制转换,因为编译器自动加入了类型转换指令(当然,如果没有变量接纳,而单纯调用get方法,则返回类型参数的边界类型的实例)。对应地,在外部使用set方法时如果传入的不是我们期望的类的实例,则会编译报错这是编译器在编译时自动进行类型检查的缘故总之,在使用泛型类时,编译器会帮我们自动完成类型检查和强制转换的工作

1.2. 泛型的用法

1.2.1. 声明泛型类的变量时可给出类型参数的实际类型

对于上面定义的Entry<T>,可以像下面这样使用。

1 public static void main(String[] args) {2     Entry
entry = new Entry
();3 entry.setValue("hello world");4 }

可以看到,声明泛型类的变量时可给出类型参数的实际类型。当然,这里的“给出类型参数的实际类型”“实际类型”可以是确定的,如这里的String,也可以不确定,而是一个范围,也就是后面要讲的通配符

1.2.2. 可为类型参数指定边界类型

可为类型参数指定边界类型,如下。

1 
>2

其中,extends后的类型为边界类型,可以是一个类或接口,后面还可以使用&”连接多个接口

<E>实际等效于<E extends Object>

指定边界类型的好处在于,可以确定类型参数具有边界类型的某些方法以便编译期就可以使用这些方法,如上面的T类型的实际类型必然具有compareTo方法

1.2.3. 泛型类可以有多个类型参数

所谓泛型类,是具有一个或多个类型参数的类

1 public class Pair
{ 2 private K key; 3 private V value; 4 5 public void setKey(K key) { 6 this.key = key; 7 } 8 9 public K getKey() {10 return key;11 }12 13 public void setValue(V value) {14 this.value = value;15 }16 17 public V getValue() {18 return value;19 }20 }

泛型类Pair<K, V>的类型参数为KV这里的KKey的首字母,代表的意思,VValue的首字母,代表的意思常用的类型参数名还有EElement)、TType)等当然其他字母也可以的。

 

泛型类使用如下。

1 Pair
pair = new Pair
()

1.2.4. 泛型方法的定义和使用

所谓泛型方法,是带有类型参数的方法既可以定义在泛型类中,也可以定义在普通类中

1 public class Util {2     public static 
boolean process(Pair
pair) {3 return true;4 }5 }

类型参数定义于修饰符的后面返回类型的前面之所以不放在方法名后是因为返回类型也可能是类型参数指定的类型

 

泛型方法的使用如下。

1 Util.
process(new Pair
())

或者

1 Util.process(new Pair
())

后一个利用了Java 1.7引入的类型推导。

1.2.5. 类型参数的继承关系不会传递给泛型类

1.2.5.1. 不同类型参数的泛型类实例无关系

虽然StringObject的子类,但是Entry<String>却不是Entry<Object>的子类,它们甚至都不是类与类的关系,它们都属于Entry的一种使用方式。也就是说,类型参数的继承关系不会传递给泛型类实例。在这里,我们不妨创造一个概念,就是泛型类实例,它不是类的概念,也不是对象的概念,乃是对泛型类的一种使用方式,也可以叫泛型类的使用模式不同的模式,对应着不同的类型参数,对应着不同的类型检查和类型转换。也就是说,类不能根据类型参数的多少和顺序来区分,类型参数仅只是使用方式的概念。

 

顺便看一个例子。

可以看出,Integer.class是一个对象,它的类型应该为Class<Integer>或其子类,这里有了一个新概念,对象可以属于某种使用方式。

1.2.5.2. 通配符的类型限定

上面说了,类型参数的继承关系并不会传递给泛型类实例,这有点不符我们的思维习惯。为解决这个问题,引入了通配符。

<? extends Object>的代码形式叫做通配符的子类型限定,通常用作协变还有通配符的超类型限定,代码形式<? super Integer>通常用作逆变。无限定的通配符为<?>

 

关于通配符的用法如下。

1 public Method getMethod(String name, Class
... parameterTypes)2 public static Class
forName(String className)3 public native Class
getSuperclass()4 Class
c = getSuperclass()5 public Class
asSubclass(Class clazz) {6 return (Class
) this7 public A getAnnotation(Class annotationClass)8 return (A) annotationData().annotations.get(annotationClass)

可以看出通配符?可用于方法参数、方法返回、变量类型声明、变量强制转换等处,即?用在泛型类或泛型方法的使用而不用在泛型类的定义上

 

Entry<Integer>在使用上来讲,是一个Entry<?>不是is-a”的关系,因为is-a是静态的类与类的概念。这也不是泛型类实例间的关系,而只能是一种泛型类实例属于某个泛型类实例范围的概念,通配符?的类型限定表达的正是范围的概念。

1.2.5.3. 协变与逆变

下面的语法有问题。对编译器而言,list的类型参数可以是任何Object的子类(编译期根据声明而非实例化来确定),是不确定的,是一个范围,现在给确定的String于它显然不合适,因为list的类型参数可以是Integer等。

 

下面的就没有问题了。对编译器而言,list的类型参数可以是任何String的超类,也是不确定的,现在给确定的String于它却是合适的,因为list的类型参数虽然不确定,但一旦确定下来,它总是String的超类,可以接受String类型的值。

这里的list是用来添加元素的,对元素来说,它是消费者,因而有Consumer Super一说。完整的理解就是:对于被“吞进”的元素来说,list是消费者,为了完成“吞进”工作,它应该使用通配符的超类型限定。

 

然而,这样的使用了superlist就不能成为Producer了,即不能用来“吐出”元素。类似分析,即编译器认为list的类型参数可以是任何String的超类,是不确定的,现在用String来接纳它显然不合适,因为list的类型参数可以是Object

 

解决的方法是,list又定义回使用extends,这就是Producer Extends一说的由来。

 

可以看到Producer ExtendsConsumer Super是不能兼容的,只能二居其一,这就是PECS原则。总结一下就是:对于集合类,如果是只读的,则为Producer,使用extends,为协变,如果是只写的,则为Consumer使用Super为逆变,进一步简记为“协读PE,逆写CS

下面看个例子,Collections类同时使用了两者。

1.2.5.4. 空型泛型类实例

Entry<T>来说,Entry有定义和使用两个方面的含义。作为定义泛型类的一面说,Entry被称为原始类型,作为使用泛型类的一面说,Entry等价于Entry<边界类型>,是确定的泛型类实例,不同于Entry<?>是一个泛型类实例范围,为了便于描述,不妨称它为空型泛型类实例,空型泛型类实例等价于边界类型的泛型类实例比如下面的list1,乃是List<Object>,因而添加String是可以的。但list2List<?>,可以是任何的类,现在给它赋值String,但它如果是List<Integer>就会出错。

 

再来看空型泛型类实例的字节码,处理是使用边界类型来的:

1 public class Demo {2     public static void main(String[] args) {3         List list1 = new ArrayList();4         list1.add("abc");5     }6 }

 

再看下面的例子:

1 public class Entry
{ 2 private T value; 3 4 public void setValue(T value) { 5 this.value = value; 6 } 7 8 public T getValue() { 9 return value;10 }11 }

Entry实际上等价于Entry<Number>,不等价于Entry<Object>,所以赋值String时出错。

1.2.6. 泛型使用的注意事项

1.2.6.1. 不能用基本类型来实例化类型参数

用则必须用它们的包装类。

这是因为,编译后类型参数被擦除,原始类中的类型变量(T)替换为Object,但Object类型不能存储基本类型的

1.2.6.2. 泛型类不能继承Throwable

但异常声明中可以使用类型参数

1.2.6.3. 不能创建组件类型为参数化类型的数组

Object[]可以是任何数组的父类

不能创建组件类型为参数化类型的数组,但可以声明

1 Pair
[] pairs = (Pair
[]) new Pair[10]

1.2.6.4. 泛型类静态成员不可使用泛型类的类型参数

1.2.6.5. 其他

下面是可以的:

1 public class Demo {2     public static void main(String[] args) {3         Entry
entry = new Entry
();4 System.out.println(entry instanceof Entry
);5 System.out.println(entry instanceof Entry);6 }7 }

但下面就不行:

1 entry instanceof Entry

 

总结来看,注意事项能根据原理解释几个就行,不必刻意去掌握全了。多提一嘴,因为这些规则终究是编译器或JVM的代码逻辑:有的是绝对不能怎样的,有的则是一种可与不可的选择,甚至是编码者的一个临时随意,他这样要求而已,而有些甚至是其功能实现的副作用,对于这样的原因,需要了解编译后的字节码或者JVM代码才能弄清。但这不总是有必要的,因为即便是编码者本人,也许都不清楚,而且近乎全部的使用都不会触及副作用。总之,只要能用其精髓,分析懂别人代码,遇到错能分析就已经足够,除非自己要搞一套编译器或JVM,做得更好,或者发生了某种平台性的bug,则才细究另论。

1.3. 泛型的实现原理

1.3.1. 类型擦除

1.3.1.1. 定义泛型类时对应类型参数的地方都用边界类型

先看下面的类。

1 public class Pair
{ 2 private K key; 3 private V value; 4 5 public Pair(K key, V value) { 6 this.key = key; 7 this.value = value; 8 } 9 10 public void setKey(K key) {11 this.key = key;12 }13 14 public K getKey() {15 return key;16 }17 18 public void setValue(V value) {19 this.value = value;20 }21 22 public V getValue() {23 return value;24 }25 }

 

编译器将泛型类所有的对应类型参数的地方替换为边界类型,而泛型类本身对应到原始类型,它是泛型类使用时的模板,以得到最终的字节码文件。

编译后,构造方法的字节码如下:

可以看到,参数key被保存为边界类型Number,而参数value被保存为默认的边界类型Objectset方法与之类似。

 

编译后,getKey方法的字节码如下

可以看到,字段存储使用的是Number类型,而方法描述符中返回类型也是Number

1.3.1.2. 使用泛型类使用类中会自动类型检查强制转换

来看对泛型类的使用:

1 public class Demo {2     public static void main(String[] args) {3         Integer key = 5;4         String value = "hello world";5         Pair
pair = new Pair
(key, value);6 Integer i = pair.getKey();7 }8 }

 

下面看main方法的字节码:

可以看到,在调用构造方法时,会把参数存为边界类型(set也类似)。在获取值时,自动加了一个类型转换指令,由Number转换为Integer。需要注意的是,如果编码时,构造方法参数或set方法参数类型不是实际类型的,则编译不过,因为编译器会作类型检查,如果编译通过,则还是通过边界类型存储,而不是实际类型。

 

总之,泛型类定义时,在字节码上只保留了原始类型和对边界类型的使用,当然也保留了它的类型参数标识。在泛型类使用时,编译器替我们完成了类型检查及强制转换的工作不同目标类型的类型检查及强制转换,完成了不同动态类型的区别功能,因而动态类型就没有必要而被“擦除”了,因而这种泛型的实现机制被称为类型擦除类型擦除也会发生于泛型方法中,如泛型方法public static <T extends Comparable> T min(T[] a)编译后会变成下面这样:public static Comparable min(Comparable[] a)

 

因为Java泛型是通过类型擦除实现的本质上是边界类型的继承多态实现的,而非每一个类型参数实例都对应一个类定义,因而是伪泛型伪泛型的好处在于兼容了之前的继承多态版本(如早期Java的集合框架就是使用Object继承多态实现的,伪泛型便于保持向下兼容),也可以防止类型膨胀。泛型是继基类继承、接口实现后的有一种泛化机制。

1.3.1.3. 通过泛型的实现原理分析一个实例

下面的代码没有报错:

ArrayList的类型参数指定为Integer,加入1是自然的,而加入字符串未报错又是为何?这是因为ArrayList使用了反射语法,添加操作已经不在ArrayList类里发生,在编译期都识别不出这是add操作,因而编译期检查通过了。运行的时候,调用的是add(Object),所以也没有问题。接下来,println方法需要的参数是Object,所以编译的字节码结果是arrayList.get(i)返回的是Object,没有强制类型转换操作。而最终打印出这个字符串,则是因为println一个Object的时候,通过invokevirtual指令调用了子类,即String类的重写toString方法。

 

但下面的报错了:

这是因为,虽然arrayList.get(i)获得的是Object,但马上的操作是getClass,编译器作了强制类型转换为实际的类型参数类型。原来通过Object类型保存的字符串,要转换为显式的Integer,就发生错误了。

1.3.2. 桥接方法

类型擦除会引入新问题。

 

看下面的类

1 public class IntegerStringPair extends Pair
{ 2 public IntegerStringPair(Integer key, String value) { 3 super(key, value); 4 } 5 6 @Override 7 public void setValue(String value) { 8 super.setValue(value); 9 }10 }

 

子类会从Pair类继承setValue方法,Pair类的这个方法如下:

可以看到这个方法的签名是public void setValue(Object),而子类的签名是public void setValue(String),即子类的本意是要覆写父类的该方法,但实际上变为了方法重载。

 

1 public class Demo {2     public static void main(String[] args) {3         Pair
pair = new IntegerStringPair(5, "hello world");4 pair.setValue("oh");5 }6 }

此时,pair.setValue(“oh”)调用的是父类的setValue(Object),因为发现子类没有覆写方法,所以调用到这里就结束了。这是违背动态调用指令invokevirtual和覆写语法的本意的。

 

为了弥补,编译器引入了桥接方法即在子类中覆写从泛型继承来的方法这是自动完成的,无需人工干预下面的第二个setValue

pair.setValue(“oh”)调用的就是子类的这个setValue(Object)了,可以看到这个桥接方法转换类型后,又调用子类的setValue(String),这样从外观效果上看pair.setValue(“oh”)调用了子类的setValue(String),这正是“桥接”的意义所在。顺便说一句,子类setValue(String)里面的super.setValue(value)调用父类的setValue(Object)则是与此无关的方法自身的逻辑了。

1.4. 参考资料

 

 

转载于:https://www.cnblogs.com/zhizaixingzou/p/9992847.html

转载地址:https://blog.csdn.net/weixin_30361641/article/details/95887881 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:子组件可以通过事件回调传递数据和操作到父组件
下一篇:VS2015 怎么安装RDLC报表模板?

发表评论

最新留言

第一次来,支持一个
[***.219.124.196]2024年04月02日 10时56分00秒