9.1 泛型入门

9.1 泛型入门

Java集合有个缺点——把一个对象”丢进”集合里之后,集合就会”忘记”这个对象的数据类型当再次取出该对象时,该对象的编译类型就变成了Object类型(其运行时类型没变)
Java集合之所以被设计成这样,是因为集合的设计者不知道我们会用集合来保存什么类型的对象,所以他们把集合设计成能保存任何类型的对象,只要求具有很好的通用性。但这样做带来如下两个问题:

  1. 集合对元素类型没有任何限制,这样可能引发一些问题。例如,想创建一个只能保存Dog对象的集合,但程序也可以轻易地将Cat对象”丢”进去,所以可能引发异常。
  2. 由于把对象”丢进”集合时,集合丢失了对象的状态信息,集合只知道它盛装的是Object,因此取出集合元素后通常还需要进行强制类型转换。这种强制类型转换既增加了编程的复杂度,也可能引发ClassCastException异常。

下面将深入介绍编译时不检查类型可能引发的异常,以及如何做到在编译时进行类型检查。

9.1.1 编译时不检查类型的异常

下面程序将会看到编译时不检查类型所导致的异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.*;

public class ListErr {
public static void main(String[] args) {
// 创建一个只想保存字符串的List集合
List strList = new ArrayList();
strList.add("疯狂Java讲义");
strList.add("疯狂Android讲义");
// "不小心"把一个Integer对象"丢进"了集合
strList.add(5); // ①
strList.forEach(str -> System.out.println(((String) str).length())); // ②
}
}

上面程序创建了一个List集合,而且只希望该List集合保存字符串对象—但程序不能进行任何限制,
如果程序在①处”不小心”把一个Integer对象”丢进”了List集合中,
这将导致程序在②处引发ClassCastException异常,
因为程序试图把一个Integer对象转换为String类型。

1
2
3
4
5
6
8
11
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at ListErr.lambda$0(ListErr.java:11)
at java.util.ArrayList.forEach(ArrayList.java:1257)
at ListErr.main(ListErr.java:11)

9.1.2 使用泛型

Java5以后,Java引入了”参数化类型( parameterized type)”的概念,允许程序在创建集合时指定集合元素的类型,正如在第8章的ShowHand.java程序中见到的List<String>,这表明该List只能保存字符串类型的对象。Java参数化类型被称为泛型(Generic)。
对于前面的ListErr.java程序,可以使用泛型改进这个程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.*;

public class GenericList {
public static void main(String[] args) {
// 创建一个只想保存字符串的List集合
List<String> strList = new ArrayList<String>(); // ①
strList.add("疯狂Java讲义");
strList.add("疯狂Android讲义");
// 下面代码将引起编译错误
// strList.add(5); // ②
strList.forEach(str -> System.out.println(str.length())); // ③
}
}

上面程序成功创建了一个特殊的List集合: strList,这个List集合只能保存字符串对象,不能保存其他类型的对象。创建这种特殊集合的方法是:在集合接口、类后增加尖括号,尖括号里放一个数据类型,即表明这个集合接口、集合类只能保存特定类型的对象。注意①处的类型声明,它指定stylist不是一个任意的List,而是一个String类型的List,写作:List<String>。可以称List是带一个类型参数的泛型接口,在本例中,类型参数是String。在创建这个ArrayList对象时也指定了一个类型参数。
上面程序将在②处引发编译异常,因为strList集合只能添加String对象,所以不能将Integer对象”丢进”该集合。
而且程序在③处不需要进行强制类型转换,因为strList对象可以”记住”它的所有集合元素都是String类型。
上面代码不仅更加健壮,程序再也不能”不小心”地把其他对象”丢进” stylist集合中;而且程序更加简洁,集合自动记住所有集合元素的数据类型,从而无须对集合元素进行强制类型转换。这一切,都是因为Java5提供的泛型支持

9.1.3 Java9增强的”菱形”语法

Java7以前,如果使用带泛型的接口、类定义变量,那么调用构造器创建对象时构造器的后面也必须带泛型,这显得有些多余了。例如如下两条语句:

1
2
List<String> strList new ArrayList<String>();
Map<String Integer> scores=new HashMap<String,Integer>();

上面两条语句中的构造器后面的泛型信息(<String,Integer>)完全是多余的,在Java7以前这是必需的,不能省略。
Java7开始,Java允许在构造器后不需要带完整的泛型信息,只要给出一对尖括号(<>)即可,Java可以推断尖括号里应该是什么泛型信息。即上面两条语句可以改写为如下形式:

1
2
List<String> strList new ArrayList<>();
Map<String Integer> scores=new HashMap<>();

把两个尖括号并排放在一起非常像一个菱形,这种语法也就被称为**”菱形”语法**。下面程序示范了Java7的菱形语法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.*;

public class DiamondTest {
public static void main(String[] args) {
// Java自动推断出ArrayList的<>里应该是String
List<String> books = new ArrayList<>();
books.add("疯狂Java讲义");
books.add("疯狂Android讲义");
// 遍历books集合,集合元素就是String类型
books.forEach(ele -> System.out.println(ele.length()));
// Java自动推断出HashMap的<>里应该是String , List<String>
Map<String, List<String>> schoolsInfo = new HashMap<>();
// Java自动推断出ArrayList的<>里应该是String
List<String> schools = new ArrayList<>();
schools.add("斜月三星洞");
schools.add("西天取经路");
schoolsInfo.put("孙悟空", schools);
// 遍历Map时,Map的key是String类型,value是List<String>类型
schoolsInfo.forEach((key, value) -> System.out.println(key + "-->" + value));
}
}

上面程序中三行粗体字代码就是”菱形”语法的示例。从该程序不难看出,“菱形”语法对原有的泛型并没有改变,只是更好地简化了泛型编程
Java9再次增强了**”菱形”语法**,它甚至允许在创建匿名内部类时使用菱形语法,Java可根据上下文来推断匿名内部类中泛型的类型。下面程序示范了在匿名内部类中使用菱形语法。

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
interface Foo<T> {
void test(T t);
}

public class AnnoymousDiamond {
public static void main(String[] args) {
// 指定Foo类中泛型为String
Foo<String> f = new Foo<>() {
// test()方法的参数类型为String
public void test(String t) {
System.out.println("test方法的t参数为:" + t);
}
};
// 使用泛型通配符,此时相当于通配符的上限为Object
Foo<?> fo = new Foo<>() {
// test()方法的参数类型为Object
public void test(Object t) {
System.out.println("test方法的Object参数为:" + t);
}
};
// 使用泛型通配符,通配符的上限为Number
Foo<? extends Number> fn = new Foo<>() {
// 此时test()方法的参数类型为Number
public void test(Number t) {
System.out.println("test方法的Number参数为:" + t);
}
};
}
}

上面程序先定义了一个带泛型声明的接口,接下来三行粗体字代码分别示范了在匿名内部类中使用菱形语法。第一行粗体字代码声明变量时明确地将泛型指定为String类型,因此在该匿名内部类中T类型就代表了String类型;第二行粗体字代码声明变量时使用通配符来代表泛型(相当于通配符的上限为Object),因此系统只能推断出T代表Object,所以在该匿名内部类中T类型就代表了Object类型;第三行粗体字代码声明变量时使用了带上限(上限是Number)的通配符,因此系统可以推断出T代表Number类。
无论哪种方式,Java9都允许在使用匿名内部类时使用菱形语法