Effective Java - 关于泛型

自Java 1.5开始使用的泛型,泛型给人比较直观的印象是..."尖括号里写了类型我就不用检查类型也不用强转了"。

确实,那先从API的使用者的角度上想问题,泛型还有什么意义?

Discover errors as soon as possible after they are made, ideally at compile time.

泛型提供的正是这种能力。
比如有一个只允许加入String的集合,在没有声明类型参数的情况下,这种限制通常通过注视来保证。
接着在集合中add一个Integer实例,从集合获取元素时使用强转,结果导致ClassCastException。
这样的错误发生在运行时,compiler就爱莫能助了,而声明了类型参数的情况下则是compile-time error。
相比raw type,泛型的优势显而易见,即安全性和表述性。

那么,有了泛型就一定比raw type强吗?
如果类型参数是Object又如何?
这种用法和raw type有什么区别?
如果仅仅通过代码描述,可以说raw type不一定支持哪个类型,而Collection<object>支持任何类型。
正确,但没什么意义。
泛型有种规则叫subtyping rules,比如List是List的子类型,但不是List<object>的子类型。
下面的代码描述了这种情况:

// Uses raw type (List) - fails at runtime!
public static void main(String[] args) {
    List<String> strings = new ArrayList<String>();
    unsafeAdd(strings, new Integer(42));
    String s = strings.get(0); // Compiler-generated cast
}

private static void unsafeAdd(List list, Object o) {
    list.add(o);
}

上面的情况导致运行时才能发现错误,而下面这种做法则是编译无法通过:

Test.java:5: unsafeAdd(List<Object>,Object) cannot be applied
to (List<String>,Integer)
    unsafeAdd(strings, new Integer(42));
    ^

而为了应对这种情况Java提供了unbounded wildcard type
即,对于不确定的类型参数使用‘?’代替。
比如Set<?>可以理解为某个类型的集合。

编码时几乎不会用到raw type,但也有两个例外,而且都和泛型擦除有关。

  • must use raw types in class literals.
  • it is illegal to use the instanceof operator on parameterized type.

既然如此,为什么Java还保留着raw type用法?
Java 1.5发布的时候Java马上要迎来第一个十年,早已存在大量的代码。
老代码和使用的新特性的代码能够互用至关重要,即 migration compatibility

好了,接下来说说使用泛型方面的事情。
关于raw type,在某些IDE中使用了raw type就会出现警告,并提示加上@SuppressWarnings。
比如,eclipse中: 

@SuppressWarnings到底有什么作用?
作者给我们的提醒是:要尽量消除这些警告
提示我们加上@SuppressWarnings是在传达一种信息:无法在运行时检查类型转换的安全性。
而程序员消除这些警告是在传达一种信息:运行时不会出现ClassCastException。

消除警告时优先使用声明类型参数的方式,如果因为某些原因而无法消除警告并且需要证明代码是没有问题时才使用@SuppressWarnings。
如上图所示的那样,@SuppressWarnings可以可以用在变量和方法上,对此我们优先针对更小的粒度。
对于@SuppressWarnings,不忽略,且不盲目。

接着说说subtyping,总觉得因为这一特征,泛型有时反而显得麻烦。
书中原话是covariant(协变)和invaritant。
比如,数组是covariant的,比如Sub是Super的子类,则Sub[]是Super[]的子类。
反之,泛型是invariant的,List不是List的子类。

鉴于这种区别,数组和泛型的难以混合使用。
比如下面这几种写法都是非法的:

new List<E>[]
new List<String>[]
new E[]

下面通过一段代码说明泛型数组为什么非法:

// Why generic array creation is illegal - won‘t compile!
List<String>[] stringLists = new List<String>[1]; // (1)
List<Integer> intList = Arrays.asList(42); // (2)
Object[] objects = stringLists; // (3)
objects[0] = intList; // (4)
String s = stringLists[0].get(0); // (5)

首先假设第一行是合法的。
第二行本身合法,鉴于第一行是合法并且数组是协变的,第三行也是合法的。
鉴于泛型用擦除实现的,即List的运行时类型是List,相应地,List[]的运行时类型是List[],第四行是合法的。
到了第五行则变得矛盾,说好的String类型呢?为什么声明了类型参数还是来了个ClassCastException?
既然如此,索性让第一行产生compile-time error吧,泛型又变得美好了。

举一个例子,比如下面这段代码:

interface Function<T> {
    T apply(T arg1, T arg2);
}

static Object reduce(List list, Function f, Object initVal) {
    Object[] snapshot;
    snapshot = list.toArray();
    Object result = initVal;
    for (Object e : snapshot)
        result = f.apply(result, e);
    return result;
}

现在我想把reduce改为泛型方法,于是改成了如下形式:

static <E> E reduce(List<E> list, Function<E> f, E initVal) {
    E[] snapshot = (E[])list.toArray();
    E result = initVal;
    for (E e : snapshot)
        result = f.apply(result, e);
    return result;
}

显然,结果是编译无法通过。
结果是除了在list.toArray上提示加上@SuppressWarnings之外没有任何问题,完全可以正常运行。
不要忽略@SuppressWarnings! 提示我加上@SuppressWarnings是在告诉我:无法在运行时检查类型转换的安全性。
那我应该加上@SuppressWarnings吗?如果加上的话又怎么保证?

其实解决方法很简单,就是不混用数组和泛型,即:

static <E> E reduce(List<E> list, Function<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;
}

这就好了,能用泛型就用。
但换个立场,作为一个提供者而不是使用者,问题还会这样容易吗?
比如描述一个栈:

// 无法通过编译
public class Stack <E>  {
    private  E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    public Stack() {
        elements = new E[DEFAULT_INITIAL_CAPACITY];
    }
    public void push(E e) {
        ensureCapacity();
        elements[size++] = e;
    }
    public E pop() {
        if (size==0)
            throw new EmptyStackException();
        E result = elements[--size];
        elements[size] = null; // Eliminate obsolete reference
        return result;
    }
    // 检查方法略
}

很显然,数组是具体化的,new E[DEFAULTINITIALCAPACITY]上提示Cannot create a generic array of E。
于是我机制地改为(E[])new Object[DEFAULTINITIALCAPACITY]。
这样完全可以通过,然后出现提示让我加上@SuppressWarnings...
我无法忽略,但我可以证明不会出现ClassCastException。
即elements是private的,且没有任何方法可以直接访问它,push和pop的类型都是安全的。
或者我也能将elements的类型改为Object[],并且在pop的时候将元素类型转为E,这样也是可以的。

与其让使用者进行强转,倒不如提供者提供一个安全的泛型。
但不是所有的情况都像上面的例子那样的顺利。
比如我在Stack中增加了一个:

public void pushAll(Iterable<E> src) {
    for (E e : src)
        push(e);
}

然后我将Iterable传入Stack中,由于泛型不是协变的,果断来了个compile-time error。
但抛开这些不想,将一堆Integer放到一堆Number又显得那么里所应当。
于是我们就有了bounded wildcard type,关键就是这个bounded。
一个‘?‘是wildcard,为其加点限制就是bounded wildcard,即:

public void pushAll(Iterable<? extends E>  src) {
    for (E e : src)
        push(e);
}

相应地,我们再提供一个popAll方法,将pop出来的元素添加到指定集合中。
比如,Stack中的元素必然可以添加到Collection<object>中,也就是:

public void popAll(Collection<? super E>  dst) {
    while (!isEmpty())
        dst.add(pop());
}

对于泛型wildcard的使用,作者指出:PECS,stands for producer-extends, consumer-super.
即,类型参数表示一个生产者则使用<? extends T>,消费者则使用<? super T>。
再举个例子,比如我们要合并两个集合的元素,由于这是生产行为,则声明为:

public static <E> Set<E> union(Set<? extends E> s1,Set<? extends E> s2);

那么能不能在返回类型中使用通配符?
作者建议尽量不要在返回类型中使用,因为这样会让调用变得复杂。
再来个稍微复杂一些的例子,比如我要在某个参数类型的List中找到最大的元素。
最初的声明为:

public static <T extends Comparable<T>> T max(List<T> list)

返回结果从list参数获得,于是将参数声明改为List<? extends T> list。
那<T extends Comparable>有该如何处理。
以java.util.concurrent中的ScheduledFuture和Delayed为例。
(ps:interface ScheduledFuture extends Delayed 且 interface Delayed extends Comparable.)
即,类型T本身没有实现Comparable,但是他的父类实现了Comparable,于是声明为:

public static <T extends Comparable<? super T> > T max(List<? extends T> list)

最后还有一个有意思的例子,先看代码:

public static void swap(List<?> list, int i, int j) {
    list.set(i, list.set(j, list.get(i)));
}

这段代码是无法通过编译的,因为无法将null以外的元素添加到List<?>中。
当然,如果直接声明一个类型参数就没有问题,但现在假设我们只有使用通配符,并且不能使用raw type。
既然知道通过类型参数可以解决,于是我们可以这样:

public static void swap(List<?> list, int i, int j) {
    swapHelper(list, i, j);
}

private static <E> void swapHelper(List<E> list, int i, int j) {
    list.set(i, list.set(j, list.get(i)));
}
时间: 2024-10-07 12:23:06

Effective Java - 关于泛型的相关文章

Effective Java之泛型

泛型是Java在JDK1.5版本中引入的一个特性,泛型的出现大大增强了Java代码运行时的安全性,泛型主要应用于容器类中,因为这些类会包含各种各样的其他类,所以需要用泛型来对容器中所包含的类进行约束,比如为List传入一个String的类型参数,那么,这个List对象就只能包含有String类型的类,而不能向1.5之前的原始版本一样,可以包含各种不同的类,泛型的引入大大增强了集合类的约束,减少在使用集合类时报类型转换错误的概率. 书里面建议,在使用数组时尽量采用泛型容器,而不是基本的数组类型,里

Effective java -- 4 泛型

第二十三条:请不要在代码中使用原生态类型就是像Set这种待泛型的,就把泛型明确写出来. 第二十四条:消除非受检警告就是Set<String> sets = new HashSet();这种,第二个泛型不加会有一个警告. 第二十五条:列表优先于数组数组和泛型的区别: 数组是协变的.就是如果Sub是Super的子类型,那么Sub[]就是Super[]的子类型.泛型则是不可变的. 数组是可具体化的.一次数组会在运行时才知道并检查他们的元素类型约束. 第二十六条:优先考虑泛型类上加上泛型. 第二十七条

Effective Java 第三版——29. 优先考虑泛型

Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深刻的变化. 在这里第一时间翻译成中文版.供大家学习分享之用. 29. 优先考虑泛型 参数化声明并使用JDK提供的泛型类型和方法通常不会太困难. 但编写自己的泛型类型有点困难,但值得努力学习. 考虑条目 7中的简单堆栈实现: // Ob

Effective Java 第三版——32.合理地结合泛型和可变参数

Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深刻的变化. 在这里第一时间翻译成中文版.供大家学习分享之用. 32. 合理地结合泛型和可变参数 在Java 5中,可变参数方法(条目 53)和泛型都被添加到平台中,所以你可能希望它们能够正常交互; 可悲的是,他们并没有. 可变参数的目

Effective Java中的泛型部分

今天将Effective Java(第二版)中的泛型部分读完,深感自己泛型掌握有多么不熟练,还是需要多加练习. 废话少说,上点重点: 1.不要使用原型 比如: List list = new ArrayList(); 当你用该list引用指向其他带有泛型的List时,是不会出现编译错误的,只会给一个rawtype的警告,但是---- 这很容易出现挂羊头卖狗肉的情况,比如你指向了一个List<Integer>,却add了一个String,这不会出现任何编译错误,但 当你取出来转成Integer时

【总结】Effective java经验之谈,类与接口

转载请注明出处:http://blog.csdn.NET/supera_li/article/details/44940563 Effective Java系列 1.Effective java经验之谈,创建和销毁对象 2.Effective java经验之谈,泛型 3.Effective java经验之谈,类与接口 4.Effective java经验之谈,通用方法 5.Effective java经验之谈,枚举,注解,方法,通用设计,异常 6.Effective java经验之谈,并发编程

Effective java 中文版本阅读总结

点击链接查看云笔记原文 花了半天时间,贪婪的啃读了Effective java 这本书(虽然闻名已久,但是很少看书) 翻着翻着就有种废寝忘食的感觉,下班了都留下来专门看书,后来索性带回家看了. 以下是内容总结,主要是对个人感觉有用的,有很大部分没有提及,因为水平有限,还没有来得及消化 1 引言 2 创建和销毁对象 # 静态工厂方法由于构造器 Boolean.valueOf(String); 几乎总是优于构造器Boolean(String);构造器每次调用的时候都会创建一个新的对象 * 对象创建比

[Effective Java]第八章 通用程序设计

第八章      通用程序设计 45.      将局部变量的作用域最小化 将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性. 要使用局部变量的作用域最小化,最有力的方法就是在第一次使用它的地方才声明,不要过早的声明. 局部变量的作用域从它被声明的点开始扩展,一直到外围块的结束外.如果变量是在“使用它的块”之外被声明有,当程序退出该块之后,该变量仍是可见的,如果它在目标使用区之前或之后意外使用,将可能引发意外错误. 几乎每个局部变量的声明都应该包含一个初始化表达式,如

【电子书】Effective Java中文版下载

下载地址: 点击打开链接 (需要资源0分的联系我~) <Effective Java中文版(第2版)>主要内容:在Java编程中78条极具实用价值的经验规则,这些经验规则涵盖了大多数开发人员每天所面临的问题的解决方案.通过对Java平台设计专家所使用的技术的全面描述,揭示了应该做什么,不应该做什么才能产生清晰.健壮和高效的代码.第2版反映了Java 5中最重要的变化,并删去了过时的内容. <Effective Java中文版(第2版)>中的每条规则都以简短.独立的小文章形式出现,并