zoukankan      html  css  js  c++  java
  • 《Effective Java》读书笔记

    Chapter 5 Generics

    Item 23: Don’t use raw types in new code

    虽然你可以把一个List<String>传给一个List类型(raw type)的参数,但你不应该这么做(因为允许这样只是为了兼容遗留代码),举个例子:

    // 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会自动加上cast
    }
    private static void unsafeAdd(List list, Object o) {
        list.add(o);
    }
    

    以上代码说明如果你把List<String>类型转换成List类型,就可以往里面加任何东西了,编译器不会做检查,就很危险。但是你也不能把List<String>转换成List<Object>,但是你可以把List<String>转换成List<?>,读作List of some type(某种类型的List),但这时候你就不能忘里面add东西了(除了null),因为这个List包含某种类型,但你又不知道(或不关心)是具体是什么类型,那你加进去的东西很可能跟它应有的类型不匹配,所以你只能拿出来一个东西(并定义成Object类型)。如果你实在想add,可以用generic methods或bounded wildcard types。

    由于在运行时“都变成了raw type”,所以instanceof后面只能用raw type:

    // Legitimate use of raw type - instanceof operator
    if (o instanceof Set) { // Raw type
        Set<?> m = (Set<?>) o; // Wildcard type
    }
    

    这里的Cast不会造成编译器warning。

    Item 24: Eliminate unchecked warnings

    当写Generic的时候,通常会遇到很多warning。而你应该尽量eliminate掉每一个warning,这样到了runtime你的代码才更可能不会抛出ClassCastException。如果你没法消除这个warning,但你能证明是typesafe的,那你应该用@SuppressWarnings("unchecked")。SuppressWarnings可以用在类上,方法上,变量声明上,你应该将其用在the smallest scope possible,因为如果你比如用在整个class上,那你可能mask了一些关键性的warning,所以千万别这么做。有时候为了这个原则,你还不得不把某句语句拆成两句写,比如:

    return (T[]) Arrays.copyOf(elements, size, a.getClass());
    

    由于不能在return语句上加SuppressWarnings,所以你只能拆成两句:

    @SuppressWarnings("unchecked")
    T[] result = (T[]) Arrays.copyOf(elements, size, a.getClass());
    return result;
    

    每次你用@SuppressWarnings("unchecked")时,都应该注释一下你为什么要这么做。

    Item 25: Prefer lists to arrays

    数组是covariant的,意思就是如果Sub是Super的子类,那么Sub[]就是Super[]的“子类型”,这也就意味着你可以把String[]转成Object[],然后加一个Object对象进去,然后到runtime会报错。而泛型是invariant的,也就是对于任何的x和y,List<x>List<y>没有任何关系。
    你无法new一个跟泛型有关的数组,比如以下都是错误的:new List<E>[], new List<String>[], new E[]。为什么?书上举了个例子我懒得写了,反正我个人总结下来就是:都怪擦除,因为T[]到运行时其实就相当于Object[],你可以往里面放任何东西,但按理说你应该只能放是T的东西进去。所以说不要把varargs和泛型混用,因为varargs其实就相当于创建了一个数组当成参数。由于这种无法创建数组的限制以及数组的协变性,你可以考虑用List<T>代替T[],比如可以用new ArrayList<E>(list)代替(E[]) list.toArray(),会安全得多。
    总结来说,泛型提供了编译时但非运行时的type safety,而数组恰好相反。

    Item 26: Favor generic types

    generic types就是Generic classes and interfaces。这个item就是教你怎么把你的类变成泛型类,比如我们要把item 6中基于Object[]的Stack升级为泛型的,那我们就把所有“Object”替换成“E”,并在类声明中加上泛型参数。这样会有一个问题就是:elements = new E[DEFAULT_INITIAL_CAPACITY]这句话不能通过编译。因为你不能创建泛型数组,解决方法是:
    1.elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
    因为elements是个private的field,不会泄露给client,而唯一给这个数组“装元素”的地方就是push方法,而push方法push进去的东西保证是E类型的(通过编译器),所以你可以安心地加上@SuppressWarnings("unchecked")。(给这句所在constructor加,因为这句是赋值不是声明,所以加不了)
    2.先elements = new Object[DEFAULT_INITIAL_CAPACITY]; 然后把elements的定义改成Object[]类型的,最后E result = (E) elements[--size];就行了。
    同理,因为push的时候已经确保元素肯定是E,所以这里的warning也可以suppress掉。这两种方法都可以,基本只是个人喜好问题。

    Item 27: Favor generic methods

    “Static utility methods”通常是泛型化的good candidates。在调用泛型方法的时候不需要显式指定泛型参数的具体类型,编译器会自己推断出来,这叫type inference。
    后面这个generic singleton factory我来回看了几遍终于有那么一点似乎看懂了,首先例子代码如下:

    interface UnaryFunction<T> {
    	T apply(T arg);
    }
    private static UnaryFunction<Object> IDENTITY_FUNCTION = new UnaryFunction<Object>() {
        public Object apply(Object arg) {
            return arg;
        }
    };
    @SuppressWarnings("unchecked")
    public static <T> UnaryFunction<T> identityFunction() {
        return (UnaryFunction<T>) IDENTITY_FUNCTION;
    }
    

    一开始我在想new UnaryFunction<Object>(){...}这句话是什么意思,为什么这里是Object而不能是T,后来一想:匿名类是同时声明和创建的,而创建一个泛型类的实例必须指定具体的type parameter,所以这里就相当于声明了一个 实现了UnaryFunction<T>的类,然后创建了一个它的实例(泛型参数是Object)。然后identityFunction方法是一个泛型的static factory method,会把UnaryFunction<Object>类型转换成 “调用这个泛型方法的时候被推断出来的类型 的类型的UnaryFunction”。先看一下用法:

    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));
    }
    

    第一次调用identityFunction()的时候被推断出来的类型是String,第二次是Number。然后以第一次为例,在调用sameString.apply(s)的时候,相当于编译器就会调用UnaryFunction<String>接口中的public String apply(String arg)方法,所以编译器此时会检查s这玩意儿是不是Sting,发现没问题,OK,返回的结果也会被编译器cast成String,而这里你的实际方法(return arg;)啥都没做,所cast肯定不会报错。这个例子的意思关键在于static factory method里面的那个cast: (UnaryFunction<T>) IDENTITY_FUNCTION,正是因为这个cast,所以client代码才能让 任何类型的UnaryFunction 都共享同一个实例(IDENTITY_FUNCTION )。
    但是我在普通的client代码里面试了一下,无法将List<Object> cast成 List<String>,看来这个技巧也只能在泛型方法里面用了。C#的类似实现(虽然可能不是单例):

    static void Main(string[] args)
    {
        String[] strings = { "jute", "hemp", "nylon" };
        var sameString = IdentityFunction<String>();
        foreach (var s in strings)
        {
             Console.WriteLine(sameString(s));
        }
        int[] ints = { 1, 2, 3 };
        var sameInt = IdentityFunction<int>();
        foreach (var s in ints)
        {
            Console.WriteLine(sameInt(s));
        }
    }
    public static Func<T, T> IdentityFunction<T>()
    {
        return arg => arg;
    }
    

    听起来很玄乎的一个概念recursive type bound,比如:<T extends Comparable<T>> may be read as “for every type T that can be compared to itself,”。
    总之,generic methods和generic types都不需要client进行各种cast。

    Item 28: Use bounded wildcards to increase API flexibility

    为了更好的灵活性,应该在输入参数上用wildcard types。如果这个输入参数代表了一个producer就用entends,如果代表了一个consumer就用super,比如如果一个方法的参数声明是Collection<E> container,如果在方法内部只会从container读取E的instance(也就是E只能作为container方法中的返回类型),也就是container只是作为生产者,那么就应该把声明改成Collection<? extends E> container。(你可以这么记,不管返回什么,反正放到E类型的变量里肯定是安全的)同理,如果在方法内部,比如会把E的instance加到container里去,那么container就是消费者(也就是E会作为输入类型),那么就应该声明成Collection<? super E> container。如果既是生产者又是消费者,那就不能用bounded wildcards了。
    注意不要在返回类型上用wildcard types,因为如果你这么做了会迫使client code里面也要受到wildcard types的限制,比如你返回了一个Set<E extends E>,那么得到这个东西的client就只能在它上面进行get操作,而不能add了。正确的用法是,你应该让client根本不用去思考wildcard types,wildcard types只是让你的API能接受更多的类型。
    因为type inference的规则很复杂,有时候type inference会推理错,这时候你需要显示地指定一个type parameter,比如:Union.<Number>union(integers, doubles)
    item 27中有这么一个方法:

    // 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;
    }
    

    现在我们把它增强成用wildcard type,变成这样:

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

    首先把list的类型改成“生产者”很好理解,因为list只返回一个Iterator<E>,而这个Iterator<E>的next方法的声明是“E next()”,E是返回类型。但为什么这里要把Comparable<T>改成Comparable<? super T>。首先看一下Comparable<T>的定义:

    public interface Comparable<T>{
        int compareTo(T o)
    }
    

    看到了吗,T是输入参数,所以Comparable<T>的“实例”是个消费者。 比如如果你这么调用上面的那个方法:

    List<Apple> apples = new...;
    Apple maxApple = max(apples);
    

    那么这里的Apple类并不一定要实现Comparable<Apple>,也可以只实现Comparable<Fruit>,当然Fruit是Apple的基类。假设Apple只知道怎么和Fruit比,然后当运行到方法体内“t.compareTo(result)”这句的时候,t是一个Apple,result也是一个Apple,但是t只知道怎么和另一个Fruit比,但是result是一个Apple当然也是一个Fruit,所以没问题。其实上面解释地这么麻烦,不如你只要记住“always use Comparable<? super T> in preference to Comparable<T>”就行了(Comparator也一样)。
    最后记得还要把方法体中的Iterator<T>改成Iterator<? extends T>

    下面是两种等价的方法声明:

    // Two possible declarations for the swap method
    public static <E> void swap(List<E> list, int i, int j);
    public static void swap(List<?> list, int i, int j);
    

    作者说第二种更好,因为更简单,且不需要考虑type parameter。如果一个type parameter在方法声明中只出现了一次,那么就应该把它替换成unbounded wildcard或bounded wildcard。为什么必须是“只出现了一次”?书上没说但我个人理解是因为如果出现了两次:public static <E> void swap(List<E> list1,List<E> list2),那么这里的list1包含的元素和list2包含的元素应该是相同的类型,如果你全都换成“?”,那么list1和list2完全可以包含不同的类型。
    但是问题又来了,如果你单纯这么实现:

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

    会发现不行,因为不能放东西到List<?>里去,解决办法是依靠“type capture”,写个helper方法:

    public static void swap(List<?> list, int i, int j) {
        swapHelper(list, i, j);
    }
    // Private helper method for wildcard capture
    private static <E> void swapHelper(List<E> list, int i, int j) {
        list.set(i, list.set(j, list.get(i)));
    }
    

    虽然书上的解释有点莫名其妙,但我选择“信了”,感觉记住就行,只是个小技巧。我觉得可以这么理解:因为一个泛型方法被调用的时候肯定要指定泛型参数具体是什么,如果你不指定那就只能靠编译器推断,而在这里编译器就会“capture”到?代表的东西。

    Item 29: Consider typesafe heterogeneous containers

    这一小节就是告诉你怎么实现这么一个类,保存 你最喜欢的 某个类型的一个实例:

    public class Favorites {
       private Map<Class<?>, Object> favorites = new HashMap<Class<?>, Object>();
        public <T> void putFavorite(Class<T> type, T instance) {
            if (type == null) throw new NullPointerException("Type is null");
           favorites.put(type, type.cast(instance));
        }
        public <T> T getFavorite(Class<T> type) {
            return type.cast(favorites.get(type));
        }
    }
    

    你可以用putFavorite来存一个“T类型的实例”,然后通过getFavorite来获取这个实例。
    所以说这里的T和T类型的value是一一对应的,你可以放很多种不同的类型进去。你可以这样使用:

    Favorites f = new Favorites();
    f.putFavorite(String.class, "Java");
    f.putFavorite(Integer.class, 0xcafebabe);
    String favoriteString = f.getFavorite(String.class);
    int favoriteInteger = f.getFavorite(Integer.class);
    

    其实这里用Class<T>来作为“Key”是因为自JDK1.5来Class类就被泛型化了,比如String.class是Class<String>的类型、Integer.class是Class<Integer>的类型,当把这样一个“class literal”(应该就是指“String.class”这种写法)传给某个方法的时候,通常把它叫做type token。而你完全可以自己写一个类,比如Holder<T>来作为“Key”,但是不如Class好,因为Class有一些方法比如cast可以不用让你suppress warning(我个人认为的)。上面的type.cast方法其实就是Java’s cast operator对应的“动态版本”,它只是检查一下它的参数是不是被自己代表的类型,不是的话就抛出ClassCastException:

    public class Class<T> {
         T cast(Object obj);
    }
    

    另外,说些没什么关联的事儿:如果你把Class<?>类型转换成,比如Class<? extends Apple>,会得到warning,那么你可以用asSubclass这个方法,比如假设你得到了一个Class<?>类型的变量apple,然后你可以apple.asSubclass(Apple.class),意思就是“把Class<?>变成Class<Apple>”(反正你就这么理解吧),如果这个apple指向的对象并不是一个“Apple对象”的Class object,那就抛出异常。

  • 相关阅读:
    IDEA下同时使用Git和svn
    IDEA进行activiti-archetype-unittest脚手架的安装
    正则表达式
    关于JS堆栈与拷贝
    按值传递--
    JS---变量、作用域和内存问题
    laod
    待解决
    闭包2
    闭包1
  • 原文地址:https://www.cnblogs.com/raytheweak/p/7190157.html
Copyright © 2011-2022 走看看