Effective Java学习笔记(五)

[color=blue][size=large][b]第五章:类和接口(条目23-29)[/b][/size][/color]
在JDK1.5中增加了泛型(Generic)。没有泛型之前,从集合中读取到的每个对象都必须进行转换。如果有人不小心插入了类型错误的对象,在运行时的转换处理就会出错。有了泛型后,可以告诉编译器每个集合中接受哪些对象类型。

[b][size=medium][color=red]第23条:请不要在新代码中使用原生态类型[/color][/size][/b]
先来介绍一些术语。声明中具有一个或者多个类型参数(type parameter)的类或者接口,就是泛型类或者接口。泛型类和接口统称为泛型。每个泛型都对应一个原生态类型(raw type),即不带任何实际参数的泛型名称。例如,与List<E>相对应的原生态类型是List,原生态类型与Java平台没有泛型之前的接口类型List完全一样。
如果使用原生态类型,就失掉了泛型在安全性和表述性方面的优势。使用原生态类型的唯一情况就是保证兼容性。下面给出例子代码,来理解泛型的设计:

/**
* 条目23:请不要在代码中使用原生态类型-测试代码
* @author chasegalaxy
* @since 2012-8-15
*/
public class Main {
public static void main(String[] args) {
List<Double> listDouble = new ArrayList<Double>(Arrays.asList(1.1, 1d));
List<Integer> listInteger = new ArrayList<Integer>(Arrays.asList(1, 2));

print1(listDouble);
print1(listInteger);
print2(listDouble);
print2(listInteger);
print3(listDouble);
print3(listInteger);
print4(listDouble);
}

// Use of raw type for unknown element type - don't do this!
static void print1(List list) {
for (Object obj: list) {
System.out.print(obj + ",");
}
System.out.println();
}
// Unbounded wildcard type(无限通配符类型) - typesafe and flexible
static void print2(List<?> list) {
for (Object obj: list) {
System.out.print(obj + ",");
}
System.out.println();
}
// bounded wildcard type 有限通配符类型
static void print3(List<? extends Number> list) {
for (Number num: list) {
System.out.print(num + ",");
}
System.out.println();
}
// 具体类型
static void print4(List<Double> list) {
for (Double num: list) {
System.out.print(num + ",");
}
System.out.println();
}

// Use of raw type for unknown element type - don't do this!
static int numElementsInCommonA(Set s1, Set s2) {
int result = 0;
for (Object obj: s1) {
if (s2.contains(obj)) {
result++;
}
}
return result;
}
// Unbounded wildcard type - typesafe and flexible
static int numElementsInCommonB(Set<?> s1, Set<?> s2) {
int result = 0;
for (Object obj: s1) {
if (s2.contains(obj)) {
result++;
}
}
return result;
}
}


[b][size=medium][color=red]第24条:消除非受检警告[/color][/size][/b]
用泛型编程时,会遇到许多编译器警告:非受检强制转化警告(unchecked cast warnings)、非受检方法调用警告、非受检普通数组创建警告,以及非受检转换警告(unchecked conversion warnings)。当你越来越熟悉泛型之后,遇到的警告也会越来越少。要尽可能地消除每一个非受检警告。如果消除了所有警告,就可以确保代码是类型安全的,这是一件很好地事情。这意味着不会在运行时出现ClassCastException,你回更加自信自己的程序可以实现预期的功能。
如果无法消除警告,同时可以证明引起警告的代码是类型安全的,(只有在这种情况下才)可以用一个@SuppressWarnings("unchecked")注解来禁止这条警告(粒度级别请尽可能小)。如果在禁止警告之前没有先证实代码是类型安全的,那就只是给你自己一种错误的安全感而已。
每当使用@SuppressWarnings("unchecked")注解时,请添加一条注释,说明为什么这么做是安全的。总而言之,非受检警告很重要,不要忽略它们,要尽最大努力来消除这些警告。

[b][size=medium][color=red]第25条:列表优先于数组[/color][/size][/b]
数组与泛型的一个区别是:数组是协变的(covariant),如果Sub为Super的子类型,那么数组类型Sub[]就是Super[]的子类型。泛型是不可变的(invariant),对于任意两个不同的类型Type1和Type2,List<Type1>既不是<Type2>的子类型,也不是List<Type2>的超类型。你可能认为,这意味着泛型是有缺陷的,但实际上可以说数组才是有缺陷的。请看下面测试代码:

public static void main(String[] args) {
// Fails at runtime!
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in"; // Throws ArrayStoreException

// Won't compile!
List<Object> objectList = new ArrayList<Long>(); // Incompatible types
objectList.add("I don't fit in");
}

数组与泛型的第二个区别是:数组是具体化的(reified),因此数组会在运行时才知道并检查元素类型,如上面代码,如果企图将String保存到Long数组中,就会得到一个ArrayStoreException。相比之下,泛型是通过擦除(erasure)来实现的,因此泛型只在编译时强化它们的类型信息,并在运行时丢弃(或者擦除)它们的元素类型信息。擦除就是使泛型可以与没有使用泛型的代码随意进行互用。
由于上面的这两个区别,数组和泛型不能很好地混合使用。比如,创建泛型、参数化类型或者类型参数的数组是非法的:new List<E>[]、new List<String>[]、new E[],它们都会得到一个Cannot create a generic array的错误提示。为什么创建泛型数组是非法的?因为它不是类型安全的。
从技术角度来说,像E、List<E>、List<String>这样的类型应称作不可具体化的(non-reifiable)类型。唯一可具体化的(reifiable)参数化类型是无限制的通配符类型,如List<?>和Map<?, ?>,虽然不常用,但创建无限制通配符类型的数组是合法的。
当你得到泛型数组创建错误时,最好的解决办法通常是优先使用集合类型List<E>,而不是数组类型E[]。这样可能会损失一些性能或者简洁性,但是换回的却是更高的类型安全性和互用性。请看下面的一个例子来理解该条目:

/**
* 条目25:列表优先于数组-测试代码
* @author chasegalaxy
* @since 2012-8-15
*/
public class Rule25Main {
public static void main(String[] args) {
List list1 = new ArrayList(Arrays.asList(2, 3));
Object obj1 = reduce1(list1, new FunctionImpl(), 1);
System.out.println(obj1);

List list2 = new ArrayList(Arrays.asList("2", "3"));
Object obj2 = reduce2(list2, new FunctionImpl(), "1");
System.out.println(obj2);

List<String> list3 = new ArrayList<String>(Arrays.asList("2", "3"));
String obj3 = reduce3(list3, new FunctionImplEx<String>(), "1");
System.out.println(obj3);
}

// Reduction without generics, and with concurrency flaw!
static Object reduce1(List list, Function f, Object initVal) {
synchronized (list) {
Object result = initVal;
for (Object obj: list) {
result = f.apply(result, obj);
}
return result;
}
}

// Reduction without generics or concurrency flaw
static Object reduce2(List list, Function f, Object initVal) {
Object[] snapshot = list.toArray(); // Locks list internally
Object result = initVal;
for (Object el: snapshot) {
result = f.apply(result, el);
}
return result;
}

// List-based generic reduction
static <E> E reduce3(List<E> list, FunctionEx<E> f, E initVal) {
List<E> snapshot; // 这里使用泛型列表代替数组
synchronized (list) {
snapshot = new ArrayList<E>(list);
}
E result = initVal;
for (E e: snapshot) {
result = f.apply(result, e);
}
return result;
}
}

interface Function {
Object apply(Object obj1, Object obj2);
}

interface FunctionEx<E> {
E apply(E obj1, E obj2);
}

class FunctionImpl implements Function {
@Override
public Object apply(Object obj1, Object obj2) {
if (obj1 instanceof Integer && obj2 instanceof Integer) {
return Integer.parseInt(obj1.toString()) + Integer.parseInt(obj2.toString());
}
if (obj1 instanceof Double && obj2 instanceof Double) {
return Double.parseDouble(obj1.toString()) + Double.parseDouble(obj2.toString());
}
if (obj1 instanceof String && obj2 instanceof String) {
return obj1.toString().concat(obj2.toString());
}
return -1;
}
}

class FunctionImplEx<E> implements FunctionEx<E> {
@Override
public E apply(E obj1, E obj2) {
if (obj1 instanceof Integer && obj2 instanceof Integer) {
return (E) String.valueOf(Integer.parseInt(obj1.toString())
+ Integer.parseInt(obj2.toString()));
}
if (obj1 instanceof Double && obj2 instanceof Double) {
return (E) String.valueOf(Double.parseDouble(obj1.toString())
+ Double.parseDouble(obj2.toString()));
}
if (obj1 instanceof String && obj2 instanceof String) {
return (E) obj1.toString().concat(obj2.toString());
}
return (E) String.valueOf(-1);
}
}

总而言之,数组和泛型有着非常不同的类型规则。数组是协变且可以具体化的;泛型是不可变的且可以被擦除的。因此,数组提供了运行时的类型安全,但是没有编译时的类型安全。一般来说,数组和泛型不能很好地混合使用。如果你发现自己将它们混合起来使用,并且得到了编译时错误或者警告,你的第一反应就应该是用列表代替数组。

[b][size=medium][color=red]第26条:优先考虑泛型[/color][/size][/b]
一般来说,将集合声明参数化,以及使用JDK所提供的泛型和泛型方法,这些都不太困难。编写自己的泛型会比较困难一些,但是值得花些时间去学习如何编写。
在第二章时,给出了非泛型的Stack实现,现在优先考虑泛型,利用泛型实现这个类:

/**
* 泛型堆栈实现
* elements可设置成两种:Object[] / E[],选择用哪种方式来处理,则看个人的偏好了。
*/
public class Stack<E> {
private static final int DEFAULT_INITIAL_CAPACITY = 16;
// private E[] elements;
private Object[] elements;
private int size = 0;

/**
* 构造函数
* The elements array will contain only E instances from push(E).
* This is sufficient to ensure ty pe 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];
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}

public void push(E e) {
ensureCapacity();
elements[size++] = e;
}

public E pop() {
if (size == 0) {
throw new EmptyStackException();
}
@SuppressWarnings("unchecked")
E e = (E) elements[--size];
elements[size] = null;
return e;
}

public boolean isEmpty() {
return 0 == size;
}

/**
* Ensure space for at least one more element, roughly
* doubling the capacity each time the array needs to grow.
*/
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}

上面的代码中,第一种elements是类型E[],第二种是类型Object[]。第一种只需Object[]转E[]转换一次,而第二种需要多次转为E,所以第一种方式在这里会更常用。下面的代码示范了泛型Stack类的使用。

public class StackMain {
private static final String[] strs = { "a", "c", "b" };

public static void main(String[] args) {
Stack<String> stack = new Stack<String>();
for (String str: strs) {
stack.push(str);
}
while (!stack.isEmpty()) {
System.out.println(stack.pop().toUpperCase());
}
// The result is:
// B
// C
// A
}
}

看来上述实例和25条:列表优先于数组相矛盾了。实际上并不可能总是想着在泛型中使用列表。Java并不是生来就支持列表,因此有些泛型如ArrayList则必须在数组上实现。为了提升性能,其他泛型如HashMap也在数组上实现。
绝大多数泛型就像上面Stack示例一样,因为它们的类型参数没有限制:你可以创建Stack<Object>、Stack<int[]>、Stack<List<String>>,或者其他任何引用类型的Stack。注意不能创建基本类型(如int,double)的Stack,这是Java泛型系统根本的局限性,你可以通过基本包装类型(boxed primitive type)来避开这条限制。
总而言之,使用泛型比使用需要在客户端代码中进行转换的类型来得更加安全,也更加容易。这通常意味着在设计新类型的时候,最好把它设计成泛型的。只要时间允许,就把它们都泛型化。

[b][size=medium][color=red]第27条:优先考虑泛型方法[/color][/size][/b]
就如类可以从泛型中受益一般,方法也一样。静态工具方法尤其适合于泛型化。Collections中的所有“算法”方法(例如binarySearch和sort)都泛型化了。请看下面一个例子:

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"));
System.out.println(union1(guys, stooges));
System.out.println(union2(guys, stooges));
// the result is:
// [Moe, Harry, Tom, Curly, Larry, Dick]
// [Moe, Harry, Tom, Curly, Larry, Dick]
}

// Use raw types - unacceptable!
public static Set union1(Set set1, Set set2) {
Set result = new HashSet(set1);
result.addAll(set2);
return result;
}

// Generic method
public static <E> Set<E> union2(Set<E> set1, Set<E> set2) {
Set<E> result = new HashSet<E>(set1);
result.addAll(set2);
return result;
}

以上union方法的局限性在于,三个集合的类型(两个输入参数和一个返回值)必须全部相同。利用有限制的通配符类型(bounded wildcard type),可以使这个方法变得更加灵活。
泛型方法的一个显著特点是,无需明确指定类型参数的值,不像调用泛型构造器的时候是必须指定的。编译器通过检查方法参数的类型来计算类型参数的值。对于上述的程序而言,编译器发现union的两个参数都是Set<String>类型,因此知道类型参数E必须为String。这个过程称作类型推导(type inference)。请再看下面一个例子:

/**
* 通过包含该类型参数本身的表达式来限制类型参数-递归类型限制(recursive type bound)。
* @author chasegalaxy
* @since 2012-8-15
*/
public class ExampleMain {
// Generic singleton factory pattern
private static UnaryFunction<Object> IDENTITY_FUNCTION = new UnaryFunction<Object>() {
@Override
public Object apply(Object arg) {
return arg;
}
};

// IDENTITY_FUNCTION is stateless and its type parameter si
// 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;
}

// Sample program to exercise generic sigleton
public static void main(String[] args) {
String[] strings = { "jute", "hemp", "nylon" };
UnaryFunction<String> sameString = identityFunction();
for (String s: strings) {
System.out.print(sameString.apply(s) + ",");
}
System.out.println();

Number[] numbers = { 1, 2.0, 3L };
UnaryFunction<Number> sameNumber = identityFunction();
for (Number n: numbers) {
System.out.print(sameNumber.apply(n) + ",");
}
// The result is:
// jute,hemp,nylon,
// 1,2.0,3,
}
}

上面的例子虽然相对少见,但是通过某个包含该类型参数本身的表达式来限制类型参数是允许的。这就是递归类型限制(recursive type bound)。递归类型限制最普遍的用途与Comparable接口有关,它定义类型的自然顺序:

public interface Comparable<T> {
public 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;
}

递归类型限制可能比这个要复杂得多,但幸运的是,这种情况并不经常发生。如果你理解了这种习惯用法及其通配符变量,就能够处理在实践中遇到的许多递归类型限制了。
总而言之,泛型方法就像泛型一样,使用起来比要求客户端转换输入参数并返回值得方法来得更加安全和容易。就像类型一样,你应该确保新方法可以不用转换就能使用,这通常意味着需要将它们泛型化。并且就像类型一样,还应该将现有的方法泛型化,使新用户用起来更加轻松,且不会破坏现有的客户端。

[b][size=medium][color=red]第28条:利用有限通配符来提升API的灵活性[/color]
[/size][/b]
如第25条所述,参数化类型是不可变的(invariant)。换句话说,对于任何两个截然不同的类型Type1和Type2(比如List<String>和List<Object>)而言,List<String>既不是List<Object>的子类型,也不是它的超类型。虽然List<String>不是List<Object>的子类型,这与直觉相悖,但是实际上很有意义。因为我们可以将任何对象放进一个List<Object>中,却只能将字符串放进List<String>中。
我们在上面的Stack<E>类中添加一个pushAll方法,然后再进行下面的测试:

// pushAll method without wildcard type - deficient!
public void pushAll(Iterable<E> src) {
for (E e: src) {
push(e);
}
}
// 测试代码:
Stack<Number> numberStack = new Stack<Number>();
Iterable<Integer> integers = new ArrayList<Integer>(Arrays.asList(1, 2));
numberStack.pushAll(integers);
while (!numberStack.isEmpty()) {
System.out.println(numberStack.pop());
}

在上面的测试中,会报错:The method pushAll() in the type Stack<Number> is not applicable for the arguments (Iterable<Integer>)。幸运的是,有一种解决办法,Java 提供了一种特殊的参数化类型,称作有限制的通配符类型(bounded wildcard type),来处理类似的情况。pushAll的输入参数不应该为“E的Iterable接口”,而应该为“E的某个子类型的Iterable接口”,有一个通配符类型正合此意:Iterable<? extends E>(注:每个类型都是自身的子类型)。下面是修改后的pushAll方法:

// Wildcard type for parameter that serves as an E producer
public void pushAll(Iterable<? extends E> src) {
for (E e: src) {
push(e);
}
}

上面问题解决之后,现在假设想要编写popAll方法,使之与pushAll相呼应。popAll方法从堆栈中弹出每个元素,并将这些元素添加到指定的集合中。初次尝试编写的popAll方法可能像下面这样:

// popAll method without wildcard type - deficient!
public void popAll(Collection<E> dst) {
while (!isEmpty()) {
dst.add(pop());
}
}
// 下面测试代码:不能编译通过!
Stack<Number> numberStack = new Stack<Number>();
Iterable<Integer> integers = new ArrayList<Integer>(Arrays.asList(1, 2));
numberStack.pushAll(integers);
Collection<Object> collection = new ArrayList<Object>();
numberStack.popAll(collection);
for (Object obj: collection) {
System.out.println(obj);
}

以上代码中,如果目标集合的元素类型与堆栈的完全匹配,这段代码的编译正确无误。但在进行上面这种调用方式的时候,编译不能通过。让我们来修改popAll方法,使得测试代码能够编译通过:

// Wildcard type for parameter that serves as an E consumer
public void popAll(Collection<? super E> dst) {
while (!isEmpty()) {
dst.add(pop());
}
}

结论很明显。为了了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。如果某个输入参数既是生产者,又是消费者,那么通配符类型对你就没有什么好处了:因为你需要的是严格的类型匹配,这是不用任何通配符而得到的。下面的助记符便于让你记住要使用哪种通配符类型:
[size=medium][b]PESC表示producer-extends, consumer-super[/b][/size]
PECS这个助记符突出了使用通配符类型的基本原则。Naftalin和Wadler称之为Get and Put Principle。
再举个例子:在27条中的max方法可改为:

public static <T extends Comparable<? super T>> T max2(List<? extends T> list) {
Iterator<? extends T> i = list.iterator();
T result = i.next();
while (i.hasNext()) {
T t = i.next();
if (t.compareTo(result) > 0) {
result = t;
}
}
return result;
}

总而言之,在API中使用通配符类型虽然比较需要技巧,但是使API变得灵活得多。如果编写的是将被广泛使用的类库,则一定要适当地利用通配符类型。记住基本的原则:producer-extends, consumer-super(PECS)。还要记住所有的comparable和comparator都是消费者。

[b][size=medium][color=red]第29条:优先考虑类型安全的异构容器[/color]
[/size][/b]
泛型最常用于集合,如Set和Map,以及单元素的容器,如ThreadLocal和AtomicReference。在这些用法中,它都充当被参数化了的容器。这样就限制了你每个容器只能有固定数目的类型参数。一般来说,这种情况正是你想要的。
但是,有时候你回需要更多的灵活性。例如,数据库的行可以有任意多的列,如果能以类型安全的方式访问所有列就好了。幸运的是,有一种方法可以很容易地做到这一点。这种想法就是将键(key)进行参数化,而不是将容器(container)参数化,然后将参数化的键提交给容器,来插入或者获取值,用泛型系统来确保值得类型与它的键相符合。请看下面的例子:

// Typesafe heterogeneous container pattern - API
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<Class<?>, Object>();

public <T> void putFavorite(Class<T> type, T instance) {
if (null == type) {
throw new NullPointerException("Type is null");
}
favorites.put(type, instance);
}

public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
}
}
// 测试代码:
Favorites f = new Favorites();
f.putFavorite(String.class, "Java");
f.putFavorite(Integer.class, 0xcafebabe);
f.putFavorite(Class.class, Favorites.class);

String v1 = f.getFavorite(String.class);
int v2 = f.getFavorite(Integer.class);
Class<?> v3 = f.getFavorite(Class.class);
System.out.printf("%s %x %s%n", v1, v2, v3.getSimpleName());
// The result is:
// Java cafebabe Favorites

Favorites实例是类型安全(typesafe)的:当你向它请求String的时候,它从来不会返回一个Integer给你。同时它也是异构的(heterogeneous):不像普通的map,它的所有键都是不同类型的。因此,我们将Favorites称作类型安全的异构容器(typesafe heterogeneous container)。
总而言之,集合API说明了泛型的一般用法,限制你每个容器只能有固定数据的类型参数。你可以通过将类型参数放在键上而不是容器上来避开这一限制。对于这种类型安全的异构容器,可以用Class对象作为键。以这种方式使用的Class对象称作类型令牌。你也可以使用定制的键类型。例如,用一个DatabaseRow类型表示一个数据库行(容器),用泛型Column<T>作为它的键。

[b][color=red]文章本人原创,转载请注明作者和出处,谢谢。[/color][/b]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值