深入理解java泛型(java什么是泛型,有什么优点)

  本篇文章为你整理了深入理解java泛型(java什么是泛型,有什么优点)的详细内容,包含有java泛型的定义和使用 java什么是泛型,有什么优点 java泛型的作用是什么 对java泛型的理解 深入理解java泛型,希望能帮助你了解 深入理解java泛型。

  目录什么是Java泛型泛型的使用泛型类泛型接口泛型方法泛型的底层实现机制ArrayList源码解析什么是泛型擦除泛型的边界?:无界通配符extends 上边界通配符super 下边界通配符PECS原则泛型是怎么擦除的擦除类定义中的无限制类型参数擦除类定义中的有限制类型擦除擦除方法定义中的类型参数桥接方法和泛型的多态泛型擦除带来的限制与局限泛型不适用基本数据类型无法创建具体类型的泛型数组反射其实可以绕过泛型的限制尾语

  作者:小牛呼噜噜 https://xiaoniuhululu.com
 

  计算机内功、JAVA底层、面试相关资料等更多精彩文章在公众号「小牛呼噜噜 」

  什么是Java泛型

  Java 泛型(generics)是 Jdk 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制, 该机制允许程序员在编译时检测到非法的类型。

  比如 ArrayList String list= new ArrayList String () 这行代码就指明了该 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 list = new ArrayList();

 

  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);

  
 

 

  结果竟然是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 ? list = new ArrayList String 

 

  list = new ArrayList Integer

  

 

  通配符 ? 表示可以接收任意类型,此处?是类型实参,而不是类型形参。我们可以把它看做是String、Integer等所有类型的"父类"。是一种真实的类型。
 

  通配符还有:

  上边界限定通配符,如 ? extends E

  下边界通配符,如 ? super E

  ?:无界通配符

  ?是开放限度最大的,可指向任意类型,但在对于其的存取上也是限制最大的:

  入参和泛型相关的都不能使用, 除了null(禁止存入),比如ArrayList ? 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

  

 

  extends指向性被砍了一半,只能指向子类型和父类型,但方法使用上又适当放开了:

  值得注意的是:这里的extends并不表示类的继承含义,只是表示泛型的范围关系

  extends不允许存入,由于使用extends ,比如ArrayList ? extends Number list可以接收Integer、Long、Float,但是泛型本质是保证两边类型确定,这样的话在程序运行期间,再存入数据,编译器可无法知晓数据的类型,所以只能禁止了。

  但为什么ArrayList ? extends Number list可以重新指向longList来变向地"存储"值,那是因为ArrayList Long longList = new ArrayList ();这边的泛型已经约束两边的类型了,编译器知晓longList储存的数据都是Long类型

  但extends允许取出,取出来的元素可以往边界类型转

  extends中可以指定多个范围,实行泛型类型检查约束时,会以最左边的为准。

  super 下边界通配符

  

//泛型的下限只能是该类型的类型及其父类,其中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

  

 

  super允许存入编辑类型及其子类型元素,但取出元素只能为Object类型

  PECS原则

  泛型通配符的出现,是为了获得最大限度的灵活性。如果要用到通配符,需要结合业务考虑,《Effective Java》提出了:PECS(Producer Extends Consumer Super)

  需要频繁往外读取内容(生产者Producer),适合用 ? extends T

  需要频繁写值(消费者Consumer),适合用 ? super T :super允许存入子类型元素

  ? 表示不确定的 java 类型,一般用于只接收任意类型,而不对其处理的情况

  泛型是怎么擦除的

  Java 编译器通过如下方式实现擦除:

  用 Object 或者界定类型替代泛型,产生的字节码中只包含了原始的类,接口和方法;

  在恰当的位置插入强制转换代码来确保类型安全;

  在继承了泛型类或接口的类中自动产生桥接方法来保留多态性。

  擦除类定义中的无限制类型参数

  当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和 ? 的类型参数都被替换为Object
 

  擦除类定义中的有限制类型擦除

  当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,
 

  形如

   T extends Number 和 ? extends Number 的类型参数被替换为Number,

   ? super Number 被替换为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;

  

 

  由于类型擦出机制的存在,按理说编译后的文件在翻译为java应如下所示:

  

public class A{

 

   public Object get(Object a){

   //进行一些操作

   return a;

  public class B extends A{

   @override

   public String get(String a){

   //进行一些操作

   return a;

  

 

  但是,我们可以发现 @override意味着B对父类A中的get方法进行了重写,但是依上面的程序来看,只是重载,依然可以执行父类的方法,这和期望是不附的,也不符合java继承、多态的特性。

  
重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!

  重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。

  
为了解决这个问题,java在编译期间加入了桥接方法。编译后再翻译为java原文件其实是:

  

public class A{

 

   public Object get(Object a){

   //进行一些操作

   return a;

  public class B extends A{

   @override

   public String get(String a){

   //进行一些操作

   return a;

   //桥接方法!!!

   public Object get(Object a){

   return get((String)a)

  

 

  桥接方法重写了父类相同的方法,并且桥接方法中,最终调用了期望的重写方法,并且桥接方法在调用目的方法时,参数被强制转换为指定的泛型类型。桥接方法搭起了父类和子类的桥梁。

  桥接方法是伴随泛型方法而生的,在继承关系中,如果某个子类覆盖了泛型方法,则编译器会在该子类自动生成桥接方法。所以我们实际使用泛型的过程中,无需担心桥接方法。

  泛型擦除带来的限制与局限

  泛型不适用基本数据类型

  不能用类型参数代替基本类型(byte 、short 、int 、long、float 、 double、char、boolean)
 

  比如, 没有 Pair double , 只 有 Pair Double 。 其原因是泛型擦除,擦除之后只有原始类型Object, 而 Object 无法存储 double等基本类型的值。

  但Java同时有自动拆装箱特性,可以将基本类型装箱成包装类型,这样就使用泛型了,通过中转,即可在功能上实现“用基本类型实例化类型化参数”。

  
无法创建具体类型的泛型数组

  

List Integer [] l1 = new ArrayList Integer [10];// Error

 

  List String [] l2 = new ArrayList String [10];// Error

  

 

  上文我们知晓ArrayList,底层仍旧采用Object[],Integer,String类型信息都被擦除

  借助无限定通配符 ?,可以创建泛型数组,但是涉及的操作都基本上与类型无关

  

List ? [] l1 = new ArrayList ? [10];

 

  

 

  如果想对数组进行复制操作的话,可以通过Arrays.copyOfRange()方法

  

public class TestArray {

 

   public static void main(String[] args) {

   Integer[] array = new Integer[]{2, 3, 1};

   Integer[] arrNew = copy(array);

   private static E E[] copy(E[] array) {

   return Arrays.copyOfRange(array, 0, array.length);

  

 

  反射其实可以绕过泛型的限制

  由于我们知晓java是通过泛型擦除来实现泛型的,JVM只能识别原始类型Object,所以我们只需骗过编译器的校验即可,反射是程序运行时发生的,我们可以借助反射来波骚操作

  

List Integer l1 = new ArrayList ();

 

  l1.add(111);

  //l1.add("骚气的我"); // 泛型会报错

  try {

   Method method = l1.getClass().getDeclaredMethod("add",Object.class);

   method.invoke(l1,"骚气的我 又出现了");

  } catch (NoSuchMethodException e) {

   e.printStackTrace();

  } catch (IllegalAccessException e) {

   e.printStackTrace();

  } catch (InvocationTargetException e) {

   e.printStackTrace();

  for ( Object o: l1){

   System.out.println(o);

  

 

  结果:

  111

  骚气的我 又出现了

  如果你了解其他语言(例如 C++ )的参数化机制,你会发现,Java 泛型并不能满足所有的预期。由于泛型出来前,java已经有了很多项目了,为了兼容老版本,采用了泛型擦除来“实现泛型”,这会遇到很多意料之外的麻烦,但这并不是说 Java 泛型毫无用处,它大多数情况能够让代码更加优雅,后面有机会我们会继续深入聊聊泛型擦除带来的麻烦及其历史渊源。

  参考资料:
 

  《On Java8》
 

  《Effective Java》
 

  https://www.liaoxuefeng.com/wiki/1252599548343744/1265102638843296
 

  https:///mahuan2/p/6073493.html

  本篇文章到这里就结束啦,很感谢你能看到最后,如果觉得文章对你有帮助,别忘记关注我!更多精彩的文章
 

  以上就是深入理解java泛型(java什么是泛型,有什么优点)的详细内容,想要了解更多 深入理解java泛型的内容,请持续关注盛行IT软件开发工作室。

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: