来到读书笔记的第三篇了,这一次的主题是讲泛型、枚举、注解这三个Java1.5引入的语言特性。关于泛型,很久以前我写过一篇初识的文章,http://blog.csdn.net/leelit/article/details/39504873,里面的一些基础知识现在看起来依然能起到复习作用。而另外两个特性的基础知识这里不再展开,因为Effective Java这本书讲的就是一些编程技巧或者说良好的习惯。然而,这本书关于这三个特性的内容有一些还是有点太难了,看起来会有一种“我既没有看别人这样写过,我自己即使学会了也不太可能会这样写”的感觉。所以,这一部分还是会比较挑着看。
每个泛型都定义一个原生态类型(raw type),即不带任何实际类型参数的泛型名称。如果使用原生态类型,就是失掉了泛型在安全性和表述性方面的优势。
使用泛型List< Object>以允许插入任意对象还是可以的,它和原生态类型的区别是,前者明确告诉编译器它能持有任意类型,而后者则逃避了泛型检查。
虽然你可以将List< String>传递给List的参数,但是他不能传给List< Object>参数,List< String>和List< Object>都是原生类型的一个子类型,但两者之间并没有什么关系。
最后,有两个例外是需要使用原生类型的:
与有限制通配符相对应的是无限制通配符,如果使用泛型,不确定或者不关心实际的类型参数,就可以使用一个问号代替。例如,List< E>的无限制通配符类型为List< ?>。无限制通配符类型和原生类型的区别是,前者是类型安全的,后者是类型不安全的。你可以将任何元素放在原生类型的集合中,但是无限制通配符类型则不可以,因为它并不知道是否为合适的类型。
泛型是不可变的,参见3.1。为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。站在方法参数的角度,如果参数化类型表示一个T生产者,就是用< ? extends T>,如果表示一个T的消费者,就使用< ? super T>。
举个例子:
比如List接口的addAll方法,
boolean addAll(Collection extends E> c);
这种类型属于生产者,传入的参数生产出E的实例被方法消费。
比如BlockingQueue接口的drainTo方法:移除此队列中所有可用的元素,并将它们添加到给定 collection 中。
int drainTo(Collection super E> c)
这种类型则属于消费者,他是为了消费方法生产的E实例。
总而言之,在方法的角度来看,如果是消费,则参数的限制泛型类型参数使用extends,如果是被消费则是super。如果是站在方法参数的角度,则反之,正如上面举例所说。
数组和泛型相比,有两个重要的不同点。
一般来说数组和泛型不能很好地混合使用,如果你发现自己将它们混合起来使用,并且得到了编译器的错误或者警告,第一反应应该是用列表代替数组。
容器大家都明白,也叫集合,常见的就是List、Map这些实现类。类型安全指的就是泛型的编译期类型检查,而异构的意思是可变的键。我们一般固定思维会认为,一个集合类的键是不可变的,比如Map< String,String>他就代表着所有的键都为String。要实现异构,需要将键而不是容器进行参数化。比如某个泛型方法中的参数Class< T> type,使用String.class属于Class< String>类型,使用Integer.class属于Class< Integer>类型,两者都是合法的参数。当然还可以是任何其他的键作为参数,这样便称之为异构。
Java的枚举类型是功能十分齐全的类,客户端既不能创建枚举类型的实例,也不能对它进行扩展。因为没有实例,而只有声明过的枚举常量,所以它们是单例的泛型化。它比int枚举安全,表述性也更好,因为他有toString方法。
枚举类型还允许添加字段和方法,同样可以实现接口来实现多态,前面也说了他是一个功能齐全的类,而他的枚举常量可以视作单例。
当需要表示枚举常量的序数时,可以简单地使用一个int字段来表示。
没有注解以前,一般使用命名模式表明有些程序元素需要通过某种工具或者框架进行特殊处理。
但是这会有几个严重的问题:
而这几个问题注解都能很好解决,对于问题1注解可以提供编译检查;问题2、3可以通过使用注解元素来解决。
除了工具开发者,大多数程序员都不必定义注解类型,但是所有的程序员都应该使用Java平台所提供的预定义的注解类型,还要考虑使用IDE或者静态分析工具所提供的注解。比如说Override就是其中重要的一个。
简而言之,当你确信你是重写或者说覆盖父类方法或者实现接口方法时,都应该使用Override注解,编译器可以帮你防止可能存在的错误,比如说想要覆盖却写成重载。
标记接口是没有包含任何方法声明的接口,而只是表示一个类实现了具有某种属性的接口,比如常见的Serializable接口。
与标记接口类似的则是标记注解,标记注解与前者相比最大的优点在于可以为注解的目标提供元素信息。
如果想要定义一个任何新方法都不会与之关联的类型,标记接口就是最好的选择;如果想要标记的程序元素而非类和接口,考虑到未来可能要给标记添加更多的信息,标记注解是正确的选择。
原本异常应该放在方法那一节比较好,但是那一节的内容比较多,而这一节的内容又比较少,所以放在这吧,也没有太大影响。
Java语言提供了三种可抛出的结构(throwable):
如果期望调用者能够适当地恢复,对于这种情况应该使用受检异常;用运行时异常来表明编程错误;错误往往由JVM抛出,因此最好不要再实现任何新的Error子类,程序员应该使用的是RuntimeException的子类。
异常是为了在异常的情况下使用而设计的,因此不要将它们用于普通的控制流。
异常类型的toString()
方法应该尽可能地描述有关失败原因的信息,甚至是有利于分析这个异常的一些参数或字段的值,对于受检异常,还可以提供一些访问方法,以帮助调用者来恢复异常情况。
如果某个异常能够满足你的需要,就不要犹豫,使用就是。如果希望稍微增加更多的信息,可以放心地把现有的异常进行子类化。
异常 | 使用场合 |
---|---|
IllegalArgumentException | 非null的参数值不正确 |
IllegalStateException | 对于方法调用而言,对象状态不合适 |
NullPointerException | 禁止使用null的情况下参数值为null |
IndexOutOfBoundsException | 下标参数值越界 |
ConcurrentModificationException | 禁止并发修改的情况下,检测到对象的并发修改 |
UnsupportedOperationException | 对象不支持用户请求的方法 |
如果方法抛出的异常与它所执行的任务没有明显的联系,这种情况会使人不知所措。当方法传递(直接throws)由低级抽象抛出的异常时,往往会发生这种情况。
为了避免这个问题,高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常,这种做法被称为异常转译。异常转译的意思就是捕获调用的低层方法的异常,重新抛出更加合适的异常。
一种特殊的异常转译形式称为异常链,低层的异常被传到高层的异常,高层的异常提供访问方法(Throwable.getCause()
)来获得低层的异常。异常链也是异常转译,不过将低层的异常放在高层异常的构造器,异常链一般使用于,当低层的异常对于调试导致高层异常的问题有帮助的时候。
如果不能阻止或者处理来自更低层的异常,异常转译与异常链是可以考虑的选择。
失败的方法调用应该使对象保持在被调用之前的状态,这就叫方法的失败原子性。有几种途径可以实现这种效果:
一般而言,作为方法规范的一部分,即使产生任何异常,都应该让对象保持在该方法调用之前的状态。