# Java教程 - 2 泛型

当在 Java 中使用泛型(Generics),我们可以将类型参数化,从而实现更通用和可重用的代码。泛型允许我们编写一次代码,适用于多种类型,提高了代码的灵活性和可维护性。

我们前面在使用集合的时候,已经接触过泛型了。

举个栗子:

List 使用泛型:

package com.doubibiji.generics;

import java.util.ArrayList;
import java.util.List;

public class GenericsTest {
    public static void main(String[] args) {
        // 使用泛型,只能放字符串类型
        List<String> colorList = new ArrayList<>();
        colorList.add("red");
        // colorList.add(123); // 报错,只能放字符串
				// 获取元素
        String s = colorList.get(0);
        System.out.println(s);

        // 使用泛型,只能放整形类型
        List<Integer> numList = new ArrayList<>();
        numList.add(123);
        // numList.add("red"); // 报错,只能放整形
				// 获取元素
        Integer i = numList.get(0);
        System.out.println(i);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

在上面,通过对 List 使用泛型,可以对集合中元素的类型进行约束,使用泛型可以创建不同元素类型的集合,在获取元素的时候也不用进行类型转换。

List<String> colorList = new ArrayList<String>();
// JDK1.7以后可以简化
List<String> colorList = new ArrayList<>();
1
2
3

如果不使用泛型,那么集合中的元素的类型就是 Object 类型,虽然可以添加任何类型的元素,但是在获取元素的时候,就无法知道原来是什么类型的,就需要进行强制转换,如果类型不匹配,还会报错。

import java.util.ArrayList;
import java.util.List;

public class GenericsTest {
    public static void main(String[] args) {
        // 不使用泛型,没有类型限制
        List list = new ArrayList();
        list.add(123);
        list.add("doubi");

        if (list.get(0) instanceof Integer) {
            Integer i = (Integer) list.get(0);
            System.out.println(i);
        }

        if (list.get(1) instanceof String) {
            String s = (String) list.get(1);
            System.out.println(s);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

所以使用泛型可以减少类型转换,提高代码复用,使代码更安全清晰。

同样,Map也可以指定泛型:

public static void main(String[] args) {
    Map<String, Integer> map = new HashMap<>();
    map.put("zhangsan", 123);
    map.put("lisi", 234);

    Integer value1 = map.get("zhangsan");
    System.out.println(value1);
}
1
2
3
4
5
6
7
8

我们也可以使用泛型机制创建我们的泛型类、泛型方法和泛型接口。

# 2.1 泛型类

我们在开发的时候,经常需要请求服务器,获取一些信息,例如获取学生的信息,或者获取老师的信息等。

那么我们会定义相关的业务类,举个栗子:

定义学生类:

package com.doubibiji.generics;

public class Student {
    private String name;
    private int age;

    // ...getters and setters
}
1
2
3
4
5
6
7
8

定义老师类:

public class Teacher {
    private String name;
    private int age;
  
    // ...getters and setters
}
1
2
3
4
5
6

我们一般会定义一个返回的结果类,然后在类中封装返回码和返回的数据,通过这个结果类返回学生或老师的信息。

举个栗子:

public class DataResult {    
    private int code;
    private Object data;
  
    public DataResult() {
    }

    public DataResult(int code, Object data) {
        this.code = code;
        this.data = data;
    }
  
    // ...getters and setters
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

因为我们返回的数据可能是不同的类型,所以我们将数据的类型定义为 Object 类型。

测试:

public class GenericsTest {
    public static void main(String[] args) {
        // 封装数据
        Student student = new Student();
        student.setName("Doubi");
        student.setAge(12);
        DataResult result1 = new DataResult(200, student);

        // 封装数据
        Teacher teacher = new Teacher();
        teacher.setName("Niubi");
        teacher.setAge(32);
        DataResult result2 = new DataResult(200, teacher);

        // 获取数据
        if (result1.getData() instanceof Student) {
            Student s = (Student) result1.getData(); // 需要进行类型转换
            System.out.println(s.getName());
        }

        if (result2.getData() instanceof Teacher) {
            Teacher t = (Teacher) result2.getData(); // 需要进行类型转换
            System.out.println(t.getName());
        }
    }
}
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

在上面的代码中,我们先将学生和老师的信息封装到了DataResult中。然后重新从DataResult获取老师和学生的信息。

但是获取到信息以后,需要强制转换,为了安全起见,我们使用了 instanceof 进行类型判断,然后再进行了转换。

这个时候我们可以使用泛型,使用泛型我们可以编写更通用、可重用的代码,以适应不同类型的数据。

修改上面的代码:

public class DataResult<T> {
    private int code;
    private T data;

    public DataResult() {
    }

    public DataResult(int code, T data) {
        this.code = code;
        this.data = data;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}
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

在类名后面添加 <T> 表示当前类声明的泛型,泛型可以任意字母,一般类只有一个泛型的时候使用 T ,然后就可以将属性定义为 T 类型了。

测试一下代码:

public class GenericsTest {
    public static void main(String[] args) {
        // 封装数据
        Student student = new Student();
        student.setName("Doubi");
        student.setAge(12);
        DataResult<Student> result1 = new DataResult<>(200, student);

        // 封装数据
        Teacher teacher = new Teacher();
        teacher.setName("Niubi");
        teacher.setAge(32);
        DataResult<Teacher> result2 = new DataResult(200, teacher);

        // 获取数据,不用转换
        Student s = result1.getData(); // 需要进行类型转换
        System.out.println(s.getName());

        Teacher t = result2.getData(); // 需要进行类型转换
        System.out.println(t.getName());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

这个在创建 DataResult 对象的时候,使用泛型,然后在通过对象来获取 data 的时候,就不用类型转换了。

# 2.2 泛型的细节

# 1 多个泛型类型

如果泛型类有多个泛型,那么使用逗号分隔,名称自己定:

public class MultiGenericsClass <T, A, B, C, A1, B2, C3>{
}
1
2

# 2 在继承时使用泛型

父类有泛型,子类在继承父类的时候,可以指定使用的泛型,也可以继续使用泛型。

举个栗子:

有个父类:

public class SuperClass<T>{
}
1
2

有一个子类,继承父类:

public class ChildClass extends SuperClass<String> {
}
1
2

上面在继承父类的时候,指定了父类的泛型类型,此时子类就没有泛型了,就不是泛型类了。


当然也可以让子类继续保持泛型特性,继续使用泛型:

public class ChildClass<T> extends SuperClass<T> {
}
1
2

这样在使用子类的时候,还是可以使用泛型,子类仍然是泛型类,并将泛型的类型传递给父类。


如果父类有多个泛型,子类实现父类的时候,泛型可以都不保留、部分保留或全部保留。

举个栗子:

父类:

public class SuperClass<T, E, K>{
}
1
2

子类继承父类,下面的方式都可以:

public class ChildClass<T, E, K> extends SuperClass<T, E, K> {
}

public class ChildClass<T> extends SuperClass<T, String, Integer> {
}

public class ChildClass extends SuperClass<String, String, Integer> {
}

// 还可以这样
public class ChildClass extends SuperClass {
}
// 相当于
public class ChildClass extends SuperClass<Object, Object, Object> {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

子类在继承父类的时候,还可以拥有自己的泛型。

父类:

public class SuperClass<T, E, K>{
}
1
2

子类继承父类:

public class ChildClass<A, B, T, E, K> extends SuperClass<T, E, K> {
}

public class ChildClass<A, B> extends SuperClass {
}
1
2
3
4
5

上面的子类中的情况,AB 都是子类自己的泛型。

# 3 泛型变量赋值

使用泛型的引用变量,如果使用的是不同的泛型,则无法相互赋值。

举个例子:

import java.util.ArrayList;
import java.util.List;

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

        List list1 = new ArrayList();
        list1.add(123);
        list1.add("abc");

        List list2 = new ArrayList();
        list2.add(123);
        list2.add("abc");

        List<String> list3 = new ArrayList<>();
        List<Integer> list4 = new ArrayList<>();

        list1 = list2;  // 可以赋值
        // list3 = list4;  // 不可以赋值,他们都使用了泛型,泛型类型不同
        list1 = list3;  // 可以赋值
        list3 = list2;  // 可以赋值
        for (String str : list3) {  // 这里报错,因为是将list2赋值给list3,list2中包含了多种类型
            System.out.println(str);
        }
    }
}
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

# 4 其他细节

泛型类上的泛型,可以被非静态属性、非静态方法使用,但是不能被静态属性和方法使用。

因为泛型是创建对象的时候指定的,而静态方法是在类加载的时候就确定了。

// 代码报错,静态方法无法使用泛型
public static void setData(T data) {
        
}
1
2
3
4

异常类不能声明为泛型类。

// 下面的代码报错,异常类不能声明为泛型类。
public class MyException<T> extends RuntimeException {
}
1
2
3

# 2.3 泛型接口

泛型接口和泛型类是一样的。

在 Java 中,泛型接口是指可以具有泛型类型参数的接口。泛型接口允许在接口定义中使用类型参数,并在实现接口时指定具体的类型参数。

举个栗子:

定义一个泛型接口。

// 定义一个泛型接口
public interface Repository<T> {
    void save(T data);
    T findById(String id);
}
1
2
3
4
5

然后定义一个实体类:

public class User {
    private String id;
    private String name;

    //... getters and setters
}
1
2
3
4
5
6

下面写一个类来实现这个泛型接口:

// 实现泛型接口
public class UserRepository implements Repository<User> {
    @Override
    public void save(User data) {
        // 假装将数据保存到了数据库
        System.out.println("Saving user:" + data.getName());
    }

    @Override
    public User findById(String id) {
        // 假装从数据库获取数据
        return new User();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

我们在实现接口的时候,指定了泛型的类型,那么类中用到泛型的地方,类型就确定了。

我们还可以定义其他的子类来实现该接口,例如 MessageRepository, 并使用不同的实体类 Message 作为泛型的数据类型,让代码更优雅规范。

调用:

public class GenericsTest {
    public static void main(String[] args) {
        User user = new User();
        user.setId("123");
        user.setName("doubibiji");

        UserRepository userRepository = new UserRepository();
        userRepository.save(user);

        user = userRepository.findById("123");
        System.out.println(user.getName());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

现在对上面的使用感触可能不深,在以后学习一些 Web 框架的时候,例如 Mybatis 的时候,才有较深的认识。

在实际的开发中,一张数据库表对应一个 Java 实体类,然后可以通过对应的 XxxRepository 就可以操作对应的表中的数据。

# 2.4 泛型方法

泛型方法是一种具有泛型类型参数的方法,可以在方法签名中使用类型参数来实现更通用和灵活的代码。泛型方法可以在参数、返回类型或方法体中使用这些类型参数。

举个栗子:

我们写一个方法获取列表的最后一个元素:

import java.util.List;

public class GenericsTest {
    // 获取列表的最后一个元素
    public static <E> E getLast(List<E> list) {
        return list.get(list.size() - 1);
    }

    public static void main(String[] args) {
        List<Integer> intList = List.of(1, 2, 3);
        List<String> strList = List.of("abc", "bcd", "cde", "def");

        int i = getLast(intList);
        System.out.println(i);

        String s = getLast(strList);
        System.out.println(s);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

在上面的代码中定义了一个泛型方法 public static <E> E getLast(List<E> list),方法签名中的 <E> 表示泛型类型,可以被方法参数和返回值使用。

类上的泛型不能被静态方法使用,但是泛型方法可以是静态的,泛型方法的泛型和类的泛型没有关系,泛型方法所属的类是不是泛型类也没有关系。

通过使用泛型定义,我们不同数据类型的列表都可以调用,而且在获取到元素后,不要类型转换。

# 2.5 泛型在继承上的问题

先看一下下面的代码:

public class GenericsTest {
    public static void main(String[] args) {
        // 情况一
        Object obj = null;
        String str = null;
        // String是Object的子类,编译没问题
        obj = str;

        // 情况二
        Object[] arr1 = null;
        String[] arr2 = null;
        // String是Object的子类,编译没问题
        arr1 = arr2;

        // 情况三
        List<Object> list1 = null;
        List<String> list2 = new ArrayList<>();
        // 编译不通过,报错
        // list1 = list2;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

第三种情况为什么不行呢?

我们先假设可以,那么 list1list2 都指向了堆中的相同的内存空间,这个内存空间原来是用来存储 String 类型的,现在 list1 也指向了这块内存,通过 list1 来操作的话,就可以放任何类型的数据了。所以编译器限制了这样的操作。

所以虽然 A 类是 B 类的父类或接口,但是 G<A>G<B> 两者之间不具备父子关系,它们是并列的。

但是如果 A 类是 B 类的父类或接口,A<T>B<T> 的父类,如下:

import java.util.ArrayList;
import java.util.List;

public class GenericsTest {
    public static void main(String[] args) {
        List<String> list1 = null;
        ArrayList<String> list2 = null;
        
        // 可以
        list1 = list2;
        // 不可以
        // list2 = list1;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 2.6 通配符的使用

上面出现的问题: A 类是 B 类的父类或接口,但是 G<A>G<B> 两者之间不具备父子关系。

这种情况会导致在使用泛型的时候存在很多不便。

举个栗子:

例如有 Dog 和 Cat 两个类分别继承 Animal 类:

abstract class Animal {
    abstract void eat();
}

class Dog extends Animal {

    @Override
    public void eat() {
        System.out.println("我喜欢啃骨头");
    }
}

class Cat extends Animal {

    @Override
    public void eat() {
        System.out.println("我喜欢吃鱼");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

但是在使用的时候,子类泛型变量却无法调用参数是父类泛型变量的方法,如下:

public class GenericsTest {
    public static void main(String[] args) {
        List<Dog> dogList = new ArrayList<>();
        dogList.add(new Dog());

        List<Cat> catList = new ArrayList<>();
        catList.add(new Cat());

        // 无法调用
        print(dogList);
        print(catList);
    }

    public static void print(List<Animal> list) {
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

List<Dog>List<Cat> 无法传递给 List<Animal> 类型的变量,这样遍历 dogList 和 CatList 就需要写两个方法,是不优雅的。


这个时候可以使用通配符 ? ,它相当于所有类的一个公共父类一样。

举个栗子:

public class GenericsTest {
    public static void main(String[] args) {
        List<Dog> dogList = new ArrayList<>();
        dogList.add(new Dog());

        List<Cat> catList = new ArrayList<>();
        catList.add(new Cat());
      
        List<?> list;
      
        // 可以
        list = dogList;
        // 可以
        list = catList;

        // 也可以
        print(dogList);
        print(catList);
    }

    // 使用通配符定义
    public static void print(List<?> list) {
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

List<?> 中的元素类型是 Object 类型。

所以 A 类是 B 类的父类或接口,虽然 G<A>G<B> 两者之间不具备父子关系,但是两者共同的父类是 G<?>

但是仍然存在问题啊,List<?> 在获取元素的时候类型是 Object,泛型不起作用啊,还需要强转:

public class GenericsTest {
    public static void main(String[] args) {
        List<Dog> dogList = new ArrayList<>();
        dogList.add(new Dog());

        List<Cat> catList = new ArrayList<>();
        catList.add(new Cat());

        print(dogList);
        print(catList);
    }

    public static void print(List<?> list) {
        for (Object obj : list) {
            // 需要强制转换
            Animal animal = (Animal) obj;
            animal.eat();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

不急,慢慢来,一步一步讲。


# 1 通配符?的读写限制

可以向 List<?> 中添加元素吗?

List<Dog> dogList = new ArrayList<>();
List<?> list = dogList;
1
2

不可以,因为 List<?> 中的元素是 Object 类型,现在 list 引用也指向的是 List<Dog> ,如果 list 可以添加元素添加的就是 Object 类型了,那么就可以将任何类型添加到 List<Dog> 中了,所以不可以;但是可以添加 null 值,因为所有的引用类型都可以是 null

所以 List<?> 类型的引用不管指向谁,除了添加 null 值,都不能添加元素。


List<?> 读取元素是没有问题的,读取的是 Object 类型。

# 2 带约束的通配符

虽然通配符 <?> 可以适配任何泛型,但是使用限制很大,读取的还是 Object 类型。

我们还可以为 <?> 指定上限和下限。

使用方式有,举个栗子:

<? extends Animal>  // 设置上限,表示只允许Animal及Animal的子类,也就是:类型<=Animal
<? super Animal>  // 设置下限,表示只允许Animal及Animal的父类,也就是:类型>=Animal
1
2

演示一下:

public class GenericsTest {
    public static void main(String[] args) {
        List<? extends Animal> list1;
        List<? super Animal> list2;

        List<Dog> list3 = new ArrayList<>();
        List<Animal> list4 = new ArrayList<>();
        List<Object> list5 = new ArrayList<>();

        // 继承Animal以及Animal类都可以
        list1 = list3;
        list1 = list4;
        // list1 = list5; // list5是Object类型,父类不可以

        // Animal父类以及Animal类都可以
        // list2 = list3; // list3是Dog类型,子类不可以
        list2 = list4;
        list2 = list5;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

有了上面的方式,我们就可以实现一开始的需求了,可以将子类型泛型的引用传递给父类泛型的引用了:

public class GenericsTest {
    public static void main(String[] args) {
        List<Dog> dogList = new ArrayList<>();
        dogList.add(new Dog());

        List<Cat> catList = new ArrayList<>();
        catList.add(new Cat());

        // 无法调用
        print(dogList);
        print(catList);
    }

    public static void print(List<? extends Animal> list) {
        for (Animal animal : list) {
            // 不需要强制转换
            animal.eat();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 3 带约束的通配符读写限制

获取元素

首先读取数据是没有问题,关键是读取数据的类型。

举个例子:

public class GenericsTest {
    public static void main(String[] args) {
        List<? extends Animal> list1;
        List<? super Animal> list2;

        List<Dog> list3 = List.of(new Dog());
        List<Animal> list4 = List.of(new Dog());

        list1 = list3;
        // 获取到的是Animal类型,因为元素是Animal及其子类
        Animal animal = list1.get(0);
        
        list2 = list4;
        // 获取到的是Object类型,因为元素是Animal及其父类
        Object animal2 = list2.get(0);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

List<? extends Animal> 获取到的数据是 Animal 类型,因为其中的元素是Animal及其子类;

List<? super Animal> 获取到的数据是 Object 类型,因为其中的元素是Animal及其父类;

显而易见,没什么问题。


添加元素

List<? extends Animal> 类型引用只能添加 null 值,不能添加其他元素。

public class GenericsTest {
    public static void main(String[] args) {
        List<? extends Animal> list1;
        List<Dog> list3 = new ArrayList<>();

        list1 = list3;
        
        // 不可以添加元素
        list1.add(new Dog());
        // 可以
        list1.add(null);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

为什么呢?获取元素的时候不是 Animal 类型的数据吗,为什么不能添加子类型吗?

这个地方稍微有点绕,但是也好理解,因为可以将 List<Dog> 类型的引用 list3 赋值给 list1,那么他们都指向了堆中的同一块内存,本来 list3 是存储 Animal 的子类 Dog 类型的,如果可以通过 list1 添加元素,就可以添加 Animal 类型了,甚至 Cat 类型(Dog、Cat 都继承自 Animal)也可以添加了,显然是不行的。


List<? super Animal> 类型引用可以放 Animal 及其子类型对象。

public class GenericsTest {
    public static void main(String[] args) {
        List<? super Animal> list2;
        List<Animal> list4 = List.of(new Dog());

        list2 = list4;

        Animal animal = new Dog();
        Dog dog = new Dog();
        Cat cat = new Cat();
        Object object = new Object();
        
        list2.add(animal);  // 可以
        list2.add(dog);  // 可以
        list2.add(cat);  // 可以
        // list2.add(object);  // 不可以
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

都有点反直觉,<? super Animal> 不是表示需要是 Animal 及其父类吗?为什么只能放 Animal 及其子类型对象。

首先 List<? super Animal> 可以放 Animal 类型对象,DogAnimal 子类型,肯定可以放啊,多态嘛。

其实 <? super Animal> 表示的是 类型,什么对象能放在 Animal 类型(及其父类)的引用中呢?显然 Animal 及其子类型对象都可以放。所以 Object 类型的对象显然无法放到 Animal 类型(及其父类)的引用。

# 2.7 泛型类泛型方法使用泛型约束

在泛型类也可以使用泛型约束。

举个栗子:

父类的泛型使用上限限制:

// 父类型
public class SuperClass<T extends Animal> {
}
1
2
3

子类型的泛型类型必须是父类中的泛型或其子类:

public class ChildClass<T extends Animal> extends SuperClass<T> {
}
public class ChildClass<T extends Cat> extends SuperClass<T> {
}
1
2
3
4

需要注意:泛型类中不支持为泛型类型参数设置下限,<T super Animal>,因为接受 SuperClass<T super Animal> 就可能接收一个 SuperClass<Object>,这样就可以放入任意类型的对象,这违背了泛型提供类型安全的初衷。


同样泛型方法上的泛型也可以使用上线约束:

public class GenericsTest {
    // 设置泛型上限
    public static <E extends Animal> E getLast1(List<E> list) {
        return list.get(list.size() - 1);
    }

    // 报错,无法设置下限
//    public static <T super Animal> T getLast2(List<T> list) {
//        return list.get(list.size() - 1);
//    }

    public static void main(String[] args) {

        List<Dog> list1 = new ArrayList<>();
        List<Animal> list2 = new ArrayList<>();
        List<Object> list3 = new ArrayList<>();

        getLast1(list1);  // 可以
        getLast1(list2);  // 可以
        //getLast1(list3);  // 不可以,和之前的通配符限制一样
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

和泛型类一样,泛型方法中的泛型也不能设置下限,注意说的是泛型方法的泛型,不是方法的参数的泛型。