Effective java笔记(四),泛型

泛型为集合提供了编译时类型检查。

23、不要在代码中使用原生态类型

声明中具有一个或多个类型参数的类或接口统称为泛型。List<E>是一个参数化类,表示元素类型为E的列表。为了提供兼容性,每个泛型都定义一个原生态类型,即不带任何实际类型参数的泛型名称。例如,List<E>的原生态类型为List

使用原生态类型将逃避编译时的类型检查,失掉泛型在安全性和表述性方面的优势。出错时(运行时错误)代码所处的位置与包含错误的代码可能相距很远,难以调试。不应该在代码中使用原生态类型。

原生态类型List与参数化类型List<Object>的区别:

  • 前者逃避了泛型检查,后者明确告知编译器,它能够持有任意类型的对象

  • List<String>是原生态类型List的子类型,但不是List<Object>的子类型(泛型不是协变的)。可将List<String>传递给类型List的参数,但不能传给类型List<Object>的参数。

如果要使用泛型,但不确定或者不关心实际的类型参数,可使用无限制的通配符类型(<?>)。

注意:不能将任何元素(除null外)放进Collection<?>中,而且无法猜测能从中得到哪种类型的对象。若无法接受这些限制可使用泛型方法或有限制的通配符类型。

不要在程序中使用原生态类型例外情况:

  • 在类文字中必须使用原生态类型。List.class, String[].class合法,但List<String>.class,List<?>.class不合法。

  • 在参数化类型上使用instanceof操作符是非法的(无限制通配符类型除外,其是可具体化的类型),因为泛型信息在运行时会被擦除。

例如:

if(o instanceof Set) { 
    Set<?> m = (Set<?>) o; //必须转换成通配符类型Set<?>,而不是原生态类型Set
    ...
}

原生态类型只是为了与引入泛型之前的遗留代码进行兼容和互用而提供。Set<Object>是参数化类型,表示可以包含任何对象类型的一个集合。Set<?>是一个通配符类型,表示只能包含某种未知对象类型的一个集合。

24、消除非受检警告

泛型编程时,要尽可能的消除每一个非受检警告。消除所有警告可以保证代码的类型安全,避免出现ClassCastException异常。

如果无法消除警告,同时可以证明引起警告的代码是类型安全的。可以使用@SuppressWarnings("unchecked")注解来禁止这条警告。SuppressWarnings注解可以用在任何粒度的级别中,应该在尽可能小的范围中使用SuppressWarnings注解,永远不要在一个类上使用SuppressWarnings注解。将一个SuppressWarnings注解放在return语句中是非法的,因为它不是一个声明,应该声明一个局部变量来保存返回值,并注解其声明。

例如:ArrayList类的toArray方法

public <T> T[] toArray(T[] a) {
    return (T[]) Arrays.copyOf(elements, size, a.getClass());
}

//改为
public <T> T[] toArray(T[] a) {
    @SuppressWarnings("unchecked") 
    T[] result = (T[]) Arrays.copyOf(elements, size, a.getClass());
    return result;
}

当使用@SuppressWarnings("unchecked")注解时,使用注释把禁止该警告的原因记录下来。

25、列表优先于数组

数组与泛型的区别:

  • 数组是协变的(covariant,表示如果Sub为Super的子类型,则Sub[]就是Super[]的子类型);而泛型是不可变的(因为List<String>不是List<Object>的子类型)。数组是有缺陷的
  • 数组是具体化的,运行时检查它们的元素类型约束。泛型则是通过擦除来实现的,因此泛型只在编译时强化它们的类型信息,运行时丢弃类型信息。擦除是泛型可以与没有使用泛型的代码随意进行互用。
Object[] obj = new Long[1]; //正确
obj[0] = "I don't fit in"; //出错

List<Object> list = new ArrayList<Long>(); //出错

基于上面的原因,数组和泛型不能很好的混用,不能创建泛型数组。例如,new List<E>[], new List<String>[], new E[]都是非法的。E、List<E>、List<String>被称作不可具体化的类型,具体的说,不可具体化的类型是指其运行时表示法包含的信息比它的编译时表示法包含的信息更少的类型。唯一可具体化的参数类型为无限制的通配符类型类型,如List<?>Map<?,?>。所以创建无限制通配符类型的数组是合法的。

  • 泛型不能返回它的元素类型数组。

  • 可变参数方法与泛型结合使用会产生警告,因为每当调用可变参数方法时,都会创建一个数组存放参数。除了使用@SuppressWarnings("unchecked")注解把它们禁止,并避免在API中混合使用泛型和可变参数外,别无它法。

代码E[] ele = (E[]) new Object[10];无法再运行时检查转换的安全性,因为元素类型会在运行时从泛型中被擦除。不可具体化的类型的数组转换只能在特殊情况下使用。

数组是协变且可以具体化的;泛型是不可变的且可以被擦除。数组提供了运行时的类型安全,而泛型提供了编译时的类型检查。但数组和泛型不能很好地混用,列表应该优先于数组使用。

26、优先考虑泛型

    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 o) {
            ensureCapacity();
            elements[size++] = o;
        }

        public E pop() {
            if(size == 0) 
                throw new EmptyStackException();
            E result = elements[--size];
            elements[size] = null; //【避免内存泄漏】
            return result;
        }

        private void ensureCapacity() {
            if(elements.length == size) {
                elements = Arrays.copyOf(sx, 2*size + 1);
            }
        }
    }

如上所示,不能创建不可具体化的类型的数组,解决这个问题的办法:

方法一:创建一个Object的数组,并将它转换成泛型数组类型,证明这个转化是否安全,若安全,使用注解消除警告并给出注释。

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

方法二:将elements域的类型改为Object[], 并为所用使用Object[]数组中元素的地方进行强转

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

第25条鼓励优先使用对表(List),但有时为了性能或程序的兼容性必须使用数组。如,HashMap

注意:基本数据类型不能用于泛型,这是java泛型系统根本的局限性。可以使用基本包装类型代替

27、优先考虑泛型方法

静态工具方法尤其适合于泛型化。声明类型参数的类型参数列表,处在方法的修饰符(public、static、final等)及其返回类型之间。

泛型方法的一个显著特点是,无需明确指定类型参数的值,调用泛型构造器时必须指定(jdk1.8后不需要),编译器可通过返回类型和传入的参数类型进行类型推导。

编写一个恒等函数,若每次需要时都重新创建一个,这会很浪费,因为它是无状态的。如果泛型被具体化了,每个类型都需要一个恒等函数,但它们被擦除以后,就只需要一个泛型单例。例如:

public iterface UnaryFunction<T> {
    T apply(T arg);
}

private static UnaryFunction<Object> IDENTITY_FUNCTION = new UnaryFunction<Object>() {
    pubic Object apply(Object arg){ return arg;}
}

@SuppressWarnings("unchecked")
public static <T> UnaryFunction<T> identityFunction(){
    return (UnaryFunction<T>) IDENTITY_FUNCTION;
}

public static void main(String[] args) {
    String[] strings = {"aaa", "bbb", "ccc"};
    UnaryFunction<String> sameString = identityFunction();
    for(String s : strings) {
        System.out.println(sameString.apply(s));
    }
}

通过某个包含该类型参数本身的表达式来限制类型参数,被称为递归类型限制。如,<T extends Comparable<T>>表示针对可以与自身进行比较的每个类型T

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

参数化类型是不可变的,对于两个截然不同的类型Type1和Type2,List<Type1>既不是List<Type2>的子类型,也不是它的超类型。

有时我们需要的灵活性要比不可变类型所能提供的更多。如:

//泛型类Stack中的方法
public void pushAll(Iterable<E> src) {
    for(E e: src) 
        push(e);
}

//将Integer类型放入Number类型栈中
Stack<Number> numberStack = new Stack<Number>();
Iterable<Integer> integers = ...
numberStack.pushAll(integers); //报错,pushAll(Iterable<Number>) cannot be applied to (Iterable<Integer>)

可以通过有限制的通配符类型来解决这个问题,pushAll的输入参数应该为“E的某个子类型的Iterable接口”;类似的方法popAll(从栈中弹出每个元素,并将这些元素添加到指定的集合中)的输入参数应该为“E的某个超类的集合”。

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

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

结论
为了获得最大限度的灵活性,要在表示生产者或消费者的输入参数上使用通配符类型。若输入参数既是生产者又是消费者,使用严格的类型匹配,不要使用通配符。PECS表示producer-extends, consumer-super,若T是生产者使用<? extends T>,若T是消费者使用<? super T>

注意:不要用通配符类型作为返回类型

若编译器不能推断你所希望它拥有的类型,可以使用一个显式的类型参数。例如:

import java.util.*;
public class Test {
    public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) {
        Set<E> result = new HashSet<>(s1);
        result.addAll(s2);
        return result;
    }

    public static void main(String[] args) {
        Set<Integer> integers = new HashSet<>();
        integers.add(1);integers.add(2);

        Set<Double> doubles = new HashSet<>();
        doubles.add(3.0);doubles.add(4.0);

        Set<Number> numbers = Test.<Number>union(integers, doubles); //显示的类型推断
        Set<Number> numbers = Test.union(integers, doubles);//jdk1.8后不报错,之前报错
    }
}

Comparable,Comparator始终是消费者,所以Comparable<? super T>优先于Comparable<T>,Comparator一样
T extends Comparable<? super T>表示T类型不仅能和T类型作比较,还能和T类型的父类型作比较。例如:

public static <T extends Comparable<? super T> T max(List<? extends T> list) {
    Iterator<? extends T> i = list.iterator(); //iterator()方法返回T的某个子类型的iterator
    T result = i.next();
    while(i.hasNext()) {
        T t = i.next();
        if(t.compareTo(result) > 0) {
            result = t;
        }
    }
    return result;
}

如果类型参数只在方法声明中出现一次,就可以使用通配符代替它。有限制的类型参数使用有限制的通配符取代它,无限制的类型参数使用无限制的通配符类型取代它。例如,

public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j); //无限制通配符

第二种声明用于swap方法会有一个问题,下面这个实现不能编译:

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

不能将元素放回到刚刚从中取出的列表中,问题在于list的类型为List<?>,你不能把null之外的任何值放到List<?>中。解决办法是编写一个私有的辅助方法来捕捉通配符类型。如:

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

通过swapHelper,我们可以导出swap这个比较好的基于通配符的声明到外部API接口

29、优先考虑类型安全的异构容器

泛型常用于容器(集合以及单元素的容器),每个容器只能有固定数目的类型参数,可以通过将类型参数放在键上而不是容器上来避开这一限制。当一个类的字面文字被用在方法中,来传达编译时和运行时的类型信息时,就被称作 type token

例如:

import java.util.*;
public class Favorites {
    private Map<Class<?>, Object> favorites = new HashMap<>();

    public <T> void putFavorite(Class<T> type, T instance){
        if(type == null) {
            throw new NullPointerException("Type is null");
        }
        favorites.put(type, type.cast(instance)); //用Class的cast方法,防止使用原生态形式的Class对象
    }

    public <T> T getFavorite(Class<T> type){
        return type.cast(favorites.get(type)); //Class的cast方法
    }

    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 favoriteString = f.getFavorite(String.class);
        int favoriteInteger = f.getFavorite(Integer.class);

        System.out.printf("%s %x", favoriteString, favoriteInteger);
    }
}

上面代码中,Favorites实例是类型安全的,同时也是异构的(它的所有键都是不同类型),因此Favorites被称作类型安全的异构容器。Class的cast方法:将对象引用动态地转换成Class对象所表示的类型,cast方法是Java的cast操作符的动态模拟,它检验它的参数是否为Class对象所表示的类型的实例。如果是,就返回参数;否则抛出ClassCastException异常。

注意:由于无限制通配符类型的关系,你可能认为将不能把任何东西放进这个Map中,但事实正好相反。Map<Class<?>, Object>中通配符类型是嵌套的,这里通配符类型指的不是Map而是键的类型。

Favorites类的局限性:不能用在不可具体化的类型中。如,不能保存List<String>

可以使用有限制的类型令牌(bounded type token)来限制传给方法的类型。如,

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

annotationType表示注解类型的有限制的类型令牌,如果元素有这种类型的注解,该方法就将它返回,若没有返回null。
Class的asSubclass(Class clazz)方法:将class对象转换成其参数表示的类的一个子类,若转换成功,返回它的参数。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • 开发人员在使用泛型的时候,很容易根据自己的直觉而犯一些错误。比如一个方法如果接收List作为形式参数,那么如果尝试...
    时待吾阅读 1,040评论 0 3
  • 前言 人生苦多,快来 Kotlin ,快速学习Kotlin! 什么是Kotlin? Kotlin 是种静态类型编程...
    任半生嚣狂阅读 26,139评论 9 118
  • 这是一个重阳节的故事。 周末睡得迷迷糊糊的时候,猛然间听到手机的嗡鸣,无意识的摸到手机准备看看是谁扰了自己周末清晨...
    烟云阑珊阅读 189评论 0 1
  • 2018越发近了,2017你还有什么未完成的心愿吗?说来听听…… 愿你一切安好,晚安!
    何时再出发阅读 62评论 5 0
  • 初看电影《遗愿清单》的时候,一眼就认出了摩根弗里曼,觉得这电影有看头,可以看看。 开头不错,通过一个旁白代入到影片...
    与Winter的五百天阅读 334评论 0 2