Effective Java(四)

四、泛型

1. 请不要再新代码中使用原生态类型

        泛型类/接口:声明中具有一个或者多个类型参数的类/接口。

        每个泛型都定义一个原生态类型,即不带任何实际类型参数的泛型名称。如与List<E>相对应的原生态类型就是List。原生态类型没有泛型在安全性表述性方面的优势,它的存在仅是为了兼容引入泛型之前的遗留代码,不应在新代码中继续使用。

//使用原生态类型
private final List stamps = new ArrayList();
stamps.add( new Stamp() );
stamps.add( new Coin() ); //可以正常添加
Stamp stamp = (Stamp)stamps.get(1); //运行时错误,抛出ClassCastException。

//使用泛型
private final List<Stamp> stamps = new ArrayList<Stamp>();
stamps.add( new Stamp() );
stamps.add( new Coin() ); //提示错误,无法通过编译
Stamp stamp = stamps.get(0); //使用时无需进行手工转换

由上述代码可以看出,使用泛型的两个好处:

  • 由编译器确保插入正确的元素类型
  • 从集合获取元素时不再需要手工转换了 

        如果要使用泛型,但不确定或不关心实际的类型参数,可以使用一个?代替,称作无限制的通配符类型,如泛型Set<E>的无限制通配符类型为Set<?>,读作某个类型的集合。通配符类型是安全的,原生态类型不安全。

不在新代码中使用原生态类型这条规则有两种例外情况:
(1)在类文字中必须使用原生态类型

//正确的用法
List.class
String[].class
int.class

//错误的用法
List<String.class>
List<?>.class

(2)在instanceof操作符中必须使用原生态类型 

if (o instanceof Set) {
    Set<?> m = (Set<?>)o;
}

        上述两种例外都是源于泛型信息可以在运行时被擦除。 

2. 消除非受检警告

        用泛型编程时,会遇到很多编译器警告:

  • 非受检强制转换警告
  • 非受检方法调用警告
  • 非受检普通数组创建警告
  • 非受检转换警告

        要尽可能地消除每一个非受检警告,这可以确保代码是类型安全的,意味着代码在运行时不会出现ClassCastException异常。

        如果无法消除警告,同时可以证明引起警告的代码是类型安全的。只有在这种条件下才可以使用 @SuppressWarnings("unchecked") 注解来禁止这种警告。

        SuppressWarnings注解可以用在任何粒度的级别中,从单独的局部变量声明到整个类的定义都可以。应该始终在尽可能小的范围中使用SuppressWarnings注解永远不要在整个类上使用SuppressWarnings,因为这么做可能会掩盖重要的警告信息。

        每当使用@SuppressWarnings("unchecked")注解时,都要添加一条注释,说明为什么这么做是安全的。这样做可以帮助他人理解代码,更重要的是,可以尽量减少其他人修改代码后导致计算不安全的概率。

3. 列表优先于数组

数组与泛型相比,有两个重要的不同点:
(1)数组是协变的
        协变指的是如果Sub为Super的子类型,那么数组类型Sub[]就是Super[]的子类型;
        泛型是不可变的,对于任意两个不同的类型Type1和Type2,List<Type1>既不是List<Type2>的子类型,也不是List<Type2>的超类型。
(2)数组是具体化的
        数组在运行时才知道并检查它们的元素类型约束;
        泛型则通过类型擦除来实现,它在编译时强化它们的类型信息,在运行时丢弃(或擦除)它们的元素类型信息。

        由于数组的协变性和具体化,它是有缺陷的:

//数组具有协变性,Object是Long的父类,声明合法
Object[] objectArray = new Long[1];
//Long[] 退化为Object[],此处赋值也是合法的
objectArray[0] = "I don't fit in"; 

        上述代码可以通过编译,但运行时却抛出ArrayStoreException。
        改为列表后,则无法通过编译时的类型检查: 

//无法通过编译,List<Object>和List<Long>是不同的类型
List<Object> ol = new ArrayList<Long>();
ol.add("I don't fit in");

        因为数组和泛型之间有着根本性的区别,数组和泛型不能很好地混合使用。如下列类型的表达式都是非法的:new List<E>[]、new List<String>[]、new E[]。
        创建泛型数组是非法的,是因为泛型数组不是类型安全的。如下代码所示:

List<String>[] strLists = new List<String>[1]; //假设此处合法
List<Integer> intList = Arrays.asList(42);
Object[] objects = strLists; //数组是协变的,此处合法
objects[0] = intList;
String s = strLists[0].get(0); //运行时ClassCastException异常

        当得到泛型数组创建错误时,最好的解决办法通常是优先使用集合类型List<E>,而不是数组类型E[]。这样可能会损失一些性能或简洁性,但换回的却是更高的类型安全性和互用性。 

4.优先考虑泛型

        编写自己的泛型相对比较困难,但很值得花时间去学习如何编写。
        下面以一个Stack类为例来说明:

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    
    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }
    
    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }
    
    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }
    
    public boolean isEmpty() {
        return size == 0;
    }
    
    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * DEFAULT_INITIAL_CAPACITY + 1);
        }
    }
}

        上述Stack类的实现,主要问题有如下两点:
(1)push操作无法保证类型安全

//可以向stack中放入任意类型
Stack stack = new Stack();
stack.push("stack");
stack.put(new Integer(100));

(2)pop操作获得元素需要外部手工进行类型转换,且可能会产生ClassCastException异常。

String str = (String)stack.pop();

将上述Stack类进行泛型化,主要步骤为:

  • 给它的声明添加一个或者多个类型参数
  • 用相应的类型参数替换所有的Object类型,尝试编译
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;
        return result;
    }
    
    public boolean isEmpty() {
        return size == 0;
    }
    
    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * DEFAULT_INITIAL_CAPACITY + 1);
        }
    }
}

消除泛型数组的方法有两种:
(1)直接绕过创建泛型数组,创建一个Object数组

//用法合法,但整体上而言不是类型安全的
elements = (E[])Object[DEFAULT_INITIAL_CAPACITY];

(2)将域的类型从E[]改为Object[](推荐使用此种方法)

public class Stack<E> {
    private Object[] elements;
    ...

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public E pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        E result = (E) elements[--size];
        elements[size] = null;
        return result;
    }
}

5. 优先考虑泛型方法

        静态工具方法通常比较适合泛型化。
        编写泛型方法与编写泛型类相似,如下述代码:

public static Set union(Set s1, Set s2) {
    Set result = new HashSet(s1);
    result.addAll(s2);
    return result;
}

        上述union方法并不是类型安全的,将其泛型化的代码如下:

public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
    Set<E> result = new HashSet<E>(s1);
    result.addAll(s2);
    return result;
} 

        泛型后的union方法不仅适用性更强,也是类型安全的,它确保了待合并集合的类型一致性,外部使用也无需进行手工强制转换。

6. 利用有限制通配符来提升API的灵活性

        泛型不具备协变性,但有时,我们又需要使用协变带来的灵活性,于是Java提供了有限制的通配符类型这种特殊的参数化类型:

  • GenericType<? extends E>:子类型通配符,通配符?表示E的某个子类型
  • GenericType<? super E>:超类型通配符,通配符?表示E的某个超类型

        考虑Stack的公共API:

public class Stack<E> {
    public Stack();
    public void push(E e);
    public E pop();
    public boolean isEmpty();
}

        假如我们想增加一个方法,让它按顺序把一系列元素添加到Stack中,尝试如下:

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

        如果src中的元素类型与Stack的泛型参数类型完全匹配,是完全没有问题的。但考虑这样一种情形:有一个Stack<Number>,且调用了push(int val),从逻辑上讲,下面的实现应该是可以的:

Stack<Number> stack = new Stack<Number>();
Iterable<Integer> integers = ...;
stack.pushAll(integers);

        实际情况是上述办法并不可行,会导致编译错误。
        显然,我们的目的是想将E的某个子类型也放入Stack中(Integer是Number的子类),可以利用子类型通配符来做有限制的规定:

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

        假设现在需要编写一个popAll方法,使之与pushAll方法相呼应,popAll方法从Stack中弹出每个元素,并将这些元素添加到指定的集合中,尝试如下:

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

        如果dst的元素类型与Stack完全匹配,上述实现是没有问题的。但考虑这样一种情形:有一个Stack<Number>和Collection<Object>,从逻辑上讲,下面的实现应该是可以的:

Stack<Number> numStack = new Stack<Number>();
Collection<Object> coll = ...;
numStack.popAll(coll);

        实际情况是上述办法并不可行,会导致编译错误。Collection<Object>并不是Collection<Number>的超类型。
我们的目的是为了将类型为E的元素加入到目标泛型集合中,且目标集合的泛型参数类型只要是类型E的父类型即可(Object是Number的父类),Java提供了父类型通配符来实现这种需求:

//此处的限定是:通配符类型是泛型参数类型的父类即可
public void popAll(Collection<? super E> dst) {
    while(!isEmpty()) {
        dst.add(pop());
    }
}

        为了便于记住要使用哪种通配符,引入下面的助记符:
        PECS表示producer-extends,consumer-super。
        如果参数化类型表示一个T生产者,就使用<? extends T>;如果它表示一个T消费者,就使用<? super T>。在Stack示例中,pushAll的src参数产生E实例供Stack使用,因此src相应的类型为Iterable<? extends E>;popAll的dst参数通过Stack消费E实例,因此dst相应的类型为Collection<? super E>。
        PECS助记符突出了使用通配符类型的基本原则

7. 优先考虑类型安全的异构容器

        Java泛型也提供了另一种用法:将键(key)进行参数化而不是将容器参数化,然后将参数化的键提交给容器,来插入或者获取值,用泛型系统来确保值的类型与它的键相符

        类Class在Java1.5中被泛化了,类的类型从字面上看不再只是简单的Class,而是Class<T>,意味着String.class是属于Class<String>类型,Integer.class属于Class<Integer>类型。
        当一个类的字面文字被用在方法中,来传达编译时和运行时的类型信息时,被称作type token

        假如需要设计一个Favorites类,它允许其客户端从任意数量的其他类中,保存并获得一个“最喜爱”的实例,代码如下:

public class Favorites {
    private Map<Class<?>, Object> favorities = new HashMap<Class<?>, Object>();

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

    public <T> T getFavorite(Class<T> type) {
        return type.cast(favorities.get(type));
    }

    public static void main(String[] args) {
        Favorites f = new Favorites();
        f.putFavorite(String.class, "Java");
        f.putFavorite(Integer.class, 0xcafebabe);
        f.putFavorite(Class.class, Favorites.class);

        String fString = f.getFavorite(String.class);
        int fInteger = f.getFavorite(Integer.class);
        Class<?> fClass = f.getFavorite(Class.class);

        System.out.printf("%s %x %s%n", fString, fInteger, fClass.getSimpleName());
    }
}
//代码打印结果为:Java cafebabe Favorites

        Favorites实例是类型安全的:当你向它请求String的时候,它不会返回一个Integer。同时它也是异构的:不像普通的map,它的所有键都是不同类型的
        像Favorites这种类被称为类型安全的异构容器。对于这种类型安全的异构容器,可以用Class对象作为键。
        以这种方式使用的Class对象被称作类型令牌。Favorites使用的类型令牌是无限制的,还可以利用有限制类型参数或有限制通配符来限制可以表示的类型:

    public <T extends Annotation> T getAnnotation(Class<T> annotationType);

    Annotation getAnnotation(AnnotatedElement element, String annotationTypeName) {
        Class<?> annotationType = null;
        try {
            annotationType = Class.forName(annotationTypeName);
        } catch (Exception e) {
            throw new IllegalArgumentException();
        }
        return element.getAnnotation(annotationType.asSubclass(Annotation.class));
    }
发布了51 篇原创文章 · 获赞 53 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_34519487/article/details/104233173
今日推荐