Java泛型

泛型是从 Java 5 开始增加的对于数据类型的规范和检测机制,使用泛型有如下两个优点

  1. 可以有效地避免 ClassCastException ,在编译期就可以使用泛型发现类型转换错误,或者类型不匹配
  2. 避免强类型转换

一、泛型引入

请看下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ShowExample {
public static void main(String[] args) {
List list = new ArrayList();
// 往 list 中添加一个 String
list.add("这是一个String");
// 往 list 中添加一个 整形
list.add(123);
printSomeThing(list);
}
public static void printSomeThing(List list) {
for (int i = 0; i < list.size(); i++) {
// 进行类型转换
String temp = (String) list.get(i);
System.out.println(temp);
}
}
}

运行结果如下

1
2
这是一个String
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

第一行被正确打印,但是第二行的整形却在进行强制类型转换的时候失败了,这个如果发生在程序运行期间,会给整个项目带来安全问题。所以,如果能在编译期间就发现这个问题,会对程序的健壮性有很大的提高。

泛型使用一对尖括号来设置变量的类型,多用在集合框架中。比如,如下代码定义了一个只能存放 String 类型数据的 List

1
List<String> stringList = new ArrayList<String>();

二、泛型类

当类中要操作的引用数据类型不确定时,就需要使用泛型。

JDK 1.5 之前使用 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
public class GenericClassDemo {
public static void main(String[] args) {
ToolOld toolOld = new ToolOld();
// 通过 Object 暂存数据
toolOld.setObject(new Cat());
// 将数据强制装换回原来的类型
Cat cat = (Cat) toolOld.getObject();
// 如果强制准换的类型于原来的类型不一致,就会出现类型转换错误
// ClassCastException
// Dog dog = (Dog) toolOld.getObject();
}
}
class Cat {
}
class Dog {
}
/**
* 此方法可以用来暂存和获取一个对象
*/
class ToolOld {
private Object object;

public Object getObject() {
return object;
}

public void setObject(Object object) {
this.object = object;
}
}

运行结果没有报错,因为 Object 是所有类的父类,上面的 ToolOld 通过 object 属性暂存数据,当需要获取数据时再通过强制转换,转换成原来的类型。

定义泛型类

泛型类的定义方法如下

1
2
3
4
5
6
7
public class ClassName<泛型标识符> {
private 泛型标识符 属性名称;
// 定义一个参数类型是指定泛型的方法
private void testMethod(T t) {
...
}
}

使用泛型类

上面 Object 的方法可以使用泛型来代替。下面定义了一个泛型类 ToolGeneric,每次创建对象时可以传入类型,在调用 set 方法时,如果类型不匹配,则编译不会通过。

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
public class GenericClassDemo {
public static void main(String[] args) {
// 这里指定了类中只能传入 toolGeneric 类型
ToolGeneric<Cat> toolGeneric = new ToolGeneric<Cat>();
// 如果传入 new Dog() 就会报错
toolGeneric.setT(new Cat());
Cat cat = toolGeneric.getT();
}
}

class Cat {

}
class Dog {

}
// 泛型类 传入的类型用T来标记
class ToolGeneric<T> {
private T t;

public T getT() {
return t;
}
// 调用set 函数时传入的参数类型必须与创建对象时泛型的类型相同
public void setT(T t) {
this.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
public class GenericClassShowCase {
public static void main(String[] args) {
PrintTool<String> printToolStr = new PrintTool<String>();
printToolStr.printSomeThing("你好");
// 因为已经指定 printToolStr 传入的类型为 String 下面的语句不能通过编译
// printToolStr.printSomeThing(12312);

PrintTool<Integer> printToolInt = new PrintTool<Integer>();
printToolInt.printSomeThing(3123412);
// 因为已经指定 printToolStr 传入的类型为 Integer 下面的语句不能通过编译
// printToolInt.printSomeThing("你好");
}
}

class PrintTool<T> {
public void printSomeThing(T t) {
System.out.println(t);
}
// 需要注意的是 静态方法不能引用上面的泛型 T,
// 但是可以定义泛型静态方法
// 'PrintTool.this' cannot be referenced from a static context
/*
public static void showIt(T t) {
System.out.println(t);
}*/
}

运行结果 :

1
2
你好
3123412

上面的泛型类通过每次指定不同的类型,可以打印不同的数据类型。但是每打印一个新的数据类型都需要运行 PrintTool<XXX> printTool = new PrintTool<XXX>() 来创建新的类,这样也不方便,所以有必要使用泛型方法

泛型方法

泛型方法定义时需要将一对尖括号包裹的泛型类型写在函数的返回值之前。

1
2
3
public <T> void getName(T t) {
// TODO
}

下面是一个泛型方法的例子

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
public class GenericMethodShow {
public static void main(String[] args) {
GenericMethod<String> stringGenericMethod = new GenericMethod<String>();
stringGenericMethod.show("你好");

// 编译不通过,应为 show 方法使用的是类传来的参数类型,
// 所以在本例中只能打印 String 类型的数据
// stringGenericMethod.show(13123);

// show 方法的参数类型在调用时通过传入的参数确定,
// 所以两种类型都可以打印
stringGenericMethod.print("你好");
stringGenericMethod.print(123124);

// 调用静态泛型方法
GenericMethod.showStatic("static");

}
}

class GenericMethod<T> {
// 这是一个普通的方法,只是参数的类型是泛型
public void show(T t) {
System.out.println("show: " + t);
}

// 这是一个泛型方法,这里泛型使用的是 T2,而不是类中的T
public <T2> void print(T2 t) {
System.out.println("print: " + t);
}

// 静态方法可以定义泛型函数,可以使用用户调用时传入的参数的类型 T3
// 但是无法使用本类的泛型 T
public static <T3> void showStatic(T3 t3) {
System.out.println("static: " + t3);
}
}
1
2
3
4
show: 你好
print: 你好
print: 123124
static: static

泛型接口

泛型接口的定义方式为

IterName{
1
2
interface IterName<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
29
30
31
32
33
34
35
36
37
public class GenericInterfaceShow {
public static void main(String[] args) {
ShowInterImpl1 showInterImpl1 = new ShowInterImpl1();
showInterImpl1.show("哈哈");
// 编译不通过
//showInterImpl1.show(123);

ShowInterImpl2 showInterImpl2 = new ShowInterImpl2();
// 在调用时确定参数类型
showInterImpl2.show("哈哈2");
showInterImpl2.show(1234);
}
}

// 定义一个泛型接口
interface ShowInter<T> {
void show(T t);
}

// 在定义接口类时传入泛型
class ShowInterImpl1 implements ShowInter<String> {
@Override
// 这里的参数类型必须写 String
public void show(String s) {
System.out.println(s);
}
}

// 在调用时定义参数类型
// 方法定义时必须在类名和接口名后都加入泛型
class ShowInterImpl2<T> implements ShowInter<T> {

@Override
public void show(T t) {
System.out.println(t);
}
}

运行结果如下

1
2
3
哈哈
哈哈2
1234

泛型限定

当不知道数据类型时,可以用 ? 作为通配符

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
public class GenericExtends {
public static void main(String[] args) {
ArrayList<Person> personList = new ArrayList<Person>();
personList.add(new Person("aaa"));
personList.add(new Person("bbb"));
personList.add(new Person("ccc"));

ArrayList<Student> studentList = new ArrayList<Student>();
personList.add(new Person("aaa1"));
personList.add(new Person("bbb2"));
personList.add(new Person("ccc3"));

// printData(personList);
// printData(studentList);

ArrayList<String> strings = new ArrayList<String>();
strings.add("a");
strings.add("b");
strings.add("c");

ArrayList<Integer> integers = new ArrayList<Integer>();
integers.add(1);
integers.add(2);
integers.add(3);
printData(strings);
printData(integers);

}

// 使用 ? 作为限定符
public static void printData(ArrayList<?> dataList) {
Iterator<?> iterator = dataList.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
// 这里也可以使用T, 这种写法与 printData 的差别不大,区别在于可以通过 t 对变量进行操作
public static <T> void printData2(ArrayList<T> dataList) {
Iterator<T> iterator = dataList.iterator();
while (iterator.hasNext()) {
T t = iterator.next();
System.out.println(t);
}
}
}

运行结果如下

1
2
3
4
5
6
a
b
c
1
2
3

可以看出:使用 ? 可以匹配不同类型的数据

这个例子说明 子父类之间的泛型不能通过多态调用

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
// 泛型限定
public class GenericExtends {
public static void main(String[] args) {
ArrayList<Person> personList = new ArrayList<Person>();
personList.add(new Person("aaa"));
personList.add(new Person("bbb"));
personList.add(new Person("ccc"));

ArrayList<Student> studentList = new ArrayList<Student>();
studentList.add(new Student("aaa1"));
studentList.add(new Student("bbb2"));
studentList.add(new Student("ccc3"));
printPerson(personList);
// 下面的代码编译不通过
// 说明 ArrayList<Person> peopleList = new ArrayList<Student>();是不成立的
// 同样 ArrayList<Student> peopleList = new ArrayList<Person>(); 也是不成立的
// printPerson(studentList);
}
public static void printPerson(ArrayList<Person> personList) {
Iterator<Person> iterator = personList.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next().getName());
}
}
}

class Person {
private String name;

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

public String getName() {
return name;
}
}

class Student extends Person {
public Student(String name) {
super(name);
}

}

现在我们确实需要使用 printPerson 方法来打印 studentList ,将 printPerson 申明为泛型方法是一个途径:

1
2
3
4
5
6
7
8
public static <T> void printPerson(ArrayList<T> personList) {
Iterator<T> iterator = personList.iterator();
while (iterator.hasNext()) {

System.out.println(((Person) iterator.next()).getName());
}

}

或者使用

1
2
3
4
5
6
7
public static void printPerson(ArrayList<?> personList) {
Iterator<?> iterator = personList.iterator();
while (iterator.hasNext()) {
System.out.println(((Person) iterator.next()).getName());
}

}

但是这两种方法都有局限性:无论 personList 中存储的是什么类型的数据都可以传入。这就存在了风险:就上面的例子来说,如果 personList 里边存储的不是 Person 类(或者子类),就会造成类型转换异常。为了解决这个问题,引入了类型限定。

使用 <? extends Person> 来限定 personList 只能存储 Person 类或者其子类型

下面是一个类型限定的例子

1
2
3
4
5
6
public static void printPersonExtend(ArrayList<? extends Person> personList) {
Iterator<? extends Person> iterator = personList.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next().getName());
}
}

增加一个汽车类 Car

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Car {
private String brandName;

public Car(String brandName) {
this.brandName = brandName;
}

public String getBrandName() {
return brandName;
}

public void setBrandName(String brandName) {
this.brandName = brandName;
}
}

调用打印方法

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
public class GenericExtends {
public static void main(String[] args) {
ArrayList<Person> personList = new ArrayList<Person>();
personList.add(new Person("aaa"));
personList.add(new Person("bbb"));
personList.add(new Person("ccc"));

ArrayList<Student> studentList = new ArrayList<Student>();
studentList.add(new Student("aaa1"));
studentList.add(new Student("bbb2"));
studentList.add(new Student("ccc3"));
// 打印 Person
printPersonExtend(personList);
// 因为 Stduent 是 Person 的子类,所以也可以打印
printPersonExtend(studentList);


ArrayList<Car> carList = new ArrayList<Car>();
carList.add(new Car("Benz"));
carList.add(new Car("BMW"));
carList.add(new Car("Porsche"));
// 编译失败,因为 Car 类和 Person 类没有任何关系
// printPersonExtend(carList);

}

public static void printPersonExtend(ArrayList<? extends Person> personList) {
Iterator<? extends Person> iterator = personList.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next().getName());
}

}

与此同时,使用 <? super Student> 限定只能传入 Student 类及其父类,TreeSet 的一个构造方法就使用如下的限定符:

1
2
3
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<E,Object>(comparator));
}

下面是一个使用 <? super E> 的例子:

定义一个 Worker 类,它同时也是 Person 的子类

1
2
3
4
5
6
class Worker extends Person {

public Worker(String name) {
super(name);
}
}

现在我们用 TreeSet 分别存储 学生 和 工人,并将它们倒序输出

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 GenericSuper {
public static void main(String[] args) {
// 创建 studentTreeSet 并用 StudentComparator 指定比较的方法
TreeSet<Student> studentTreeSet = new TreeSet<Student>(new StudentComparator());
studentTreeSet.add(new Student("s--001"));
studentTreeSet.add(new Student("s--002"));
studentTreeSet.add(new Student("s--003"));
Iterator<Student> studentIterator = studentTreeSet.iterator();

while (studentIterator.hasNext()) {
System.out.println(studentIterator.next().getName());
}
// 创建 studentTreeSet 并用 WorkerComparator 指定比较的方法
TreeSet<Worker> workerTreeSet = new TreeSet<Worker>(new WorkerComparator());
workerTreeSet.add(new Worker("w--001"));
workerTreeSet.add(new Worker("w--002"));
workerTreeSet.add(new Worker("w--003"));

Iterator<Worker> workerIterator = workerTreeSet.iterator();
while (workerIterator.hasNext()) {
System.out.println(workerIterator.next().getName());
}
}
}


// 用来比较 Student 对象
class StudentComparator implements Comparator<Student> {

@Override
public int compare(Student s1, Student s2) {
return s2.getName().compareTo(s1.getName());
}
}
// 用来比较 Worker 对象
class WorkerComparator implements Comparator<Worker> {

@Override
public int compare(Worker w1, Worker w2) {
return w2.getName().compareTo(w1.getName());
}
}

输出结果如下

1
2
3
4
5
6
s--003
s--002
s--001
w--003
w--002
w--001

上面两种不同的对象比较,使用了不同的比较器,是否可以将其合并成一个呢?

查看源码 TreeSet 当前的构造函数如下

1
2
3
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<E,Object>(comparator));
}

这个源码说明比较器参数可以传入 E 类型,或者 E 类型的 父类型

现在重写一个通用的比较器

1
2
3
4
5
6
7
8
// 这里指定了类型时 Student 和 Worker 的共同父类 Person 
// 所以,当传入 Student 或者 Worker 时,比较器都是可以使用的
class AllPersonComp implements Comparator<Person> {
@Override
public int compare(Person s1, Person s2) {
return s2.getName().compareTo(s1.getName())
}
}

在定义 TreeSet 时传入上面的比较器

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 GenericSuper {
public static void main(String[] args) {
// 构造函数中传入的泛型是Student,后面的参数就可以传入以Student的父类作为泛型的比较器
TreeSet<Student> studentTreeSet = new TreeSet<Student>(new AllPersonComp());
studentTreeSet.add(new Student("s--001"));
studentTreeSet.add(new Student("s--002"));
studentTreeSet.add(new Student("s--003"));
Iterator<Student> studentIterator = studentTreeSet.iterator();

while (studentIterator.hasNext()) {
System.out.println(studentIterator.next().getName());
}


// 构造函数中传入的泛型是Worker,后面的参数就可以传入以Worker的父类作为泛型的比较器
TreeSet<Worker> workerTreeSet = new TreeSet<Worker>(new AllPersonComp());
workerTreeSet.add(new Worker("w--001"));
workerTreeSet.add(new Worker("w--002"));
workerTreeSet.add(new Worker("w--003"));

Iterator<Worker> workerIterator = workerTreeSet.iterator();
while (workerIterator.hasNext()) {
System.out.println(workerIterator.next().getName());
}
}


}

运行结果与上面相同

作者

Bruce Liu

发布于

2019-03-14

更新于

2022-11-12

许可协议

You need to set install_url to use ShareThis. Please set it in _config.yml.
You forgot to set the business or currency_code for Paypal. Please set it in _config.yml.

评论

You forgot to set the shortname for Disqus. Please set it in _config.yml.