深入理解java泛型

打印 上一主题 下一主题

主题 927|帖子 927|积分 2781

目录

作者:小牛呼噜噜 | https://xiaoniuhululu.com
计算机内功、JAVA底层、面试相关资料等更多精彩文章在公众号「小牛呼噜噜 」
什么是Java泛型

Java 泛型(generics)是 Jdk 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制, 该机制允许程序员在编译时检测到非法的类型。
比如 ArrayList list= new ArrayList() 这行代码就指明了该 ArrayList 对象只能 存储String类型,如果传入其他类型的对象就会报错。
让我们时光回退到Jdk5的版本,那时ArrayList内部其实就是一个Object[] 数组,配合存储一个当前分配的长度,就可以充当“可变数组”:
  1. public class ArrayList {
  2.     private Object[] array;
  3.     private int size;
  4.     public void add(Object e) {...}
  5.     public void remove(int index) {...}
  6.     public Object get(int index) {...}
  7. }
复制代码
我们来举个简单的例子,
  1. ArrayList<Object> list = new ArrayList<String>();
  2. list.add("test");
  3. list.add(666);
复制代码
我们本意是用ArrayList来装String类型的值,但是突然混进去了Integer类型的值,由于ArrayList底层是Object数组,可以存储任意的对象,所以这个时候是没啥问题的,但我们不能只存不用啊,我们需要把值给拿出来使用,这个时候问题来了:
  1. for(Object item: list) {
  2.     System.out.println((String)item);
  3. }
复制代码
结果:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
由于我们需要String类型的值,我们需要把ArrayList的Object值强制转型,但是之前混进去了Integer ,虽然编译阶段通过了,但程序的运行结果会以崩溃结束,报ClassCastException异常
为了解决这个问题,在Jdk 5版本中就引入了泛型的概念,而引入泛型的很大一部分原因就是为了解决我们上述的问题,允许程序员在编译时检测到非法的类型。不是同类型的就不允许在一块存放,这样也避免了ClassCastException异常的出现,而且因为都是同一类型,也就没必要做强制类型转换了。
我们可以把ArrayList 变量参数化:
  1. public class ArrayList<T> {
  2.     private T[] array;//我们 假设 ArrayList<T>内部会有个T[] array  
  3.     private int size;
  4.     public void add(T e) {...}
  5.     public void remove(int index) {...}
  6.     public T get(int index) {...}
  7. }
复制代码
其中T叫类型参数 ,T可以是任何class类型,现在ArrayList我们可以如下使用:
  1. // 存储String的ArrayList
  2. ArrayList<String> list = new ArrayList<String>();
  3. list.add(666);//编译器会在编译阶段发现问题,从而提醒开发者
复制代码
泛型其本质是参数化类型,也就是说数据类型 作为 参数,解决不确定具体对象类型的问题。
泛型的使用

泛型一般有三种使用方式,分别为:泛型类、泛型接口、泛型方法,我们简单介绍一下泛型的使用
泛型类
  1. //此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
  2. //在实例化泛型类时,必须指定T的具体类型
  3. public class Generic<T>{
  4.     private T key;
  5.     public Generic(T key) {
  6.         this.key = key;
  7.     }
  8.     public T getKey(){
  9.         return key;
  10.     }
  11. }
复制代码
如何实例化泛型类:
  1. Generic<Integer> genericInteger = new Generic<Integer>(666);
  2. Generic<String> genericStr = new Generic<String>("hello");
复制代码
泛型接口
  1. //定义一个泛型接口
  2. public interface Generator<T> {
  3.     public T method();
  4. }
  5. //实现泛型接口,不指定类型
  6. class GeneratorImpl<T> implements Generator<T>{
  7.     @Override
  8.     public T method() {
  9.         return null;
  10.     }
  11. }
  12. //实现泛型接口,指定类型
  13. class GeneratorImpl<T> implements Generator<String>{
  14.     @Override
  15.     public String method() {
  16.         return "hello";
  17.     }
  18. }
复制代码
泛型方法
  1. public class GenericMethods {
  2.     public <T> void f(T x){
  3.         System.out.println(x.getClass().getName());
  4.     }
  5.     public static void main(String[] args) {
  6.         GenericMethods gm = new GenericMethods();
  7.         gm.f("啦啦啦");
  8.         gm.f(666);
  9.     }
  10. }
复制代码
结果:
java.lang.String
java.lang.Integer
泛型的底层实现机制

ArrayList源码解析

通过上文我们知道,为了让ArrayList存取各种数据类型的值,我们需要把ArrayList模板化,将变量的数据类型 给抽象出来,作为类型参数
  1. public class ArrayList<T> {
  2.     private T[] array;// 我们以为ArrayList<T>内部会有个T[] array
  3.     private int size;
  4.     public void add(T e) {...}
  5.     public void remove(int index) {...}
  6.     public T get(int index) {...}
  7. }
复制代码
但当我们查看Jdk8 的ArrayList源码,底层数组还是Object数组:transient Object[] elementData;
那ArrayList为什么还能进行类型约束和自动类型转换呢?
什么是泛型擦除

我们再看一个经典的例子:
  1. public class genericTest {
  2.     public static void main(String [] args) {
  3.         String str="";
  4.         Integer param =null;
  5.         ArrayList<String> l1 = new ArrayList<String>();
  6.         l1.add("aaa");
  7.         str = l1.get(0);
  8.         ArrayList<Integer> l2 = new ArrayList<Integer>();
  9.         l2.add(666);
  10.         param = l2.get(0);
  11.         System.out.println(l1.getClass() == l2.getClass());
  12.         
  13.     }
  14. }
复制代码
结果竟然是true,ArrayList.class 和 ArrayList.class 应该是不同的类型。通过getClass()方法获取他们的类的信息,竟然是一样的。我们来查看这个文件的class文件:
  1. public class genericTest {
  2.     public genericTest() {
  3.     }
  4.     public static void main(String[] var0) {
  5.         String var1 = "";
  6.         Integer var2 = null;
  7.         ArrayList var3 = new ArrayList();//泛型被擦擦了
  8.         var3.add("aaa");
  9.         var1 = (String)var3.get(0);
  10.         ArrayList var4 = new ArrayList();//泛型被擦擦了
  11.         var4.add(666);
  12.         var2 = (Integer)var4.get(0);
  13.         System.out.println(var3.getClass() == var4.getClass());
  14.     }
  15. }
复制代码
我们在对其反汇编一下:
  1. $ javap -c genericTest
  2. ▒▒▒▒: ▒▒▒▒▒▒▒ļ▒genericTest▒▒▒▒com.zj.demotest.test5.genericTest
  3. Compiled from "genericTest.java"
  4. public class com.zj.demotest.test5.genericTest {
  5.   public com.zj.demotest.test5.genericTest();
  6.     Code:
  7.        0: aload_0
  8.        1: invokespecial #1                  // Method java/lang/Object."<init>":()V
  9.        4: return
  10.   public static void main(java.lang.String[]);
  11.     Code:
  12.        0: ldc           #2                  // String
  13.        2: astore_1
  14.        3: aconst_null
  15.        4: astore_2
  16.        5: new           #3                  // class java/util/ArrayList
  17.        8: dup
  18.        9: invokespecial #4                  // Method java/util/ArrayList."<init>":()V
  19.       12: astore_3
  20.       13: aload_3
  21.       14: ldc           #5                  // String aaa
  22.       16: invokevirtual #6                  // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
  23.       19: pop
  24.       20: aload_3
  25.       21: iconst_0
  26.       22: invokevirtual #7                  // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
  27.       25: checkcast     #8                  // class java/lang/String
  28.       28: astore_1
  29.       29: new           #3                  // class java/util/ArrayList
  30.       32: dup
  31.       33: invokespecial #4                  // Method java/util/ArrayList."<init>":()V
  32.       36: astore        4
  33.       38: aload         4
  34.       40: sipush        666
  35.       43: invokestatic  #9                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
  36.       46: invokevirtual #6                  // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
  37.       49: pop
  38.       50: aload         4
  39.       52: iconst_0
  40.       53: invokevirtual #7                  // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
  41.       56: checkcast     #10                 // class java/lang/Integer
  42.       59: astore_2
  43.       60: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
  44.       63: aload_3
  45.       64: invokevirtual #12                 // Method java/lang/Object.getClass:()Ljava/lang/Class;
  46.       67: aload         4
  47.       69: invokevirtual #12                 // Method java/lang/Object.getClass:()Ljava/lang/Class;
  48.       72: if_acmpne     79
  49.       75: iconst_1
  50.       76: goto          80
  51.       79: iconst_0
  52.       80: invokevirtual #13                 // Method java/io/PrintStream.println:(Z)V
  53.       83: return
  54. }
复制代码

  • 看第16、46处,add进去的是原始类型Object;
  • 看第22、53处,get方法获得也是Object类型,String、Integer类型被擦出,只保留原始类型Object。
  • 看25、55处,checkcast指令是类型转换检查 ,在结合class文件var1 = (String)var3.get(0);``var2 = (Integer)var4.get(0);我们知晓编译器自动帮我们强制类型转换了,我们无需手动类型转换

经过上面的种种现象,我们可以发现,在类加载的编译阶段,泛型类型String和Integer都被擦除掉了,只剩下原始类型,这样他们类的信息都是Object,这样自然而然就相等了。这种机制就叫泛型擦除。
我们需要了解一下类加载生命周期:

详情见:https://mp.weixin.qq.com/s/v91bqRiKDWWgeNl1DIdaDQ
泛型是和编译器的约定,在编译期对代码进行检查的,由编译器负责解析,JVM并无识别的能力,一个类继承泛型后,当变量存入这个类的时候,编译器会对其进行类型安全检测,当从中取出数据时,编译器会根据与泛型的约定,会自动进行类型转换,无需我们手动强制类型转换。
泛型类型参数化,并不意味这其对象类型是不确定的,相反它的对象类型 对于JVM来说,都是确定的,是Object或Object[]数组
泛型的边界

来看一个经典的例子,我们想要实现一个ArrayList对象能够储存所有的泛型:
  1. ArrayList<Object> list = new ArrayList<String>();
复制代码
但可以的是编译器提示报错:

明明 String是Object类的子类,我们可以发现,泛型不存在继承、多态关系,泛型左右两边要一样
别担心,JDK提供了通配符?来应对这种场景,我们可以这样:
  1. ArrayList<Object> list = new ArrayList<String>();list = new ArrayList();
复制代码
通配符表示可以接收任意类型,此处?是类型实参,而不是类型形参。我们可以把它看做是String、Integer等所有类型的"父类"。是一种真实的类型。
通配符还有:

  • 上边界限定通配符,如 list不可以添加任何类型,因为并不知道实际是哪种类型
  • 返回值和泛型相关的都只能用Object接收
extends 上边界通配符
  1. //泛型的上限只能是该类型的类型及其子类,其中Number是Integer、Long、Float的父类   
  2. ArrayList<? extends Number> list = new ArrayList<Integer>();
  3. ArrayList<? extends Number> list2 = new ArrayList<Long>();
  4. ArrayList<? extends Number> list3 = new ArrayList<Float>();
  5. list.add(1);//报错,extends不允许存入
  6. ArrayList<Long> longList = new ArrayList<>();
  7. longList.add(1L);
  8. list = longList;//由于extends不允许存入,list只能重新指向longList
  9. Number number = list.get(0);  // extends 取出来的元素(Integer,Long,Float)都可以转Number
复制代码
如果想对数组进行复制操作的话,可以通过Arrays.copyOfRange()方法
  1. //泛型的下限只能是该类型的类型及其父类,其中Number是Integer、Long、Float的父类   
  2. ArrayList<? super Integer> list = new ArrayList<Integer>();
  3. ArrayList<? super Integer> list2 = new ArrayList<Number>();
  4. ArrayList<? super Integer> list3 = new ArrayList<Long>();//报错
  5. ArrayList<? super Integer> list4 = new ArrayList<Float>();//报错
  6. list2.add(123);//super可以存入,只能存Integer及其子类型元素
  7. Object aa =  list2.get(0);//super可以取出,类型只能是Object
复制代码
反射其实可以绕过泛型的限制

由于我们知晓java是通过泛型擦除来实现泛型的,JVM只能识别原始类型Object,所以我们只需骗过编译器的校验即可,反射是程序运行时发生的,我们可以借助反射来波骚操作
  1. public class A<T>{
  2.     public T get(T a){
  3.         //进行一些操作
  4.         return a;
  5.     }
  6. }
  7. public class B extends A<String>{
  8.     @override
  9.     public String get(String a){
  10.         //进行一些操作
  11.         return a;
  12.     }
  13. }
复制代码
结果:
111
骚气的我 又出现了

尾语

如果你了解其他语言(例如 C++ )的参数化机制,你会发现,Java 泛型并不能满足所有的预期。由于泛型出来前,java已经有了很多项目了,为了兼容老版本,采用了泛型擦除来“实现泛型”,这会遇到很多意料之外的麻烦,但这并不是说 Java 泛型毫无用处,它大多数情况能够让代码更加优雅,后面有机会我们会继续深入聊聊泛型擦除带来的麻烦及其历史渊源。
参考资料:
《On Java8》
《Effective Java》
https://www.liaoxuefeng.com/wiki/1252599548343744/1265102638843296
https://www.cnblogs.com/mahuan2/p/6073493.html
本篇文章到这里就结束啦,很感谢你能看到最后,如果觉得文章对你有帮助,别忘记关注我!更多精彩的文章


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

数据人与超自然意识

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表