Java 的“泛型”特性,你以为自己会了?(万字长文)
使用 Java 的小伙伴,对于 Java 的一些高级特性一定再熟悉不过了,例如集合、反射、泛型、注解等等,这些可以说我们在平时开发中是经常使用到的,尤其是集合,基本是只要写代码没有用不到的,今天我们先来谈谈 泛型 。
1. 定义
在了解一个事物之前,我们必定要先知道他的定义,所以我们就从定义开始,去一步一步揭开泛型的神秘面纱。
注意:泛型不接受基本数据类型,换句话说,只有引用类型才能作为泛型方法的实际参数
2. 为什么要使用泛型?
说到为什么要使用,那肯定是找一大堆能说服自己的优点啊。
另外,泛型还具有以下的优点:
在泛型规范正式发布之前,泛型的程序设计是通过继承来实现的,但是这样子有两个严重的问题:
① 取值的时候需要 强制类型转换 ,否则拿到的都是 Object
② 编译期不会有错误检查
我们来看下这两个错误的产生
2.1 编译期不会有错误检查
程序不但不会报错,还能正常输出
2.2 强制类型转换
因为你并不知道实际集合中的元素到底是哪些类型的,所以在使用的时候也是不确定的,如果在强转的时候,那必然会带来意想不到的错误,这样潜在的问题就好像是定时炸弹,肯定是不允许发生的。 所以这就更体现了泛型的重要性 。
3. 泛型方法
在 java 中,泛型方法可以使用在 成员方法、构造方法和静态方法 中。语法如下:
public <申明泛型的类型> 类型参数 fun();如 public T fun(T t);这里的 T 表示一个泛型类型, 而 表示我们定义了一个类型为 T 的类型,这样的 T 类型就可以直接使用了 ,且 需要放在方法的 返回值类型之前 。T 即在申明的时候是不知道具体的类型的,只有的使用的时候才能明确其类型,T 不是一个类,但是可以当作是一种 类型 来使用。
下面来通过具体的例子来解释说明,以下代码将数组中的指定的两个下标位置的元素进行交换(不要去关注实际的需求是什么),第一种 Integer 类型的数组
第二种是 String 类型的数组
编译直接都不会通过,那是必然的,因为方法定义的参数就是 Integer[] 结果你传一个 String[],玩呢。。。所以这个时候只能是再定义一个参数类型是 String[]的。
那要是再来一个 Double 呢?Boolean 呢?是不是这就产生问题了,虽然说这种问题不是致命的,多写一些重复的代码就能解决,但这势必导致代码的冗余和维护成本的增加。所以这个时候泛型的作用就体现了,我们将其改成泛型的方式。
接下来调用就简单了
问题迎刃而解,至于 普通的泛型方法和静态的泛型方法 是一样的使用,只不过是一个数据类一个属于类的实例的,在使用上区别不大(但是需要注意的是如果在泛型类中 静态泛型方法是不能使用类泛型中的泛型类型的,这个在下文的泛型类中会详细介绍的)。
最后再来看下 构造方法
然后假设他有一个子类是这样子的
这里强调一下,因为在 Father 类中是没有无参构造器的,取而代之的是一个有参的构造器,只不过这个构造方法是一个泛型的方法,那这样子的子类必然需要显示的指明构造器了。
通过泛型方法获取集合中的元素测试
既然说泛型是在申明的时候类型不是重点,只要事情用的时候确定就可以下,那你看下面这个怎么解释?
此时想往集合中添加元素,却提示这样的错误,连编译都过不了。这是为什么?
因为此时集合 List 的 add 方法,添加的类型为 T,但是很显然 T 是一个泛型,真正的类型是在使用时候才能确定的,但是 在 add 的并不能确定 T 的类型,所以根本就无法使用 add 方法,除非 list.add(null),但是这却没有任何意义。
4. 泛型类
先来看一段这样的代码,里面的使用到了多个泛型的方法,无需关注方法到底做了什么
可以看到里面的 是不是每个方法都需要去申明一次,那要是 100 个方法呢?那是不是要申明 100 次的,这样时候泛型类也就应用而生了。那泛型类的形式是什么样子的呢?请看代码
下面我们将刚刚的代码优化如下,但是这里不得不说一个很基础,但是却很少有人注意到的问题,请看下面的截图中的文字描述部分。
所以说这个泛型类中的静态方法直接这么写就可以啦
多个泛型类型同时使用
我们知道 Map 是键值对形式存在,所以如果对 Map 的 Key 和 Value 都使用泛型类型该怎么办?一样的使用,一个静态方法就可以搞定了,请看下面的代码
到此,泛型的常规的方法和泛型类已经介绍为了。
5. 通配符
通配符 ? 即占位符的意思,也就是在使用期间是无法确定其类型的,只有在将来实际使用的时候指明类型,它有三种形式
无限定的通配符。是让泛型能够接受**未知类型**的数据
< ? extends E>有 上限 的通配符。能接受 指定类及其子类类型 的数据,E 就是该泛型的上边界
有**下限**的通配符。能接受**指定类及其父类类型**的数据,E 就是该泛型的下边界
5.1 通配符之
上面刚刚说到了使用一个类型来表示反省类型是必须要申明的,也即 ,那是不是不申明就不能使用泛型呢?当然不是,这小节介绍的 <?> 就是为了解决这个问题的。
表示,但是话又说话来了,那既然可以不去指明具体类型,那 ? 就不能表示一个具体的类型也就是说如果按照原来的方式这么去写,请看代码中的注释
而又因为任何类型都是 Object 的子类,所以,这里可以使用 Object 来接收,对于 ? 的具体使用会在下面两小节介绍
另外,大家要搞明白 泛型和通配符不是一回事
5.2 通配符之
表示有上限的通配符,能接受其类型和其子类的类型 E 指上边界,还是写个例子来说明 public class GenericExtend { public static void main(String[] args) { List listF = new ArrayList<>(); List listS = new ArrayList<>(); List listD = new ArrayList<>(); testExtend(listF); testExtend(listS); testExtend(listD); } private static void testExtend(List list) {} } class Father {} class Daughter extends Father{} class Son extends Father { } 这个时候一切都还是很和平的,因为大家都遵守着预定,反正 List 中的泛型要么是 Father 类,要么是 Father 的子类。但是这个时候如果这样子来写(具体原因已经在截图中写明了) ![Java 的“泛型”特性,你以为自己会了?(万字长文)](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/969eae2bec1e4f6bb7b69962d28e9b5d~tplv-k3u1fbpfcp-zoom-1.image) 5.3 通配符之 <?super E> =================== <?super E> 表示有下限的通配符。也就说能接受指定类型及其父类类型,E 即泛型类型的下边界,直接上来代码然后来解释 public class GenericSuper { public static void main(String[] args) { List listS = new Stack<>(); List listF = new Stack<>(); List listG = new Stack<>(); testSuper(listS); testSuper(listF); testSuper(listG); } private static void testSuper(List list){} } class Son extends Father{} class Father extends GrandFather{} class GrandFather{} 因为 List list 接受的类型只能是 Son 或者是 Son 的父类,而 Father 和 GrandFather 又都是 Son 的父类,所以以上程序是没有任何问题的,但是如果再来一个类是 Son 的子类(如果不是和 Son 有关联的类那更不行了),那结果会怎么样?看下图,相关重点已经在图中详细说明 ![Java 的“泛型”特性,你以为自己会了?(万字长文)](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3306865d6d18448fb881b4d5bd312e8c~tplv-k3u1fbpfcp-zoom-1.image) 好了,其实泛型说到这里基本就差不多了,我们平时开发能遇到的问题和不常遇见的问题本文都基本讲解到了。最后我们再来一起看看泛型的另一个特性:**泛型擦除**。 6\. 泛型擦除 ======== 先来看下泛型擦除的定义 # 泛型擦除 因为泛型的信息只存在于 java 的编译阶段,编译期编译完带有 java 泛型的程序后,其生成的 class 文件中与泛型相关的信息会被擦除掉,以此来保证程序运行的效率并不会受影响,也就说泛型类型在 jvm 中和普通类是一样的。 别急,知道你看完概念肯定还是不明白什么叫泛型擦除,举个例子 public class GenericWipe { public static void main(String[] args) { List listStr = new ArrayList<>(); List listInt = new ArrayList<>(); List listDou = new ArrayList<>(); System.out.println(listStr.getClass()); System.out.println(listInt.getClass()); System.out.println(listDou.getClass()); } } ![Java 的“泛型”特性,你以为自己会了?(万字长文)](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/30db531487c44a82b050305a6ec0fa1c~tplv-k3u1fbpfcp-zoom-1.image) ![Java 的“泛型”特性,你以为自己会了?(万字长文)](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/47f848004cb84df898d6b443cfce214f~tplv-k3u1fbpfcp-zoom-1.image) 这也就是说 java 泛型在生成字节码以后是根本不存在泛型类型的,甚至是在编译期就会被抹去,说来说去好像并没有将泛型擦除说得很透彻,下面我们就以例子的方式来一步一步证明 * 通过反射验证编译期泛型类型被擦除 class Demo1 { public static void main(String[] args) throws Exception { List list = new ArrayList<>(); //到这里是没有任何问题的,正常的一个 集合类的添加元素 list.add(1024); list.forEach(System.out::println); System.out.println("-------通过反射证明泛型类型编译期间被擦除-------"); //反射看不明白的小伙伴不要急,如果想看反射的文章,请留言反射,我下期保证完成 list.getClass().getMethod("add", Object.class).invoke(list, "9527"); for (int i = 0; i < list.size(); i++) { System.out.println("value = " + list.get(i)); } } } ![Java 的“泛型”特性,你以为自己会了?(万字长文)](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8cbfdab9c1bf4a34ab7530718333d93d~tplv-k3u1fbpfcp-zoom-1.image) 打印结果如下: ![Java 的“泛型”特性,你以为自己会了?(万字长文)](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/73dcfa4bfc2f43429433a0f00e1f0d17~tplv-k3u1fbpfcp-zoom-1.image) 但是直接同一个反射似乎并不能让小伙伴们买账,我们为了体验差异,继续写一个例子 class Demo1 { public static void main(String[] args) throws Exception { //List 实际上就是一个泛型,所以我们就不去自己另外写泛型类来测试了 List list = new ArrayList<>(); //到这里是没有任何问题的,正常的一个 集合类的添加元素 list.add(1024); list.forEach(System.out::println); System.out.println("-------通过反射证明泛型类型编译期间被擦除-------"); list.getClass().getMethod("add", Object.class).invoke(list, "9527"); for (int i = 0; i < list.size(); i++) { System.out.println("value = " + list.get(i)); } //普通的类 FanShe fanShe = new FanShe(); //先通过正常的方式为属性设置值 fanShe.setStr(1111); System.out.println(fanShe.getStr()); //然后通过同样的方式为属性设置值 不要忘记上面的 List 是 List 是泛型哦!不要连最基本的知识都忘记了 fanShe.getClass().getMethod("setStr", Object.class).invoke(list, "2222"); System.out.println(fanShe.getStr()); } } //随便写一个类 class FanShe{ private Integer str; public void setStr(Integer str) { this.str = str; } public Integer getStr() { return str; } } ![Java 的“泛型”特性,你以为自己会了?(万字长文)](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a32364c8f8c94273aeede18533bc2271~tplv-k3u1fbpfcp-zoom-1.image) 测试结果显而易见,不是泛型的类型是不能通过反射去修改类型赋值的。 * 由于泛型擦除带来的自动类型转换 因为泛型的类型擦除问题,导致所有的泛型类型变量被编译后都会被替换为原始类型。既然都被替换为原始类型,那么为什么我们在获取的时候,为什么不需要强制类型转换? ![Java 的“泛型”特性,你以为自己会了?(万字长文)](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1114e3efc82b435db85fdd3c49fe6c08~tplv-k3u1fbpfcp-zoom-1.image) 下面这么些才是一个标准的带有泛型返回值的方法。 public class TypeConvert { public static void main(String[] args) { //调用方法的时候返回值就是我们实际传的泛型的类型 MyClazz1 myClazz1 = testTypeConvert(MyClazz1.class); MyClazz2 myClazz2 = testTypeConvert(MyClazz2.class); } private static T testTypeConvert(Class tClass){ //只需要将返回值类型转成实际的泛型类型 T 即可 return (T) tClass; } } class MyClazz1{} class MyClazz2{} * 由泛型引发的数组问题 名字怪吓人的,实际上说白了就是不能创建泛型数组 ![Java 的“泛型”特性,你以为自己会了?(万字长文)](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6d3a612b3d5b42b7ba8353fda1b30b50~tplv-k3u1fbpfcp-zoom-1.image) 看下面的代码 ![Java 的“泛型”特性,你以为自己会了?(万字长文)](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dc2c7bcd8cde40e2ab710e4118c65b68~tplv-k3u1fbpfcp-zoom-1.image) 为什么不能创建泛型类型的数组? 因为 List 和 List 被编译后在 JVM 中等同于 List ,所有的类型信息在编译后都等同于 List,也就是说编译器此时也是无法区分数组中的具体类型是 Integer 类型还是 String 。 但是,使用通配符却是可以的,我上文还特意强调过一句话:**泛型和通配符不是一回事**。请看代码 ![Java 的“泛型”特性,你以为自己会了?(万字长文)](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5cc8eaf20d6f446c87be59de92d761f3~tplv-k3u1fbpfcp-zoom-1.image) 那这又是为什么?? 表示未知的类型,他的操作不涉及任何的类型相关的东西,所以 JVM 是不会对其进行类型判断的,因此它能编译通过,但是这种方式只能读不能写,也即只能使用 get 方法,无法使用 add 方法。 为什么不能 add ? 提供了只读的功能,也就是它删减了增加具体类型元素的能力,只保留与具体类型无关的功能。它不管装载在这个容器内的元素是什么类型,它只关心元素的数量、容器是否为空,另外上面也已经解释过为什么不能 add 的,这里就当做一个补充。
好了,关于泛型知识,今天就聊到这里,感谢大家的支持!
版权声明: 本文为 InfoQ 作者【比伯】的原创文章。
原文链接:【http://xie.infoq.cn/article/fd51304b3fde106ef0b8fb555】。文章转载请联系作者。
评论