目录
作者:小牛呼噜噜 | https://xiaoniuhululu.com
计算机内功、JAVA底层、面试相关资料等更多精彩文章在公众号「小牛呼噜噜 」
什么是Java泛型
Java 泛型(generics)是 Jdk 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制, 该机制允许程序员在编译时检测到非法的类型。
比如 ArrayList list= new ArrayList() 这行代码就指明了该 ArrayList 对象只能 存储String类型,如果传入其他类型的对象就会报错。
让我们时光回退到Jdk5的版本,那时ArrayList内部其实就是一个Object[] 数组,配合存储一个当前分配的长度,就可以充当“可变数组”:- public class ArrayList {
- private Object[] array;
- private int size;
- public void add(Object e) {...}
- public void remove(int index) {...}
- public Object get(int index) {...}
- }
复制代码 我们来举个简单的例子,- ArrayList<Object> list = new ArrayList<String>();
- list.add("test");
- list.add(666);
复制代码 我们本意是用ArrayList来装String类型的值,但是突然混进去了Integer类型的值,由于ArrayList底层是Object数组,可以存储任意的对象,所以这个时候是没啥问题的,但我们不能只存不用啊,我们需要把值给拿出来使用,这个时候问题来了:- for(Object item: list) {
- System.out.println((String)item);
- }
复制代码 结果:
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 变量参数化:- public class ArrayList<T> {
- private T[] array;//我们 假设 ArrayList<T>内部会有个T[] array
- private int size;
- public void add(T e) {...}
- public void remove(int index) {...}
- public T get(int index) {...}
- }
复制代码 其中T叫类型参数 ,T可以是任何class类型,现在ArrayList我们可以如下使用:- // 存储String的ArrayList
- ArrayList<String> list = new ArrayList<String>();
- list.add(666);//编译器会在编译阶段发现问题,从而提醒开发者
复制代码 泛型其本质是参数化类型,也就是说数据类型 作为 参数,解决不确定具体对象类型的问题。
泛型的使用
泛型一般有三种使用方式,分别为:泛型类、泛型接口、泛型方法,我们简单介绍一下泛型的使用
泛型类
- //此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
- //在实例化泛型类时,必须指定T的具体类型
- public class Generic<T>{
- private T key;
- public Generic(T key) {
- this.key = key;
- }
- public T getKey(){
- return key;
- }
- }
复制代码 如何实例化泛型类:- Generic<Integer> genericInteger = new Generic<Integer>(666);
- Generic<String> genericStr = new Generic<String>("hello");
复制代码 泛型接口
- //定义一个泛型接口
- public interface Generator<T> {
- public T method();
- }
- //实现泛型接口,不指定类型
- class GeneratorImpl<T> implements Generator<T>{
- @Override
- public T method() {
- return null;
- }
- }
- //实现泛型接口,指定类型
- class GeneratorImpl<T> implements Generator<String>{
- @Override
- public String method() {
- return "hello";
- }
- }
复制代码 泛型方法
- public class GenericMethods {
- public <T> void f(T x){
- System.out.println(x.getClass().getName());
- }
- public static void main(String[] args) {
- GenericMethods gm = new GenericMethods();
- gm.f("啦啦啦");
- gm.f(666);
- }
- }
复制代码 结果:
java.lang.String
java.lang.Integer
泛型的底层实现机制
ArrayList源码解析
通过上文我们知道,为了让ArrayList存取各种数据类型的值,我们需要把ArrayList模板化,将变量的数据类型 给抽象出来,作为类型参数- public class ArrayList<T> {
- private T[] array;// 我们以为ArrayList<T>内部会有个T[] array
- private int size;
- public void add(T e) {...}
- public void remove(int index) {...}
- public T get(int index) {...}
- }
复制代码 但当我们查看Jdk8 的ArrayList源码,底层数组还是Object数组:transient Object[] elementData;
那ArrayList为什么还能进行类型约束和自动类型转换呢?
什么是泛型擦除
我们再看一个经典的例子:- public class genericTest {
- public static void main(String [] args) {
- String str="";
- Integer param =null;
- ArrayList<String> l1 = new ArrayList<String>();
- l1.add("aaa");
- str = l1.get(0);
- ArrayList<Integer> l2 = new ArrayList<Integer>();
- l2.add(666);
- param = l2.get(0);
- System.out.println(l1.getClass() == l2.getClass());
-
- }
- }
复制代码 结果竟然是true,ArrayList.class 和 ArrayList.class 应该是不同的类型。通过getClass()方法获取他们的类的信息,竟然是一样的。我们来查看这个文件的class文件:- public class genericTest {
- public genericTest() {
- }
- public static void main(String[] var0) {
- String var1 = "";
- Integer var2 = null;
- ArrayList var3 = new ArrayList();//泛型被擦擦了
- var3.add("aaa");
- var1 = (String)var3.get(0);
- ArrayList var4 = new ArrayList();//泛型被擦擦了
- var4.add(666);
- var2 = (Integer)var4.get(0);
- System.out.println(var3.getClass() == var4.getClass());
- }
- }
复制代码 我们在对其反汇编一下:- $ javap -c genericTest
- ▒▒▒▒: ▒▒▒▒▒▒▒ļ▒genericTest▒▒▒▒com.zj.demotest.test5.genericTest
- Compiled from "genericTest.java"
- public class com.zj.demotest.test5.genericTest {
- public com.zj.demotest.test5.genericTest();
- Code:
- 0: aload_0
- 1: invokespecial #1 // Method java/lang/Object."<init>":()V
- 4: return
- public static void main(java.lang.String[]);
- Code:
- 0: ldc #2 // String
- 2: astore_1
- 3: aconst_null
- 4: astore_2
- 5: new #3 // class java/util/ArrayList
- 8: dup
- 9: invokespecial #4 // Method java/util/ArrayList."<init>":()V
- 12: astore_3
- 13: aload_3
- 14: ldc #5 // String aaa
- 16: invokevirtual #6 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
- 19: pop
- 20: aload_3
- 21: iconst_0
- 22: invokevirtual #7 // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
- 25: checkcast #8 // class java/lang/String
- 28: astore_1
- 29: new #3 // class java/util/ArrayList
- 32: dup
- 33: invokespecial #4 // Method java/util/ArrayList."<init>":()V
- 36: astore 4
- 38: aload 4
- 40: sipush 666
- 43: invokestatic #9 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
- 46: invokevirtual #6 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
- 49: pop
- 50: aload 4
- 52: iconst_0
- 53: invokevirtual #7 // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
- 56: checkcast #10 // class java/lang/Integer
- 59: astore_2
- 60: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
- 63: aload_3
- 64: invokevirtual #12 // Method java/lang/Object.getClass:()Ljava/lang/Class;
- 67: aload 4
- 69: invokevirtual #12 // Method java/lang/Object.getClass:()Ljava/lang/Class;
- 72: if_acmpne 79
- 75: iconst_1
- 76: goto 80
- 79: iconst_0
- 80: invokevirtual #13 // Method java/io/PrintStream.println:(Z)V
- 83: return
- }
复制代码
- 看第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对象能够储存所有的泛型:- ArrayList<Object> list = new ArrayList<String>();
复制代码 但可以的是编译器提示报错:

明明 String是Object类的子类,我们可以发现,泛型不存在继承、多态关系,泛型左右两边要一样
别担心,JDK提供了通配符?来应对这种场景,我们可以这样:- ArrayList<Object> list = new ArrayList<String>();list = new ArrayList();
复制代码 通配符表示可以接收任意类型,此处?是类型实参,而不是类型形参。我们可以把它看做是String、Integer等所有类型的"父类"。是一种真实的类型。
通配符还有:
- 上边界限定通配符,如 list不可以添加任何类型,因为并不知道实际是哪种类型
- 返回值和泛型相关的都只能用Object接收
extends 上边界通配符
- //泛型的上限只能是该类型的类型及其子类,其中Number是Integer、Long、Float的父类
- ArrayList<? extends Number> list = new ArrayList<Integer>();
- ArrayList<? extends Number> list2 = new ArrayList<Long>();
- ArrayList<? extends Number> list3 = new ArrayList<Float>();
- list.add(1);//报错,extends不允许存入
- ArrayList<Long> longList = new ArrayList<>();
- longList.add(1L);
- list = longList;//由于extends不允许存入,list只能重新指向longList
- Number number = list.get(0); // extends 取出来的元素(Integer,Long,Float)都可以转Number
复制代码 如果想对数组进行复制操作的话,可以通过Arrays.copyOfRange()方法- //泛型的下限只能是该类型的类型及其父类,其中Number是Integer、Long、Float的父类
- ArrayList<? super Integer> list = new ArrayList<Integer>();
- ArrayList<? super Integer> list2 = new ArrayList<Number>();
- ArrayList<? super Integer> list3 = new ArrayList<Long>();//报错
- ArrayList<? super Integer> list4 = new ArrayList<Float>();//报错
- list2.add(123);//super可以存入,只能存Integer及其子类型元素
- Object aa = list2.get(0);//super可以取出,类型只能是Object
复制代码 反射其实可以绕过泛型的限制
由于我们知晓java是通过泛型擦除来实现泛型的,JVM只能识别原始类型Object,所以我们只需骗过编译器的校验即可,反射是程序运行时发生的,我们可以借助反射来波骚操作- public class A<T>{
- public T get(T a){
- //进行一些操作
- return a;
- }
- }
- public class B extends A<String>{
- @override
- public String get(String a){
- //进行一些操作
- return a;
- }
- }
复制代码 结果:
111
骚气的我 又出现了

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

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