《Effective Java》读书笔记 - 5.泛型_weixin_30484739的博客-程序员秘密

技术标签: c#  java  runtime  

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,那就抛出异常。

转载于:https://www.cnblogs.com/raytheweak/p/7190157.html

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_30484739/article/details/97691569

智能推荐

python+selenium自动化脚本怎样跳过不安全的连接_如何在从python selenium脚本打开私有浏览器时禁用“不安全连接”?..._Guo Xun的博客-程序员秘密

在为firefox浏览器运行python selenium脚本时,我遇到一个问题Your connection is not secure它不允许我添加异常和阻止Confirm Security Exception以及(即使手动使用首选项)。因此,我试图添加像“webdriver_accept_untrusted_certs”,“webdriver_accept_untrusted_certs”这...

棋盘问题 dfs_m_turtle的博客-程序员秘密

在一个给定形状的棋盘(形状可能是不规则的)上面摆放棋子,棋子没有区别。要求摆放时任意的两个棋子不能放在棋盘中的同一行或者同一列,请编程求解对于给定形状和大小的棋盘,摆放k个棋子的所有可行的摆放方案C。Input输入含有多组测试数据。 每组数据的第一行是两个正整数,n k,用一个空格隔开,表示了将在一个n*n的矩阵内描述棋盘,以及摆放棋子的数目。 n 当为-1 -1时表示输入结束。

【图解数据结构】排序全面总结(二)_知心宝贝的博客-程序员秘密

一、前言二、选择类排序选择类:每次从待排序的无序序列中,选择一个最大或最小的数字,放到前面,数据元素为空时排序结束1.简单选择排序动态演示:算法讲解:首先通过n-1次比较,从n个记录中找出最小值,将它与第一个元素交换 再通过n-2次比较,从剩余的n-1个记录中找出次小的值,将它与第二个记录交换 重复上述操作n-1,排序完成代码:void SelectSort(RecordType r[], int length)/*对记录数组r做简单选择排序,..

知识图谱完整项目实战(附源码)(2)_知识图谱实战_数据饕餮的博客-程序员秘密

一、前言本文是《知识图谱完整项目实战(附源码)》系列博文的第2篇:汽车知识图谱完整项目案例演示,主要介绍汽车领域知识图谱前端界面的功能演示。知识图谱的学习是一个基础到实战,从入门到精通的一个逐渐深入的、渐进式的过程。在这个过程中,一个完整的项目,起到的作用往往是对过往所学全部知识的串联和融合。只有经过一个完整项目的实践,才能真正把所学的、离散的、点状的知识点融合到一起,从而形成理论到实战的...

CentOS Liunx 7 下安装docker-compose_道一哥哥的博客-程序员秘密

1.参考传送门:CentOS7 安装 Docker 和 Docker-compose2. 问题国内下载偶尔会抽风 下载特别慢3.解决下载文章的资源解压获取到两个版本 选择liunx版本通过Xftp或者其他方式上传到 /usr/local/bin 路径通过Xshell 重命名文件命mv /usr/local/bin/docker-compose-Linux-x86_64 ...

系统测试_增量覆盖率统计方法_Linux C GCOV_brda fn fnda_dingboshuang的博客-程序员秘密

1 摘要 去年系统测试阶段测试覆盖工具试运行过几个项目,取得的效果一般,主要目前事业部的项目以维护为主,每次测试,都是增量加核心业务测试(用例很少),所以结果中会出现很多覆盖率为0或者很低的情况,所以如果分析测试覆盖会占用很多时间,投入产出比不合适;如果是新项目,版本变更比较频繁,亦不试用于项目的开始阶段。基于上面的问题,避免升级测试出现测试覆盖不到的情况,在现有的覆盖率统计工具进行二

随便推点

Excel 宏 将工作表中的数据按照顺序分拆到 本工作簿 的其他工作表_.cells(1, num) = myarray(num, 1)_度假的小鱼的博客-程序员秘密

(本文版本office2016)1.需要打开“开发工具”选项2.定义宏3.代码(文档最后)4.执行5.拆分完成Sub CF() Dim myRange As Variant Dim myArray Dim titleRange As Range Dim title As Variant Dim col...

如何才能成为一个好的Java架构师_java_jiagoushi的博客-程序员秘密

一、架构的定义所谓一千个架构师中有一千种“最好的架构”模式。“架构”是我们这行业种一个很常见的词,表明其必然也是经历了很长的岁月打磨所形成的一个词。架构的这个词出现的意义是什么?为了解决什么问题?只有把这2个问题想明白了,才能设计出一个良好的项目架构。我认为 架构类似于画房屋设计图,在刚开始我们盖一层楼的小房子的时候,拍拍脑门想一下,脑子里有个大概的样子就开始动工了,想怎么盖就怎么盖,大部...

15.IDA-查看XREF列表_ida7.7 如何查看xrefs_花熊的博客-程序员秘密

查看XREF列表在某个位置显示的交叉引用注释的数量由ida.cfg控制,其默认设置为2,当然你可以改变它SHOW_XREFS = 2 // Show 2 cross-references (the rest is accessible by Ctrl-X)示例代码:int read_it; int write_it; int ref_it; void callflow() {} int

【ORA】ORA-00257 archiver error. 错误的处理方法_zclinux_的博客-程序员秘密

今天连接数据库,结果报错,ora-00257查看[[email protected] oracle]$ oerr ora 0025700257, 00000, "archiver error. Connect internal only, until freed."// *Cause:  The archiver process received an error while trying

嗅探的基本原理 _密码嗅探原理_inject2006的博客-程序员秘密

嗅探的基本原理 [ 作者:佚名    转贴自:Internet    点击数:2607    更新时间:2006-11-29    文章录入:Admin ]  一 前言   SNIFF真是一个古老的话题,关于在网络上采用SNIFF来获取敏感信息已经不是什么新鲜事,也不乏很多成功的案例,那么,SNIFF究竟是什么呢?SNIFF就是嗅探器,就是窃听器,SNIFF静悄悄的工作在网络的底层,把你的秘密全部

软件开发过程中的Visio使用_跨系统流程图_one_week的博客-程序员秘密

文章目录前言一、思维导图二、使用步骤1.引入库2.读入数据总结前言在软件需求分析和设计过程中,时常需要通过图形来梳理和表达软件的运行原理。visio是一款专业的办公绘图软件,具有简单、便捷等关键特性。它能够帮助我们将自己的思想、设计与最终产品演变成形象化的图形进行传播。对于从事软件行业的同学们来说,我们可以用它绘制思维导图、用列图、逻辑部署、物理部署、流程图、时序图等等。本文以实际项目来介绍项目中的图形应用一、思维导图示例:pandas 是基于NumPy 的一种工具,该工具是为了解决数据分析任务