Java泛型之上界下界通配符详解

泛型,继承和子类

如你所知,只要类型兼容,就可以将一种类型的对象分配给另一种类型的对象。例如,你可以指定一个整数一个对象,因为对象是一个整数的超类型:

Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger; // 好

在面向对象的术语中,这被称为“是一种”关系。由于Integer 是一种Object,因此允许赋值。但是Integer也是一种Number,所以下面的代码也是有效的:

public void someMethod(Number n){/ * ... * /}
someMethod(new Integer(10)); // 好
someMethod(new Double(10.1)); // 好

泛型也是如此。您可以执行泛型类型调用,将Number作为其类型参数传递,如果参数与Number兼容,则允许任何后续的add调用:

Box <Number> box = new Box <Number>();
box.add(new Integer(10)); // 好
box.add(new Double(10.1)); // 好

现在考虑以下方法:

public void boxTest(Box <Number> n){/ * ... * /}

它接受什么类型的论据?通过查看其签名,您可以看到它接受一个类型为Box<Number>的参数。但是,这是什么意思?您是否可以按照您的预期传递Box<Integer>或Box<Double>?答案是“否”,因为Box<Integer>和Box<Double>不是Box<Number>的子类型。

这是在使用泛型编程时一个常见的误解,也是一个需要学习的重要概念。

Box<Integer>不是Box<Number>的子类型,即使Integer是Number的子类型。

注意:给定两个具体类型 A 和 B(例如,Number和Integer),MyClass<A> 与 MyClass<B>无关,无论 A 和 B 是否相关。MyClass<A> 和 MyClass<B> 的公共父是Object。

有关如何在类型参数相关时在两个泛型类之间创建类似子类型关系的信息,请参阅下面的通配符和子类型一节。

泛型类和子类型化

您可以通过扩展(extends)泛型类或实现(implements)泛型接口来对其进行子类型化。一个类或接口的类型参数与另一个类或接口的类型参数之间的关系由extends和implements子句确定。

使用Collections类作为示例,ArrayList<E> 实现 List<E>,List<E> 扩展Collection<E>。所以 ArrayList<String>是List<String>的子类型,它是Collection<String>的子类型。只要不改变类型参数,就会在类型之间保留子类型关系。

显示Collection层次结构示例的图表:ArrayList<String>是 List<String>的子类型,二者都是Collection<String>的子类型。

现在假设我们想要定义我们自己的列表接口PayloadList,它将可选值泛型类型参数P的与每个元素相关联。它的声明可能如下:

interface PayloadList<E,P> extends List<E> {
void setPayload(int index, P val);
...
}

PayloadList的以下参数化是List<String>的子类型:

PayloadList<String,String>
PayloadList<String,Integer>
PayloadList<String,Exception>

PayLoadList层次结构的示意图:PayloadList<String,String>是List<String>的子类型,它是Collection<String>的子类型。 在PayloadList<String,String>的相同级别是PayloadList <String,Integer>和PayloadList<String,Exceptions>。

通配符和子类型

如 泛型,继承和子类一节中所述,泛型类之间或接口之间几乎并不因它们的类型参数而相关。但是,您可以使用通配符在泛型类或接口之间创建关系。

给定以下两个常规(非泛型)类:

class A { /* ... */ }
class B extends A { /* ... */ }

编写以下代码是合理的:

B b = new B();
A a = b;

此示例显示常规类的继承遵循此子类型规则:如果B扩展A,则类B是类A的子类型。此规则不适用于泛型类型:

List<B> lb = new ArrayList<>();
List<A> la = lb; //编译时错误

鉴于Integer是Number的子类型,List<Integer> 和 List<Number> 之间的关系是什么?

尽管Integer是Number的子类型,但List<Integer>不是List<Number>的子类型,实际上,这两种类型不相关。List<Number>和 List<Integer> 的公共父是 List<?>。

上界(extends)的通配符与下界(super)通配符

为了在这些类之间创建关系以便代码可以通过 List<Integer> 的元素访问Number的方法,请使用上界的通配符:

List<? extends Integer> intList = new ArrayList<>();
List<? extends Number> numList = intList; // OK, List<?extends Integer>是 List< ? extends Number>的子类型

因为Integer是Number的子类型,而numList是Number对象的列表,所以intList(是一个Integer对象列表)和numList之间现在存在关系。下图显示了使用上限和下限通配符声明的多个 List 类之间的关系。

通配符使用指南

学习使用泛型编程时,更令人困惑的一个方面是确定何时使用上限有界通配符以及何时使用下限有界通配符。本文提供一些设计代码时要遵循的一些准则。

为讨论方便,认为变量具备两个功能:

一个“In”变量

“in”变量向代码提供数据。想象一下带有两个参数的复制方法:copy(src,dest)。该SRC参数提供的数据被复制,因此它是“in”参数。

一个“Out”变量
“out”变量保存数据以供其他地方使用。在复制示例中,copy(src,dest),dest参数接受数据,因此它是“out”参数。

当然,一些变量既用于“in”又用于“out”目的 - 这种情况也在本文中也用到了。

在决定是否使用通配符以及适合使用哪种类型的通配符时,可以使用“in”和“out”原则。以下列表提供了遵循的准则:

通配符指南:

  • 使用extends关键字, 定义带有上界通配符的“in”变量。
  • 使用super关键字,使用下界通配符定义“out”变量。
  • 在可以使用Object类中定义的方法访问“in”变量的情况下,使用无界通配符。
  • 在代码需要作为“in”和“out”变量访问变量的情况下,不要使用通配符。

这些指南不适用于方法的返回类型。应该避免使用通配符作为返回类型,因为它强制程序员使用代码来处理通配符。

List<? extends ...> 可以被非正式地认为是只读的,但这不是一个严格的保证。假设您有以下两个类:

class NaturalNumber {
private int i;
public NaturalNumber(int i) { this.i = i; }
// ...
}
class EvenNumber extends NaturalNumber {
public EvenNumber(int i) { super(i); }
// ...
}

请考虑以下代码:

List<EvenNumber> le = new ArrayList<>();
List<? extends NaturalNumber> ln = le;
ln.add(new NaturalNumber(35)); // compile-time error //编译时错误

因为List<EvenNumber>是List<? extends NaturalNumber>,您可以赋值le给ln。但是你不能使用ln将自然数添加到偶数列表中。列表中的以下操作是可能的:

  • 您可以添加null。
  • 你可以调用清除。
  • 您可以获取迭代器并调用remove。
  • 您可以捕获通配符并写入从列表中读取的元素。

你可以看到List<? extends NaturalNumber>在严格意义上不是只读的,但您可能会这样想,因为您无法存储新元素或更改列表中的现有元素。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Java 泛型总结(三):通配符的使用

    简介 前两篇文章介绍了泛型的基本用法.类型擦除以及泛型数组.在泛型的使用中,还有个重要的东西叫通配符,本文介绍通配符的使用. 这个系列的另外两篇文章: Java 泛型总结(一):基本用法与类型擦除 Java 泛型总结(二):泛型与数组 数组的协变 在了解通配符之前,先来了解一下数组.Java 中的数组是协变的,什么意思?看下面的例子: class Fruit {} class Apple extends Fruit {} class Jonathan extends Apple {} class

  • Java泛型类型通配符和C#对比分析

    c#的泛型没有类型通配符,原因是.net的泛型是CLR支持的泛型,而Java的JVM并不支持泛型,只是语法糖,在编译器编译的时候都转换成object类型 类型通配符在java中表示的是泛型类型的父类 public void test(List<Object> c) { for(int i = 0;i < c.size();i++) { System.out.println(c.get(i)); } } //创建一个List<String>对象 List<String&g

  • 一看就懂 详解JAVA泛型通配符T,E,K,V区别

    1. 先解释下泛型概念 泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数.这种参数类型可以用在类.接口和方法的创建中,分别称为泛型类.泛型接口.泛型方法.Java语言引入泛型的好处是安全简单. 在Java SE 1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的"任意化","任意化"带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的.对于强制类型转

  • 详谈Java泛型中T和问号(通配符)的区别

    类型本来有:简单类型和复杂类型,引入泛型后把复杂类型分的更细了. 概述 泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数.这种参数类型可以用在类.接口和方法的创建中,分别称为泛型类.泛型接口.泛型方法. Java语言引入泛型的好处是安全简单. 在Java SE 1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的"任意化","任意化"带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对

  • 浅谈Java泛型通配符解决了泛型的许多诟病(如不能重载)

    泛型: package Java基础增强; import java.util.ArrayList; import java.util.List; import org.junit.Test; public class Test2 { @Test public void fun1(){ Object[] objects = new Object[10]; List list = new ArrayList(); String[] strings = new String[10]; List<Str

  • Java泛型之上界下界通配符详解

    泛型,继承和子类 如你所知,只要类型兼容,就可以将一种类型的对象分配给另一种类型的对象.例如,你可以指定一个整数一个对象,因为对象是一个整数的超类型: Object someObject = new Object(); Integer someInteger = new Integer(10); someObject = someInteger; // 好 在面向对象的术语中,这被称为"是一种"关系.由于Integer 是一种Object,因此允许赋值.但是Integer也是一种Num

  • Java泛型的类型擦除示例详解

    目录 前言 泛型的类型擦除原则是: 1 擦除类定义中的类型参数 1.1 无限制类型擦除 1.2 有限制类型擦除 2 擦除方法定义中的类型参数 3 桥接方法和泛型的多态 总结 参考资料 前言 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了"伪泛型"的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的"类型擦除"(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生

  • Java泛型定义与用法实例详解

    本文实例讲述了Java泛型定义与用法.分享给大家供大家参考,具体如下: 1. 泛型的由来 先看如下代码: import java.util.List; import java.util.ArrayList; public class TestGeneric { @SuppressWarnings({ "rawtypes", "unchecked" }) public static void main(String[] args) { List list = new

  • Java泛型与数据库应用实例详解

    本文实例讲述了Java泛型与数据库应用.分享给大家供大家参考,具体如下: 一 点睛 BaseDao定义了基本的数据库增删查改, 之后可以继承该泛型类,实现各自的增删查改,或者使用超类的增删查改,同时每个继承类还能增加自己的操作. 二 实战 1 BaseDao.java import java.lang.reflect.Field; import java.sql.Connection; import java.sql.DriverManager; import java.sql.Prepared

  • Java泛型之类型擦除实例详解

    目录 前言 泛型是什么? 泛型的定义和使用 泛型类 泛型方法 泛型类与泛型方法的共存现象 泛型接口 通配符 ? 无限定通配符 <?> <? extends T> 类型擦除 类型擦除带来的局限性 泛型中值得注意的地方 Java 不能创建具体类型的泛型数组 泛型,并不神奇 总结 前言 泛型,一个孤独的守门者. 大家可能会有疑问,我为什么叫做泛型是一个守门者.这其实是我个人的看法而已,我的意思是说泛型没有其看起来那么深不可测,它并不神秘与神奇.泛型是 Java 中一个很小巧的概念,但同时

  • Java泛型继承原理与用法详解

    本文实例讲述了Java泛型继承原理与用法.分享给大家供大家参考,具体如下: 一 点睛 当创建了带泛型声明的接口.父类之后,可以为该接口创建实现类,或从该父类来派生子类,但值得指出的是,当使用这些接口.父类时不能再包含类型形参. 如果使用泛型类时没有传入实际的类型参数,Java编译器可能发出警告:使用了未经检查或不安全的操作--这就是泛型检查的警告. 二 实战--传入实际的类型参数 public class A1 extends Apple<String> { // 正确重写了父类的方法,返回值

  • Java泛型机制的程序演示详解

    本文为大家分享了Java泛型机制的程序演示具体代码,供大家参考,具体内容如下 package packA; import java.util.*; public class GenericDemo { public static void main(String[] args) { TreeSet<String> ts = new TreeSet<String>( new LenSort() ); //<String> 泛型 ts.add("hidwju&qu

  • java泛型通配符详解

    前言 Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许开发者在编译时检测到非法的类型. 泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数. 泛型带来的好处 在没有泛型的情况的下,通过对类型 Object 的引用来实现参数的"任意化","任意化"带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的.对于强制类型转换错误的情况,编译器可能不提示错

  • Java基础之java泛型通配符详解

    前言 Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许开发者在编译时检测到非法的类型. 泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数. 泛型带来的好处 在没有泛型的情况的下,通过对类型 Object 的引用来实现参数的"任意化","任意化"带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的.对于强制类型转换错误的情况,编译器可能不提示错

  • Java 中泛型 T 和 ? 的区别详解

    目录 泛型中 T 类型变量 和 ? 通配符 区别 Generic Types 类型变量 用法 2.声明通用的方法 – 泛型方法: 有界类型参数 Wildcards 通配符 1.上界通配符:? extend 上界类型 2.无界通配符:? 3.下界通配符:? super 子类 类型擦除 泛型中 T 类型变量 和 ? 通配符 区别 定义不同 :T 是类型变量,? 是通配符 使用范围不同: ? 通配符用作 参数类型.字段类型.局部变量类型,有时作为返回类型(但请避免这样做) T 用作 声明类的类型参数.

随机推荐