Java Generic
java泛型是java 1.5版本中增加的最重要特性,虽然在java语法本身上增加了非常高的复杂度,却为开发者带来极大的便利,尤其是在容器相关的使用中。它为代码提供了编译时检查,减少了手动的类型强转,在很大程度上,提高了代码的类型安全。由于java泛型是通过擦除来实现的,对于JVM而言,并不知道泛型的存在,也就是说java泛型实际上是一种java的语法糖,与java的内部类,受检异常,枚举,注解等一样。
Java Generic 相关的术语说明
- Type Parameter: 类型参数,泛型类、泛型构造器、泛型方法和泛型接口声明中的类型变量(
type variable),如Iterable<T>中的标识符T和List<E>中的标识符E,这个E即是类型参数(type parameter)。 - Type Argument: 类型参数,不同于
type parameter,在表达式中替换泛型类、泛型构造器、泛型方法和泛型接口中声明的type parameter,可以使用通配符(即?),也可是完全限定标识符,如List<?>和List<java.lang.String>。 - Type Variable: 类型变量,是在泛型类、泛型构造器、泛型方法和泛型接口中用作类型的
非限定标识符,如<E extends T>中的T,即为类型变量(type variable),类型变量有交集类型,如<T extends C & I>中的C & I,除了交集类型的类型变量,type variable必须是一个非限定标识符,类型变量即可以出现在泛型类和泛型接口的声明中,也可出现在表达式中。 - Generic Type: 泛型类型,是通过类型参数化的类或接口。
- Raw Type: 原生类型,将参数化类型擦除之后的类型,如将
List<String>的类型参数擦除之后,使用余下的原生类型List与遗留系统进行交互,非泛型类和非泛型接口则不是raw type,如String和int。 - Parameterized Type: 参数化类型,泛型类或者泛型接口的声明定义了一个参数化类型集,如泛型接口
List<E>的参数化类型List<String>,其中List是泛型名,而<String>是表示此泛型的特定参数化形式的类型变量列表。 - Generic Class: 泛型类,一个类声明的同时,也声明了一个或者多个类型变量,如
ArrayList<E>。 - Generic Interface: 泛型接口,与泛型类定义相似,如
Comparable<T>和Map<K, V>。 - Generic Method: 泛型方法,一个方法为自己引入了类型变量,这些类型变量也就是此方法的类型参数,静态方法和非静态方法都可以声明为泛型方法。
- Generic Constructor: 泛型构造器,与泛型方法类似,不管是否是在泛型类中,构造器可以引入自身的类型变量。
- Generic and Subtypes: 通过
extends和implements泛型类和泛型接口,创建子类型,如List<E> extends Collection<E>,只要不改变类型参数,继承关系就仍然存在,如List<String>就是Collection<String>的子类型,但是List<String>和List<Object>却是二个完全不同的类型,并不存在继承关系,除了共同的父类List<?>和Object。 - Bounded Type Parameters: 有界类型参数,限制类型参数在某个指定范围内,如类型参数
<E extends Number>,表示类型变量E必须是Number或者其子类型。 - Type Inference: 类型推断,JVM在编译期根据方法调用和其类型参数的声明,来推断传入的
type arguments是什么类型,从而使方法调用是正确的。 - Wildcards: 通配符,在泛型中使用问号
?来表示type argument,代表了某个未知类型,通配符不能用在泛型类和泛型接口的声明中。 - Upper Bounded Wildcards: 上界通配符,通过上界通配符放宽对类型参数的限制,如方法参数类型
List<? extends Number>,使得List<Integer>,List<Double>和List<Number>都可以符合方法的参数要求。 - Unbounded Wildcards: 无界通配符,如
List<?>,称之为未知类型的List(a list of unknown type)。 - Lower Bounded Wildcards: 下界通配符,指定类型参数为某个指定类型的父类,如
List<? super Integer>。 - Wildcards and Subtyping:
List<Integer>和List<Number>这2个参数化类型的除了泛型名都是List之外就并无关系,而带有通配符的参数化类型List<? extends Integer>则是List<? extends Number>的子类型。 - Wildcard Capture: 通配符捕获,在某些情况下,编译器会推断通配符的类型。 例如,当评估表达式
List<?>时,编译器会从代码中,为?推断出一个特定的类型,并在编译器内部以CAP#1或者capture#1这种形式标示通配符?,这种情况被称为通配符捕获。 - Heap Pollution: 堆污染,当
参数化类型的变量引用不是该参数类型的对象时,会发生堆污染,如在接受可变长的类型参数时就可能发生堆污染,void faultyMethod(List<String>... list),泛型数组可能发生Heap Pollution。 - Recursive Type Bound: 递归式类型绑定,类型参数中的类型变量以递归方式表示,如
java.lang.Enum类中的类型参数<E extends Enum<E>>和泛型方法java.util.Collections#min(Collection<? extends T>)中的类型参数<T extends Object & Comparable<? super T>>。 - Type Erasure: 类型擦除,Java通过
Erasure擦除来实现泛型,这个实现在Java界也是备受质疑,它提供了编译期的类型安全检查,避免了一些类型强转及导致的ClassCastException运行时异常,并且在运行期没有实际产生大量的泛型类,从而减少了运行时的开销。 - Reifiable Types: 可具化类型,因为泛型实现是通过类型擦除实现的,因此并非所有类型信息在运行时都可用,在运行时完全可用的类型称为可具化类型
reifiable type,如String、Integer、int、int[]和List<?>。 - non-Reifiable Types: 相对于
reifiable type而言,如List<? extends Number>和List<? extends C & T>就是不可具化类型。 - Covariant: 协变,指方法调用的协变返回,当子类重写父类的方法时,放宽了返回类型的限制,可以精细化返回结果,返回原来返回类型的子类型。泛型不是协变的,而数组却是协变的,
Integer[]是Number[],List<Integer>却不是List<Number>。 - PECS: 从
容器的角度来观察其中元素的操作,如果只是从容器中获取元素,容器此时就扮演了生产者Producer的角色,这时候就使用上界通配符<? extends T>;反之只是往容器里放元素进去,而不需要取出来,此时容器相当于一个消费者Consumer,就使用下界通配符<? super T>,为方便记忆,Josh Bloch将之简单归纳在一起作为一个助记符号,就是PECS,也有些人归纳为Get/Put Priciple。如果即要往容器里放元素,同要往外取元素,那就不要使用通配符<T>。
Java 泛型术语英文说明及示例
Type Variables
A type variable is an unqualified identifier used as a type in class, interface, method, and constructor bodies.
A type variable is introduced by the declaration of a type parameter of a generic class, interface, method, or constructor.
TypeParameter: {TypeParameterModifier} Identifier [TypeBound]TypeParameterModifier: AnnotationTypeBound: extends TypeVariable extends ClassOrInterfaceType {AdditionalBound}AdditionalBound: & InterfaceType |
The scope of a type variable declared as a type parameter is specified in Generic Classes and Type Parameters.
Every type variable declared as a type parameter has a bound. If no bound is declared for a type variable, Object is assumed. If a bound is declared, it consists of either:
- a single type variable
T, or - a class or interface type
Tpossibly followed by interface typesI1 & ... & In.
Generic Classes and Type Parameters
A class is generic if it declares one or more Type Variables.
These type variables are known as the type parameters of the class. The type parameter section follows the class name and is delimited by angle brackets(<>).
TypeParameters: < TypeParameterList >TypeParameterList: TypeParameter {, TypeParameter} |
In a class's type parameter section, a type variable T directly depends on a type variable S if S is the bound of T, while T depends on S if either T directly depends on S or T directly depends on a type variable U that depends on S (using this definition recursively). It is a compile-time error if a type variable in a class's type parameter section depends on itself.
A generic class declaration defines a set of parameterized types, one for each possible parameterization of the type parameter section by type arguments. All of these parameterized types share the same class at run time.
For instance, executing the code:
Vector<String> x = new Vector<String>();Vector<Integer> y = new Vector<Integer>();boolean b = x.getClass() == y.getClass(); |
will result in the variable b holding the value true.
It is a compile-time error if a generic class is a direct or indirect subclass of Throwable. This restriction is needed since the catch mechanism of the Java Virtual Machine works only with non-generic classes.
Type Parameter and Type Argument Terminology
Type Parameter and Type Argument Terminology: Many developers use the terms type parameter and type argument interchangeably, but these terms are not the same. When coding, one provides type arguments in order to create a parameterized type. Therefore, the T in Foo<T> is a type parameter and the String in Foo<String> f is a type argument.
Generic Type
A generic type is a generic class or interface that is parameterized over types, such as:
List<String>List<integer>Set<int[]>
Raw Types
A raw type is the name of a generic class or interface without any type arguments, such as:
ListList[]
To facilitate interfacing with non-generic legacy code, it is possible to use as a type the erasure of a parameterized type or the erasure of an array type whose element type is a parameterized type. Such a type is called a raw type.
More precisely, a raw type is defined to be one of:
- The reference type that is formed by taking the name of a generic type declaration without an accompanying type argument list.
- An array type whose element type is a raw type.
- A non-static member type of a raw type
Rthat is not inherited from a superclass or superinterface ofR.
A non-generic class or interface type is not a raw type.
Parameterized Types
A class or interface declaration that is generic defines a set of parameterized types, such as:
List<String>List<Integer>List<Long>
Generic Methods
A method is generic if it declares one or more type variables. These type variables are known as the type parameters of the method. The form of the type parameter section of a generic method is identical to the type parameter section of a generic class.
Generic methods are methods that introduce their own type parameters. This is similar to declaring a generic type, but the type parameter's scope is limited to the method where it is declared. Static and non-static generic methods are allowed, as well as generic class constructors.
The syntax for a generic method includes a list of type parameters, inside angle brackets, which appears before the method's return type. For static generic methods, the type parameter section must appear before the method's return type.
Generics, Inheritance, and Subtypes
As you already know, it is possible to assign an object of one type to an object of another type provided that the types are compatible. For example, you can assign an Integer to an Object, since Object is one of Integer's supertypes:
Object someObject = new Object();Integer someInteger = new Integer(10);someObject = someInteger; // OK |
In object-oriented terminology, this is called an is a relationship. Since an Integer is a kind of Object, the assignment is allowed. But Integer is also a kind of Number, so the following code is valid as well:
Box<Number> box = new Box<Number>();box.add(new Integer(10)); // OKbox.add(new Double(10.1)); // OK |
Note: Given two concrete types
AandB(for example,NumberandInteger),MyClass<A>has no relationship toMyClass<B>, regardless of whether or notAandBare related. The common parent ofMyClass<A>andMyClass<B>isObject.
For information on how to create a subtype-like relationship between two generic classes when the type parameters are related, see Wildcards and Subtyping.
Generic Classes and Subtyping
The subtype and supertype relations are binary relations on types.
Subtyping does not extend through parameterized types: T is subtype of S does not imply that C<T> is subtype of C<S>.
You can subtype a generic class or interface by extending or implementing it. The relationship between the type parameters of one class or interface and the type parameters of another are determined by the extends and implements clauses.
Using the Collections classes as an example, ArrayList<E> implements List<E>, and List<E> extends Collection<E>. So ArrayList<String> is a subtype of List<String>, which is a subtype of Collection<String>. So long as you do not vary the type argument, the subtyping relationship is preserved between the types.
Bounded Type Parameters
There may be times when you want to restrict the types that can be used as type arguments in a parameterized type. For example, a method that operates on numbers might only want to accept instances of Number or its subclasses. This is what bounded type parameters are for.
To declare a bounded type parameter, list the type parameter's name, followed by the extends keyword, followed by its upper bound, which in this example is Number. Note that, in this context, extends is used in a general sense to mean either "extends" (as in classes) or "implements" (as in interfaces): <N extends Number>
Multiple Bounds
The preceding example illustrates the use of a type parameter with a single bound, but a type parameter can have multiple bounds: <T extends B1 & B2 & B3>
Type Inference
Type inference is a Java compiler's ability to look at each method invocation and corresponding declaration to determine the type argument (or arguments) that make the invocation applicable. The inference algorithm determines the types of the arguments and, if available, the type that the result is being assigned, or returned. Finally, the inference algorithm tries to find the most specific type that works with all of the arguments.
To illustrate this last point, in the following example, inference determines that the second argument being passed to the pick method is of type Serializable:
static <T> T pick(T a1, T a2) { return a2; }Serializable s = pick("d", new ArrayList<String>()); |
Wildcards
In generic code, the question mark (?), called the wildcard, represents an unknown type. The wildcard can be used in a variety of situations: as the type of a parameter, field, or local variable; sometimes as a return type (though it is better programming practice to be more specific). The wildcard is never used as a type argument for a generic method invocation, a generic class instance creation, or a supertype.
The following sections discuss wildcards in more detail, including upper bounded wildcards, lower bounded wildcards, and wildcard capture.
Unbounded Wildcards
The unbounded wildcard type is specified using the wildcard character (?), for example, List<?>. This is called a list of unknown type. There are two scenarios where an unbounded wildcard is a useful approach:
- If you are writing a method that can be implemented using functionality provided in the
Objectclass. - When the code is using methods in the generic class that don't depend on the type parameter. For example,
List.sizeorList.clear. In fact,Class<?>is so often used because most of the methods inClass<T>do not depend onT.
Upper Bounded Wildcards
You can use an upper bounded wildcard to relax the restrictions on a variable. For example, say you want to write a method that works on List<Integer>, List<Double>, and List<Number>; you can achieve this by using an upper bounded wildcard.
To declare an upper-bounded wildcard, use the wildcard character (?), followed by the extends keyword, followed by its upper bound. Note that, in this context, extends is used in a general sense to mean either "extends" (as in classes) or "implements" (as in interfaces).
To write the method that works on lists of Number and the subtypes of Number, such as Integer, Double, and Float, you would specify List<? extends Number>. The term List<Number> is more restrictive than List<? extends Number> because the former matches a list of type Number only, whereas the latter matches a list of type Number or any of its subclasses.
Consider the following process method:
public static void process(List<? extends Foo> list) { /* ... */ } |
The upper bounded wildcard, <? extends Foo>, where Foo is any type, matches Foo and any subtype of Foo. The process method can access the list elements as type Foo:
public static void process(List<? extends Foo> list) { for (Foo elem : list) { // ... }} |
In the foreach clause, the elem variable iterates over each element in the list. Any method defined in the Foo class can now be used on elem.
List<? extends Shape> is an example of a bounded wildcard. The ? stands for an unknown type, just like the wildcards we saw earlier. However, in this case, we know that this unknown type is in fact a subtype of Shape. (Note: It could be Shape itself, or some subclass; it need not literally extend Shape.) We say that Shape is the upper bound of the wildcard.
Lower Bounded Wildcards
The Upper Bounded Wildcards section shows that an upper bounded wildcard restricts the unknown type to be a specific type or a subtype of that type and is represented using the extends keyword. In a similar way, a lower bounded wildcard restricts the unknown type to be a specific type or a super type of that type.
A lower bounded wildcard is expressed using the wildcard character (?), following by the super keyword, followed by its lower bound: <? super A>.
Note: You can specify an upper bound for a wildcard, or you can specify a lower bound, but you cannot specify both.
Wildcards and Subtyping
As described in Generics, Inheritance, and Subtypes, generic classes or interfaces are not related merely because there is a relationship between their types. However, you can use wildcards to create a relationship between generic classes or interfaces.
Given the following two regular (non-generic) classes:
class A { /* ... */ }class B extends A { /* ... */ } |
It would be reasonable to write the following code:
B b = new B();A a = b; |
This example shows that inheritance of regular classes follows this rule of subtyping: class B is a subtype of class A if B extends A. This rule does not apply to generic types:
List<B> lb = new ArrayList<>();List<A> la = lb; // compile-time error |
Although Integer is a subtype of Number, List<Integer> is not a subtype of List<Number> and, in fact, these two types are not related. The common parent of List<Number> and List<Integer> is List<?>.
In order to create a relationship between these classes so that the code can access Number's methods through List<Integer>'s elements, use an upper bounded wildcard:
List<? extends Integer> intList = new ArrayList<>();List<? extends Number> numList = intList; // OK. List<? extends Integer> is a subtype of List<? extends Number> |
Because Integer is a subtype ofNumber, and numList is a list of Number objects, a relationship now exists between intList (a list of Integer objects) andnumList. The following diagram shows the relationships between several List classes declared with both upper and lower bounded wildcards.
Wildcard Capture and Helper Methods
In some cases, the compiler infers the type of a wildcard. For example, a list may be defined as List<?> but, when evaluating an expression, the compiler infers a particular type from the code. This scenario is known as wildcard capture.
For the most part, you don't need to worry about wildcard capture, except when you see an error message that contains the phrase "capture of".
The WildcardError example produces a capture error when compiled:
import java.util.List;public class WildcardError { void foo(List<?> i) { i.set(0, i.get(0)); }} |
In this example, the compiler processes the i input parameter as being of type Object. When the foo method invokes List.set(int, E), the compiler is not able to confirm the type of object that is being inserted into the list, and an error is produced. When this type of error occurs it typically means that the compiler believes that you are assigning the wrong type to a variable. Generics were added to the Java language for this reason — to enforce type safety at compile time.
The WildcardError example generates the following error when compiled by Oracle's JDK 8 javac implementation:
javac WildcardError.javaWildcardError.java:6: error: incompatible types: Object cannot be converted to CAP#1 i.set(0, i.get(0)); ^ where CAP#1 is a fresh type-variable: CAP#1 extends Object from capture of ?Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output1 error |
javac -Xdiags:verbose WildcardError.javaWildcardError.java:11: error: class, interface, or enum expectedjavac WildcardError.java^WildcardError.java:12: error: illegal character: '#'WildcardError.java:6: error: incompatible types: Object cannot be converted to CAP#1 ^WildcardError.java:14: error: class, interface, or enum expected ^WildcardError.java:15: error: illegal character: '#' where CAP#1 is a fresh type-variable: ^WildcardError.java:16: error: illegal character: '#' CAP#1 extends Object from capture of ? ^WildcardError.java:17: error: class, interface, or enum expectedNote: Some messages have been simplified; recompile with -Xdiags:verbose to get full output ^6 errors |
The method
set(int, capture#1-of ?)in the typeList<capture#1-of ?>is not applicable for the arguments(int, capture#2-of ?).
capture#1-of ?表示什么?当编译器遇到一个在其类型中带有通配符的变量,比如rebox()的box参数,它认识到必然有一些T,对这些T而言box是Box<T>。它不知道T代表什么类型,但它可以为该类型创建一个占位符来指代T的类型。占位符被称为这个特殊通配符的捕获(capture)。这种情况下,编译器将名称capture#1-of ?以box类型分配给通配符。每个变量声明中每出现1个通配符都将获得1个不同的捕获,因此在泛型声明foo(Pair<?, ?> x, Pair<?, ?> y)中,编译器将给这4个通配符?的捕获分配4个不同的名称,因为任意未知的类型参数之间没有关系。错误消息告诉我们不能调用
put(),因为它不能检验put()的实参类型与其形参类型是否兼容,因为形参的类型是未知的。在这种情况下,由于?实际表示? extends Object,编译器已经推断出box.get()的类型是Object,而不是capture#1-of ?。它不能静态地检验对由占位符capture#1-of ?所识别的类型而言,Object是否是一个可接受的值。
In this example, the code is attempting to perform a safe operation, so how can you work around the compiler error? You can fix it by writing a private helper method which captures the wildcard. In this case, you can work around the problem by creating the private helper method, fooHelper, as shown in WildcardFixed:
public class WildcardFixed { void foo(List<?> i) { fooHelper(i); } /** * Helper method created so that the wildcard can be captured through type inference. */ private <T> void fooHelper(List<T> l) { l.set(0, l.get(0)); }} |
Thanks to the helper method, the compiler uses inference to determine that T is CAP#1, the capture variable, in the invocation. The example now compiles successfully.
By convention, helper methods are generally named originalMethodNameHelper.
Type Erasure
Generics were introduced to the Java language to provide tighter type checks at compile time and to support generic programming. To implement generics, the Java compiler applies type erasure to:
- Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded. The produced bytecode, therefore, contains only ordinary classes, interfaces, and methods.
- Insert type casts if necessary to preserve type safety.
- Generate bridge methods to preserve polymorphism in extended generic types.
Type erasure ensures that no new classes are created for parameterized types; consequently, generics incur no runtime overhead.
Reifiable Types
Because some type information is erased during compilation, not all types are available at run time. Types that are completely available at run time are known as reifiable types.
A type is reifiable if and only if one of the following holds:
- It refers to a
non-genericclass or interface type declaration. - It is a parameterized type in which all type arguments are
unbounded wildcards. - It is a
raw type. - It is a
primitive type. - It is an array type whose element type is reifiable.
- It is a nested type where, for each type
Tseparated by a.,Titself is reifiable.
For example, if a generic class X<T> has a generic member class Y<N>, then the type X<?>.Y<?> is reifiable because X<?> is reifiable and Y<?> is reifiable. The type X<?>.Y<Object> is not reifiable because Y<Object> is not reifiable.
An intersection type is not reifiable, such as <T extends C & I>.
Mutually Recursive Type Variable Bounds
interface ConvertibleTo<T> { T convert();}public class ReprChange<T extends ConvertibleTo<S>, S extends ConvertibleTo<T>> { T t; void set(S s) { t = s.convert(); } S get() { return t.convert(); }} |
Nested Generic Classes
class Seq<T> { T head; Seq<T> tail; Seq() { this(null, null); } Seq(T head, Seq<T> tail) { this.head = head; this.tail = tail; } boolean isEmpty() { return tail == null; } class Zipper<S> { Seq<Pair<T,S>> zip(Seq<S> that) { if (isEmpty() || that.isEmpty()) { return new Seq<Pair<T,S>>(); } else { Seq<T>.Zipper<S> tailZipper = tail.new Zipper<S>(); return new Seq<Pair<T,S>>( new Pair<T,S>(head, that.head), tailZipper.zip(that.tail)); } } }}class Pair<T, S> { T fst; S snd; Pair(T f, S s) { fst = f; snd = s; }}public class TestNestedGenericClasses { public static void main(String[] args) { Seq<String> strs = new Seq<String>( "a", new Seq<String>("b", new Seq<String>())); Seq<Number> nums = new Seq<Number>( new Integer(1), new Seq<Number>(new Double(1.5), new Seq<Number>())); Seq<String>.Zipper<Number> zipper = strs.new Zipper<Number>(); Seq<Pair<String,Number>> combined = zipper.zip(nums); }} |
Covariant in Overriding
If a method declaration d1 with return type R1 overrides or hides the declaration of another method d2 with return type R2, then d1 must be return-type-substitutable for d2, or a compile-time error occurs.
This rule allows for covariant return types - refining the return type of a method when overriding it.
Covariant Return Types
The following declarations are legal in the Java programming language from Java SE 5.0 onwards:
class C implements Cloneable { C copy() throws CloneNotSupportedException { return (C)clone(); }}public class D extends C implements Cloneable { D copy() throws CloneNotSupportedException { return (D)clone(); }} |
Class Instance Creation Expressions
15.9. Class Instance Creation Expressions
A class instance creation expression is used to create new objects that are instances of classes.
ClassInstanceCreationExpression: UnqualifiedClassInstanceCreationExpression ExpressionName . UnqualifiedClassInstanceCreationExpression Primary . UnqualifiedClassInstanceCreationExpressionUnqualifiedClassInstanceCreationExpression: new [TypeArguments] ClassOrInterfaceTypeToInstantiate ( [ArgumentList] ) [ClassBody]ClassOrInterfaceTypeToInstantiate: {Annotation} Identifier {. {Annotation} Identifier} [TypeArgumentsOrDiamond]TypeArgumentsOrDiamond: TypeArguments <> |
The following production is shown here for convenience:
ArgumentList: Expression {, Expression} |
A class instance creation expression specifies a class to be instantiated, possibly followed by type arguments or a diamond (<>) if the class being instantiated is generic, followed by (a possibly empty) list of actual value arguments to the constructor.
If the type argument list to the class is empty — the diamond form <> — the type arguments of the class are inferred. It is legal, though strongly discouraged as a matter of style, to have white space between the < and > of a diamond.
If the constructor is generic, the type arguments to the constructor may similarly either be inferred or passed explicitly. If passed explicitly, the type arguments to the constructor immediately follow the keyword new.
It is a compile-time error if a class instance creation expression provides type arguments to a constructor but uses the diamond form for type arguments to the class.
This rule is introduced because inference of a generic class's type arguments may influence the constraints on a generic constructor's type arguments.
If TypeArguments is present immediately after new, or immediately before (, then it is a compile-time error if any of the type arguments are wildcards.
Java Generic and Exception
利用泛型擦除特性,骗过编译器,使受检异常抛出却不会编译报错。
public class SneakyThrowable { /** * Sneaky throw any type of Throwable. */ static void sneakyThrow(Throwable throwable) { // 利用java泛型擦除 // 使用运行时异常来欺骗编译器 SneakyThrowable.<RuntimeException>sneakyThrow0(throwable); } /** * Sneaky throw any type of Throwable. */ ("unchecked") static <E extends Throwable> void sneakyThrow0(Throwable throwable) throws E { // JVM 中并没有受检异常 // 受检异常只对编译器有用 throw (E) throwable; } public static void main(String[] args) { // Compiler doesn't see that checked Exception is being thrown sneakyThrow(new Exception()); }} |
这里定义了一个泛型声明为<E extends Throwable>方法,在内部将传入的Throwable强转为E之后再抛出,E的具体类型取决于调用这个方法时指定的类型参数。在这里,只要将类型参数指定为RuntimeException,然后不管传入一个什么异常,都可以直接抛出去,而不用throws声明。
由于类型擦除的存在,sneakyThrow0在被调用的时候,E在运行时实际上是擦除为Throwable类型,从IOException转成Throwable一点问题都不会有。因此基于类型擦除的泛型和受检异常的设计实际上是冲突的。
Heap Pollution
在接受可变长的类型参数(T... args)和使用泛型数组T[]时就可能发生堆污染,并且为了类型安全,Java 中不能直接创建一个泛型数组,如下示例:
/** * warning: [varargs] Possible heap pollution from parameterized vararg type */public static void faultyMethod(List<String>... lists) { Object[] objectArray = lists; // Valid // List[] objectArray = lists; // Valid objectArray[0] = Arrays.asList(42); String s = lists[0].get(0); // ClassCastException thrown here} |
使用原生的集合类也可能产生堆污染:
public void rawTypeHeapPollution() { List<String> list1 = new ArrayList<>(); list1.add("a"); List list2 = list1; list2.add(0, 1); // Unchecked call to add(int, E) as a member of raw type of List System.out.println(list1.size()); // 2 System.out.println(list2.size()); // 2 System.out.println(list1.get(1)); // a System.out.println(list1.get(0)); // java.lang.ClassCastException:} |
Generic Array Creation
一般很少会使用到泛型数组T[],主要是可以用List<T>替代,并且能更好的提供类型安全,如果想使用以下方式直接创建泛型数组会抛出编译错误:
compile error example 1
List<String>[] listArray = new ArrayList<String>[]{}; // compile error: generic array creation |
compile error example 2
public class GenericArrayError<E> { private E[] a; public GenericArrayError(Class<E> c, int s) { // Type parameter 'E' can not initialized directly this.a = new E[]{}; // compile error: generic array creation }} |
编译以上代码,加入编译参数-g和-Xlint:unchecked,查看编译警告信息:
javac -g -Xlint:unchecked GenericArrayError.javacom/example/test/lang/generic/GenericArrayError.java:12: error: generic array creation this.a = new E[]{}; // error: generic array creation ^1 error |
为了创建泛型数组,需要用一些特别的方式来创建,如下代码所示,更详细的说明可参考stackoverflow的这个问题。
import java.lang.reflect.Array;import java.util.function.Supplier;/** * T[] array = (T[])Array.newInstance(cls, d1); * T[][] array = (T[][])Array.newInstance(cls, d1, d2); * T[][][] array = (T[][][])Array.newInstance(cls, d1, d2, d3); * T[][][][] array = (T[][][][])Array.newInstance(cls, d1, d2, d3, d4); * T[][][][][] array = (T[][][][][])Array.newInstance(cls, d1, d2, d3, d4, d5); * * @author yuweijun 2018-03-23. */public class GenericArrayByArrayNewInstance<E> { private E[] a; public GenericArrayByArrayNewInstance(Class<E> c, int s) { // found: Object // where T is a type-variable: // T extends Object declared in method <T>build(Class<T>) ("unchecked") final E[] a = (E[]) Array.newInstance(c, s); this.a = a; // Type parameter 'E' can not initialized directly // this.a = new E[]{}; // error: generic array creation } E get(int i) { return a[i]; }}/** * {@link java.util.Collections#zeroLengthArray(Class)} */class ZeroLengthGenericArray { static <T> T[] build(Class<T> type) { return (T[]) Array.newInstance(type, 0); } public static void main(String[] args) { System.out.println(ZeroLengthGenericArray.build(String.class).length); }}/** * Unchecked: weak typing. No type checking is actually done on any of the objects passed as argument. * * @param <E> */class UncheckedGenericeArray<E> { private Object[] a; public UncheckedGenericeArray(int s) { a = new Object[s]; } E get(int i) { ("unchecked") final E e = (E) a[i]; return e; }}/** * This is one of the suggested ways of implementing a generic collection in Effective Java; Item 26. */class Stack<E> { private E[] elements; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() { ("unchecked") final E[] elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; this.elements = elements; }}/** * Here's how to use generics to get an array of precisely the type you’re looking for while preserving type safety * (as opposed to the other answers, which will either give you back an Object array or result in warnings at compile time) * * @param <E> */class CreateGenericArrayUsingClassLiteral<E> { private E[] a; public CreateGenericArrayUsingClassLiteral(Class<E[]> clazz, int length) { a = clazz.cast(Array.newInstance(clazz.getComponentType(), length)); } public static <T> T[] newArray(Class<T[]> type, int size) { return type.cast(Array.newInstance(type.getComponentType(), size)); } public static void main(String[] args) { CreateGenericArrayUsingClassLiteral<String> cs = new CreateGenericArrayUsingClassLiteral<>(String[].class, 16); System.out.println(cs.a.length); String[] ns = CreateGenericArrayUsingClassLiteral.newArray(String[].class, 16); System.out.println(ns.length); }}/** * In Java 8, we can do a kind of generic array creation using a lambda or method reference. * This is similar to the reflective approach (which passes a Class), but here we aren't using reflection. * <p> * For example, this is used by <A> A[] Stream.toArray(IntFunction<A[]>). */class CreateGenericArrayUsingSupplier<E> { interface ArraySupplier<E> { E[] get(int length); } private E[] array; public CreateGenericArrayUsingSupplier(ArraySupplier<E> supplier) { this.array = supplier.get(16); } public CreateGenericArrayUsingSupplier(Supplier<E[]> supplier) { this.array = supplier.get(); } public static void main(String[] args) { CreateGenericArrayUsingSupplier<String> ss = new CreateGenericArrayUsingSupplier<>(() -> new String[16]); System.out.println(ss.array.length); CreateGenericArrayUsingSupplier<Double> ds = new CreateGenericArrayUsingSupplier<>(Double[]::new); System.out.println(ds.array.length); }} |
PECS 原则
这个原则主要就是定义了 Java 泛型中的通配符?的使用场景:
- 如果只是从容器里获取元素,可以使用上界通配符
<? extends T>,其缺点就是除了null之外,不能放入其他新元素,包括T本身。 - 如果只是放元素进容器,则可以使用下界通配符
<? super T>,其缺点是取出来时都是Object,失去类型信息。 - 如果即需要读取,又需要放入元素,则不要使用通配符
<T>。
通配符的协变性
- 通配符
<?>和<? extends Object>能匹配所有类型。 - 上界通配符
<? extends T>可以匹配所有T的子类型。 - 下界通配符
<? super T>可以匹配所有T的父类型。
public class GenericCoVariance { static void f1(List<Fruit> a) { } static void f2(List<? extends Fruit> list) { } static void f3(List<?> a, List<? extends Object> b) { } static void f4(List<? super Apple> a) { } static void f5(List<Object> c){ } static void f6(List w) { } public static void main(String[] args) { // f1(new ArrayList<Apple>()); // compile error f2(new ArrayList<Apple>()); f3(new ArrayList<Integer>(), new ArrayList<String>()); f4(new ArrayList<Fruit>()); // f5(new ArrayList<Fruit>()); // compile error f6(new ArrayList<Fruit>()); }}class Fruit {}class Apple extends Fruit {} |
References
- The Java™ Tutorials - Generics
- The Java™ Tutorials - Generics extra tutorial
- 8.1.2. Generic Classes and Type Parameters
- 4.4. Type Variables
- 9.1.2. Generic Interfaces and Type Parameters
- 15.9. Class Instance Creation Expressions
- java泛型相关知识点
- 绕过 Java 编译器检查,在任何地方抛出受检异常
- How to create a generic array in Java
- What is PECS