# 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);
}
}
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<>();
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);
}
}
}
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);
}
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
}
2
3
4
5
6
7
8
定义老师类:
public class Teacher {
private String name;
private int age;
// ...getters and setters
}
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
}
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());
}
}
}
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;
}
}
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());
}
}
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>{
}
2
# 2 在继承时使用泛型
父类有泛型,子类在继承父类的时候,可以指定使用的泛型,也可以继续使用泛型。
举个栗子:
有个父类:
public class SuperClass<T>{
}
2
有一个子类,继承父类:
public class ChildClass extends SuperClass<String> {
}
2
上面在继承父类的时候,指定了父类的泛型类型,此时子类就没有泛型了,就不是泛型类了。
当然也可以让子类继续保持泛型特性,继续使用泛型:
public class ChildClass<T> extends SuperClass<T> {
}
2
这样在使用子类的时候,还是可以使用泛型,子类仍然是泛型类,并将泛型的类型传递给父类。
如果父类有多个泛型,子类实现父类的时候,泛型可以都不保留、部分保留或全部保留。
举个栗子:
父类:
public class SuperClass<T, E, K>{
}
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> {
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
子类在继承父类的时候,还可以拥有自己的泛型。
父类:
public class SuperClass<T, E, K>{
}
2
子类继承父类:
public class ChildClass<A, B, T, E, K> extends SuperClass<T, E, K> {
}
public class ChildClass<A, B> extends SuperClass {
}
2
3
4
5
上面的子类中的情况,A
和 B
都是子类自己的泛型。
# 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);
}
}
}
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) {
}
2
3
4
异常类不能声明为泛型类。
// 下面的代码报错,异常类不能声明为泛型类。
public class MyException<T> extends RuntimeException {
}
2
3
# 2.3 泛型接口
泛型接口和泛型类是一样的。
在 Java 中,泛型接口是指可以具有泛型类型参数的接口。泛型接口允许在接口定义中使用类型参数,并在实现接口时指定具体的类型参数。
举个栗子:
定义一个泛型接口。
// 定义一个泛型接口
public interface Repository<T> {
void save(T data);
T findById(String id);
}
2
3
4
5
然后定义一个实体类:
public class User {
private String id;
private String name;
//... getters and setters
}
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();
}
}
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());
}
}
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);
}
}
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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
第三种情况为什么不行呢?
我们先假设可以,那么 list1
和 list2
都指向了堆中的相同的内存空间,这个内存空间原来是用来存储 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;
}
}
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("我喜欢吃鱼");
}
}
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) {
}
}
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) {
}
}
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();
}
}
}
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;
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
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;
}
}
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();
}
}
}
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);
}
}
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);
}
}
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); // 不可以
}
}
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
类型对象,Dog
是 Animal
子类型,肯定可以放啊,多态嘛。
其实 <? super Animal>
表示的是 类型,什么对象能放在 Animal
类型(及其父类)的引用中呢?显然 Animal
及其子类型对象都可以放。所以 Object 类型的对象显然无法放到 Animal
类型(及其父类)的引用。
# 2.7 泛型类泛型方法使用泛型约束
在泛型类也可以使用泛型约束。
举个栗子:
父类的泛型使用上限限制:
// 父类型
public class SuperClass<T extends Animal> {
}
2
3
子类型的泛型类型必须是父类中的泛型或其子类:
public class ChildClass<T extends Animal> extends SuperClass<T> {
}
public class ChildClass<T extends Cat> extends SuperClass<T> {
}
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); // 不可以,和之前的通配符限制一样
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
和泛型类一样,泛型方法中的泛型也不能设置下限,注意说的是泛型方法的泛型,不是方法的参数的泛型。