【Java】java 中的泛型通配符——从“偷偷地”地改变集合元素说起

一直没注意这方面的内容,想来这也算是基础了,就写了这个笔记。

首先java的通配符共有三种————先别紧张,现在只是粗略的过一下,看不看其实无所谓

类型 介绍
<?> 无限定通配符,等价于 <? extends Object>
<? extends Number> 上限通配符,表示参数类型只能是 Number 或是 Number 的子类。
<? super Number> 下限通配符,表示参数类型只能是 Number 或是 Number 的父类。

然后再让我们定义四个类,下面会用到

class A {
    public String getName() {
        return "A";
    }
}

class B extends A{
    @Override
    public String getName() {
        return "B";
    }
}

class BAge extends B{
    @Override
    public String getName() {
        return "C";
    }

    public int getAge() {
        return 100;
    }
}

class BSize extends B{
    @Override
    public String getName() {
        return "D";
    }

    public int getSize() {
        return -1;
    }
}

从一个奇怪的现象说起

  1. 首先,我们再引入一个类 PrintAges ,用于打印 BAge 的 getAge()

    class PrintAges{
    public static void print(BAge[] ages){
        if (ages == null)
            return;
    
        for (BAge bage : ages){
            if (bage != null)
                System.out.println(bage.getAge());
        }
    }
    }

    仔细看看上面这个类,你觉得我写的 PrintAges 怎样?够完美吗,不会引发异常吧?我觉得也很完美了,肯定不会有异常出现在我的代码里了。

  2. 我们测试下
    BAge[] temps = new BAge[]{new BAge(), new BAge()};
    PrintAges.print(temps);

输出:

100
100

完美运行。

  1. 我们再增加两行

    BAge[] temps = new BAge[]{new BAge(), new BAge()};
    B[] barray = temps;  // 新增加的第一行
    barray[0] = new BSize(); // 新增加的第二行
    PrintAges.print(temps);

    你猜怎么着?我偷偷地改变了数组中的元素!我在 BAge 类型的数组中的元素赋了一个 BSize 的对象!

    而且,编译通过了。但是肯定会有异常出现,你猜是在哪一行?

输出:

Exception in thread "main" java.lang.ArrayStoreException: JavaApp.BSize at JavaApp.JavaApplicationStudyGen.main(JavaApplicationStudyGen.java:33)

本来我以为会在 PrintAges 的 print 方法中发生异常,但是实际上新增加的第二行发生了运行时错误,赋值错误。

而在C#中,这种问题出现的可能性就更小了。C#中,新增的第一行是无法通过编译的。

那么,这种问题在集合……准确地说是在泛型里会不会出现呢?

  1. 上述问题在泛型中的讨论。

我们先对 PrintAges 添加一个 print 函数的重载

class PrintAges{
    public static void print(ArrayList<BAge> list) {
        if (list == null)
            return;

        for(BAge age : list) System.out.println(age.getAge());
    }
    public static void print(BAge[] ages){
        if (ages == null)
            return;

        for (BAge bage : ages){
            if (bage != null)
                System.out.println(bage.getAge());
        }
    }
}

然后我们对用再次运行如下代码:

ArrayList<BAge> list = new ArrayList<BAge>();
list.add(new BAge());
ArrayList<B> yourList = list; // 编译错误
yourList.set(0, new BSize()); // star 1
BAge age = list.get(0); // star 2
PrintAges.print(list);

这次,Java 处理的比较严格,在把 ArrayList<BAge> 赋值给 ArrayList<B> 类型的对象时产生了编译错误。

在 C# 里,也是一样的,在把 ArrayList<BAge> 赋值给 ArrayList<B> 类型的对象时会产生编译错误。

一开始,我不理解这样做对 list 引用的对象 ArrayList

但是,不能赋值的原因,把一个 BSize 类型的对象放在了一个实际上是 ArrayList

经过类型擦除之后, star 2 所在行的代码就会变成

BAge age = (BSize)list.get(0); // star 2

这样就是完全不正确的了。

也就是说,我们应该禁止类似 ArrayList<B> yourList = new ArrayList<BAge>() 这样的赋值,否则,就会出现这样的错误和意外。

说实话,B[] barray = new BAge[]{new BAge(), new BAge()} 这样的赋值操作也该被禁止的,但是 Java 就可以。看看人家 C# 就不允许这样做(笑)

记住这样的错误。接下来,我们就可以讨论 Java 的泛型通配符了。

通配符出现的原因

所以所,通配符的出现就是为了在错误避免上述错误的同时,给程序员提供一点便利

而通配符是怎么样发生作用的呢?是通过编译器给定的三条“游戏规则”(也即是上面给的表格里的规则)发生作用的。

在一开始理解的时候是需要一点逻辑能力的:

  1. 上限通配符 <? extends B> 确保了可读性, <? extends B> 表示参数类型只能是 B 或是 B 的子类 可以被编译通过的语句:
ArrayList<? extends B> list = new ArrayList<A>(); // 编译错误
ArrayList<? extends B> list = new ArrayList<B>(); // ok
ArrayList<? extends B> list = new ArrayList<BAge>(); // ok
ArrayList<? extends B> list = new ArrayList<BSize>(); // ok

基于以上的编译规则,我们可以得出以下事实:

  • 你一定能从 list 中读取到一个 B 元素,因为 list 要么指向 ArrayList<B> ,要么指向包含 B 子类对象的 ArrayList<B>
  • 你不能不能插入一个 B 元素 ,因为 list 可能指向的是 ArrayList<BSize> 或者指向 ArrayList<BAge>
  • 你不能不能插入一个 BAge 元素 ,因为 list 可能指向的是 ArrayList<BSize>
  • 你不能不能插入一个 BSize 元素 ,因为 list 可能指向的是 ArrayList<BAge>

注意,上述代码中, list 中的 T 被替换成了 ? extends B

也就是说,读取操作可以被确保,你一定能从 list 中读取到一个 B 元素 这样, list.get 方法就可以被正常使用了。

list.set(int, T) 就被替换成了 list.set(int, ? extends B),这个方法就被编译器“禁止”了。也就是说,如果你写出 list.set(0, new B())list.set(0, new BSize()) 是不行的。

在这里你肯定要提出疑问了,你不是说符合“游戏规则” <? extends B> 表示参数类型只能是 B 或是 B 的子类 就行的吗? 我只能说,文字所能传达的信息是有限的,这个表述也只适用于 ArrayList<? extends B> list = new ArrayList<A>(); 这样的赋值时刻。还是得看上述推导的“事实”

  1. 下限通配符 <? super B> 确保了写入性

    ```Java

    ArrayList<? super B> list = new ArrayList


```

基于以上的编译规则,我们可以得出以下事实:

  • 你一定能插入一个 B 类型的对象或者 B 子类型的对象。因为, list 要么指向包含 B 类型的 ArrayList,要么指向包含 B 超类型的 ArrayList 对象,比如: list 可能是 ArrayList<Object>ArrayList<A>
  • 你一定你不能保证读取到 B ,因为 list 可能指向 ArrayList<Object> 或者是 ArrayList<B>

这样, list.set 方法就可以被正常使用了。假设 list 指向 ArrayList<Object> ,我们把一个 B 类型的对象添加到 ArrayList<Object> 中也没错啊。

  • 或者,我们把一个 BAge 对象添加到 ArrayList<Object>ArrayList<A> 中也没错啊。
  • 或者,我们把一个 BSize 对象添加到 ArrayList<Object>ArrayList<A> 中也没错啊。

总结

  1. 通配符的出现是为了让程序员在避免上述错误的情况下能放宽一点要求,即所谓的“符合我编译器的规则,就让你舒服”
  2. ? extends B 确保了可读性,? super B 确保了写入性。
  3. ? extends B? super B 给人的感觉是逆操作。

时间: 2024-10-10 01:08:00

【Java】java 中的泛型通配符——从“偷偷地”地改变集合元素说起的相关文章

java中的泛型(转)

什么是泛型? 泛型(Generic type 或者 generics)是对 Java 语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类.可以把类型参数看作是使用参数化类型时指定的类型的一个占位符,就像方法的形式参数是运行时传递的值的占位符一样. 可以在集合框架(Collection framework)中看到泛型的动机.例如,Map 类允许您向一个 Map 添加任意类的对象,即使最常见的情况是在给定映射(map)中保存某个特定类型(比如 String)的对象. 因为 Map.get(

java高新技术中《九》

一:注解 1.注解(Annotation) 注解相当一个类或接口,每一个注解都是一个实例对象    注解的使用形式:@interface即@注解类名 定义注解类: @interface A {…} 使用了“注解类”的类: @A class B{} 对“使用了注解类的类”进行反射操作: class C { B.class.isAnnotationPresent(A.class); A a=(A)B.class.getAnnotation(A.class); } 2.注解类的生命周期 源文件(.ja

java核心(九):泛型

一.泛型的目的 泛型解决的是:向下转型时存在的安全隐患: 泛型的核心是:在定义类或接口的时候,不需要显示地声明参数或属性的类型. 二.类中使用泛型 注:在类.接口或方法中,泛型可以定义多个,泛型的名称自定义: 注:使用泛型中,在显示指定具体类型时,只能是引用类型,不能是基本数据类型: 注:使用泛型中,如果没有显示指定具体类型,则系统默认使用Object作为具体类型: package com.study.Type; /** * 在声明类的时候,定义了两个泛型:T.A * @param <T> *

Java——Java接口

接口(英文:Interface),在JAVA编程语言中是一个抽象类型,是抽象方法的集合,接口通常以interface来声明. 一个类通过继承接口的方式,从而来继承接口的抽象方法. 接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念.类描述对象的属性和方法.接口则包含类要实现的方法. 除非实现接口的类是抽象类,否则该类要定义接口中的所有方法. 接口无法被实例化,但是可以被实现. 一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类. 另外,在Java中,接口类型可用

java----数据结构与算法----集合元素的遍历:迭代器--------&gt;JavaAPI:java.util.Iterator+java.util.ListIterator

概述: 迭代器用于集合元素的遍历 迭代器有两种,分别是Iterator和ListIterator Iterator可以用于任何类型集合的遍历 ListIterator只能用于List集合的遍历 ListIterator接口继承了Iterator接口,所以前者拥有后者所定义的所有成员函数,同时,ListIterator还添加了一些具有List集合特性的操作函数,如按照索引访问集合元素.替换/添加集合元素等等 java.util.Iterator /** * @author chen * @date

java中的泛型【T】与通配符【?】概念入门

使用泛型的目的是利用Java编译机制,在编译过程中帮我们检测代码中不规范的有可能导致程序错误的代码.例如,我们都知道List容器可以持有任何类型的数据,所以我们可以把String和Integer等类型同时放入同一个List容器中,但是这种做法是极其危险的.在泛型机制中,这种操作就会导致编译不通过,会强制要求你将List容器中的数据类型修改为统一类型.这种机制可以帮助我们减少程序运行中隐藏的Bug. 泛型[T] 泛型在代码中使用广泛. 泛型的用法 根据泛型使用的位置,即使用在类(Class),属性

Java 中的泛型详解-Java编程思想

Java中的泛型参考了C++的模板,Java的界限是Java泛型的局限. 2.简单泛型 促成泛型出现最引人注目的一个原因就是为了创造容器类. 首先看一个只能持有单个对象的类,这个类可以明确指定其持有的对象的类型 class Holder1 { private Circle a; public Holder1(Circle a) { this.a = a; } Circle get() { return a; } } 上面的类的可重用性不怎么样,无法持有其他类型的任何对象,下面通过持有Object

Java中的泛型 (上) - 基本概念和原理

本节我们主要来介绍泛型的基本概念和原理 后续章节我们会介绍各种容器类,容器类可以说是日常程序开发中天天用到的,没有容器类,难以想象能开发什么真正有用的程序.而容器类是基于泛型的,不理解泛型,我们就难以深刻理解容器类.那,泛型到底是什么呢? 什么是泛型? 一个简单泛型类 我们通过一个简单的例子来说明泛型类的基本概念.实现原理和好处. 基本概念 我们直接来看代码: public class Pair<T> { T first; T second; public Pair(T first, T se

Java泛型-- 通配符

转自:http://blog.csdn.net/flfna/article/details/6576394 ———————————————————————————————————————————— 通配符 在本文的前面的部分里已经说过了泛型类型的子类型的不相关性.但有些时候,我们希望能够像使用普通类型那样使用泛型类型: ◆ 向上造型一个泛型对象的引用 ◆ 向下造型一个泛型对象的引用 向上造型一个泛型对象的引用 例如,假设我们有很多箱子,每个箱子里都装有不同的水果,我们需要找到一种方法能够通用的处