Java优先考虑泛型和泛型方法
1、优先考虑泛型
下面我们举个例子,将他作为泛型化的主要备选对象,换句话说,可以适当的强化这个类来利用泛型。
public class Stack {
private Object[] element;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 1;
public Stack() {
element = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
element[size++] = e;
}
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object result = element[--size];
element[size] = null;
return result;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (element.length == size) {
element = Arrays.copyOf(element, 2 * size + 1);
}
}
}
首先我们代码进行简单的转换,先用类型参数替换所有的Object
类型:
public class StackImprove1<E> {
private E[] element;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 1;
public StackImprove() {
//这里会爆编译错误
element=new E[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
element[size++] = e;
}
public E pop() {
if (size == 0) {
throw new EmptyStackException();
}
E result = element[--size];
element[size] = null;
return result;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (element.length == size) {
element = Arrays.copyOf(element, 2 * size + 1);
}
}
}
通常,你将至少得到一个错误或者警告,这个类也不例外。幸运的是,这个类只产生一个错误,如下:
Stack.java:8: generic array creation
elements = new E[DEFAULT_INITIAL_CAPACITY];
你不能创建不可具体化的(non-reifiable)类型的数组,如E。每当编写用数组支持的泛型时,都会出现这个问题。解决这个问题有两种方法。
第一种,直接绕过创建泛型数组的禁令:创建一个Object的数组,并将它转换成泛型数组类型
。现在错误是消除了,但是编译器会产生一条警告。这种用法是合法的,但(整体上而言)不是类型安全的:
Stack.java:8: warning: [unchecked] unchecked cast
found: Object[], required: E[]
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
^
编译器不可能证明你的程序是类型安全的,但是你可以证明。你自己必须确保未受检的转换不会危及到程序的类型安全性。问题中的数组(elements)被保存在一个私有的域中,永远不会被返回到客户端,或者传给任何其他方法。
- 这个数组中保存的唯一元素,是传给push方法的那些元素,它们的类型为E,因此未受检的转换不会有任何危害。
一旦你证明了未受检的转换是安全的,就要在尽可能小的范围中禁止警告(第27项)。在这种情况下,构造器只包含未受检的数组创建,因此可以在整个构造器中禁止这条警告。通过增加一条注解
来完成禁止,Stack能够正确无误地进行编译,你就可以使用它了,无需显示的转换,也无需担心会出现ClassCastException
异常:
// The elements array will contain only E instances from push(E).
// This is sufficient to ensure type safety, but the runtime
// type of the array won't be E[]; it will always be Object[]!
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
2)第二种方法:将elements
域的类型从E[ ]
改为Object[ ]
,之后把数组中获得的元素由Object转换为E
。
Stack.java:19: warning: [unchecked] unchecked cast
found: Object, required: E
E result = (E) elements[--size]; ^
- 由于E是一个不可具体化的类型,编译器无法在运行时检验转换。你还是可以自己证实未受检的转换是安全的,因此可以禁止该警告。
- 我们只要在包含未受检转换的赋值上禁止警告,而不是在整个pop方法上就可以了,如下:
下面我们对上面的代码进行第二种方式的改造:
// Appropriate suppression of unchecked warning
public E pop() {
if (size == 0)
throw new EmptyStackException();
// push requires elements to be of type E, so cast is correct
@SuppressWarnings("unchecked")
E result = (E) elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
全部代码
public class StackImprove2<E> {
private Object[] element;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 1;
public StackImprove2() {
element = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
element[size++] = e;
}
//Appropriate suppression of unchecked warning 在尽可能小的范围使用禁止警告
public E pop() {
if (size == 0) {
throw new EmptyStackException();
}
//push requires elements to be of type E,so cast is correct
@SuppressWarnings("unchecked") E result =
(E) element[--size];
element[size] = null;
return result;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (element.length == size) {
element = Arrays.copyOf(element, 2 * size + 1);
}
}
public static void main(String[] args) {
StackImprove2<String> stringStack=new StackImprove2<>();
List<String> stringList=new ArrayList<>();
stringList.add("cc");
stringList.add("dd");
for (String s : stringList) {
stringStack.push(s);
}
System.out.println(stringStack.element.length);
while (!stringStack.isEmpty()){
System.out.println(stringStack.pop().toUpperCase());
}
}
}
消除通用数组创建的这两种技术都有它们的支持者。
- 第一个更具有可读性:数组声明为E[]类型,清楚地表明它只包含E实例。它也更简洁:在典型的泛型类中,你可以在代码中的很多地方读取数组;第一种技术只需要一次转换(创建数组的位置)
- 第二种技术每次读取数组元素时都需要单独的转换。
因此,优选第一种,并且在实践中更常用第一种。但是,它会导致堆污染(heap pollution)(第32项):数组的运行时类型与其编译时的类型不匹配(除非E恰好是Object)。这使得一些程序猿非常不安,他们选择第二种技术,尽管在这种情况下堆污染是无害的。
下面的程序示范了泛型Stack类的使用。程序以相反的顺序打印出它的命令行参数,并转换成大写字母。如果要在从堆栈中弹出的元素上调用String的toUpperCase方法,并不需要显式的转换,并且会确保自动生成的转换会成功:
// Little program to exercise our generic Stack
public static void main(String[] args) {
Stack<String> stack = new Stack<>();
for (String arg : args)
stack.push(arg);
while (!stack.isEmpty())
System.out.println(stack.pop().toUpperCase());
}
上面的示例可能看起来与第28项相矛盾,第28项鼓励优先使用列表而不是数组。在泛型中使用列表并不总是可行或可取的。Java并不是生来就支持列表,因此有些泛型如ArrayList,则必须在数组上实现。为了提升性能,其他泛型如HashMap也在数组上实现。
绝大多数泛型与我们的Stack示例类似,因为它们的类型参数没有限制:你可以创建Stack、Stack<\int[]>、Stack<List>
,或者任何其他对象引用类型的Stack。
- 注意
不能创建基本类型的Stack
:企图创建Stack<\int>
或者Stack<\double>
会产生一个编译时错误。这是Java泛型系统根本的局限性。 - 你可以通过使用基本包装类型(boxed primitive type)来避开这条限制(第61项)。
总结
总而言之,使用泛型比使用需要在客户端代码中进行转换的类型来得更加安全
,也更加容易。在设计新类型的时候,要确保它们不需要这种转换就可以使用。这通常意味着要把类做成泛型。如果你现在有任何类型应该是通用的但却不是通用的,就把现有的类型都泛型化。这对于这些类型的新用户来说会变得更加轻松,又不会破坏现有的客户端(第26项)。
2、优先考虑泛型方法
静态工具方法尤其适合于泛型化。Collections中的所有“算法”方法(例如binarySearch和sort)都泛型化了。
编写泛型方法与编写泛型类型相类似。例如下面这个方法,它返回两个集合的联合:
// Use raw types - unacceptable! (Item 23)
public static Set union(Set s1 , Set s2) {
Set result = new HashSet(s1);
result.addAll(s2);
return result;
}
这个方法可以编译,但是有两条警告:
Union.java:5: warning : [unchecked] unchecked call to HashSet(Collection<? extends E>) as a member of raw type HashSet
Set result = new HashSet(s1);
Union.java:6: warning : [unchecked] unchecked call to addAll(Collection<? extends E>) as a member of raw type Set
result.addAll(s2);
为了修正这些警告,使方法变成是类型安全的,要将方法声明修改为声明一个类型参数,表示这三个集合的元素类型(两个参数和一个返回值),并在方法中使用类型参数。
在这个示例中,类型参数列表为,返回类型为Set。类型参数的命名惯例与泛型方法以及泛型的相同:
// Generic method
public static <E> Set<E> union(Set<E> s1 , Set<E> s2) {
Set<E> result = new HashSet<E>(s1);
result.addAll(s2);
return result;
}
至少对于见到那的泛型方法而言,就是这么回事了。现在该方法编译时不会产生任何警告,并提供了类型安全性,也更容易使用。以下是一个执行该方法的简单程序。程序中不包含转换,编译时不会有错误或者警告:
// Simple program to exercise generic method
public static void main(String[] args) {
Set<String> guys = new HashSet<String>(
Arrays.asList("Tom" , "Dick" , "Harry"));
Set<String> stooges = new HashSet<String>(
Arrays.asList("Larry" , "Moe" , "Curly"));
Set<String> aflCio = union(guys , stooges);
System.out.println(aflCio);
}
运行这段程序时,会打印出[Moe , Harry , Tom , Curly , Larry , Dick]
。元素的顺序是依赖于实现的。
union方法的局限性在于,三个集合的类型(两个输入参数和一个返回值)必须全部相同。利用有限制的通配符类型(bounded wildcard type),可以使这个方法变得更加灵活。
泛型方法的一个显著特性是,无需明确指定类型参数的值,不像调用泛型构造器的时候是必须指定的。编译器通过检查方法参数的类型来计算类型参数的值。
- 对于上述的程序而言,编译器发现union的两个参数都是
Set<String>
类型,因此知道类型参数E必须为String。这个过程称作类型推导(type inference)
。
可以利用泛型方法调用所提供的类型推导,使创建参数化类型实例的过程变得更加轻松。提醒一下:在调用泛型构造器的时候,要明确传递类型参数的值可能有点麻烦。类型参数出现在了变量声明的左右两边,显得有些冗余:
// Parameterized type instance creation with constructor
Map<String , List<String>> anagrams = new HashMap<String , List<String>>();
为了消除这种冗余,可以编写一个泛型静态工厂方法(generic static factory method)
,与想要使用的每个构造器相对应。例如,下面是一个与无参的HashMap构造器相对应的泛型静态工厂方法:
// Generic static factory
public static <K , V> HashMap<K , V> newHashMap() {
return new HashMap<K , V>();
}
通过这个泛型静态工厂方法,可以用下面这段简洁的代码来取代上面那个重复的声明:
// Parameterized type instance creation with static factory
Map<String , List<String>> anagrams = newHashMap();
相关的模式是泛型单例工厂(generic singleton factory)。有时,会需要创建不可变但又适合于许多不同类型的对象。由于泛型是通过擦除实现的,可以给所有必须的类型参数使用单个对象,但是需要编写一个静态工厂方法,重复的给每个必要的类型参数分发对象。这种模式最常用于函数对象,如Collections.reverseOrder,但也适用于像Collections.emptySet这样的集合。
假设有一个接口,描述了一个方法,该方法接受和返回某个类型T的值:
public interface UnaryFunction<T> {
T apply(T arg);
}
现在假设要提供一个恒等函数(identity function)。如果在每次需要的时候都重新创建一个,这样会很浪费,因为他是无状态的(stateless)。如果泛型被具体化了,每个类型都需要一个恒等函数,但是他们被擦除以后,就只需一个泛型单例。请看以下示例:
// Generic singleton factory pattern
private static UnaryFunction<Object> IDENTITY_FUNCTION = new UnaryFunction<Object>(){
public Object apply(Object arg) { return arg; }
};
// IDENTITY_FUNCTION is stateless and its type parameter is
// unbounded so it's safe to share one instance across all types.
@SuppressWarnings("unchecked")
public static <T> UnaryFunction<T> identityFunction() {
return (UnaryFunction<T>) IDENTITY_FUNCTION;
}
IDENTITY_FUNCTION
转换成(UnaryFunction<T>)
,产生了一条未受检的转换警告,因为UnaryFunction<Object>
对于每个T来说并非都是个UnaryFunction<T>
。但是恒等函数很特殊:它返回未被修改的参数,因此我们知道无论T的值是什么,用它作为UnaryFunction<T>
都是类型安全的。因此,我们可以放心的禁止由这个转换所产生的未受检转换警告。一旦禁止,代码在编译时就不会出现任何错误或者警告。
以下是一个范例程序,利用泛型单例作为UnaryFunction<String>
和UnaryFunction<Number>
。像往常一样,他不包含转换,编译时没有出现错误或者警告:
// Sample program to exercise generic singleton
public static void main(String[] args) {
String[] strings = {"jute" , "hemp" , "nylon"};
UnaryFunction<String> sameString = identityFunction();
for (String s : strings ) {
System.out.println(sameString.apply(s));
}
Number[] numbers = {1 , 2.0 , 3L};
UnaryFunction<Number> sameNumber = identityFunction();
for (Number n : numbers ) {
System.out.println(sameNumber.apply(n));
}
}
虽然相对少见,但是通过某个包含该类型参数本身的表达式来限制类型参数是允许的,这就是递归类型限制(recursive type bound)
。递归类型限制最普遍的用途与Comparable接口有关,它定义类型的自然顺序:
public interface Comparable<T> {
int compareTo(T o);
}
类型参数T定义的类型,可以与实现Comparable<T>
的类型的元素进行比较。实际上,几乎所有的类型都只能与他们自身的类型的元素相比较。因此,例如String
实现Comparable<String>,Integer
实现Comparable<Integer>
,等等。
有许多方法都带有一个实现Comparable
接口的元素列表,为了对列表进行排序,并在其中进行搜索,计算出它的最小值或者最大值,等等。要完成这其中的任何一项工作,要求列表中的每个元素要都能与列表中的每个其他元素相比较,换句话说,列表的元素可以互相比较(mutually comparable)
。下面是如何表达这种约束条件的一个示例:
// Using a recursive type bound to express mutual comparability
public static <T extends Comparable<T>> T max(List<T> list) {...}
类型限制<T extends Comparable<T>>
,可以读作“针对可以与自身进行比较的每个类型T”,这与互比性的概念或多或少有些一致。
下面的方法就带有上述声明。他根据元素的自然顺序计算列表的最大值,编译时没有出现错误或者警告:
// Returns the maximum value in a list - uses recursive type bound
public static <T extends Comparable<T>> T max(List<T> list) {
Iterator<T> i =list.iterator();
T result = i.next();
while (i.hasNext()) {
T t = i.next();
if (t.compareTo(result) > 0) result = t;
}
return result;
}
总结
总而言之,泛型方法就像泛型一样,使用起来比要求客户端转换输入参数并返回值的方法来得更加安全,也更加容易。就像类型一样,你应该确保新方法可以不用转换就能使用,这通常意味着要将它们泛型化。并且就像类型一样,还应该将现有的方法泛型化,使新用户使用起来更加轻松,且不会破坏现有的客户端。
参考
1、建议:优先考虑泛型方法。
2、Effective Java 优先考虑泛型
3、Effective Java笔记第四章泛型第四节优先考虑泛型