java泛型梳理

腾飞      2022-04-15     798

关键词:

java泛型梳理

概述
  • 泛型,即参数化类型,是在JDK1.5之后才开始引入的。
  • 所谓参数化类型是指所操作的数据类型在定义时被定义为一个参数,然后在使用时传入具体的类型。
  • 这种参数类型可以用在类,接口,方法的创建中,分别被称为泛型类、泛型接口和泛型方法。
  • 泛型值存在于java的编译期,编译后生成字节码文件泛型是被擦除的;
Java泛型的底层原理
  • 泛型思想最早在C++语言的模板(Templates)中产生,Java后来也借用了这种思想。虽然思想一致,但是他们存在着本质性的不同。
  • C++中的模板是真正意义上的泛型,在编译时就将不同模板类型参数编译成对应不同的目标代码,ClassName和ClassName是两种不同的类型,这种泛型被称为真正泛型。这种泛型实现方式,会导致类型膨胀,因为要为不同具体参数生成不同的类。
  • Java中ClassName和ClassName虽然在源代码中属于不同的类,但是编译后的字节码中,他们都被替换成原始类型(ClassName),而两者的原始类型的一样的,所以在运行时环境中,ClassName和ClassName就是同一个类。Java中的泛型是一种特殊的语法糖,通过类型擦除实现(后面介绍),这种泛型称为伪泛型。由于Java中有这么一个障眼法,如果没有进行深入研究,就会在产生莫名其妙的问题。值得一提的是,不少大牛对Java的泛型的实现方式很不满意。
  • JVM类型擦除
    • Java中的泛型是通过类型擦除来实现的。所谓类型擦除,是指通过类型参数合并,将泛型类型实例关联到同一份字节码上。编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上。类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法。
    • 实际上,Java的泛型除了类型擦除之外,还会自动生成checkcast指令进行强制类型转换。
泛型解决的问题
// 定义一个List,add()可以存放Object及其子类实例
List list = new ArrayList();
list.add(123); // 合法
list.add("123"); // 合法

// 我们在编译时无法知晓list到底存放的什么数据,于是在进行强制转换时发生异常
int i = (Integer) list.get(1); // 抛出ClassCastException异常

  • 上面的代码首先实例化一个ArrayList对象,它可以存放所有Object及其子类实例。分别add一个Integer类型对象和String类型对象,我们原本以为list中存放的全部是Integer类型对象,于是在使用get()方法获取对象后进行强制转换。从代码中可以看到,索引值为1的位置放置的String类型,很显然在进行强制转换时会抛出ClassCastException(类型转换异常)。由于这种异常只会发生在运行时,我们在开发时稍有不慎,就会直接掉到坑里,还很难排查出问题。
  • 为什么会出现这种问题呢?
    • 集合本身无法对其存放的对象类型进行限定,可以涵盖Java中的所有类型。缺口太大,导致各种蛇、蚁、虫、鼠通通都可以进来。
    • 由于我们要使用的实际存放类型的方法,所以不可避免地要进行类型转换。小对象转大对象很容易,大对象转小对象则有很大的风险,因为在编译时,我们无从得知对象真正的类型。
    • 泛型就是为了解决这类问题而诞生的
泛型类的定义和使用
  • 一个泛型类(generic class)就是具有一个或多个类型变量的类。上面的例子中的List就是一个典型的泛型类。泛型类的定义结构类似下面的代码
  • 注意,在Java编码规范中,类型变量通常使用较短的大写字母,并且最好与其作用相匹配。譬如:List中的变量使用E,对应单词Element,Map中的K,V变量对应单词Key和Value。当然这些都是约定性质的东西,其实类型变量的命名规则与Java中的普通变量命名规则是一致的。
public class ClassName<T1, T2> { // 可以任意多个类型变量

    public void doSomething(T1 t1) {
        System.out.println(t1);
    }
}

ClassName<String, String> a = new ClassName<String, String>();
a.doSomething("hello world");
泛型接口的定义和使用
  • 接口本质上来说就是一种特殊的类,所以泛型接口的定义和使用与泛型类相差无几。
public interface InterfaceName<T1, T2> { // 可以任意多个类型变量

    public void doSomething(T1 t1);
}

public class ConcreteName<T2> implements InterfaceName<String, T2> {

    public void doSomething(String t1) {
        System.out.println(t1);
    }
}

InterfaceName<String, String> a = new ConcreteName<String>();
a.doSomething("hello world");
泛型方法的定义和使用
  • 泛型类和泛型接口的类型变量都是定义在类型级别,其作用域可覆盖成员变量和成员方法。泛型方法的类型参数定义在方法签名中.
/**
 * 创建一个指定类型的无参构造的对象实例。
 * @param <T> 待创建对象的类型。
 * @param t 指定类型所对应的Class对象。
 * @return 返回创建的对象。
 * @throws Exception
 */
public <T> T getObject(Class<T> t) throws Exception {
    return t.newInstance();
}

String newStr = generic.getObject(String.class);
泛型变量的类型限定
public <T> T getMax(T t1, T t2) {
    if (t1.compareTo(t2) > 1) { // 编译错误
        return t1;
    } else {
        return t2;
    }
}
  • 在上面的代码无法通过编译,由于我们都没有对类型变量对任何的约束限制,那么实际上这个类型可以是任意Object及其子类。那么在使用这个类型变量时,只能调用Object类中的方法。而Object本身就是Java中对顶层的类,没有实现Comparable接口,所以无法调用compareTo方法来比较对象的大小。这时候可以通过限定类型变量来达到目的。
public <T extends Comparable<T>> T getMax(T t1, T t2) {
    if (t1.compareTo(t2) > 1) {
        return t1;
    } else {
        return t2;
    }
}
  • 注意到上面的代码使用extends关键字限定了类型变量T必须继承自Comparable,于是变量t1和t2就可以使用Comparable接口中的compareTo方法了。
  • 不管是泛型类、泛型接口还是泛型方法,都可以进行类型限定。类型限定的特点如下:
    • 不管该限定是类还是接口,统一都使用extends关键字。
    • 使用&符号进行多个限定,那么传入的具体类型必须同时是这些类型的子类。
    • 由于Java中不支持多继承,所以不存在一个同时继承两个以上的类的类。所以,在泛型的限定中,&连接的类型最多只能有一个类,而接口数量则没有限制。同时,如果同时限定类和接口,则必须将类写在最前面。
public <T extends Serializable&Cloneable&Comparable> T getMax(T t1, T t2) {
    ...
}

public <T extends Object&Serializable&Cloneable&Comparable> T getMax(T t1, T t2) { // 合法
    ...
}
public <T extends Object&ArrayList> T getMax(T t1, T t2) { // 同时限定两个类,不合法
    ...
}
public <T extends Serializable&Cloneable&Comparable&Object> T getMax(T t1, T t2) { // 将类写在最后面,不合法
    ...
}
泛型通配符
  • 我们知道Ingeter是Number的一个子类,同时在特性章节中我们也验证过Generic<Ingeter>Generic<Number>实际上是相同的一种基本类型。那么问题来了,在使用Generic<Number>作为形参的方法中,能否使用Generic<Ingeter>的实例传入呢?在逻辑上类似于Generic<Number>Generic<Ingeter>是否可以看成具有父子关系的泛型类型呢?
//为了弄清楚这个问题,我们使用Generic<T>这个泛型类继续看下面的例子:
public void showKeyValue1(Generic<Number> obj){
    Log.d("泛型测试","key value is " + obj.getKey());
}
Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);

showKeyValue(gNumber);

// showKeyValue这个方法编译器会为我们报错:Generic<java.lang.Integer> 
// cannot be applied to Generic<java.lang.Number>
// showKeyValue(gInteger);
  • 通过提示信息我们可以看到Generic不能被看作为`Generic的子类。由此可以看出:同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
    回到上面的例子,如何解决上面的问题?总不能为了定义一个新的方法来处理Generic类型的类,这显然与java中的多台理念相违背。因此我们需要一个在逻辑上可以表示同时是Generic和Generic父类的引用类型。由此类型通配符应运而生。
public void showKeyValue1(Generic<?> obj){
    Log.d("泛型测试","key value is " + obj.getKey());
}
  • 类型通配符一般是使用?代替具体的类型实参,注意了,此处’?’是类型实参,而不是类型形参 。再直白点的意思就是,此处的?和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型。
静态方法与泛型
  • 静态方法有一种情况需要注意一下,那就是在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。
    即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。
public class StaticGenerator<T> {
    ....
    ....
    /**
     * 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
     * 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
     * 如:public static void show(T t){..},此时编译器会提示错误信息:
          "StaticGenerator cannot be refrenced from static context"
     */
    public static <T> void show(T t){

    }
}
泛型的好处
  • 1,类型安全。 泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的程度上验证类型假设。没有泛型,这些假设就只存在于程序员的头脑中(或者如果幸运的话,还存在于代码注释中)。
    2,消除强制类型转换。 泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。
    3,潜在的性能收益。 泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换(没有泛型的话,程序员会指定这些强制类型转换)插入生成的字节码中。但是更多类型信息可用于编译器这一事实,为未来版本的 JVM 的优化带来可能。由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改。所有工作都在编译器中完成,编译器生成类似于没有泛型(和强制类型转换)时所写的代码,只是更能确保类型安全而已。
  • Java语言引入泛型的好处是安全简单。泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。
参考

java面向对象泛型

...清晰的。然后我将视频中所说再记忆下。java面向对象之泛型:认识泛型  JavaSE1.5的新特性,泛型的本质是参数化类型,可以代表任何数据类型。java面向对象之泛型:构造函数中使用  classGen<T>{      privateTvalue;... 查看详情

33张java高级进阶技术思维导图,白嫖大佬梳理的技术要点!只需看重点,学习效率提升300%(建议收藏)

...涨薪基地”发送【666】自动获取所有图片!Java集合 泛型反射 计算机操作系统 计算机组成原理 数据结构  NIO Java编程工具篇GI 查看详情

33张java高级进阶技术思维导图,白嫖大佬梳理的技术要点!只需看重点,学习效率提升300%(建议收藏)

...涨薪基地”发送【666】自动获取所有图片!Java集合 泛型反射 计算机操作系统 计算机组成原理 数据结构  NIO Java编程工具篇GI 查看详情

java——泛型(代码片段)

文章目录Java中泛型的概述Java中泛型的语法规则泛型集合泛型方法泛型方法示例(代码演示)小结泛型类泛型类示例(代码演示)小结泛型接口泛型接口示例(代码演示)高级泛型总结Java中泛型的概述泛型... 查看详情

java泛型:泛型

一、二、三、四、五、六、七、八、  查看详情

java知识树梳理

Java知识树梳理  1、前端    HTML    CSS    JavaScript  2、后台    Java基础     算法    web方面    分布式、中间件、服务器等方面    大数据方面  3、数据库 查看详情

java中那些绕不开的内置接口--serializable

...使用实践的同时,把这个过程中遇到的各种相关知识点:泛型、​​Lambada​​​、​​Stream​​操作,一并给大家做了梳理。从这篇开始我们进入下一部分,用三到五部分给大家梳理一下,在用Java编程时,那些我们绕不开的​... 查看详情

java_泛型笔记(代码片段)

文章目录Java_泛型简介Java_泛型的概念Java_为什么存在泛型Java_自定义泛型结构Java_泛型的声明Java_泛型的实例化Java_泛型类Java_泛型方法Java_泛型在继承上的体现Java_通配符的使用Java_泛型简介集合容器类在设计阶段/声明阶段不能确... 查看详情

java基础系列知识梳理

查看详情

java泛型泛型简介(泛型类|泛型方法|静态方法的泛型|泛型类与泛型方法完整示例)(代码片段)

文章目录一、泛型简介二、泛型类三、泛型方法四、静态方法的泛型五、泛型类与泛型方法完整示例一、泛型简介泛型可以简单理解为参数化类型,主要作用在类,方法,接口上;java泛型与C++模板:Java中的泛型,是仿照C++中... 查看详情

java泛型与非泛型java(代码片段)

查看详情

java高级--java泛型

类型的参数化泛型类可以同时设置多个参数泛型类可以继承泛型类泛型类可以实现泛型接口示例--泛型类packagecom.date;publicclassGenericDemo{publicstaticvoidmain(String[]args){ GenClass<String>genClass=newGenClass<String>("tanlei"); System.out 查看详情

java泛型

1、什么是泛型Java泛型(generics)是JDK5中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许开发人员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。2、... 查看详情

集合泛型的使用

...,特别是其中事件触发地方。该例子是对通过基于集合的泛型类来构建一个自定义的类,并且基于四个方法,重写了其增删改查的操作。该例子是通过一个改变的改变的通知事件,在每一个方法的最后被触发。其中,DinosaursChange... 查看详情

java之泛型

1、为什么要有泛型Generic?2、总结Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不... 查看详情

java泛型是什么?一文带你吃透泛型(代码片段)

文章目录1.Java泛型2.泛型类3.泛型接口4.泛型方法5.泛型集合Java编程基础教程系列1.Java泛型Java泛型是JDK1.5中引入的一个新特性,其本质是参数化类型,把类型作为参数传递。其主要的形式有泛型类,泛型接口和泛型方法。泛型概... 查看详情

java--泛型

泛型简而言之就是类型的参数化泛型分为泛型接口、泛型类和泛型方法,不存在泛型数组。举例//泛型类;publicclassData<T>{privateTdata;publicData(Tdata){this.data=data;}publicTgetData(){returndata;}}publicclassTest02{publicstaticvoidmain(String[]arg 查看详情

java泛型

Java泛型Java泛型(generics)是JDK5中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。假定我们有这... 查看详情