Post

手撕泛型

手撕泛型
泛型(generic)
是一种编写更通用(泛型、通用是同一个英语单词,generic)的代码的方法,意为是用或者可兼容大批的类。

一定程度上可以理解为标签,JDK5.0新增特性。所谓泛型,就是允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型。这个类型参数将在使用时(例如,继承或实现这个接口,用这个类型声明变量、创建对象时)确定——即传入实际的类型参数,也成为类型实参。

在Java中提起泛型,相信部分人会第一个联想到的就是集合。但是泛型的目的是为了实现更强大的表达能力,而不仅仅是为了创建类型安全的集合。类型安全的集合只是更通用的代码创建能力的副产品。

一些约定

缩写含义使用场景
T(Type)类型Java类,调用时的指定类型,包括基本的类和我们自己定义的类
E(Element)元素集合中使用,表示在集合中存放的元素
K(Key)类似于Map的场景中使用,表示key
V(Value)类似于Map的场景中使用,表示value
N(Number)数值表示针对于数值类的泛型
?通配符不确定的java类型

初级篇

在学习之前,让我们准备以下几个基础类,这些类都很简单,应该没有任何理解上的难度。getter、setter、等重写方法省略。可以先跳过这一部分代码,接下来用到的时候参考即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
interface Juicy<T> {
    void juice(T t);
}

class Food {}
// 食物
class Meat extends Food {
    public void cook() {
        System.out.println("炒肉");
    }
}

public class Fruit extends Food {
    public void fruit() {
        System.out.println("得到了一个水果");
    }
}

// 肉类
class Pork extends Meat {
    public void hamburger() {
        System.out.println("汉堡");
    }
}

class Beef extends Meat {
    public void steak() {
        System.out.println("牛排");
    }
}

class Fish extends Meat {
    public void soup() {
        System.out.println("鱼汤");
    }
}
// 水果类
class Apple extends Fruit {}

class Banana extends Fruit {
    public String color;
    public Banana(String color) {
        this.color = color;
    }
}

class Orange extends Fruit implements Juicy<Orange> {
    @Override
    public void juice(Orange orange) {
        System.out.println(orange + "榨汁");
    }
}

// 苹果类
public class YellowApple extends Apple {
    @Override
    public String toString() {
        return "黄苹果";
    }
}

public class RedApple extends Apple {
    @Override
    public String toString() {
        return "红苹果";
    }
}

public class GreenApple extends Apple {
    @Override
    public String toString() {
        return "青苹果";
    }
}

泛型类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Person<T> {
    private T food;

    public Person() {}
    public Person(T food) {
        this.food = food;
    }
    
    public void eat() {
        System.out.println(food);
    }
    
    // 测试代码
    public static void main(String[] args) {
        Person<Meat> person = new Person<>(new Meat());
        // 会输出Meat.toString();
        person.eat();
    }
}

通过上述代码可以先直观的感受一下泛型的语法,下面我们来对比一下有无泛型前后的代码有何不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Person {
    private Food food;

    public Person() {}
    public Person(Food food) {
        this.food = food;
    }

    public void eat() {
        System.out.println(food);
    }
    // 测试代码
    public static void main(String[] args) {
        Person person = new Person(new Meat());
        // 同样会输出Meat.toString();
        person.eat();
    }
}

你可能会说,这看起来没有任何不一样,我可以完全实现与上述代码完全相同的功能。那么请看接下来的这段代码,我需要获取某种具体的食物。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Person<T> {

    private T meat;

    public Person() {}
    public Person(T meat) {
        this.meat = meat;
    }

    public T get() {
        return meat;
    }
    // 测试代码
    public static void main(String[] args) {
        Person<Beef> person = new Person<>(new Beef());
        Beef beef = person.get();
        // 牛排
        beef.steak();
    }
}

这个时候,如果使用刚刚不带泛型的方式就会稍显不妥,代码就会变得比较臃肿。因为steak() hamburger() soup()是子类私有的方法,没有办法通过多态完成这个操作。

如果定义了泛型类,实例化时没有指明类的泛型,则认为此泛型类型为Object类型。 建议在实例化时要指明类的泛型。

  1. 泛型类可能有多个参数,此时应将多个参数一起放在尖括号内,比如<T1, T2, T3>
  2. 实例化以后,操作原来泛型位置的结构必须与指定类型的泛型一致。
  3. 泛型不同的引用不能相互赋值。
  4. 如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象。
  5. 泛型如果不指定,将被擦除,泛型对应的类型均按照Object处理,但不等价于Object。 泛型要使用一路都用。要不用,一路都不用。
  6. 类型推断:泛型的指定中不能使用基本数据类型,可以使用包装类替换
  7. 在类/接口上声明的泛型,在本类或本接口中即代表某种,可以作为非静态属性的类型,非静态方法的参数类型,非静态方法的返回值类型。 但在静态方法中不能使用类的泛型
  8. 异常类不能是泛型的。
  9. 不能使用new T[]。但是可以T[] elements = (T[])new Object[capacity];参考ArrayList源码中声明:Object[] elementData而非泛型参数类型数组
  10. 父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型:
    1. 子类不保留父类的泛型:按需实现。
      1. 没有类型擦除
      2. 具体类型
    2. 子类保留父类的泛型:泛型子类
      1. 全部保留
      2. 部分保留
    3. 结论:子类必须是富二代,子类除了指定或保留父类的泛型,还可以增加自己的泛型

集合中的泛型

泛型最重要的初衷之一是用于创建集合类。集合可以在我们使用一组对象的时候持有这些对象的具体类型,而不是Object,因此集合是复用性最高的库之一。

在集合中不使用泛型的问题:

  1. 类型不安全
  2. 强转时,可能出现ClassCastException
  3. 集合接口或集合类在JDK5.0时都修改为带泛型的结构。
  4. 在实例化集合类时,可以指明具体的泛型类型
  5. 指明之后,在集合类或接口中凡是定义类或接口时,内部结构(比如:方法,构造器,属性等)使用到类的泛型的位置,都指定为实例化的泛型类型。
  6. 注意点:泛型的类型必须是类,不能是基本数据类型。需要用到基本数据类型的位置,拿包装类替换。
  7. 如果实例化时没有指明泛型的类型,默认类型为java.lang.Object类型

泛型方法

泛型可以对类内部的方法进行参数化。类自身可以是泛型的,也可以不是,他和是否存在泛型方法无关。泛型方法可以改变方法的行为而不受类的影响。如果某个方法是静态的,他没有访问类的泛型类型参数的权限

泛型参数是在调用他的运行时决定,而类初始化的时候并不知道他即将被实例化成什么类型。而泛型方法的参数是在调用方法时确定的,不在实例化类时确定。因此如果要用到泛型能力,他就必须是泛型方法。

使用泛型类时,在实例化类的时候必须指定参数,而使用泛型方法的时候,通常不需要指定参数类型,因为编译器会帮你检测出来,称为参数类型推断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Person<T> {

    private T meat;

    public Person() {}
    public Person(T meat) {
        this.meat = meat;
    }

    public T get() {
        return meat;
    }
    // 静态泛型方法
    public static <F> F getFood(Class<F> c) {
        try {
            return c.getDeclaredConstructor().newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    // 测试代码
    public static void main(String[] args) {
        Fish fish = Person.getFood(Fish.class);
    }
}

泛型接口

泛型同样可用于接口

1
2
3
4
5
6
7
8
9
10
class Orange extends Fruit implements Juicy<Orange> {
    @Override
    public void juice(Orange orange) {
        System.out.println(orange + "榨汁");
    }
}

interface Juicy<T> {
    void juice(T t);
}

高级篇

泛型的本质是参数化类型,提供了编译时类型的安全监测机制。该机制允许程序在编译时检测非法的类型。在不使用泛型的情况下,我们可以通过引用Object类来实现参数的任意化,但在具体使用时需要进行强制类型转换。强制类型转换要求开发者必须明确知道实际参数的引用类型,不然可能引起强制类型转换异常。在编译期无法识别,只能在运行期检测。使用泛型的好处就是在编译期就能检查类型是否安全,同时所有强制性类型转换都是自动和隐式进行的,提高了代码的安全性和重用性。

类型擦除

在编码阶段使用泛型时加上的类型参数会被编译器在编译时去掉,这个过程就被称为类型擦除。因此,泛型主要用于编译阶段。编译后生成的字节码文件中不包含泛型中的类型信息,也就是说泛型代码内部并不存在有关泛型参数类型的可用信息。

Java类型的擦除过程:

  1. 查找用来替换类型参数的具体类,一般为Object。如果指定了类型参数的上界,则以该上界作为替换时的具体类。
  2. 把代码中的类型参数都替换为具体的类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
    // 来看一个具体的例子
    ArrayList<String> strings = new ArrayList<>();
    ArrayList<Integer> integers = new ArrayList<>();
    
    // class java.util.ArrayList
    System.out.println(strings.getClass());
    // class java.util.ArrayList
    System.out.println(integers.getClass());
    // true
    System.out.println(strings.getClass() == integers.getClass());
    // [E]
    System.out.println(Arrays.toString(strings.getClass().getTypeParameters()));
    // [E]
    System.out.println(Arrays.toString(integers.getClass().getTypeParameters()));
}

根据JDK文档的描述,Class.getTypeParameters()会返回一个由TypeVariable对象组成的数组,代表泛型声明的类型变量。然而如输出所示,他并不是参数的类型信息,我们只能获取到参数的标识符。

C++的实现方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
using namespace std;

// C++模板
template<class T> class Person {
// Person类中存放了一个T类型对象
    T food;
public:
    Person(T x) {
        food = x;
    }
    // dinner()调用了food中的eat()
    void dinner() {
        food.eat();
    }
};

class Meat {
public:
    void eat() {
        cout << "吃肉" << endl;
    }
};

int main() {
    Meat meat;
    /* 
     * C++编译器会在实例化模板的时候检查参数T中有无eat()。
     * 在实例化Person<Meat>时,编译器会看到Meat中存在eat()。
     * 如果Meat中并没有eat()方法,就会出现编译错误,从而保证类型安全
     */
    Person<Meat> person(meat);
    // 吃肉
    person.dinner();
}

换成Java则无法编译以下代码

1
2
3
4
5
6
7
8
9
10
11
12
class Person<T> {
    private T food;
    
    Person(T food) {
        this.food = food;
    }
    
    public void dinner() {
        // 此处无法编译
        food.eat();
    }
}

由于类型擦除,Java无法将『dinner()必须调用foodeat()』的要求关联到Meat中存在的eat()。当然,Java会有他自己的办法实现这个功能,会在后文的边界中提到。

Java的妥协

关于类型擦除,你必须明白他并不是一项语言特性。泛型语法是在JDK1.5中才有的实现,泛型并不是Java与生俱来的一部分。如果泛型在JDK1.0的时候就已经是这门语言的一部分了,那么这个特性就不会用类型擦除来实现,而会通过具体化来将类型参数保持为第一类公民,这样就可以对类型参数执行基于类型的操作了。类型擦除降低了泛型的泛化性。泛型在Java中仍是有用的,只不过没完全发挥作用,原因就是类型擦除。

In programming language design, a first-class citizen (also object, entity, or value) in a given programming language is an entity which supports all the operations generally available to other entities. These operations typically include being passed as a parameter, returned from a function, and assigned to a variable.

类型擦除的核心初衷是,希望让泛化的调用方可以依赖于非泛化的库,反之亦然,称为迁移兼容性。

类型擦除的补偿

1
2
3
4
class Person<T> {
    // 编译失败,不会成功。
    private T food = new T();
}

以上Java代码编译并不会通过,原因有二

  1. 类型擦除
  2. 编译器无法验证T中是否存在空参构造器。

但是在C++中可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template<class T> class Person {
    // T类型的字段
    T food;
    // 指向T的指针
    T* pointer;
public:
    // 初始化指针
    Person() {
        pointer = new T();
    }
};

class Meat {};

int main() {
    Person<Meat> p1;
    // 并且可以使用基本数据类型
    Person<int> p2;
}

Java的解决方案是传入一个工厂对象,这种方式也有一定问题,即如果像Banana.java类一样,没有空参构造器,则会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class FoodFactory<T> implements Supplier<T> {
    Class<T> food;

    public FoodFactory(Class<T> food) {
        this.food = food;
    }

    @Override
    public T get() {
        try {
            return food.getDeclaredConstructor().newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    
    public static void main(String[] args) {
        // 运行时报错,因为Banana类没有空参构造器
        Banana banana = new FoodFactory<Banana>(Banana.class).get();
    }
}

泛型数组

我们无法直接创建泛型数组,但是可以通过转型实现。

1
2
3
4
5
6
7
8
9
10
11
class Person<T> {
    public void array() {
        // 报错
        T[] t = new T[10];
    }
    
    public void array() {
        // 这种方式可以通过编译,但是会产生类型转换警告。
        T[] t = (T[]) new Object[10];
    }
}

数组的问题在于,数组在创建的时候就已经掌握了他们的实际的类型信息,所以尽管new Object[10]被转型成T[],该信息也只会存在于编译时。运行时,数组仍然是Object类型,而这会导致一些问题。唯一可以成功创建泛型数组的方法就是创建一个类型为被擦除了的新数组,再对其进行转换。 请继续看以下代码,对比一下他们的不同,并猜猜看运行时会有什么问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class Canteen<T> {

    public T[] foods;

    public Canteen() {
        foods = (T[]) new Object[10];
    }

    public void put(int index, T t) {
        foods[index] = t;
    }

    public T get(int index) {
        return foods[index];
    }

    public T[] convert() {
        return foods;
    }

    public static void main(String[] args) {
        Canteen<Meat> canteen = new Canteen<>();
        canteen.put(0, new Pork());
        Meat[] meats = canteen.convert();
        System.out.println(Arrays.toString(meats));
    }
}

public class Canteen<T> {

    public Object[] foods;

    public Canteen() {
        foods = new Object[10];
    }

    public void put(int index, T t) {
        foods[index] = t;
    }

    public T get(int index) {
        return (T) foods[index];
    }

    public T[] convert() {
        return (T[]) foods;
    }

    public static void main(String[] args) {
        Canteen<Meat> canteen = new Canteen<>();
        canteen.put(0, new Pork());
        Meat[] meats = canteen.convert();
        System.out.println(Arrays.toString(meats));
    }
}

讨论下convert(),在main()中,正如我们预期的那样,在编译时会返回一个Meat类型的数组,且会通过编译。但是当我们运行之后,两种写法都会产生类型转换异常,这是因为运行时的实际类型仍是 Object[]。所以,无论我们通过怎样的办法,都没有办法将Object[]转换成T[],没有任何办法可以推翻底层的数组类型。

边界

由于类型擦除移除了类型信息,对于无边界的泛型参数,我们只能调用Object中可用的方法。不过如果能够将参数类型限制在某个类型的子集中,我们就可以调用该子集上可用的方法了。为了应用这种限制,Java复用了extendssuper关键字。

先来看理解下如下概念,会有助于后续的阅读

  • 协变性:能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型
  • 逆变性:能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型
  • 不变性:对于不支持协变和逆变的情况称为不变性。
  • 协变和逆变是指宽类型和窄类型在某种情况下(如参数、泛型、返回值)替换或交换的特性。

extends

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class FruitShop<T extends Fruit> {
    T fruit;

    FruitShop(T fruit) {
        this.fruit = fruit;
    }

    public void getFruit() {
        // 可以调用边界的方法
        fruit.fruit();
    }
}
// 如果边界要同时确定类和接口,那么类要写在接口前边,并用&连接。和继承一样,只能继承一个具体类,但是可以实现多个接口
class DrinkBar<T extends Fruit & Juicy<T>> {
    T fruit;

    DrinkBar(T fruit) {
        this.fruit = fruit;
    }
    
    public void juice(T t) {
        // 同样可以使用边界接口的方法 
        fruit.juice(t);
    }
    
    public void fruit() {
        fruit.fruit();
    }
}

协变性

Java的数组具有协变性,我们来看如下例子,涉及到的类均在文章开头处有定义。

1
2
3
4
5
6
public static void main(String[] args) {
    Apple[] apples = new RedApple[10];
    apples[0] = new RedApple();
    apples[1] = new YellowApple();
    apples[2] = new Apple();
}

以上代码编译均可通过。那么我们来详细看看这几行代码

  • 第二行:创建了一个红苹果数组,并将其赋值给一个指向苹果数组的引用。这可以说得通,因为红苹果也是一种苹果,所以红苹果数组也应该是一种苹果数组。
  • 第三行:把一个红苹果对象赋值给数组中下标为0的元素,完全合理合法。
  • 第四行:把一个黄苹果对象赋值给数组中下标为1的元素。这对于编译器来说是说的通的,因为apples持有的是Apple类型的引用。编译器有什么理由不允许放入一个黄苹果呢?
  • 第五行:把一个苹果对象赋值给数组中下标为2的元素。由于apples持有的是Apple类型的引用,所以这对于编译器来说也完全合理,编译的时候不会有任何错误。

以上代码说明了数组的协变性,但是在运行的时候问题就随之出现。因为运行时的数组机制知道自己是在处理RedApple,所以会在向该数组中放入异构类型对象时抛出异常,也就是第四行和第五行会产生ArrayStoreException

说这里是向上转型其实是不准确的,我们实际是将一个数组赋值给另一个数组,而数组的行为则是持有其他对象。但是由于我们能进行向上转型,所以数组对象本身可以维持其内部元素对象的规则。但是数组能够意识到他们真正持有的是什么类型的对象,所以我们无法对数组进行滥用。

虽说如此,这种错误在编译时却完全合法,然我们接着往下看泛型是如何处理这个问题的。

1
2
3
4
public static void main(String[] args) {
    // 如下代码无法通过编译
    List<Apple> list = new ArrayList<RedApple>();
}

上边的代码块是无法通过编译的,这告诉我们无法将RedApple集合赋值给Apple集合。这里虽然用到的是集合类型,但是我想告诉你的并不只是集合,而是我们无法将包含RedApple的泛型赋值给包含Apple的泛型。

看上去如上代码好像应该是合法的,但是其实不难理解Java为什么这么干,如下代码会给出很好的解释。

1
2
3
4
5
6
7
8
public static void main(String[] args) {
    List<RedApple> redApples = new ArrayList<>();
    redApples.add(new RedApple());
    // 如果Java允许这么写的话,则他想表达的意思是把红苹果集合赋值给另外一个苹果集合。换句话说就是让apples集合持有redApples的引用。
    List<Apple> apples = redApples;
    // 这里语义就出现了混乱。apples本是持有的红苹果引用,而如果只看这行代码的话却又完全合法,但是矛盾点就在于一个红苹果集合里却合理的添加了一个绿苹果!
    apples.add(new GreenApple());
}

虽然看起来奇怪,但是红苹果的集合并不是苹果的集合。苹果集合可以持有红苹果、黄苹果和青苹果对象,但是红苹果集合在类型上并不等价与苹果集合。如果不好理解可以参考《白马非马》的故事,故事大概讲的就是这个意思。大意说的是如果白马等于马,黑马也等于马,所以白马=马=黑马,但白马不是黑马,所以白马不是马。

泛型不同于数组,数组具有协变性,而泛型没有。

通配符

通配符,即泛型参数表达式中的问号。引入通配符之后,我们继续看上边数组的问题。

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
    List<? extends Apple> list = new ArrayList<>();
    // 编译不通过
    list.add(new RedApple());
    // 编译不通过
    list.add(new Apple());
    // 写法合法,但是没有任何意义
    list.add(null);
}

通过查看ArrayList.add()的源码发现,该方法接受的参数是泛型类型,因此add()的参数变成了<? extends Apple>,编译器无法知道该处具体指的是哪种子类型,因此不会接受任何类型,编译器会直接拒绝调用方法。

而对于ArrayList中的contains()indexOf()来说,参数类型是Object,并不包含我们定义的通配符,所以编译器允许调用。

如上代码的通配符写法,可以理解为由某种继承自Apple的任意类型组成的List。但是这并不意味着list真的会持有任何Apple类型,因此,如上写法翻译成『未指定具体类型的某种Apple』更为贴切,而list必须持有某种具体的类型。如果list不能确定他具体持有的是什么类型,那list本身就不会知道他里边的元素可以做什么事情,我们也不能安全的向其中添加元素。如果这种写法合法化,也就会同样带来了白马非马的问题。如果我们就需要把白马、黑马,或者红苹果、黄苹果给放进一个集合中,使用如下写法便可直接达到目的。

1
2
3
4
5
6
7
public static void main(String[] args) {
    // 这种写法就非常合理了。list中不会对任何颜色的苹果加以区分,list中的元素只能使用他们共同的苹果父类中的方法,而不能使用每个颜色的苹果的特有方法,十分合理。
    List<Apple> list = new ArrayList<>();
    list.add(new RedApple());
    list.add(new GreenApple());
    list.add(new Apple());
}

super

super的合理使用方式

  1. <? super Apple>
  2. 可以使用类型参数:<? super T>
  3. 无法给泛型参数设置父类边界:<T super Apple>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Shop {
    // 这里apples的意思是 由Apple的某一父类组成的list。因此我们可以安全的添加Apple或者Apple的子类
    static void apples(List<? super Apple> apples) {
        apples.add(new Apple());
        apples.add(new YellowApple());
        // 编译不通过
        apples.add(new Banana());
    }
    // 测试代码
    public static void main(String[] args) {
        List<Apple> apples = new ArrayList<>();
        Shop.apples(apples);
        
        List<RedApple> redApples = new ArrayList<>();
        // 编译不通过
        Shop.apples(redApples);
        
        List<Fruit> fruits = new ArrayList<>();
        Shop.apples(fruits);
    }
}

我们可以这样理解<? super Apple>,即Apple类的某一未知父类或者Apple类自己,他可能是Fruit类,可能是Food类,甚至可能是Object类。这些类都是Apple类的父类,但是无论传进来的是以上哪种类型的list,他都可以安全的添加Apple对象和他的子类对象,但是不能添加Banana对象。这时我们称Apple类为下界。

回顾

本节我们来测试一下你对上述内容是否掌握扎实,请阅读下边代码,看看是否可以理解其中的代码为什么可以编译或者为什么不可以编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class Demo {
    static List<Apple> apples = Arrays.asList(new Apple());
    static List<Fruit> fruits = Arrays.asList(new Fruit());

    static <T> T func1(List<T> list) {
        return list.get(0);
    }
    
    // 类被实例化后,其类型即被确定
    static class C2<T> {
        T func2(List<T> list) {
            return list.get(0);
        }
    }
    
    static class C3<T> {
        T func3(List<? extends T> list) {
            return list.get(0);
        }
    }
    
    public static void main(String[] args) {
        // func1
        Apple a1 = func1(apples);
        Fruit f1 = func1(fruits);
        f1 = func1(apples);
        
        // func2
        C2<Fruit> c2 = new C2<>();
        // 不可编译
        Fruit a2 = c2.func2(apples);
        Fruit f2 = c2.func2(fruits);
        
        // func3
        C3<Fruit> c3 = new C3<>();
        Fruit a3 = c3.func3(apples);
        Fruit f3 = c3.func3(fruits);
    }
}
  • func1:使用了精确类型,因此传入对象的类型参数是Apple,即可返回一个Apple对象。如果是Fruit,即可返回一个Fruit对象。并且Apple对象可以向上转型成Fruit对象。
  • func2:在我们实例化C2的时候,已经确定了C2的类型参数,即TFruit类型。所以,func2()的返回值为T,即Fruit类型,入参为List类型。前面我们讨论过,List并不是List,所以c2.func2(apples)不合法。
  • func3:在我们实例化C3的时候,确定了func3()的返回值为Fruit类型,入参为List<? extends Fruit>类型,也就是说,这个list中的元素,至少都是个Fruit,也可能是Fruit的某个子类。所以我们可以从List<? extends Fruit>中读取一个Fruit

无界通配符

无界通配符<?>似乎意味着任何类型,所以使用无界通配符就等于使用某个原始类型。在许多场景中,编译器可以不用关心你用的是原始类型还是<?>。在这些场景下,你可以把<?>当成一种装饰。但其实他是有用的,他表达的意思是:我写这段代码时考虑到了泛型,但并不是说要使用Object,只是在当前场景下,泛型可以为任何类型。下面这个实例演示了<?>的一种重要用途。

1
2
3
4
5
6
7
8
9
10
public class Demo {
    // 将参数初始化为某种具体类型
    Map<?, ?> map;
    Map<Fruit, ?> fruitMap;
    
    public Demo() {
        map = new HashMap<String, String>(16);
        fruitMap = new HashMap<Fruit, String>(16);
    }
}

让我们再看一组例子,下面这组例子都会成功运行。只不过我们会发现,编译器会在一些地方给我们一些警告。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class Demo {

    static List l1;
    static List<?> l2;
    static List<? extends Object> l3;

    static void func1(List list) {
        l1 = list;
        l2 = list;
        l3 = list;
    }

    static void func2(List<?> list) {
        l1 = list;
        l2 = list;
        l3 = list;
    }

    static void func3(List<? extends Object> list) {
        l1 = list;
        l2 = list;
        l3 = list;
    }
    
    public static void main(String[] args) {
        ArrayList l4 = new ArrayList();
        ArrayList<Object> l5 = new ArrayList<>();
        ArrayList<String> l6 = new ArrayList<>();
        // func1
        func1(l4);
        func1(l5);
        func1(l6);
        // func2
        func2(l4);
        func2(l5);
        func2(l6);
        // func3
        func3(l4);
        func3(l5);
        func3(l6);

    }
}

从以上代码可以看出,编译器并不总是关心ListList<?>的区别,因此他们看起来可以是同一类事物,但事实上并不是这样,通常我们可以这么理解

  • List:指只有任意Object类型的原生List
  • List<?>:持有某种具体类型的非原生List,但我们并不知道那是什么类型。

下面我们具体说说编译器会在什么时候关心原始类型和带有无界通配符的类型之间的区别。我们可以先定义如下一个Shop类,作为Holder(持有器)存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
class Shop<T> {
    private T fruit;
    
    public Shop() {}
    public Shop(T t) {
        fruit = t;
    }
    
    public void set(T t) {
        fruit = t;
    }

    public T get() {
        return fruit;
    }
}

public class Demo {
    static void func1(Shop shop, Object obj) {
        // 会产生警告
        shop.set(obj);
        // 报错,因为shop中并没有T类型的相关信息
        T t = shop.get();
        // 这样可以,但是丢失了类型信息
        Object o = shop.get();
    }
    
    static void func2(Shop<?> shop, Object obj) {
        // 报错。编译器不允许我们这么操作
        shop.set(obj);
        // 报错,因为shop中并没有T类型的相关信息
        T t = shop.get();
        // 这样可以,但是丢失了类型信息
        Object o = shop.get();
    }
    // 没有问题
    static <T> T func3(Shop<T> shop, T t) {
        shop.set(t);
        return shop.get();
    }
    
    static <T> T func4(Shop<? extends T> shop, T t) {
        // 报错
        shop.set(t);
        return shop.get();
    }
    
    static <T> void func5(Shop<? super T> shop, T t) {
        shop.set(t);
        // 报错
        T t1 = shop.get();
        Object obj = shop.get();
    }
    
    // 测试代码,需要把上述方法中的报错方法注释掉
    public static void main(String[] args) {
        Shop s1 = new Shop();
        Shop<?> s2 = new Shop<>();
        Shop<? extends Fruit> s3 = new Shop<>();
        Shop<Fruit> fruitShop = new Shop<>();
        Fruit fruit = new Fruit();

        func1(s1, fruit);
        func1(s2, fruit);
        func1(s3, fruit);
        func1(fruitShop, fruit);

        func2(s1, fruit);
        func2(s2, fruit);
        func2(s3, fruit);
        func2(fruitShop, fruit);

        // 编译器产生警告
        func3(s1, fruit);
        // 报错
        func3(s2, fruit);
        // 报错
        func3(s3, fruit);
        func3(fruitShop, fruit);

        // 编译器产生警告
        func4(s1, fruit);
        func4(s2, fruit);
        func4(s3, fruit);
        func4(fruitShop, fruit);
        
        // 编译器产生警告
        func5(s1, fruit);
        // 报错
        func5(s2, fruit);
        // 报错
        func5(s3, fruit);
        func5(fruitShop, fruit);
    }
}

我们继续来看上述代码的几种情况

  • func1:编译器知道Shop是泛型类,所以即使这里被表示为原始类型,编译器也会知道set()是不安全的。而编译器为了安全起见,不论传进来的对象究竟是什么类型,统一都会当做Object类型处理,所以get()中无法返回除了Object类型之外的任何类型。
  • func2:这个方法中可以看出ShopShop的区别。与func1()不同的是,方法1中只是产生警告,但是方法2会直接报错。因为原生Shop可以持有任何类型的,而Shop则只能持有由某种具体类型组成的单类型集合。虽然我们不清楚这个类型到底是什么,但是我们不能传入Object
  • func3:这个应该比较容易想得明白,略。
  • func4:对Shop类型的限制放宽了,但是会有一些个别问题。我们再一次来慢慢说一下。
    • func4的参数中,允许一个继承了T类的类型参数的shop和一个T类型的参数被传进来。在实例中,TFruit类型,所以,shop就可以为苹果店、橘子店或者香蕉店,即ShopShopShop
    • 在方法内部,我们并不知道将来调用方法时会被传进来什么,有可能是苹果商店、也有可能是橘子商店。所以我们为了防止向苹果商店中放入一个橘子,所以编译器不允许我们调用set()
    • 但无论如何,我们知道get()返回的一定是一个T,即Fruit,因为传入进来的shop的类型参数最小也是个Fruit,所以我们可以返回一个Fruit对象。
  • func5:可以理解为与func4()相反,Shop的类型参数可以为Fruit的任何父类,因此我们可以使用set()shop中插入一个Fruit。但是我们在调用get()的时候,只能获取到一个Object

以上案例可以看出,什么样的操作允许<?>,限制在于,你无法get()或者set()一个T,因为T并不存在。

捕获转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Demo {

    static <T> void func1(Shop<T> shop) {
        T t = shop.get();
        System.out.println(t.getClass().getSimpleName());
    }

    static <T> void func2(Shop<?> shop) {
        // 用捕获的类型来调用方法
        func1(shop);
    }

    public static void main(String[] args) {
        Shop<Apple> appleShop = new Shop<>(new Apple());
        Shop shop = new Shop();
        shop.set(new Object());

        // Apple
        func1(appleShop);
        // Apple
        func2(appleShop);

        // Object
        func1(shop);
        // Object
        func2(shop);
    }
}

func2中,参数是无界通配符,因此他看起来像是未知的类型。但是func2()调用了func1(),而func1()需要已知类型的参数,所以在这个过程中捕获了参数的类型,并用于对func1()的调用。

This post is licensed under CC BY 4.0 by the author.