Java中的泛型

先来看一道经典的测试题:

java 复制代码
public class GenericDemo2 {
    public static void main(String[] args) {
        ArrayList<String> list1 = new ArrayList<>();
        ArrayList<Integer> list2 = new ArrayList<>();
        System.out.println(list1.getClass() == list2.getClass());
    }
}

正确答案是true。为什么呢?因为编译成功的时候会将所有与泛型有关的信息进行擦除。

1、什么是泛型?

泛型的英文是Generic,中文意思是通用的。

泛型是一种类型参数化机制。

当成员变量、形参、方法的返回值类型不确定时使用泛型,把类型当作参数进行传递。

总的来说就是:

(1)使得数据的类型可以像参数一样由外部传递进来。

(2)类型安全:当数据的类型确定的时候又提供了一种编译时强类型检查机制。

(3)提高代码的复用性:当方法的功能完全一样,只是数据类型不一样时,使用泛型不用每种类型都实现一遍。

(4)避免了类型转换。

(5)良好的可读性。

细节:

  1. 泛型必须是引用数据类型,不能传递基本数据类型,如果要使用要提供其包装类。
  2. 在指定具体的数据类型后,可以添加该类型或者其子类类型的对象。
  3. 如果不写泛型,代码不会报错,默认类型是Object。

对于细节2的代码实现:

java 复制代码
import java.util.ArrayList;

public class GenericDemo5 {
    public static void main(String[] args) {
        ArrayList<Ye> list1 = new ArrayList<>();
        //在往集合中添加元素的时候,也可以添加其子类的对象
        list1.add(new Ye());
        list1.add(new Fu());
        list1.add(new Zi());
        method(list1);
    }
    public static void method(ArrayList<Ye> list){

    }
}
class Ye {}
class Fu extends Ye {}
class Zi extends Fu {}
class Student {}

为什么呢?

在使用ArrayList的时候,当指定具体的类型时,为什么可以向其中添加子类的对象?

因为可以将其赋给父类引用,不会出现类型转换异常,不会报错。

2、泛型如何定义以及如何使用?

根据使用的地方分为3种,分别是泛型类,泛型方法和泛型接口。

(1)泛型类

泛型类的定义

在类名的后面加一对尖括号,并在括号中填写类型参数,参数可以有多个,多个参数之间使用逗号分隔。

java 复制代码
public class GenericTest <E>{
    private E value;
    public E getValue(){
        return value;
    }
    public void setValue(E e){
        this.value = e;
    }
}

Java 还是建议我们用单个大写字母来代表类型参数。常见的如:

  1. T 代表Type的意思,表示任意的类。
  2. E 代表 Element 的意思,或者 Exception 异常的意思。
  3. K 代表 Key 的意思。
  4. V 代表 Value 的意思,通常与 K 一起配合使用。
  5. S 代表 Subtype 的意思,文章后面部分会讲解示意。
泛型类的使用

只需要在创建对象的时候指定相应的类型就可以了。

(2)泛型方法

泛型方法的定义

如果只在一个方法中使用,类型参数也就是尖括号那一部分也可以写在返回值之前。

java 复制代码
public class GenericTest2 {
    public <E> void set(E e){

    }
}

有一点需要注意:不能使用别的方法中使用定义的泛型,会报错。可以理解为此泛型的作用范围只有本方法。

泛型方法的使用
java 复制代码
public class GenericDemo2 {
    public static void main(String[] args) {
        GenericTest2 g2 = new GenericTest2();
        g2.set("123");
    }
}

类型推断:编译器会根据调用方法时参数的类型会将E指定为相应的类型。例如,在上述代码中,编译器根据传递的参数"123" 将E指定为String 类型,它发生在编译时。

练习1:

定义一个工具类ListUtil,其中有一个静态方法,可以向不同的集合中添加多个元素。

java 复制代码
public class ListUtil {
    private ListUtil(){

    }
    public static <E> void addAll (ArrayList<E> list, E e1, E e2){
        list.add(e1);
        list.add(e2);
    }
}

在调用此方法将list传递过去的时候,会将E指定为String类型。

细节:而且这个方法可以传递任意的类型过去。

java 复制代码
public class GenericDemo3 {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        ListUtil.addAll(list, "3", "4");
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String s = iterator.next();
            System.out.println(s);
        }
    }
}

(3)泛型接口

泛型接口的定义
java 复制代码
public interface Iterable<T> {
}
泛型接口的使用

根据实现的时候是否确定类型有两种方式去实现接口。

方式1:实现类给出具体类型。

java 复制代码
public class GenericDemo6 implements List<String> {
    @Override
    public int size() {
        return 0;
    }

    @Override
    public boolean isEmpty() {
        return false;
    }

...

}

这里的类GenericDemo6在实现时给出具体的String类型,那么在创建实现类的对象时就不用再给出类型了,并且操作的只能是String类型的数据。

方式2:实现类依然延续泛型,创建对象时再指定类型。

java 复制代码
public class GenericDemo7<E> implements List<E> {
    @Override
    public int size() {
        return 0;
    }

    @Override
    public boolean isEmpty() {
        return false;
    }

...

}

细节:

一个.java文件里可以有多个类。

多个类中只能有一个public类,而且文件名只能是public类的名字;

如果多个类中没有public类,则文件名可以是任意一个类的名字。

3、通配符

通配符是用于解决泛型之间的引用传递问题的特殊语法。

下面来看一个例子:

java 复制代码
import java.util.ArrayList;

public class GenericDemo5 {
    public static void main(String[] args) {
        ArrayList<Ye> list1 = new ArrayList<>();
        ArrayList<Fu> list2 = new ArrayList<>();
        ArrayList<Zi> list3 = new ArrayList<>();
        ArrayList<Student> list4 = new ArrayList<>();
        method(list1);//正确
        method(list2);//编译不通过,因为只能传递集合中元素是Ye的list
        method(list3);//编译不通过
        method(list4);//编译不通过
    }
    public static void method(ArrayList<Ye> list){

    }

}
class Ye {}
class Fu extends Ye {}
class Zi extends Fu {}
class Student {}

可以发现,虽然Fu类、Zi类与Ye类有直接和间接的继承关系,但传递的时候依然只能传集合中元素是Ye的list,本质与传完全无关的Student类的list是一样报错的。

前面在练习1中写一个方法是可以传递任意的数据类型,但是有时候,传递的时候就行传一定范围的类型,于是就出现了通配符。

?也表示不确定的类型,但它可以进行类型的限定。

  1. <? extends Ye>:表示类型参数可以是Ye类或者其子类类型;
  2. <? super Zi>:表示类型参数可以是Zi类或者其父类类型。

修改之后的代码为:

java 复制代码
import java.util.ArrayList;

public class GenericDemo5 {
    public static void main(String[] args) {
        ArrayList<Ye> list1 = new ArrayList<>();
        ArrayList<Fu> list2 = new ArrayList<>();
        ArrayList<Zi> list3 = new ArrayList<>();
        ArrayList<Student> list4 = new ArrayList<>();
        method(list1);//正确
        method(list2);//编译不通过,因为只能传递集合中元素是Ye的list
        method(list3);//编译不通过
        method(list4);//编译不通过
    }
    public static void method(ArrayList<? extends Ye> list){

    }

}
class Ye {}
class Fu extends Ye {}
class Zi extends Fu {}
class Student {}

细节:可以传 Array List<Ye > 或者 ArrayList<Fu> 或者 ArrayList <Zi>,但是没传之前谁知道传得是哪个,随便操作会出问题的。

练习2:

对于继承体系中每一个类的实现这里就不具体展开了,只列出3个要求的实现:

java 复制代码
    public static void keepCat(ArrayList<? extends Cat> list){
        for (Cat cat : list) {
            cat.eat();
        }
    }

    public static void keepDog(ArrayList<? extends Dog> list){
        for (Dog dog : list) {
            dog.eat();
        }
    }

    public static void keepPet(ArrayList<? extends Animal> list){
        for (Animal animal : list) {
            animal.eat();
        }
    }

来看一下<?>的应用:

java 复制代码
import java.util.ArrayList;

public class GenericDemo5 {
    public static void main(String[] args) {
        ArrayList<Ye> list1 = new ArrayList<>();
        list1.add(new Ye());
        list1.add(new Fu());
        list1.add(new Zi());
        ArrayList<Fu> list2 = new ArrayList<>();
        ArrayList<Zi> list3 = new ArrayList<>();
        ArrayList<Student> list4 = new ArrayList<>();
        method(list1);
        method(list2);
        method(list3);
        method(list4);
    }
    public static void method(ArrayList<?> list){

    }
}
class Ye {}
class Fu extends Ye {}
class Zi extends Fu {}
class Student {}

可以看到将method方法中的参数修改为<?>后,就可以传递任意类型的数据了,看起来与<E>有点像。

问题:Java 中 List<?> 和 List< Object > 之间的区别是什么?

可以把 List< String >、 List< Integer > 等集合赋值给 List<?> 的引用;而只能把 List< Object > 赋值给 List< Object > 的引用,但是 List< Object > 集合中可以加入任意类型的数据,因为 Object 类是最高父类。

PECS原则

即Producer Extends Consumer Super的缩写。

? extends E

并不知道集合中存储的是范围中的哪个类型,如果向集合中写入的刚好是同一级的子类,此时就会出现类型转换异常错误,所以为了类型安全禁止写入。

但是在读取的时候集合中的所有元素都可以向上转型为父类,详情可见练习2中的遍历。

? super E

因为集合中存的肯定是E或者其父类的引用,所以必定可以向其中写入E及其子类的对象,但是禁止写入任何父类的对象,因为有可能会超过集合中存储的数据类型,会抱错。而且读取的时候并不知道集合中存储的是什么类型的元素,所有元素可以全部向上转为Object类型,但是失去了意义。

从上述两个方面进行总结可以得到:

如果想从集合中读取,并且不能写入,可以使用<? extends E>通配符,即生产者Producer。

如果要向集合中写入,不需要读取,可以使用<? super E>通配符,即消费者Consumer。

类型擦除

  1. 泛型信息(包括泛型类、接口、方法)只在代码编译阶段存在,在代码成功编译后,其内的所有泛型信息都会被擦除,并且类型参数 T 会被统一替换为其原始类型(默认是 Object 类,若有 extends 或者 super 则另外分析);
  2. 在泛型信息被擦除后,若还需要使用到对象相关的泛型信息,编译器底层会自动进行类型转换(从原始类型转换为未擦除前的数据类型)。

先看一个例子,假设定义一个泛型类如下:

java 复制代码
public class Caculate<T> {
    private T num;
}

在该泛型类中定义了一个属性 num,该属性的数据类型是泛型类声明的类型参数 T ,这个 T 具体是什么类型,我们也不知道,它只与外部传入的数据类型有关。将这个泛型类反编译。

代码如下:

java 复制代码
public class Caculate {
    public Caculate() {}// 默认构造器,不用管
    
    private Object num;// T 被替换为 Object 类型
}

可以发现编译器擦除了 Caculate 类后面的泛型标识 < T >,并且将 num 的数据类型替换为 Object 类型,而替换了 T 的数据类型我们称之为原始数据类型。

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

答案是否定的,大部分情况下,类型参数 T 被擦除后都会以 Object 类进行替换;而有一种情况则不是,那就是使用到了 extends 和 super 语法的有界类型参数(即泛型通配符,后面我们会详细解释)。

再看一个例子,假设定义一个泛型类如下:

java 复制代码
public class Caculate<T extends Number> {
    private T num;
}

将其反编译:

java 复制代码
public class Caculate {
    public Caculate() {}// 默认构造器,不用管

    private Number num;
}

可以发现,使用到了 extends 语法的类型参数 T 被擦除后会替换为 Number 而不再是 Object。

extends 和 super 是一个限定类型参数边界的语法,extends 限定 T 只能是 Number 或者是 Number 的子类。 也就是说,在创建 Caculate 类对象的时候,尖括号 <> 中只能传入 Number 类或者 Number 的子类的数据类型,所以在创建 Caculate 类对象时无论传入什么数据类型,Number 都是其父类,于是可以使用 Number 类作为 T 的原始数据类型,进行类型擦除并替换。

相关推荐
所待.383几秒前
JavaEE之线程初阶(上)
java·java-ee
Winston Wood4 分钟前
Java线程池详解
java·线程池·多线程·性能
手握风云-8 分钟前
数据结构(Java版)第二期:包装类和泛型
java·开发语言·数据结构
喵叔哟28 分钟前
重构代码中引入外部方法和引入本地扩展的区别
java·开发语言·重构
尘浮生34 分钟前
Java项目实战II基于微信小程序的电影院买票选座系统(开发文档+数据库+源码)
java·开发语言·数据库·微信小程序·小程序·maven·intellij-idea
不是二师兄的八戒1 小时前
本地 PHP 和 Java 开发环境 Docker 化与配置开机自启
java·docker·php
爱编程的小生1 小时前
Easyexcel(2-文件读取)
java·excel
带多刺的玫瑰1 小时前
Leecode刷题C语言之统计不是特殊数字的数字数量
java·c语言·算法
计算机毕设指导62 小时前
基于 SpringBoot 的作业管理系统【附源码】
java·vue.js·spring boot·后端·mysql·spring·intellij-idea
Gu Gu Study2 小时前
枚举与lambda表达式,枚举实现单例模式为什么是安全的,lambda表达式与函数式接口的小九九~
java·开发语言