Java进阶-泛型(2)

16 篇文章 0 订阅
Wildcards and Subtyping (通配符和子类型)
如泛型,继承和子类型中所述,泛型类或接口不仅仅因为它们的类型之间存在关系而相关。但是,你可
以使用通配符在通用类或接口之间创建关系。
给定以下两个常规(非泛型)类:
class A { /* ... */ }
编写以下代码是合理的:
B b = new B();
A a = b;
此示例显示常规类的继承遵循此子类型规则:如果 B 扩展了 A ,则类 B 是类 A 的子类型。此规则不适用于
通用类型:
List < B > lb = new ArrayList <> ();
List < A > la = lb ; // compile-time error
假定 Integer Number 的子类型,则 List List 之间是什么关系?
尽管 Integer Number 的子类型,但 List 不是 List 的子类型,实际上,这两种类型无关。 List
List 的公共父级是 List
为了在这些类之间创建关系,以便代码可以通过 List 的元素访问 Number 的方法,请使用上限通配
符:
List <? extends Integer > intList = new ArrayList <> ();
List <? extends Number > numList = intList ; // OK. List<? extends Integer>
is a subtype of List<? extends Number>
由于 Integer Number 的子类型,并且 numList Number 对象的列表,因此 intList (一个 Integer
对象的列表)和 numList 之间现在存在关系。下图显示了使用上下限通配符声明的几个 List 类之间的
关系。
通配符使用准则 部分提供了有关使用上下限通配符的后果的更多信息。
Wildcard Capture and Helper Methods (通配符捕获和帮助方
法)
在某些情况下,编译器会推断通配符的类型。例如,可以将列表定义为 List ,但是在评估表达式时,
编译器会从代码中推断出特定类型。这种情况称为通配符捕获。
在大多数情况下,你无需担心通配符捕获,除非你看到包含短语 “capture of” 的错误消息。
WildcardError 示例在编译时产生捕获错误:
import java . util . List ;
public class WildcardError {
          void foo ( List <?> i ) {
             i . set ( 0 , i . get ( 0 ));
         }
}
在此示例中,编译器将 i 输入参数处理为 Object 类型。当 foo 方法调用 List.set(int, E) 时,编译器
无法确认要插入列表中的对象的类型,并产生错误。当发生这种类型的错误时,通常意味着编译器认为
你正在将错误的类型分配给变量。为此,将泛型添加到 Java 语言中(以便在编译时强制类型安全)。
Oracle JDK 7 javac 实现编译时, WildcardError 示例将生成以下错误:
在此示例中,代码正在尝试执行安全操作,那么如何解决编译器错误?你可以通过编写捕获通配符的私
有帮助器方法来修复它。在这种情况下,你可以通过创建私有帮助器方法 fooHelper 来解决此问题,
WildcardFixed 中所示:

由于使用了辅助方法,编译器在调用中使用推断来确定 T CAP 1 (捕获变量)。该示例现在可以成功
编译。
按照约定,辅助方法通常命名为 originalMethodNameHelper
现在考虑一个更复杂的示例 WildcardErrorBad
在此的示例代码正在尝试不安全的操作。例如,考虑对 swapFirst 方法的以下调用:
List < Integer > li = Arrays . asList ( 1 , 2 , 3 );
List < Double > ld = Arrays . asList ( 10.10 , 20.20 , 30.30 );
swapFirst ( li , ld );
虽然 List List 都满足了 List 的条件,但从 Integer 值列表中提取一个项并试图将其放入 Double
值列表中显然是不正确的。
使用 Oracle JDK javac 编译器编译代码会产生以下错误:
没有解决此问题的辅助方法,因为代码根本上是错误的。
Guidelines for Wildcard Use (通配符使用准则)
在学习使用泛型编程时,更令人困惑的方面之一是确定何时使用上限的通配符以及何时使用下限的通配
符。此页面提供了一些在设计代码时要遵循的准则。
为了便于讨论,将变量视为提供以下两个功能之一将很有帮助:
输入 变量 :输入变量将数据提供给代码。想象一个具有两个参数的复制方法: copy(src, dest)
src 参数提供要复制的数据,因此它是输入参数。
输出 变量 :输出变量保存要在其它地方使用的数据。在复制示例 copy(src, dest) 中, dest 参数接
受数据,因此它是输出参数。
当然,某些变量既用于 输入 又用于 输出 目的(准则中也解决了这种情况)。
在决定是否使用通配符以及哪种类型的通配符时,可以使用 输入 输出 原理。以下列表提供了要遵
循的准则:
通配符准则:
  • 使用上限通配符定义输入变量,使用 extends 关键字。
  • 使用下限通配符定义输出变量,使用 super 关键字。
  • 如果可以使用 Object 类中定义的方法访问输入变量,请使用无界通配符( ? )。
  • 如果代码需要同时使用输入和输出变量来访问变量,则不要使用通配符。
这些准则不适用于方法的返回类型。应该避免使用通配符作为返回类型,因为这会迫使程序员使用代码
来处理通配符。
List 定义的列表可以被非正式地认为是只读的,但这并不是一个严格的保证。假设你有以下两个
类。
考虑以下代码:
List < EvenNumber > le = new ArrayList <> ();
List <? extends NaturalNumber > ln = le ;
ln . add ( new NaturalNumber ( 35 )); // compile-time error
因为 List List 的一个子类型,所以可以将 le 赋给 ln 。但不能用 ln 将自然数添加到偶数列表中。可以
对该列表进行以下操作。
可以添加 null
可以调用 clear
可以获取迭代器( iterator )和调用 remove
可以捕获通配符和写入从列表中读取的元素。
你可以看到由 List 定义的列表不是最严格意义上的只读,但你可能会这样想,因为你不能在列表中存
储一个新的元素或改变一个现有的元素。
 
Type Erasure (类型擦除)
Java 语言引入了泛型,以在编译时提供更严格的类型检查并支持泛型编程。 为了实现泛型, Java 编译器
将类型擦除应用于:
如果类型参数不受限制,则将通用类型中的所有类型参数替换为其边界(上下限)或 Object 。因
此,产生的字节码仅包含普通的类,接口和方法。
必要时插入类型转换,以保持类型安全。
生成桥接方法以在扩展的泛型类型中保留多态。
类型擦除可确保不会为参数化类型创建新的类;因此,泛型不会产生运行时开销。
Erasure of Generic Types (泛型类型的擦除)
在类型擦除过程中, Java 编译器将擦除所有类型参数,如果类型参数是有界的,则将每个参数替换为其
第一个边界;如果类型参数是无界的,则将其替换为 Object
考虑以下表示单个链接列表中的节点的通用类:
由于类型参数 T 是无界的,因此 Java 编译器将其替换为 Object
在下面的示例中,通用 Node 类使用限定类型参数:
Java 编译器将绑定类型参数 T 替换为第一个绑定类 Comparable
Erasure of Generic Methods (通用方法的擦除)
Java 编译器还会擦除通用方法参数中的类型参数。考虑以下通用方法:

 假设定义了以下类:

class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }
你可以编写一个通用方法来绘制不同的形状:
public static < T extends Shape > void draw ( T shape ) { /* ... */ }
Java 编译器用 Shape 替换 T
public static void draw ( Shape shape ) { /* ... */ }
 
 
Effffects of Type Erasure and Bridge Methods (类型擦除和桥接
方法的影响)
有时类型擦除会导致可能无法预料的情况。以下示例显示了这种情况的发生方式。该示例(在 桥接方
中进行了介绍)展示了编译器有时如何创建一个综合方法,称为桥接方法,作为类型擦除过程的一
部分。
给定以下两类
 
考虑以下代码:
MyNode mn = new MyNode ( 5 );
Node n = mn ; // A raw type - compiler throws an unchecked warning
n . setData ( "Hello" );
Integer x = mn . data ; // Causes a ClassCastException to be thrown.
类型擦除后,此代码变为:
MyNode mn = new MyNode ( 5 );
Node n = ( MyNode ) mn ; // A raw type - compiler throws an unchecked
warning
n . setData ( "Hello" );
Integer x = ( String ) mn . data ; // Causes a ClassCastException to be thrown.
执行代码时会发生以下情况:
n.setData("Hello") ; 导致在 MyNode 类的对象上执行 setData(Object) 方法( MyNode 类继承
Node setData(Object) )。
setData(Object) 的主体中,将 n 引用的对象的数据字段分配给 String
可以访问通过 mn 引用的同一对象的数据字段,并且该字段应该是整数(因为 mn MyNode ,它
Node )。
尝试将 String 分配给 Integer 会导致 Java 编译器在分配时插入的强制转换导致
ClassCastException
Bridge Methods (桥接方法)
在编译扩展参数化类或实现参数化接口的类或接口时,作为类型擦除过程的一部分,编译器可能需要创
建一个称为桥接方法的综合方法。你通常不必担心桥接方法,但是如果其中一个出现在堆栈跟踪中,你
可能会感到困惑。
类型擦除后, Node MyNode 类变为:
类型擦除后,方法签名不匹配。 Node 方法变为 setData(Object) ,而 MyNode 方法变为
setData(Integer) 。因此, MyNode setData 方法不会覆盖 Node setData 方法。
为了解决此问题并在类型擦除后保留泛型类型的多态性, Java 编译器生成了一个桥接方法来确保子类型
能够按预期工作。对于 MyNode 类,编译器为 setData 生成以下桥接方法:
 
可以看到,在类型擦除后, MyNode 类的桥接方法( setData(Object) )与 Node 类的
setData(Object) 方法具有相同的方法签名,它委托给原来的 setData(Integer) 方法。
Non-Reififiable Types (不可具体化类型)
类型擦除 部分讨论了编译器删除与类型参数和类型参数有关的信息的过程。类型擦除的结果与变量参
数(也称为 varargs )方法有关,这些方法的 varargs 形式参数具有不可更改的类型。有关 varargs 方法的
更多信息,请参见 将信息传递给方法或构造方法 中的 任意参数数目
此页面涵盖以下主题:
不可具体化类型。
堆污染。
具有不可具体化形式参数的 Varargs 方法的潜在漏洞。
防止使用不可具体化形式参数的 Varargs 方法发出警告。
Non-Reififiable Types (不可具体化类型)
具体化类型是其类型信息在运行时完全可用的类型。这包括基本类型,非通用类型,原始( raw )类型
以及未绑定通配符的调用。
非具体化类型是指在编译时通过类型擦除法删除了信息的类型(对通用类型的调用没有被定义为非绑定
通配符)。非具体化类型在运行时并不具备所有的信息。非具体化类型的例子是 List List JVM
运行时无法区分这些类型。正如 对通用类型的限制 中所示,在某些情况下,非具体化类型不能使用:例
如,在 instanceof 表达式中,或者作为数组中的元素。
Heap Pollution (堆污染)
当参数化类型的变量引用的对象不是该参数化类型的对象时,就会发生堆污染。如果程序执行某些操作
会在编译时产生未经检查的警告,则会发生这种情况。如果在编译时(在编译时类型检查规则的范围
内)或在运行时,无法确定涉及参数化类型的操作(例如,强制转换或方法调用)的正确性,则会生成
未经检查的警告。例如,当混合原始( raw )类型和参数化类型时,或者执行未经检查的强制转换时,
就会发生堆污染。
在正常情况下,当同时编译所有代码时,编译器会发出未经检查的警告,以引起你对潜在堆污染的注
意。如果分别编译代码部分,则很难检测到堆污染的潜在风险。如果确保代码在没有警告的情况下进行
编译,则不会发生堆污染。
Potential Vulnerabilities of Varargs Methods with Non-Reififiable Formal
Parameters (具有不可具体化形式参数的 Varargs 方法的潜在漏洞)
包含 vararg 输入参数的泛型方法可能导致堆污染。
考虑以下 ArrayBuilder 类:
以下示例 HeapPollutionExample 使用 ArrayBuiler 类:
编译后, ArrayBuilder.addToList 方法的定义会产生以下警告:
warning : [ varargs ] Possible heap pollution from parameterized vararg type T
当编译器遇到 varargs 方法时,它将 varargs 形式参数转换为数组。但是, Java 编程语言不允许创建参数
化类型的数组。在方法 ArrayBuilder.addToList 中,编译器将 varargs 形式参数 T ... 元素转换为形
式参数 T[] 元素,即数组。但是,由于类型擦除,编译器将 varargs 形式参数转换为 Object[] 元素。
因此,存在堆污染的可能性。
以下语句将 varargs 形式参数 l 分配给对象数组 objectArgs
Object [] objectArray = l ;
该语句可能会导致堆污染。可以将与 varargs 形式参数 l 的参数化类型匹配的值分配给变量 objectArray
从而可以将其分配给 l 。但是,编译器不会在此语句上生成未经检查的警告。当编译器将 varargs 形式参
List... l 转换为形式参数 List[] l 时,已经生成了警告。此声明有效;变量 l 具有类型 List[]
它是 Object[] 的子类型。
因此,如果将任何类型的 List 对象分配给 objectArray 数组的任何数组组件,则编译器不会发出警告或
错误,如以下语句所示:
objectArray [ 0 ] = Arrays . asList ( 42 );
该语句将 List 对象分配给 objectArray 数组的第一个数组组件,该 List 对象包含一个 Integer 类型的
对象。
假设你使用以下语句调用 ArrayBuilder.faultyMethod
ArrayBuilder . faultyMethod ( Arrays . asList ( "Hello!" ), Arrays . asList ( "World!" ));
在运行时, JVM 在以下语句中引发 ClassCastException
// ClassCastException thrown here
String s = l [ 0 ]. get ( 0 );
存储在变量 l 的第一个数组组件中的对象的类型为 List ,但是此语句期望使用类型为 List 的对象。
Prevent Warnings from Varargs Methods with Non-Reififiable Formal
Parameters (防止使用不可具体化形式参数的 Varargs 方法发出警告)
如果你声明具有参数化类型参数的 varargs 方法,并确保由于对 varargs 形式参数的处理不当,该方法的
主体不会引发 ClassCastException 或其它类似的异常,则可以避免警告编译器通过为静态和非构造方
法声明添加以下注解,为此类 varargs 方法生成:
@SafeVarargs
@SafeVarargs 注解是该方法契约的书面部分;该注解断言该方法的实现不会不适当地处理 varargs
式参数。
尽管不太理想,但也可以通过在方法声明中添加以下内容来抑制此类警告:
@SuppressWarnings ({ "unchecked" , "varargs" })
但是,这种方法不能抑制从该方法的调用站点生成的警告。如果你不熟悉 @SuppressWarnings 语法,
请参阅 注解
Restrictions on Generics (对泛型的限制)
为了有效地使用 Java 泛型,必须考虑以下限制:
无法实例化具有基本类型的泛型类型。
无法创建类型参数的实例。
无法声明类型为类型参数的静态字段。
无法将 Casts instanceof 与参数化类型一起使用。
无法创建参数化类型的数组。
无法创建,捕获或抛出参数化类型的对象。
无法重载每个重载的形式参数类型都擦除为相同原始( raw )类型的方法。
Cannot Instantiate Generic Types with Primitive Types (无法实例化具有基
本类型的泛型类型)
考虑以下参数化类型:
创建对对象时,不能用基本类型替换类型参数 K V
 
Pair < int , char > p = new Pair <> ( 8 , 'a' ); // compile-time error
你只能将非基本类型替换为类型参数 K V
Pair < Integer , Character > p = new Pair <> ( 8 , 'a' );
请注意, Java 编译器自动将 8 装箱为 Integer.valueOf(8) ,将 'a' 装箱为 Character('a')
Pair < Integer , Character > p = new Pair <> ( Integer . valueOf ( 8 ), new
Character ( 'a' ));
有关自动装箱的更多信息,请参阅 数字和字符串 课程中的 自动装箱和拆箱
Cannot Create Instances of Type Parameters (无法创建类型参数的实例)
你不能创建类型参数的实例。例如,以下代码会导致编译时错误:
public static < E > void append ( List < E > list ) {
     E elem = new E (); // compile-time error
     list . add ( elem );
}  
解决方法是,可以通过反射创建类型参数的对象:
public static < E > void append ( List < E > list , Class < E > cls ) throws Exception {
E elem = cls . newInstance (); // OK
list . add ( elem );
}
你可以按以下方式调用 append 方法:
List < String > ls = new ArrayList <> ();
append ( ls , String . class );
Cannot Declare Static Fields Whose Types are Type Parameters (无法声明
类型为类型参数的静态字段)
类的静态字段是该类的所有非静态对象共享的类级别变量。因此,不允许使用类型参数的静态字段。考
虑以下类别:
public class MobileDevice < T > {
private static T os ;
// ...
}
如果允许使用类型参数的静态字段,那么以下代码将被混淆:
MobileDevice < Smartphone > phone = new MobileDevice <> ();
MobileDevice < Pager > pager = new MobileDevice <> ();
MobileDevice < TabletPC > pc = new MobileDevice <> ();
因为静态字段 os Smartphone Pager TabletPC 共享,所以 os 的实际类型是什么?它不能同时是
Smartphone Pager TabletPC 。因此,你无法创建类型参数的静态字段。
Cannot Use Casts or instanceof With Parameterized Types (无法将 Casts
instanceof 与参数化类型一起使用)
因为 Java 编译器会擦除通用代码中的所有类型参数,所以你无法验证在运行时使用的是通用类型的参数
化类型:
传递给 rtti 方法的参数化类型的集合是:
 
S = { ArrayList < Integer > , ArrayList < String > LinkedList < Character > , ... }
运行时不跟踪类型参数,因此无法区分 ArrayList ArrayList 之间的区别。你最多可以做的是使用
无界通配符来验证列表是否为 ArrayList
通常,除非使用不受限制的通配符对其进行参数化,否则无法将其转换为参数化类型。例如:
 
 
List < Integer > li = new ArrayList <> ();
但是,在某些情况下,编译器知道类型参数始终有效并允许强制转换。例如:
List < String > l1 = ...;
ArrayList < String > l2 = ( ArrayList < String > ) l1 ; // OK
Cannot Create Arrays of Parameterized Types (无法创建参数化类型的数
组)
你不能创建参数化类型的数组。例如,以下代码无法编译:
List < Integer > [] arrayOfLists = new List < Integer > [ 2 ]; // compile-time error
以下代码说明了将不同类型插入到数组中时发生的情况:
Object [] strings = new String [ 2 ];
strings [ 0 ] = "hi" ; // OK
strings [ 1 ] = 100 ; // An ArrayStoreException is thrown.
如果你对通用列表尝试相同的操作,则会出现问题:
如果允许参数化列表的数组,那么前面的代码将无法抛出所需的 ArrayStoreException
Cannot Create, Catch, or Throw Objects of Parameterized Types (无法创
建,捕获或抛出参数化类型的对象)
泛型类不能直接或间接扩展 Throwable 类。例如,以下类将无法编译:
方法无法捕获类型参数的实例:
但是,你可以在 throws 子句中使用类型参数:
Cannot Overload a Method Where the Formal Parameter Types of Each
Overload Erase to the Same Raw Type (无法重载每个重载的形式参数类型都
擦除为相同原始( raw )类型的方法)
一个类不能有两个重载的方法,这些方法在类型擦除后将具有相同的签名。
public class Example {
       public void print ( Set < String > strSet ) { }
       public void print ( Set < Integer > intSet ) { }
}
重载将共享相同的类文件表示形式,并且将生成编译时错误。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值