Java集合被设计成能保存任何类型的对象,具有很好的通用性。但是这样设计带来了两个问题:
★集合对元素没有任何限制,可能引发一些问题:想创建一个保存Dog对象的集合,但程序可以将Cat对象轻易地添加进去。
★由于把"对象"丢进集合时,集合失去了对象的状态信息,集合只知道它盛装的时Object,因此取出集合元素时后通常需要强制转换。
下面介绍编译时不检查类型可能引发的异常,以及如何做到在编译时进行类型检查。
一、编译时不检查类型的异常
import java.util.*;
public class ListError
{
public static void main(String[] args)
{
//创建一个只想保存字符串的List集合
var strList=new ArrayList();
strList.add("疯狂Java讲义");
strList.add("疯狂Android讲义");
//一不小心加入一个Integer对象
strList.add(5);
strList.forEach(str->System.out.println(((String) str).length()));
}
}
---------- 运行Java捕获输出窗 ----------
8
11
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
at ListError.lambda$main$0(ListError.java:12)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
at ListError.main(ListError.java:12)
输出完成 (耗时 0 秒) - 正常终止
二、使用泛型
Java 5以后,Java引入了"参数化类型(parameterized type)"的概念,允许程序在创建集合时指定元素的类型。Java的参数化类型被称为泛型(Generic)。
import java.util.*;
public class GenericList
{
public static void main(String[] args)
{
//创建一个只想保存字符串的集合
List<String> strList=new ArrayList<String>();
strList.add("疯狂Java讲义");
strList.add("疯狂Android讲义");
//下面代码将引起编译错误
strList.add(5);//GenericList.java:11: 错误: 不兼容的类型: int无法转换为String
strList.forEach(str->System.out.println(((String) str).length()));
}
}
上面创建了一个只能保存字符串的List集合。创建该种集合的方法:在集合接口、类后
三、Java 9增强的"菱形"语法
在Java 7 以前使用带泛型的接口、类定义变量,那么调用构造器创建对象时构造器的后面也必须带有泛型。例如:
List<String> strList=new ArrayList<String>();
Map<String,Integer> scores=new HashMap<String,Integer>();
从Java 7开始,Java允许在构造器后不需要带完整的泛型信息,只要给出一对尖括号(<>)即可,Java可以推断出尖括号里是什么泛型信息。所以上面两条语句可以改成:
List<String> strList=new ArrayList<>();
Map<String,Integer> scores=new HashMap<>();
把两个尖括号放在一起非常想菱形,这种语法被称为"菱形"语法
下面示范Java 7以后版本的菱形语法:
import java.util.List;
import java.util.ArrayList;
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+"————>"+ele.length()));
//自动推断出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));
}
}
---------- 运行Java捕获输出窗 ----------
疯狂Java讲义————>8
疯狂Android讲义————>11
孙悟空-->[三星洞, 水帘洞]
输出完成 (耗时 0 秒) - 正常终止
从上面的程序可以看出,"菱形"语法对原有的泛型并没有改变,只是更好地简化泛型编程。但需要var声明变量时,编译器无法推断泛型类型。因此,使用var声明变量时,程序无法使用"菱形"语法。
Java 9再次增强了"菱形"语法,它甚至允许在创建匿名内部类时使用菱形语法,Java根据上下文来推断匿名内部类的泛型类型。下面展示在匿名内部类中使用菱形语法:
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;第三次声明变量时使用了带上限(上限是Number)的通配符,因此可以推断出T代表Number类。