Java泛型中的子类型化

泛型类型为Java程序引入了新的类型安全范围。 在同一类型上,泛型类型可以表现得很好,尤其是在使用通配符时 。 在本文中,我想解释子类型如何与Java泛型一起工作。

关于泛型类型子类型化的一般思考

不同泛型类型相同的类或接口的不定义亚型层级线性尽可能通用参数类型的子类型的层次结构。 例如,这意味着List <Number>不是List <Integer>的超类型。 下面的突出示例很好地说明了为什么禁止这种子类型化:

// assuming that such subtyping was possible
ArrayList<Number> list = new ArrayList<Integer>();
// the next line would cause a ClassCastException
// because Double is no subtype of Integer
list.add(new Double(.1d))

在进一步详细讨论之前,让我们首先考虑一下有关类型的一般信息:类型为程序引入了冗余。 当您将变量定义为Number类型时,请确保该变量仅引用知道如何处理Number定义的任何方法(例如Number.doubleValue)的对象。 这样,您可以确保可以安全地在变量当前表示的任何对象上调用doubleValue,并且不再需要跟踪变量引用的对象的实际类型。 (只要引用不为null。null引用实际上是Java严格类型安全性的少数例外之一。当然,null的“对象”不知道如何处理任何方法调用。)但是,如果您试图将String类型的对象分配给此Number类型的变量,Java编译器将认识到该对象实际上不理解Number所需的方法,并且会引发错误,因为它不能保证将来可能会调用例如doubleValue将被理解。 但是,如果我们缺少Java中的类型,则程序不会仅凭此更改其功能。 只要我们从不进行错误的方法调用,那么没有类型的Java程序就等效。 从这个角度来看,类型仅仅是为了防止我们的开发人员在愚蠢的事情上夺走一点自由。 此外,类型是隐式记录程序的一种好方法。 (诸如Smalltalk之类的其他编程语言不知道类型,并且除了在大多数时候困扰之外,这也有其好处。)

有了这个,让我们回到泛型。 通过定义通用类型,您可以允许通用类或接口的用户为其代码添加某种类型安全性,因为他们可以限制自己仅以某种方式使用您的类或接口。 例如,当您通过定义List <Number>将List定义为仅包含Numbers时,建议您每次尝试将String类型的对象添加到此列表中时,Java编译器都将引发错误。 在使用Java泛型之前,您只需要相信列表仅包含数字即可。 当您将集合的引用交给第三方代码中定义的方法或从该代码接收到集合时,这可能会特别痛苦。 使用泛型,即使在编译时,您也可以确保List中的所有元素都是某个超类型。

同时,通过使用泛型,您会泛型类或接口失去一些类型安全性。 例如,当您实现通用列表时

class MyList<T> extends ArrayList<T> { }

您不知道MyList中T的类型,并且必须期望该类型可以像Object一样简单。 这就是为什么您可以限制通用类型要求某些最小类型的原因:

class MyList<T extends Number> extends ArrayList<T> {
  double sum() { 
  double sum = .0d;
    for(Number val : this) {
      sum += val.doubleValue();
    }
  return sum;
  }
}

这使您可以假定MyList中的任何对象都是Number的子类型。 这样,您就可以在泛型类中获得某种类型的安全性。

通配符

Java中的通配符等效于说出任何类型 。 因此,在实例化类型(即定义泛型类的某些实例应代表哪种具体类型)时,不允许使用通配符。 例如,在将对象实例化为新的ArrayList <Number>时发生类型实例化,其中您隐式调用包含在其类定义中的ArrayList的类型构造函数

class ArrayList<T> implements List<T> { ... }

ArrayList <T>是带有单个参数的简单类型构造函数。 因此,在ArrayList的类型构造函数定义(ArrayList <T>)中或在此构造函数的调用(新ArrayList <Number>)中,都不允许使用通配符。 但是,如果仅引用类型而不实例化新对象,则可以使用通配符,例如在局部变量中。 因此,允许以下定义:

ArrayList<?> list;

通过定义此变量,可以为任何通用类型的ArrayList创建占位符。 但是,由于对通用类型的这种限制很小,因此无法通过此变量对其的引用将对象添加到列表中。 这是因为您对变量列表所代表的泛型做出了这样的一般假设,即添加一个类型为String的对象并不安全,因为超出列表的列表可能需要某种其他任何子类型的对象。 通常,此必需的类型是未知的,并且不存在任何类型的子类型的对象,可以安全地添加该对象。 (例外是取消了类型检查的空引用。但是,您永远不应在集合中添加空值。)同时,从列表中删除的所有对象都将是对象类型,因为这是关于a的唯一安全假设此变量表示的所有可能列表的常见超类型 。 因此,您可以使用extends和super关键字形成更复杂的通配符:

ArrayList<?> list = new ArrayList<List<?>>();

在此示例中,由于不将通配符应用于类型实参,而不应用于构造的类型本身,因此满足了不得使用通配符类型构造ArrayList的要求。

至于泛型类的子类型化,我们可以总结一下,如果原始类型是子类型,并且泛型类型都是彼此的子类型,则某些泛型类型是另一种类型的子类型。 因此,我们可以定义

List<? extends Number> list = new ArrayList<Integer>();

因为原始类型ArrayList是List的子类型,并且因为泛型Integer是?的子类型? 扩展Number。

最后,请注意,通配符List <?>是List <?的快捷方式。 扩展Object>,因为这是一种常用的类型定义。 但是,如果泛型类型构造函数确实实施了另一个较低的类型边界,例如

class GenericClass<T extends Number> { }

变量GenericClass <?>而是GenericClass <?的快捷方式。 扩展Number>。

取放原则

这种观察将我们引到了“ 获取-放出”原理 。 另一个著名的例子可以最好地解释这一原理:

class CopyClass {
  <T> void copy(List<T> from, List<T> to) {
    for(T item : from) to.add(item);
  }
}

此方法定义不是很灵活。 如果您有一些列表List <Integer>,则无法将其内容复制到某些List <Number>甚至List <Object>。 因此,“获取和放置”原则规定,当您仅从通用实例(通过return参数)读取对象时,应始终使用下限通配符(?extends),而在以下情况下应始终使用上限通配符(?super)。您只提供通用实例方法的参数。 因此,更好的MyAddRemoveList实现如下所示:

class CopyClass {
  <T> void copy(List<? extends T> from, List<? super T> to) {
    for(T item : from) to.add(item);
  }
}

由于您仅从一个列表中读取内容,然后再写入另一个列表中,因此很遗憾,这是很容易被忽略的,您甚至可以在Java核心API中找到不采用“获取与放置”原理的类。 (请注意,上述方法还描述了泛型类型构造函数。)

请注意,类型List <? 扩展T>和List <? 超级T>都没有List <T>的要求那么具体。 还要注意,这种子类型对于非通用类型已经是隐式的。 如果定义的方法要求使用Number类型的方法参数,则可以自动接收任何子类型的实例,例如Integer。 但是,即使期望超型Number,也始终可以安全地读取您收到的此Integer对象。 而且由于无法回该引用,即您不能用Double的实例覆盖Integer对象,因此Java语言不需要通过声明方法签名(如void someMethod(<?扩展Number> number)。 同样,当您答应从方法中返回整数时,调用者只需要一个Number类型的对象,您仍然可以从方法中返回( )任何子类型。 同样,由于无法从假设的返回变量中读取值,因此在方法签名中声明返回类型时,不必通过通配符放弃这些假设的读取权限。

参考: 我的Java日常博客中来自我们JCG合作伙伴 Rafael Winterhalter的Java泛型子类型化

翻译自: https://www.javacodegeeks.com/2013/12/subtyping-in-java-generics.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值