Java集合有个缺点—把一个对象“丢进”集合里之后, 集合就会 “忘记”这个对象的数据类型,当再次取出该对象时,该对象的编译 类型就变成了Object类型(其运行时类型没变)。
Java集合之所以被设计成这样, 是因为集合的设计者不知道我们 会用集合来保存什么类型的对象,所以他们把集合设计成能保存任何 类型的对象, 只要求具有很好的通用性。 但这样做带来如下两个问题:
➢ 集合对元素类型没有任何限制, 这样可能引发一些问题。 例 如,想创建一个只能保存Dog对象的集合,但程序也可以轻易地 将Cat对象“丢”进去,所以可能引发异常。
➢ 由于把对象“丢进”集合时,集合丢失了对象的状态信息,集 合只知道它盛装的是Object, 因此取出集合元素后通常还需要 进行强制类型转换。 这种强制类型转换既增加了编程的复杂度,也可能引发ClassCastException异常。
下面程序将会看到编译时不检查类型所导致的异常。
public class ListErr {
public static void main(String[] args) {
// 创建一个只想保存字符串的List集合
ArrayList 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、使用泛型 从 Java 5 以 后 , Java 引 入 了 “ 参 数 化 类 型 ( parameterized type)”的概念, 允许程序在创建集合时指定集合元素的类型, 正如 在第8章的ShowHand.java程序中见到的List, 这表明该List 只 能 保 存 字 符 串 类 型 的 对 象 。 Java 的 参 数 化 类 型 被 称 为 泛 型 (Generic)。
对于前面的ListErr.java程序,可以使用泛型改进这个程序。
public class GenericList {
public static void main(String[] args) {
// 创建一个只想保存字符串的List集合
List strList = new ArrayList(); // ①
strList.add("Java");
strList.add("Android");
// 下面代码将引起编译错误
strList.add(5); // ②
strList.forEach(str -> System.out.println(str.length())); // ③
}
}
上面程序将在②处引发编译异常, 因为strList集合只能添加 String对象,所以不能将Integer对象“丢进”该集合。
而且程序在③处不需要进行强制类型转换,因为strList对象可以 “记住”它的所有集合元素都是String类型。
上面代码不仅更加健壮, 程序再也不能“不小心”地把其他对象 “丢进”strList集合中;而且程序更加简洁,集合自动记住所有集合 元素的数据类型,从而无须对集合元素进行强制类型转换。
在Java 7以前, 如果使用带泛型的接口、类定义变量, 那么调用 构造器创建对象时构造器的后面也必须带泛型,这显得有些多余了。 例如如下两条语句:
Mapmap = new HashMap (); List strList = new ArrayList ();
上面两条语句中"<>"里的代码部分完全是多余的, 在Java 7以前这是必需的, 不能省略。 从Java 7开始, Java允许在构造器后不需要 带完整的泛型信息, 只要给出一对尖括号(<>)即可, Java可以推断 尖括号里应该是什么泛型信息。 即上面两条语句可以改写为如下形式:
Mapmap = new HashMap<>(); List strList = new ArrayList<>();
把两个尖括号并排放在一起非常像一个菱形, 这种语法也就被称 为“菱形”语法。下面程序示范了Java 7及以后版本的菱形语法。
public class DiamondTest {
public static void main(String[] args) {
// Java自动推断出ArrayList的<>里应该是String
List books = new ArrayList<>();
books.add("Java");
books.add("Android");
// 遍历books集合,集合元素就是String类型
books.forEach(ele -> System.out.println(ele.length()));
// Java自动推断出HashMap的<>里应该是String, List
Map> schoolsInfo = new HashMap<>();
// Java自动推断出ArrayList的<>里应该是String
List schools = new ArrayList<>();
schools.add("斜月三星洞");
schools.add("西天取经路");
schoolsInfo.put("孙悟空", schools);
// 遍历Map时,Map的key是String类型,value是List类型
schoolsInfo.forEach((key, value) -> System.out.println(key + "-->" + value));
}
}
Java 9再次增强了“菱形”语法, 它甚至允许在创建匿名内部类 时使用菱形语法, Java可根据上下文来推断匿名内部类中泛型的类 型。下面程序示范了在匿名内部类中使用菱形语法。
interface Foo{ void test(T t); } public class AnnoymousDiamond { public static void main(String[] args) { // 指定Foo类中泛型为String Foo 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); } }; } }
上面程序先定义了一个带泛型声明的接口, 接下来123处码分别示范了在匿名内部类中使用菱形语法。第1处代码声明变量时明确地将泛型指定为String类型,因此在该匿名内部类中T类型就代表了String类型;第2处字代码声明变量时使用通配符来代表泛型(相当于通配符的上限为Object),因此系统只能推断出T代表 Object,所以在该匿名内部类中T类型就代表了Object类型;第3处代码声明变量时使用了带上限(上限是Number)的通配符, 因此系统可以推断出T代表Number类。
2、深入泛型 2.1、定义泛型接口、类可以为任何类、接口增加泛型声明(并不是只有集合类才可以使 用泛型声明,虽然集合类是泛型的重要使用场所)。下面自定义一个 Apple类,这个Apple类就可以包含一个泛型声明。
public class Apple{ // 使用T类型定义实例变量 private T info; public Apple() { } // 下面方法中使用T类型来定义构造器 public Apple(T info) { this.info = info; } public void setInfo(T info) { this.info = info; } public T getInfo() { return this.info; } public static void main(String[] args) { // 由于传给T形参的是String,所以构造器参数只能是String Apple a1 = new Apple<>("苹果"); System.out.println(a1.getInfo()); // 由于传给T形参的是Double,所以构造器参数只能是Double或double Apple a2 = new Apple<>(5.67); System.out.println(a2.getInfo()); } }
上面程序定义了一个带泛型声明的Apple类(不要理会这个泛 型形参是否具有实际意义),使用Apple类时就可为T形参传入实际类型, 这样就可以生成如Apple、Apple…形式的多 个 逻 辑 子 类 ( 物 理 上 并 不 存 在 ) 。 这 就 是 可 以 使 用 List、ArrayList等类型的原因—JDK在定义List、 ArrayList等接口、类时使用了泛型声明,所以在使用这些类时为之传入了实际的类型参数。
当创建带泛型声明的自定义类,为该类定义构造器时,构造器 名还是原来的类名, 不要增加泛型声明。 例如, 为Apple类定义 构造器,其构造器名依然是Apple,而不是Apple!调用该构造器 时却可以使用Apple的形式,当然应该为T形参传入实际的类型参 数。Java 7提供了“菱形”语法,允许省略<>中的类型实参。
当创建了带泛型声明的接口、父类之后, 可以为该接口创建实现 类,或从该父类派生子类,需要指出的是,当使用这些接口、父类时 不能再包含泛型形参。例如,下面代码就是错误的。
//定义类A继承Apple类, Apple类不能跟泛型形参 public class A extends Apple{}
如果想从Apple类派生一个子类,则可以改为如下代码:
// 使用Apple类时为T形参传入 String类型 public class A extends Apple{}
调用方法时必须为所有的数据形参传入参数值, 与调用方法不同的是,使用类、接口时也可以不为泛型形参传入实际的类型参数,即下面代码也是正确的。
// 使用Apple类时,没有为形参传入实际的类型参数
public class A extends Apple{}
像这种使用Apple类时省略泛型的形式被称为原始类型(raw type)。
如果从Apple类派生子类,则在Apple类中所有使用T类型 的地方都将被替换成String类型, 即它的子类将会继承到String getInfo()和void setInfo(String info)两个方法, 如果子类需要重 写父类的方法,就必须注意这一点。下面程序示范了这一点。
public class A1 extends Apple2.3、并不存在泛型类{ // 正确重写了父类的方法,返回值 // 与父类Apple 的返回值完全相同 public String getInfo() { return "子类" + super.getInfo(); } // 下面方法是错误的,重写父类方法时返回值类型不一致 public Object getInfo() { return "子类"; } }
前面提到可以把ArrayList类当成ArrayList的子类, 事 实上, ArrayList类也确实像一种特殊的ArrayList类:该 ArrayList对象只能添加String对象作为集合元素。 但实际 上, 系统并没有为ArrayList生成新的class文件, 而且也不 会把ArrayList当成新类来处理。
看下面代码的打印结果是什么?
//分別创建Iist对象和List 对象 List l1 =new ArrayList<>(); List l2 new ArrayList<>(); //调用 getclass()方法来比较l1和l2的类是否相等 System.out.println(l1.getclass() == l2.getclass());
运行上面的代码片段,可能有读者认为应该输出false,但实际输 出true。 因为不管泛型的实际类型参数是什么, 它们在运行时总有同 样的类(class)。
不管为泛型形参传入哪一种类型实参, 对于Java来说, 它们依然被当成同一个类处理,在内存中也只占用一块内存空间,因此在静态方法、静态初始化块或者静态变量(它们都是类相关的)的声明和初始化中不允许使用泛型形参。下面程序演示了这种错误。
public class R{ // 下面代码错误,不能在静态变量声明中使用泛型形参 static T info; T age; public void foo(T msg) { } // 下面代码错误,不能在静态方法声明中使用泛型形参 public static void bar(T msg){} }
由于系统中并不会真正生成泛型类, 所以instanceof运算符后不 能使用泛型类。例如,下面代码是错误的。
java.util.Collection3、类型通配符cs = new java.util.ArrayList<>(); // 下面代码编译时引起错误: instanceof运算符后不能使用泛型 if(cs instanceof java.util.Arraylist ) { }
当使用一个泛型类时(包括声明变量和创建对 象两种情况),都应该为这个泛型类传入一个类型实参。如果没有传 入类型实际参数,编译器就会提出泛型警告。假设现在需要定义一个 方法,该方法里有一个集合形参,集合形参的元素类型是不确定的, 那应该怎样定义呢?
例如如下程序实例。
public void test(list c) {
for (var i=0: i
上面程序当然没有问题:这是一段最普通的遍历List集合的代 码。 问题是上面程序中List是一个有泛型声明的接口, 此处使用List 接口时没有传入实际类型参数, 这将引起泛型警告。 为此, 考虑为 List接口传入实际的类型参数—因为List集合里的元素类型是不确定 的,将上面方法改为如下形式:
public void test(list



