泛型概述-基本概念


泛型程序设计 (Generic programming) 意味着编写的代码可以被很多不同的类型的对象所重用。

原始类型(Raw Type)

下面我们会用一些例子来说明,为什么使用泛型编写的代码可以被不同类型的对象所重用。在没有泛型之前 ArrayList 的代码是这样的:

public class RawArrayList {

    private Object[] element;

    public Object getElement(int position) {
        //...
    }
    
    public void addElement(Object element) {
        //...
    }
}
public class RawArrayListTest {
    public static void main(String[] args) {
        RawArrayList rawArrayList = new RawArrayList();
        rawArrayList.addElement("string");
        rawArrayList.addElement(new File("file"));

        String string = (String) rawArrayList.getElement(0);
        
        //此处会出现类型转换异常
        String file = (String) rawArrayList.getElement(1);
    }
}

上面的代码有两个问题:

  1. 编译器没有错误检查,我们可以调用 setElement("string") 方法向 RawArrayList 中放入一个 String 类型的字符串,之后我仍然可以向其中放入一个其他类型的对象,例如 setElement(new File("file"))。编译器并不会有任何警告。
  2. 因为 RawArrayList 内部使用 Object 数组 来存储对象,这样我们在获取对象的时候就必须使用强制类型转换,String string = (String) rawArrayList.getElement(0);,由于 RawArrayList 没有对放入的类型做限制,所以就有可能出现类型转换异常 java.lang.ClassCastException

类型参数

泛型提供了类型参数 (type parameters) 来帮助我们改善上述的代码。
可以认为是给 RawArrayList 中声明一个参数,这个参数就代表着列表中元素的类型,我们会在声明 RawArrayList 的时候指明参数的具体类型。

类型参数用尖括号加任意字母表示 : <T>,字母一般为单个大写字母,并有一定含义,例如 T(type),E(element),K(key),V(value) 等等

下面来看看用使用类型参数之后的 ArrayList:

public class ArrayListTest {
    public static void main(String[] args) {
        ArrayList<String> stringList = new ArrayList<String>();
        stringList.add("string");
//        下面一行代码,编译器会报错,无法将 File 对象应用于 String 类型的 ArrayList
//        stringList.add(new File("file"));

        String string=stringList.get(0);
        System.out.println(string);
    }
    
    // print > string
}

ArrayList<String> stringList = new ArrayList<String>(); 这一行代码中,可以省略创建 ArrayList 对象的时候传递的参数类型如 ArrayList<String> stringList = new ArrayList<>();,编译器可以从声明中推断出省略的类型。

注意尖括号不能省略,不然可能造成类型不安全的隐患( 这相当于将原始类型的对象传递给泛型类型的引用 )。例如我们将之前的内部具有 String 类型和 File 对象的 RawArrayList 传递给 ArrayList<String> 类型的引用,会造成什么影响呢?

 RawArrayList list=new RawArrayList();
        list.add(1);
        list.add("string");

        ArrayList<String> stringArrayList=list;
        for (String s : stringArrayList) {
            System.out.println(s);//boom
        }

很不幸会发生类型转换异常,我们最好不要将原生类型和泛型类型这样使用,除非你能保证类型安全。

泛型的一个目的就是尽早的发现可能出现的异常,在使用了泛型提供的类型参数之后,有两个显著的好处是

  1. 我们不需要自己进行类型转换了,编译器能推断出返回类型,可读性提高
  2. 编译器会对插入数据做类型检查,避免插入了错误的类型,安全性提高

好像 RawArrayList 代码的例子没有明显体现出我们在开头所说的,泛型代码可以被很多不同的类型的对象所重用。

接下来在让我们看看水果和果盘的例子:

Apple

public class Apple {
    private String name;

    public Apple(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Orange

public class Orange {
    private String name;

    public Orange(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

现在我们再来定义一个果盘,用来存放水果。我们需要定义一个苹果果盘,用来存放苹果,再定义一个橘子果盘,用来存放橘子。

ApplePlate

public class ApplePlate {
    private List<Apple> appleList;

    public ApplePlate(List<Apple> appleList) {
        this.appleList = appleList;
    }

    public void setAppleList(List<Apple> appleList) {
        this.appleList = appleList;
    }

    public List<Apple> getAppleList() {
        return appleList;
    }
    
}

OrangePlate

public class OrangePlate {
    private List<Orange> orangePlate;

    public OrangePlate(List<Orange> orangePlate) {
        this.orangePlate = orangePlate;
    }

    public List<Orange> getOrangePlate() {
        return orangePlate;
    }

    public void setOrangePlate(List<Orange> orangePlate) {
        this.orangePlate = orangePlate;
    }
}

将苹果放进苹果果盘

private static void createApple() {
    //生成苹果
    List<Apple> apples = new ArrayList<>();
    Apple apple1 = new Apple("苹果1");
    Apple apple2 = new Apple("苹果2");
    Apple apple3 = new Apple("苹果3");
    apples.add(apple1);
    apples.add(apple2);
    apples.add(apple3);
    //将苹果放入苹果果盘
    ApplePlate applePlate = new ApplePlate(apples);

    //取出刚放入的苹果们
    for (Apple apple : applePlate.getAppleList()) {
        System.out.println(apple.getName());
    }
}

现在将橘子放进橘子果盘的话,只需要按照 createApple() 方法在编写一个 createOrange() 就可以了。

那如果现在我要新增一个水果类型怎么办,我还需要对应的再增加一个该水果类型的果盘。而且可以看到,我们水果的属性,方法,果盘的方法,除了类型不同之外,没什么不同。这时候就可以使用泛型来解决这个问题。

泛型类/接口

先让我们看看泛型类的概念:

具有一个或者多个类型参数的类/接口,就是泛型类/泛型接口

在定义类的时候,我们在类名的后面加上一个形如 <T> 的类型参数。类中属性的声明,方法的参数类型,包括返回类型等,都可以用类型 T 替代。泛型接口与泛型类的定义相同,我们就不展开叙述了。

现在我们将果盘(XXXPlate)改写为泛型类是什么样子

public class Plate<T> {
    private List<T> fruitList;

    public Plate(List<T> fruitList) {
        this.fruitList = fruitList;
    }

    public List<T> getFruitList() {
        return fruitList;
    }

    public void setFruitList(List<T> fruitList) {
        this.fruitList = fruitList;
    }
}

现在我们先抽象出一个水果类 Fruit

public class Fruit {
    private String name;

    public Fruit(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

为了对比,我们新创建一种水果类型 Cherry 使他继承 Fruit

public class Cherry extends Fruit {
    public Cherry(String name) {
        super(name);
    }
}

首先通过抽象将共有的属性和方法抽象到父类中去,这样子类只需要实现一个构造函数即可。

接下来我们看看,使用了泛型之后,是如何将 Cherry 装进果盘中去的。

public static void createCherry() {
    //生成车厘子
    List<Cherry> cherryList = new ArrayList<>();
    Cherry cherry1 = new Cherry("车厘子1");
    Cherry cherry2 = new Cherry("车厘子2");
    Cherry cherry3 = new Cherry("车厘子3");
    cherryList.add(cherry1);
    cherryList.add(cherry2);
    cherryList.add(cherry3);

    //将刚买的车厘子放入车厘子果盘
    Plate<Cherry> cherryPlate = new Plate<>(cherryList);
    for (Cherry cherry : cherryPlate.getFruitList()) {
        System.out.println(cherry.getName());
    }
}

假如我们现在又增加了一种水果 Pear ,这个时候我们只需要将 Plate<T> 中的类型参数指定为 Pear 这个样子 Plate<Pear> 即可。

这就体现了我们上面所说的,泛型代码可以被不同类型的对象所重用。可以这么认为:我们封装了一套数据结构和算法,用来处理一类操作,他与具体的类型无关,或者与限定的类型有关,这个时候,我们就可以使用泛型,只关注具体的操作,不用关心具体的类型。

泛型方法

我们类比泛型类可以知道,泛型方法就是具有一个或者多个类型参数的方法

将类型参数 <T> 放在修饰符的后面返回类型的前面,这样我们的返回值,方法中的局部变量,参数类型都可以指定为我们声明的 T 类型。

我们这样定义一个泛型方法:

 public static <T> T getMiddleFruit(Plate<T> plate) {
    int middle = plate.getFruitList().size();
    return plate.getFruitList().get(middle);
}

这段代码的意思是,获取 Plate<T> 中间的元素,也就是获取果盘最中间的水果。

我们可以这样来调用它:

public static void createCherry() {
    /...省略之前创建水果,将水果放进果盘的操作
    Cherry middleCherry=PlateUtils.getMiddleFruit(cherryPlate);
    System.out.println(middleCherry.getName());
}

泛型限定

在回头看我们上面定义的泛型类 Plate<T>,我们的类型参数是没有做任何限定的,类型参数 T 可以在声明的时候被指定为任何类型。

虽然我将 Plate,定义为果盘,可以传进来任何类型的水果,但其实由于我没有对 T 做任何的限定,那就意味着我们在声明的时候可以传递任意类型。

如下我们定义一个动物类型 Animal

public class Animal {
    private String name;

    public Animal(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

然后我们来尝试将一群动物放进果盘中:

public static void createAnimal() {
    List<Animal> animalList = new ArrayList<>();
    Animal dog = new Animal("Dog");
    Animal cat = new Animal("Cat");
    animalList.add(dog);
    animalList.add(cat);

    Plate<Animal> animalPlate = new Plate<>(animalList);
    for (Animal animal : animalPlate.getFruitList()) {
        System.out.println(animal.getName());
    }
}

尴尬😓,我们从果盘中取出一群动物,现在我们要修正上面的代码,不然就乱套了,这个时候我就可以使用类型限定,对类型参数加以约束。

我们只需要将代码改成这样:

public class Plate<T extends Fruit> {...}

将类型限定为 Fruit 类型或者是 Fruit 的子类,这样我们在尝试编译上面的createAnimal()代码的时候,编译器就会报错:

  1. Error:(172, 15) java: 类型参数model.Animal不在类型变量T的范围内
  2. Error:(172, 37) java: 无法推断model.plate.Plate<>的类型参数
    原因: 推论变量 T 具有不兼容的限制范围
    等式约束条件: model.Animal
    上限: model.base.Fruit

由于我们对 Plate 能够接收的类型做了限制,所以现在我们无法将一群动物 List<Animal> 放进果盘了。

泛型擦除

Java 核心技术:无论何时定义一个泛型类型,都会自动提供一个原始类型(Raw Type),原始类型的名字就是删去类型参数后的泛型类型名。擦除类型变量,并切换为限定类型,没有限定类型则替换为 Object

请看下面的代码:

    List<Banana> bananas = new ArrayList<>();
    Plate<Banana> bananaPlate = new Plate<>(bananas);

    List<Pear> pears = new ArrayList<>();
    Plate<Pear> pearPlate = new Plate<>(pears);
    
    System.out.println(bananaPlate.getClass());
    System.out.println(pearPlate.getClass());
    System.out.println(bananaPlate.getClass()==pearPlate.getClass());
    //print>class model.plate.Plate
    //print>class model.plate.Plate
    //print>true

在运行时,Plate<Banana>Plate<Pear> 的类型是一样的,都是 Plate 类型,我们在代码中指定的具体类型 BananaPear 不见了。

造成这个问题的原因就是类型擦除,类型擦除后的 Plate<T extends Fruit> 代码是这个样子的,为了展示类型变量被替换成为限定类型,我特意在原有的 Plate 的代码中加入一句 ``T t;`,声明一个 T 类型的变量 t,然后我们来看擦除后的代码:

public class Plate {
    private Fruit t;
    private List fruitList;

    public Plate(List fruitList) {
        this.fruitList = fruitList;
    }

    public List getFruitList() {
        return fruitList;
    }

    public void setFruitList(List fruitList) {
        this.fruitList = fruitList;
    }
}

可以看到在运行时所有有关于泛型的信息全部不见了,使用泛型声明的属性,全都替换成了限定类型 Fruit(大家可以尝试用 javap -c -s -p Plate
指令来反编译字节码,就可以看到更具体的信息)

在文章的最开始我们举了一个有关于 ArrayList 的例子,声明一个 String 类型的 ArrayList,我们先 add 一个字符串进去,在将该字符串取出,来看看编译后的文件

...
 ArrayList<String> stringList = new ArrayList<>();
        stringList.add("string");
        String string=stringList.get(0);
...
image_1c55onosh1hoslgnvl81eidffe26.png-166.6kB
image_1c55onosh1hoslgnvl81eidffe26.png-166.6kB
  1. 创建了一个原生的 ArrayList 没有泛型相关的信息
  2. 调用 add 方法,接收的参数类型是 Object 类型
  3. 调用 get 方法,方法的返回类型是 Object 类型
  4. 将 Object 类型转化为 String 类型

Java 中的泛型之所以设计成编译时泛型,就是为了兼容老代码,能够和之前的代码相互操作

拿我们的 Plate 来说说这件事,来看看原生的 Plate 类型和泛型类 Plate<T> 之间是怎么相互操作并且会带来什么影响。

public class RawAndGenericOperation {
    public static void main(String[] args) {
        List<Banana> bananas = new ArrayList<>();
        Banana banana = new Banana("banana");
        bananas.add(banana);
        Plate<Banana> bananaPlate = new Plate<>(bananas);

        Plate rawPlate = bananaPlate;

        List<Pear> pears = new ArrayList<>();
        Pear pear = new Pear("pear");
        pears.add(pear);
        rawPlate.setFruitList(pears);//

        for (Object o : rawPlate.getFruitList()) {
            System.out.println(((Banana) o).getName());
            //Exception in thread "main" java.lang.ClassCastException: model.pear.Pear cannot be cast to model.banana.Banana
        }
    }
}

在 For 循环取数据的时候💥了,类型转换异常,代码清晰可见,是由于我们自己将 List<Pear> 传递给了 Plate,原生类型没有类型检查,造成了类型不安全的隐患,我们在将泛型类型对象传递给原生类型的引用的时候,这个隐患就存在了,谁知道它们会对原生类型做些什么呢。

在来看看将原生类型的对象,传递给泛型类型的引用:

public class RawAndGenericOperation {
    public static void main(String[] args) {

        List bananaList = new ArrayList();
        bananaList.add(new Banana("banana"));
        bananaList.add(new Pear("pear"));


        List<Banana> bananas = new ArrayList<>();
        bananas = bananaList;

        for (Banana banana : bananas) {
            System.out.println(banana.getName());
            //Exception in thread "main" java.lang.ClassCastException: model.pear.Pear cannot be cast to model.banana.Banana
        }
    }
}

依然💥,由于原生类型的 List 没有对存储的元素做限制,我们在 BananaList 中混入了 Pear,然后将它赋值给 List<Banana>。在循环的时候出现了类型转换异常,除非你能保证原生 List 中的元素类型和泛型类型保持一致,不然就不要这么做。

但是在与遗留的代码进行衔接的时候,难免会出现上述的情况,但是没有关系,这里只是失去了泛型程序设计提供的附加安全性,不会变的更坏了。

泛型的继承关系

在之前我们定义了 Banana 类型,它是 Fruit 的子类。那么 List<Banana>List<Fruit> 的子类吗?那么 Banana[]Fruit[] 类型的子类吗?

在回答这两个问题之前,我们先来看看一个概念 Java中的逆变与协变。然后我们来写两段代码试试看:

public class Covariant {
    public static void main(String[] args) {
        List<Fruit> fruitList = new ArrayList<>();
        List<Banana> bananaList = new ArrayList<>();

        //编译器提示类型不兼容
        fruitList = bananaList;

        Fruit[] fruits = new Fruit[10];
        Banana[] bananas = new Banana[10];

        //不会有任何问题
        fruits = bananas;
    }
}

由上可知 List<Banana> 没有办法转化成 List<Fruit> 类型。但是 Banana[] 可以转化为 Fruit[] 类型,划重点在 Java 中数组是支持协变的,但是泛型是不支持协变的。假如让泛型支持协变会怎么样,假设 List<Banana> 可以传递给 List<Fruit> 类型的引用会发什么呢?

List<Fruit> fruitList = new ArrayList<>();
List<Banana> bananaList = new ArrayList<>();

//假设这行代码允许执行
fruitList = bananaList;
        
fruitList.add(new Pear("pear"));
fruitList.add(new Fruit("fruit"));

for (Banana banana : bananaList) {
    //类型转换异常
}

和上面的问题一样,我们丢失了类型的安全性。那为什么数组是协变的但却一点事情没有呢?
首先来看看知乎中胖胖的回答Java 中数组为什么要设计为协变

public class CovariantArray {
    public static void main(String[] args) {
        Fruit[] fruits = new Fruit[3];
        Banana[] bananas = new Banana[4];

        bananas[0] = new Banana("banana1");
        bananas[1] = new Banana("banana2");
        bananas[2] = new Banana("banana3");

        fruits = bananas;
        fruits[3] = new Pear("pear");
        for (Fruit fruit : fruits) {
            System.out.println(fruit.getName());
            //Exception in thread "main" java.lang.ArrayStoreException: model.pear.Pear
        }
    }
}

数组中带有特别的保护,数组会在创建的时候记住元素类型,如果后续的插入与之前的类型不匹配,虚拟机将会抛出 ArrayStoreException 异常。

数组在插入的时候就暴露出问题,如果是泛型协变的话,你就不知道什么时候会发现问题了。

泛型数组

在最开始你可能会编写这样一段代码:

public class GenericArray {
    public static void main(String[] args) {
        Plate<Banana>[] bananaArray = new Plate<Banana>[10];
    }
}

编译器会直接报 Error:创建泛型数组,我们是没有办法通过 new 的方式来创建泛型数组的,如果编译器允许我们创建泛型数组会怎么样?

public class GenericArray {
    public static void main(String[] args) {
    // 假设编译器没有报错
        Plate<Banana>[] bananaArray = new Plate<Banana>[2];

        List<Banana> bananas = new ArrayList<>();
        Plate<Banana> bananaPlate = new Plate<>(bananas);
        bananaArray[0] = bananaPlate;

        List<Pear> pears = new ArrayList<>();
        Plate<Pear> pearPlate = new Plate<>(pears);

        Object[] objectArray = bananaArray;
        objectArray[1] = pearPlate;
    }
}

可以看到我们将 Plate<Banana>[] 向上转型为 Object[],然后向其中追加一个 Plate<Pear>对象,这个时候编译器没有报错,原本我们期望在运行时,数组会判断出加入的数据类型不对从而报出 ArrayStoreException,但是被忘了类型擦除这回事,我们的 Plate<T> 全部被擦除成 Plate 类型,对于数组来说,无论你插入 Plate<Banana> 或者 Plate<Pear> 由于类型擦除,它都认为是同一种类型。这个时候泛型数组就变得类型不安全了。

编译器只是不允许通过 new Plate<T>[] 这种方式创建数组,我们依然可以通过其他方式来得到一个泛型数组,这里就不再介绍了

你总是可以将一个泛型类型转化为原生类型。

具体的示例在上面我们讲述与遗留代码相互调用的时候已经展示过了,相信你已经能够分析出为什么转化为原生类型是不安全的。

泛型类是可以扩展或者实现其他的泛型类的。

就像我们上面一直写的那样,ArrayList<Banana> 是可以赋值给 List<Banana> 类型的引用的。

参考文章

  1. Java 核心技术卷1
  2. 胖胖在知乎的回答
  3. Java 协变与逆变
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容