java泛型深度解读
简介
泛型是Java SE 1.5的新特性,泛型的本质是参数化类型 ( type parameters )
,也就是说所操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法的创建中.
在泛型类中定义参数化类型,在泛型表达式中,需要指定具体类型,即泛型在使用过程中将会被替换为具体的类型.
// 定义 参数类型 class ArrayList<E> // 使用中 指定具体类型 ArrayList<String> list = new ArrayList<>();
原始类型(raw type)
: 就是去掉参数类型后的类,如示例中的ArrayList
.
为什么需要泛型
我们来看一个例子:
List list = new ArrayList(); // 下列的添加方法完全没问题 list.add("one"); list.add(1); // 取的时候, 如果你小心的,也没问题 // 需要强转, 内部是以Object引用来存放 String s = (String) list.get(0); int i = (int) list.get(1); // 但是如果, 不小心在获取时 类型判断出错的话 for (int index = 0; index < list.size(); index++) { String str = (String) list.get(index); // index = 1时, 抛出java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String }
上诉方式,有两个问题.
- 由于java是静态语言,应该尽量避免在一个容器数组中,添加不相干的类型实例.否则可能引起类型转换错误.
- 这种方式,没有类型检查,只能够在运行时候,系统抛出异常后,你才会发现错误.
接下来使用泛型:
List<Animal> list = new ArrayList<>(); // 可以添加Animal及其子类 list.add(new Animal()); list.add(new Tiger()); // 编译器进行类型检査,避免插人错误类型的对象 // 编译时期报错, list.add("one");
可以看出,泛型只允许添加 声明的类及其子类,其他无关类无法加入到list中,并且尝试将其他类型加入列表,将在编译时直接报错.
由此可以看出泛型的特点:
- 能够对类型进行限定
- 在编译期对类型进行检查,编译时报错
- 对于获取明确的限定类型,无需进行强制类型转化
- 具有良好的可读性和安全性
泛型类
一个泛型类 ( generic class ) 就是具有一个或多个类型变量的类.定义的变量用尖括号 <>
括起来,放在类名的后面.
public class Holder<T> { private T obj; public Holder(T t) { obj = t; } public void put(T t) { obj = t; } public T get() { return obj; } }
泛型定义的类型变量,可以在 成员变量, 方法参数, 局部变量, 方法返回值中使用.
要注意的是,静态变量和静态方法中,不能使用类中定义的泛型参数.
这里的 T
可以代表任意类型(Object或其子类),需要注意的是,基本数据类型不能够使用泛型,需要使用它们对应的包装类(wrapper Class)
用具体的类型替换类型变量就可以实例化泛型类型,如
Holder<String> holder = new Holder<>();
泛型接口
泛型接口与泛型类对比区别别是,泛型接口中不能使用 类型参数作为成员变量.
泛型类的继承
当父类为泛型类或者接口时,子类可以使用具体类型来继承父类,也可以使用类型参数继承父类
public interface Parent<T> { ... // ====== // 使用具体类型来继承父类 public class Son implements Parent<Animal> { ... // ====== // 使用类型参数继承父类 public class Son<E> implements Parent<E> { ...
但是要注意, 一个类不能实现同一个泛型接口的两种变体,由于类型擦除的原因,这两个变体会成为县宫廷的接口
// Error public class Son implements Parent<Animal> { ... // ========= Error public class Child extends Son implements Parent<String> { ...
这种方式,Child
是实现了Parent<Animal>
和Parent<String>
,是不允许的.
泛型方法
除了泛型类,还可以声明一个泛型方法. 泛型方法可以在泛型类中声明,也可以在普通方法中声明.
注意的是,静态方法中,只能使用方法中定义的类型参数,而不能使用泛型类中的类型参数.
// 普通类中的泛型方法 public class Normal { // 成员泛型方法 public <E> String getString(E e) { return e.toString(); } // 静态泛型方法 public static <V> void printString(V v) { System.out.println(v.toString()); } } // 泛型类中的泛型方法 public class Generics<T> { // 成员泛型方法 public <E> String getString(E e) { return e.toString(); } // 静态泛型方法 public static <V> void printString(V v) { System.out.println(v.toString()); } }
一个原则:在能达到目的的情况下,尽量使用泛型方法。即,如果使用泛型方法可以取代将整个类泛化,那么应该有限采用泛型方法。
泛型类中的参数类型和泛型方法中的参数类型,即使声明为相同的类型参数,如T,两者的类型不会相互影响,甚至可以说没有任何关联.方法中的类型,由传入的参数决定,与泛型类的类型无关.
// 泛型类中的类型参数 用 T 表示 public class Holder<T> { ... // 成员泛型方法, 声明的类型参数,也用 T 表示 public <T> String getString(T t) { return t.toString(); } } public static void main(String[] args) { // 泛型类为 Animal Holder<Animal> holder = new Holder<>(new Animal()); // 泛型方法为 Vegetation String s = holder.getString(new Vegetation()); System.out.println(s); // I'm Vegetation }
泛型方法的使用过程中,无需对类型进行声明,它可以根据传入的参数,自动判断.
public class Main { public static void main(String[] args) { // 指定类型 Main.<String>printString("one"); // 不指定,自动推倒 Main.printString("two"); } static <T> void printString(T t) { System.out.println(t.toString()); } }
类型变量的限定
对于类型变量没有限定的泛型类或方法, 它是默认继承自Object
,当没有传入具体类型时,它有的能力只有Object
类中的几个默认方法实现.
如果我们要实现一个方法, 传入两个参数,返回其中大的一个,即max()
函数.
public static void main(String[] args) { // 传入 4 , 2 , 自动装箱成Integer类 int r = max(4, 2); } static <T> T max(T t1,T t2){ // Cannot resolve method 'compareTo(T)' return t1.compareTo(t2) > 0 ? t1 : t2; }
如果没有对类型进行限定,它默认只有Object
能力,它没有compareTo
方法,因此没有比较能力,此时,即使在调用的时候传入可以比较的对象, max
方法会在编译器报错.
此时, 我们就需要对 类型参数进行限定,让它能够默认拥有一些类的"能力".
public static void main(String[] args) { // 传入 4 , 2 , 自动装箱成Integer类 int r = max(4, 2); // r = 4 } // 继承 Comparable 的类具有比较功能,能够比较大小 , 该函数返回传入的最大值 static <T extends Comparable<T>> T max(T t1, T t2) { return t1.compareTo(t2) > 0 ? t1 : t2; }
从代码中可以看出, T
被限定为Comparable
的子类(Comparable
类本身是泛型类,也需要对他进行类型参数声明,否则会引发编译警告.),因此它拥有了 父类Comparable
有的能力,即比较功能,这样我们才能得到正确的结果.
类型参数的限定 可以记为 <T extends BoundingType>
,由于java有单继承类多实现接口的特点,因此还可以有多个限定. <T extends BoundingType1 & BoundingType2 & ...>
在 Java 的继承中, 可以拥有多个接口超类型, 但限定中至多有一个类。 如果用 一个类作为限定, 它必须是限定列表中的第一个.
泛型的实现原理
java中泛型的实现是采用 类型擦除
的方式实现.
所谓的类型擦除,就是程序在编译阶段,编译器会对泛型变量进行擦除(erased
)操作,并替换为限定类型 (没有限定的变量用 Object);泛型类也将擦除为原始类型; 在泛型表达式中(泛型的使用),会将类型替换为具体的类型(此时将发生强制转换)
下面我们通过,反编译泛型类的方式来揭开类型擦除
的面纱.
使用jad -sjava Holder.class
来反编译Holder
类.
// 泛型类 public class Holder<T> { private T obj; public void put(T t) { obj = t; } public T get() { return obj; } } // 泛型使用 public static void main(String[]args){ Holder<Animal> holder=new Holder<>(); holder.put(new Tiger()); // 在使用过程中没有发生强转 Animal animal=holder.get(); } // --------------------------------- // 反编译出来的类, 它的类型被擦除为Object public class Holder { private Object obj; public Holder() { } public void put(Object t) { obj = t; } public Object get() { return obj; } } public static void main(String args[]){ Holder holder=new Holder(); holder.put(new Tiger()); // 泛型的使用过程中,使用强制转换为目标类型 Animal animal=(Animal)holder.get(); } // ------------------------------------- // javap 反汇编对main方法,到处的指令码 public static void main(java.lang.String[]); Code: 0:new #2 // class generics/Holder 3:dup 4:invokespecial #3 // Method generics/Holder."<init>":()V 7:astore_1 8:aload_1 9:new #4 // class bean/Tiger 12:dup 13:invokespecial #5 // Method bean/Tiger."<init>":()V 16:invokevirtual #6 // Method generics/Holder.put:(Ljava/lang/Object;)V 19:aload_1 20:invokevirtual #7 // Method generics/Holder.get:()Ljava/lang/Object; 23:checkcast #8 // class bean/Animal 26:astore_2 27:return
由上述反编译的代码可以看出,泛型类被擦除为 原始类型;
泛型类中的类型参数变量也擦除为Object
类型;
泛型的表达式中,发生了强制转换为目标类型.
从反汇编代码中可以看出,holder.get()
方法,被分解为两条指令.
1. 原始类型调用方法,对应 invokevirtual
指令
2. Object类型强制转换为Animal类型,对应 checkcast
指令
如果限定为 T extends Animal
,则类型参数变量将被擦除为限定类型Animal
.
// 泛型类 public class Holder<T extends Animal> { private T obj; ... // 反编译类 public class Holder { private Animal obj; ...
java泛型的局限
- 不能用基本类型实例化类型参数
泛型中的 类型参数在没有限定的情况下 是默认 擦除为 Object
,而基本类型变量无法转化为 Object
类型.
不过没关系, java中8中基本类型,都有其对应的包装类(Wrapper Class), 并且基本类型 使用参数传递是,将被 (自动装箱)AutoBoxing 为包装类型.
- 运行时,无法对类型参数进行检查
由于编译时,擦除了类型参数, 因此,所有的类型查询只产生原始类型.
因此,一下的语句是不可行的.
Holder<Tiger> holder = new Holder<>(new Tiger()); // ERROR 无法对类型参数进行判断 if (holder instanceof Holder<Tiger>) ; if (holder instanceof Holder<T>) ;
由于类型擦除,Holder<String>
和Holder<Animal>
的实例,获取的类都是原始类,是一样的,所以他们的getClass()
方法的返回是一样的.
- 不能直接创建参数化类型的数组
如这样的代码Holder<Animal>[] holders = new Holder<Animal>[2]
,是通过不了编译的.
但是,可以通过以下方式来创建数组,不会报错,只是受到警告
// 使用原始类型而后强制转换 Holder<Animal>[] holders = (Holder<Animal>[]) new Holder[2]; // 使用通配符而后强制转换 Holder<Animal>[] holders = (Holder<Animal>[]) new Holder<?>[2];
- 不能够实例化类型变量
这样的语句T t = new T()
和T t = new Object()
,是通过不了编译的,一定要在泛型表达式中申明了具体类型,才能创建.
某些情况下,我们需要创建 参数类型的变量, 那么前提是一定要知道被创建的类型.可以通过以下两种方式来创建:
- 反射创建
// 使用(如) newObject(Animal.class); static <T> T newObject(Class<T> cls) throw Exception{ return cls.newInstance(); }
- jdk8以后,可使用构造器表达式
// 使用(如) newObject(Animal::new); static <T> T newObject(Supplier<T> constr) { return constr.get(); }
- 不能构造泛型数组
不能直接实例化 类型数组,如T[] arr = new T[2]
.
但是可以这样 T[] arr = new Object[2]
, 原因是数组本身也有类型,用来监控存储在虚拟机
中的数组,这个类型会被擦除为Object
.
虽然这种方式能够创建泛型数组,但是为了类型安全起见,最好提供构造器来实现泛型数据的创建.
// 使用构造器 // 使用(如) newArray(Animal[]::new, 2); static <T> T[] newArray(IntFunction<T[]> constr, int length) { return constr.apply(length); } // 使用反射 // 使用(如) newArray(String.class, 2); static <T> T[] newArray(Class<T> cls, int length) throws Exception { return (T[]) Array.newInstance(cls, length); }
- 不能在静态变量和静态方法中,使用泛型类中的类型参数
如以下的方式都不允许
public class Test<T> { // Error private static T t; // Error public static T test() { T t; } }
- 注意擦除后的冲突
- 由于类型擦除,方法重写时,会下列冲突.
public class Holder<T> { public boolean equals(T t) { ... }
由于类型擦除,该方法会被擦除为boolean equals(Object t)
,这和Object类中的equals
方法完全冲突了,返回值和方法名,参数都一致了.
这种冲突,解决方案只能是,将方法重命名!
- 由于类型擦除,方法重载时,也可能发生冲突.
再观察下面的代码:
public interface Parent<T> { T get(); } public class Son implements Parent<Animal> { @Override public Animal get() { return new Animal(); } } public class Main { public static void main(String[] args) { Parent<Animal> parent = new Son(); Animal animal = parent.get(); } }
父类中根据类型擦除, 拥有Object get()
方法, 子类传入具体的类型参数,拥有Animal get()
方法,且继承父类方法,所以子类中同时拥有这两个方法.
这两个方法,方法参数相同,就只有返回值类型不一样.
在java语法中,是不允许这样的两个方法同时在一个类中存在,会把他们认为是同一种方法(方法签名根据方法参数和方法名来确定).
但是jvm却能分辨,jvm的方法签名,是通过方法参数,方法返回值,方法名来确定的,所以jvm允许这样的方法存在, 因此,对于这种冲突,不需要我们自己来处理, jvm通过一种称为 Bridge Method
的方式来实现这种方式下的多态调用冲突.
感兴趣的可以查看,笔者的另一篇文章java中多态的实现原理.
为什么使用 类型擦除来实现泛型
因为,泛型提出来时,已经是java1.5的版本,java已经经历过10年的发展,java遗留的代码量可想而知.
为了兼容这部分的(旧)代码,而不得不采用这种方式来实现.
通配符 <?> <? extends T> <? super T>
泛型通配符解释起来比较复杂,这里就不进行展开,感兴趣的可以查看笔者的另一篇文章java泛型通配符详解及实践
引用
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Redis快速使用
1.导包(坐标) <!-- https://mvnrepository.com/artifact/redis.clients/jedis --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency> <!-- 配置使用redis启动器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> 2.使用 2.1存数据: @Autowired private RedisTemplate<String, String>...
- 下一篇
用深度神经网络修复H漫:看完这篇你就能眼中无码
AI“脑补”能力一流,现在甚至已经能画出艺术品。热爱H漫的死宅们灵光一闪,AI是否也可以把马赛克阻挡的内容也画出来呢? 果然,原始动物本能是第一科技生产力。最近就有人在GitHub上发布了一个DeepCreamPy项目,能帮你把H漫中羞羞的画面补上。 该项目使用深度完全卷积神经网络(deep fully convolutional neural network),参照了英伟达在今年4月前发布的一篇论文。当然,英伟达原文的目的可不是用来做羞羞的事情,而是为了复原画面被单色条带遮挡的问题。 从实际效果来看,复原后的图片涂抹痕迹仍然比较明显,不过处理线条比较简单的漫画可以说是绰绰有余。 接下来,就是让你“眼中无码”的DIY教程啦! 适用范围 DeepCreamPy仅适用于薄码,如果马赛克太大太厚,去码可能会失效。另外,它对真人图片无效。如果你非要尝
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- 设置Eclipse缩进为4个空格,增强代码规范
- CentOS6,CentOS7官方镜像安装Oracle11G
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- SpringBoot2全家桶,快速入门学习开发网站教程
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- MySQL8.0.19开启GTID主从同步CentOS8