说说你对泛型的理解

说说你对泛型的理解

章节目录

文章目录

简答

泛型是Java中的一个特性,它允许我们在定义类、接口或方法时使用类型参数,以实现代码的通用性和安全性。泛型的目的是在编译时进行类型检查,并提供编译期间的类型安全。

泛型的理解包括以下几个方面:

**首先,**泛型提供了代码重用和通用性。通过使用泛型,我们可以编写可重用的代码,可以在不同的数据类型上执行相同的操作。这样,我们可以避免重复编写类似的代码,提高了开发效率。

**其次,**泛型强调类型安全。编译器可以在编译时进行类型检查,阻止不符合类型约束的操作。这样可以避免在运行时出现类型错误的可能,增加了程序的稳定性和可靠性。

**另外,**使用泛型可以避免大量的类型转换和强制类型转换操作。在使用泛型集合类时,不需要进行强制类型转换,可以直接获取正确的数据类型,提高了代码的可读性和维护性。

**此外,**泛型还可以在编译时进行类型检查,提前发现潜在的类型错误。这种类型检查是在编译时进行的,避免了一些常见的运行时类型异常,减少了错误的可能性。

**最后,**泛型可以增加代码的可读性和可维护性。通过使用泛型,我们可以明确指定数据类型,并在代码中表达清晰,使得其他开发人员更容易理解代码的意图和功能。

一、泛型概述

什么是泛型?为什么要使用泛型?

泛型,即"参数化类型"。一提到参数,最熟悉的就是定义方法时有形参列表,普通方法的形参列表中,每个形参的数据类型是确定的,而变量是一个参数。在调用普通方法时需要传入对应形参数据类型的变量(实参),若传入的实参与形参定义的数据类型不匹配,则会报错

那参数化类型是什么?以方法的定义为例,在方法定义时,将方法签名中的形参的数据类型也设置为参数(也可称之为类型参数),在调用该方法时再从外部传入一个具体的数据类型和变量。

泛型的本质是为了将类型参数化, 也就是说在泛型使用过程中,数据类型被设置为一个参数,在使用时再从外部传入一个数据类型;而一旦传入了具体的数据类型后,传入变量(实参)的数据类型如果不匹配,编译器就会直接报错。这种参数化类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

泛型使用场景

在 ArrayList 集合中,可以放入所有类型的对象,假设现在需要一个只存储了 String 类型对象的 ArrayList 集合。

java 复制代码
public class demo1 {
    public static void main(String[] args) {
        ArrayList<String> list=new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add("c");
        for(String s:list){
            System.out.println(s);
        }
    }
} 
//上面代码没有任何问题,在遍历 ArrayList 集合时,只需将 Object 对象进行向下转型成 String 类型即可得到 String 类型对象。
// 但如果在添加 String 对象时,不小心添加了一个 Integer 对象,会发生什么?看下面代码:
public static void main(String[] args) {
    ArrayList list = new ArrayList();
    list.add("aaa");
    list.add("bbb");
    list.add("ccc");
    list.add(666);
    for (int i = 0; i < list.size(); i++) {
        System.out.println((String)list.get(i));
    }
}
  • 上述代码在编译时没有报错,但在运行时却抛出了一个 ClassCastException 异常,其原因是 Integer 对象不能强转为 String 类型。
  • 那如何可以避免上述异常的出现?即我们希望当我们向集合中添加了不符合类型要求的对象时,编译器能直接给我们报错,而不是在程序运行后才产生异常。这个时候便可以使用泛型了。
java 复制代码
public static void main(String[] args) {
    ArrayList<String> list = new ArrayList();      
    list.add("aaa");       
    list.add("bbb");       
    list.add("ccc");        
    //list.add(666);// 在编译阶段,编译器会报错
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }
}

< String > 是一个泛型,其限制了 ArrayList 集合中存放对象的数据类型只能是 String,当添加一个非 String 对象时,编译器会直接报错。这样,我们便解决了上面产生的 ClassCastException 异常的问题(这样体现了泛型的类型安全检测机制)。

泛型的好处

  • 统一数据类型,对于后续业务层中取出数据有很强的统一规范性,方便对数据的管理;

  • 把运行时期的问题提前到了编译期,避免了强转类型转换可能出现的异常,降低了程序出错的概率;

二、泛型类

泛型类的使用场景:当一个类中,某个变量的数据不确定时,就可以定义带有泛型的类。

我们平常所用的ArrayList类,就是一个泛型类,我们看如下源码

ArrayList 源码上显示,在ArrayList类的后面,便是 泛型,定义了这样的泛型,就可以让使用者在创建ArrayList对象时自主定义要存放的数据类型。

这里的 E 可以理解成变量,它不是用来记录数据的,而是记录数据的类型的。可以写成很多字母,T,V,K都可以,通常这些字母都是英文单词的首字母,V表示 value,K表示 key,E表示 element,T表示 type;如果你想,自己练习的时候写成ABCDEFG都可以,但建议养成好习惯,用专业名词的首字母,便于理解。

  • T :代表一般的任何类;
  • E :代表 Element 元素的意思,或者 Exception 异常的意思;
  • K :代表 Key 的意思;
  • V :代表 Value 的意思,通常与 K 一起配合使用;
  • S :代表 Subtype 的意思,文章后面部分会讲解示意。

三、泛型方法

我们什么时候会用到泛型方法呢?

通常情况下,当一个方法的形参不确定的情况下,我们会使用到泛型方法。

泛型方法其实与泛型类有着紧密的联系,通过上面我写的自定义泛型类不难看出,在泛型类中,所有方法都可以使用类上定义的泛型。

但是,泛型方法却可以脱离泛型类单独存在,泛型方法上定义的泛型只有本方法上可以使用,其他方法不可用。

java 复制代码
public static <E> void addAll(ArrayList<E> list, E ...e1) {
        for (E e : e1) {
          list.add(e);
        }
}

四、泛型接口

泛型接口与泛型方法相似,当我们的接口中,参数类型不确定的时候,就可以使用泛型。

java 复制代码
public interface MyList<E> {
    
    // 定义一个方法做简单测试
    public boolean add(E e);
}

五、类型擦除

什么是类型擦除

编译器编译带类型说明的集合时会去掉类型信息

泛型的本质是将数据类型参数化,它通过擦除的方式来实现,即编译器会在编译期间擦除代码中的所有泛型语法并相应的做出一些类型转换动作。

换而言之,泛型信息只存在于代码编译阶段,在代码编译结束后,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。也就是说,成功编译过后的 class 文件中不包含任何泛型信息,泛型信息不会进入到运行时阶段。

其实Java中的泛型本质是伪泛型

当把集合定义为string类型的时候,当数据添加在集合当中的时候,仅仅在门口检查了一下数据是否符合String类型, 如果是String类型,就添加成功,当添加成功以后,集合还是会把这些数据当做Object类型处理,当往外获取的时候,集合在把他强转String类型

代码编译到class文件的时候,泛型就消失,叫泛型的擦除

java 复制代码
public class GenericType {
    public static void main(String[] args) {
        ArrayList arrayString = new ArrayList()< String >;       
        ArrayList arrayInteger = new ArrayList()< Integer >;  
        System.out.println(arrayString.getClass() == arrayInteger.getClass());// true  
    } 
} 

在这个例子中,我们定义了两个 ArrayList 集合,不过一个是 ArrayList< String>,只能存储字符串。一个是 ArrayList< Integer>,只能存储整型对象。我们通过 arrayString 对象和 arrayInteger 对象的 getClass() 方法获取它们的类信息并比较,发现结果为true。

明明我们在 <> 中传入了两种不同的数据类型,那为什么它们的类信息还是相同呢? 这是因为,在编译期间,所有的泛型信息都会被擦除, ArrayList< Integer > 和 ArrayList< String >类型,在编译后都会变成ArrayList< Objec t>类型

!CAUTION

那么是不是所有的类型参数被擦除后都以 Object 类进行替换呢

答案是否定的,大部分情况下,类型参数 T 被擦除后都会以 Object 类进行替换;而有一种情况则不是,那就是使用到了 extends 和 super 语法的有界类型参数 (上界(T extends Something)或下界(T super Something))

类型擦除的原理

假如我们定义了一个 ArrayList< Integer > 泛型集合,若向该集合中插入 String 类型的对象,不需要运行程序,编译器就会直接报错。这里可能有小伙伴就产生了疑问:

不是说泛型信息在编译的时候就会被擦除掉吗?那既然泛型信息被擦除了,如何保证我们在集合中只添加指定的数据类型的对象呢?

换而言之,我们虽然定义了 ArrayList< Integer > 泛型集合,但其泛型信息最终被擦除后就变成了 ArrayList< Object > 集合,那为什么不允许向其中插入 String 对象呢?

Java 是如何解决这个问题的?

其实在创建一个泛型类的对象时, Java 编译器是先检查代码中传入 < T > 的数据类型,并记录下来,然后再对代码进行编译,编译的同时进行类型擦除;如果需要对被擦除了泛型信息的对象进行操作,编译器会自动将对象进行类型转换。

可以把泛型的类型安全检查机制和类型擦除想象成演唱会的验票机制:以 ArrayList< Integer> 泛型集合为例。

当我们在创建一个 ArrayList< Integer > 泛型集合的时候,ArrayList 可以看作是演唱会场馆,而< T >就是场馆的验票系统,Integer 是验票系统设置的门票类型;

当验票系统设置好为< Integer >后,只有持有 Integer 门票的人才可以通过验票系统,进入演唱会场馆(集合)中;若是未持有 Integer 门票的人想进场,则验票系统会发出警告(编译器报错)。

在通过验票系统时,门票会被收掉(类型擦除),但场馆后台(JVM)会记录下观众信息(泛型信息)。

进场后的观众变成了没有门票的普通人(原始数据类型)。但是,在需要查看观众的信息时(操作对象),场馆后台可以找到记录的观众信息(编译器会自动将对象进行类型转换)。

java 复制代码
public class GenericType {
    public static void main(String[] args) {
        
        ArrayList arrayInteger = new ArrayList();
        // 设置验票系统   
        arrayInteger.add(111);
        // 观众进场,验票系统验票,门票会被收走(类型擦除) 
        Integer n = arrayInteger.get(0);
        // 获取观众信息,编译器会进行强制类型转换   
        System.out.println(n);  
    }
} 

擦除 ArrayList< Integer > 的泛型信息后,get() 方法的返回值将返回 Object 类型,但编译器会自动插入 Integer 的强制类型转换。也就是说,编译器把 get() 方法调用翻译为两条字节码指令:

对原始方法 get() 的调用,返回的是 Object 类型;

将返回的 Object 类型强制转换为 Integer 类型;

java 复制代码
Integer n = arrayInteger.get(0);// 这条代码底层如下:

//(1)get() 方法的返回值返回的是 Object 类型
Object object = arrayInteger.get(0);
//(2)编译器自动插入 Integer 的强制类型转换
Integer n = (Integer) object;

小结

1.泛型信息(包括泛型类、接口、方法)只在代码编译阶段存在,在代码成功编译后,其内的所有泛型信息都会被擦除,并且类型参数 T 会被统一替换为其原始类型(默认是 Object 类,若有 extends 或者 super 则另外分析);

2.在泛型信息被擦除后,若还需要使用到对象相关的泛型信息,编译器底层会自动进行类型转换(从原始类型转换为未擦除前的数据类型)。

相关推荐
guslegend16 天前
说说你对设计模式的理解
大厂面试专题
guslegend16 天前
设计模式是如何分类的
大厂面试专题
guslegend1 个月前
Java中变量和常量有什么区别
大厂面试专题
guslegend1 个月前
String类能被继承吗,为什么
大厂面试专题
guslegend1 个月前
HashMap和Hashtable有什么区别
大厂面试专题
guslegend1 个月前
Java中止线程的三种方式
大厂面试专题
guslegend1 个月前
Java五种文件拷贝方式
大厂面试专题
guslegend1 个月前
提示词工程能够解决什么问题?
大厂面试专题
guslegend1 个月前
缓存淘汰机制LRU和LFU的区别
大厂面试专题