泛型出现的原因
Java的泛型是在JDK1.5开始才加上的。在此之前的Java是没有泛型的。
没有Java的泛型使用起来给人感觉非常的笨重,为了体会泛型带来的好处,
来看看如果没有泛型,我们将如何写代码,以下是样例。- List list = new ArrayList();
- list.add(1);
- list.add("Hello");
- list.add("World");
- list.add(2);
- // 现在将list中Integer类型的数据求和,并输出结果
- int res = 0;
- for (Object obj : list) {
- if (obj instanceof Integer) {
- res += (Integer)obj;
- }
- }
- System.out.println(res); // 输出:3
复制代码 由样例我们可以看出,没有泛型 -> 可以存储任意类型 -> 使用Object类型存储 -> 取出使用时需判断并强转类型。
以上流程可见其繁琐,这与当今愈见简洁的编程发展方向背道而驰。所以便有了Java泛型的出现。
泛型的演化
JDK1.5加入了泛型之后,Java代码的编写也开始变得简洁明了了。我们来试图使用泛型对上面的代码进行优化,以下是样例。- List<Integer> list = new ArrayList<Integer>();
- list.add(1);
- // list.add("Hello"); // 编译报错,不能在一个List<Integer>中存入String
- // list.add("World"); // 同上
- list.add(2);
- // 现在将list中Integer类型的数据求和,并输出结果
- int res = 0;
- for (Integer i : list) {
- res += i; // 本身就是Integer类型不需要类型判断和类型强转
- }
- System.out.println(res); // 输出:3
复制代码 由优化后的样例可以看出,泛型的基本功能就是限制容器中能够存储的数据的类型。既然存任意类型会让程序在输出的时候需要大量的类型判断与类型强转,那索性就限制容器只能存放一种类型(包含其子类),这样就从源头解决了,写大量冗余代码的问题。
然而,只是这样的话真的没有问题吗?我们来看看下面这段代码。- List<Object> list1 = new ArrayList<Integer>(); // 编译报错,List<Integer>不能强转为List<Object>
- List list2 = new ArrayList<Integer>(); // 编译通过
- List<Object> list3 = new ArrayList<>(); // 编译通过
- list3.add(1);
- list3.add("2");
复制代码 为啥会有上面的现象呢。我们分析一下。
第一行,我们假设编译通过,来看看有什么问题。申明类型是Object,而实例类型是Integer。申明类型表示list可以存放的类型,而实例类型是想处理的类型。可以存放的类型与想处理的类型不一致,这就违反了泛型出现的初衷,泛型就是就是为了解决存放类型与实际想处理类型不一致导致需要大量冗余代码问题的。矛盾,所以编译报错。
第二行,同理可知,想处理的类型是Integer,可以存储的类型未指定。由于申明类型未指定所以不检测泛型,编译通过。
第三行,由第一行代码的结论可知,使用泛型时,申明类型与实际类型必须一致。既然如此,右边的类型一定等于左边的类型,那右边索性就不写了,由此有了这种省略的写法。
解释完上述问题后,会发现这样的泛型并不能满足泛型特性的使用需求,这无疑使用起来变得麻烦,虽然不用写大量的类型判断等代码,但作为方法参数时也没办法接收泛型为子类的对象了,样例如下。- // 定义方法
- void test(List<Object> list) { /* do something */ }
- // 其它代码省略
- List<Integer> list = new ArrayList<>();
- // test(list); // 编译报错
- List<Object> list1 = new ArrayList<>();
- for (Integer i : list) {
- list1.add(i);
- }
- test(list1);
复制代码 难道为了传参还需要把list中的Integer取出一个个放入List的新对象中再传入参数吗?
为了解决这个问题,Java增加了一组新的特性:通配符,通配符的上界与下界
我们先来看看什么是通配符,以及通配符有什么特性。
通配符
- // 不使用通配符
- List list1 = List.of(1, 2); // 编译通过
- list1.add("1"); // 编译通过
- list1.add(2); // 编译通过
- // 使用通配符?
- List<?> list2 = List.of(1, 2);
- // list1.add("1"); // 编译报错
- // list1.add(2); // 编译报错
- for (Object obj : list2) { // 默认使用Object接收
- // ...
- }
复制代码 为什么会有上述现象?通配符?代表着不知道是何类型,那么就有了两个可能的发展放心,一是为了存储方便,使其可以存储任何类型的数据;二是为了元素取出后使用方便,使其不可以再存储任何类型的数据。因为Java泛型的基本职责是限制容器里存储的类型,所以这里不再能追加存储任何类型。
单纯的通配符会不会有什么限制,或者不方便的地方呢?一起来看看下面这段代码。- // 定义一个方法,使用通配符
- int sum(List<?> list) {
- int res = 0;
- for(Object obj : list) {
- res += (Integer)obj;
- }
- return res; // 返回集合中元素的和
- }
- // 省略调用位置的方法定义等
- int sum = sum(List.of("2", "1", "3")); // 编译通过
- // 上述代码执行后RuntimeException
复制代码 由上述案例可知,单纯的通配符使用起来并不方便,而且容易出现意料之外的bug,为了解决这个问题,Java又拓展了两个新的性质:通配符的上界与下界。
通配符上界
<blockquote>
语法: |