建议80:多线程使用Vector或HashTable
Vector是ArrayList的多线程版本,HashTable是HashMap的多线程版本,售票程序。把ArrayList替换成Vector后,结果依旧。仍然抛出相同的异常,
public class test {
public static void main(String[] args) {
// 火车票列表
final List<String> tickets = new ArrayList<String>(100000);
// 初始化票据池
for (int i = 0; i < 100000; i++) {
tickets.add("火车票" + i);
}
// 退票
Thread returnThread = new Thread() {
@Override
public void run() {
while (true) {
tickets.add("车票" + new Random().nextInt());
}
};
};
// 售票
Thread saleThread = new Thread() {
@Override
public void run() {
for (String ticket : tickets) {
tickets.remove(ticket);
}
}
};
// 启动退票线程
returnThread.start();
// 启动售票线程
saleThread.start();
}
}
因为混淆了线程安全和同步修改异常,基本上所有的集合类都有一个快速失败(Fail-Fast)的校验机制,当一个集合在被多个线程修改并访问时,就可能出现ConcurrentModificationException异常,这是为了确保集合方法一致而设置的保护措施,它的实现原理就是我们经常提到的modCount修改计数器:如果在读列表时,modCount发生变化(也就是有其它线程修改)则会抛出ConcurrentModificationException异常,这与线程同步是两码事,线程同步是为了保护集合中的数据不被脏读、脏写而设置的,
public static void main(String[] args) {
// 火车票列表
final List<String> tickets = new ArrayList<String>(100000);
// 初始化票据池
for (int i = 0; i < 100000; i++) {
tickets.add("火车票" + i);
}
// 10个窗口售票
for (int i = 0; i < 10; i++) {
new Thread() {
public void run() {
while (true) {
System.out.println(Thread.currentThread().getId()
+ "----" + tickets.remove(0));
if (tickets.size() == 0) {
break;
}
}
};
}.start();
}
}
结果会出现两个线程在卖同一张火车票,这才是线程不同步的问题,此时把ArrayList修改为Vector即可解决问题,因为Vector的每个方法前都加上了synchronized关键字,同时知会允许一个线程进入该方法,确保了程序的可靠性。虽然在系统开发中我们一再说明,除非必要否则不要使用synchronized,这是从性能的角度考虑的,但是一旦涉及到多线程(注意这里说的是真正的多线程,并不是并发修改的问题,比如一个线程增加,一个线程删除,这不属于多线程的范畴),Vector会是最佳选择,当然自己在程序中加synchronized也是可行的方法。HashMap的线程安全类HashTable与此相同。
建议81:非稳定排序推荐使用List
Set和List的最大区别就是Set中的元素不可以重复(这个重复指的是equals方法的返回值相等),其它方面则没有太大区别了,在Set的实现类中有一个比较常用的类需:TreeSet,该类实现了默认排序为升序的Set集合,如果插入一个元素,默认会按照升序排列(当然是根据Comparable接口的compareTo的返回值确定排序位置了),不过,这样的排序是不是在元素经常变化的场景中也适用呢?SortedSet接口(TreeSet实现了此接口)只是定义了在给集合加入元素时将其进行排序,并不能保证元素修改后的排序结果,因此TreeSet适用于不变量的集合数据排序,比如String、Integer等类型,但不适用与可变量的排序,特别是不确定何时元素会发生变化的数据集合。有两种方式解决:(1)Set集合重排序:重新生成一个Set对象,也就是对原有的Set对象重新排序,代码如下:set=new TreeSet<Person>(new ArrayList<Person>(set));使用TreeSet<SortedSet<E> s> 这个构造函数不是可以更好的解决问题吗?不行,该构造函数只是原Set的浅拷贝,如果里面有相同的元素,是不会重新排序的。(2)彻底重构掉TreeSet,使用List解决问题之所以使用TreeSet是希望实现自动排序,即使修改也能自动排序,既然它无法实现,那就用List来代替,然后使用Collections.sort()方法对List排序。两种方式都可以解决问题,如果需要保证集合中元素的唯一性,又要保证元素值修改后排序正确,那该如何处理呢?List不能保证集合中的元素唯一,它是可以重复的,而Set能保证元素唯一,不重复。如果采用List解决排序问题,就需要自行解决元素重复问题(若要剔除也很简单,转变为HashSet,剔除后再转回来)。若采用TreeSet,则需要解决元素修改后的排序问题。SortedSet中的元素被修改后可能会影响到其排序位置。
建议82:由点及面,集合大家族总结
Java中的集合类常用的ArrayList、HashMap,不常用的Stack、Queue,线程安全的Vector、HashTable,线程不安全的LinkedList、TreeMap,阻塞式的ArrayBlockingQueue,非阻塞式的PriorityQueue等,可以划分以下几类:
(1)List:实现List接口的集合主要有:ArrayList、LinkedList、Vector、Stack,其中ArrayList是一个动态数组,LinkedList是一个双向链表,Vector是一个线程安全的动态数组,Stack是一个对象栈,遵循先进后出的原则。
(2)Set:Set是不包含重复元素的集合,其主要实现类有:EnumSet、HashSet、TreeSet,其中EnumSet是枚举类型专用Set,所有元素都是枚举类型;HashSet是以哈希码决定其元素位置的Set,其原理与HashMap相似,它提供快速的插入和查找方法;TreeSet是一个自动排序的Set,它实现了SortedSet接口。
(3)Map:Map是一个大家族,他可以分为排序Map和非排序Map,排序Map主要是TreeMap类,他根据key值进行自动排序;非排序Map主要包括:HashMap、HashTable、Properties、EnumMap等,其中Properties是HashTable的子类,它的主要用途是从Property文件中加载数据,并提供方便的操作,EnumMap则是要求其Key必须是某一个枚举类型。Map中还有一个WeakHashMap类需要说明它是一个采用弱键方式实现的Map类,它的特点是:WeakHashMap对象的存在并不会阻止垃圾回收器对键值对的回收,也就是说使用WeakHashMap装载数据不用担心内存溢出的问题,GC会自动删除不用的键值对,这是好事。但也存在一个严重的问题:GC是静悄悄的回收的(何时回收,God,Knows!)我们的程序无法知晓该动作,存在着重大的隐患。
(4)Queue:分为两类一类是阻塞式队列,队列满了以后再插入元素会抛出异常,主要包括:ArrayBlockingQueue、PriorityQueue、LinkedBlockingQueue,其中ArrayBlockingQueue是一个以数组方式实现的有界阻塞队列;另一类是非阻塞队列,无边界的,只要内存允许,都可以持续追加元素,经常使用的是PriorityQuene类。还有一种是双端队列支持在头、尾两端插入和移除元素,主要实现类是:ArrayDeque、LinkedBlockingDeque、LinkedList。
(5)数组:数组与集合的最大区别就是数组能够容纳基本类型,而集合就不行,更重要的一点就是所有的集合底层存储的都是数组。
(6)工具类:数组的工具类是java.util.Arrays和java.lang.reflect.Array,集合的工具类是java.util.Collections,有了这两个工具类,操作数组和集合就会易如反掌。
(7)扩展类:集合类当然可以自行扩展了,可以使用Apache的common-collections扩展包,也可以使用Google的google-collections扩展包,这些足以应对我们的开发需要。
建议83:推荐使用枚举定义常量
Java1.5之前,只有两种方式的声明:类常量和接口常量,若在项目中使用的是Java1.5之前的版本,基本上都是如此定义的。在1.5版本以后有了改进,即新增了一种常量声明方式:枚举声明常量,看如下代码:
enum Season { Spring, Summer, Autumn, Winter; }JLS(Java Language Specification,Java语言规范)提倡枚举项全部大写,字母之间用下划线分割,这也是从常量的角度考虑的(当然,使用类似类名的命名方式也是比较友好的)。表现为1)枚举常量简单,枚举常量只需定义每个枚举项,不需要定义枚举值,而接口常量(或类常量)则必须定义值,否则编译不通过。2)枚举常量属于稳态型。3.枚举具有内置方法。4.枚举可以自定义的方法。
建议84:使用构造函数协助描述枚举项
一般来说,我们经常使用的枚举项只有一个属性,即排序号其默认值是从0、1、2......,但是除了排序号之外,枚举还有一个(或多个)属性:枚举描述,他的含义是通过枚举的构造函数,声明每个枚举项(也就是枚举的实例)必须具有的属性和行为,这是对枚举项的描述或补充,目的是使枚举项描述的意义更加清晰准确。
建议85:小心switch带来的空指针异常
Java中的switch语句只能判断byte、short、char、int类型(JDk7允许使用String类型),这是Java编译器的限制switch语句是先计算season变量的排序值,然后与枚举常量的每个排序值进行对比,例子中season是null,无法执行ordinal()方法,于是就报空指针异常了。解决很简单,在doSports方法中判断输入参数是否为null即可。
建议86:在switch的default代码块中增加AssertionError错误
switch后跟枚举类型,case后列出所有的枚举项,这是一个使用枚举的主流写法,那留着default语句似乎没有任何作用,程序已经列举了所有的可能选项,肯定不会执行到defaut语句,看上去纯属多余嘛?错误:switch代码与枚举之间没有强制约束关系,也就是说两者只是在语义上建立了联系,并没有一个强制约束,比如枚举项发生变化了,增加了一个枚举项F,如果此时我们对switch语句不做任何修改,编译虽不会出问题,但是运行期会发生非预期的错误:F类型没有输出。为了避免出现这类错误,建议在default后直接抛出一个AssertionError错误,其含义就是“不要跑到这里来,一跑到这里就会出问题”,这样可以保证在增加一个枚举项的情况下,若其它代码未修改,运行期马上就会出错,这样一来就很容易找到错误,方便立即排除。当然也有其它方法解决此问题,比如修改IDE工具,以Eclipse为例,可以把Java-->Compiler--->Errors/Warnings中的“Enum type constant not covered on 'switch' ”设置为Error级别,如果不判断所有的枚举项就不能编译通过。
建议87:使用valueOf前必须进行校验
每个枚举项都是java.lang.Enum的子类,都可以访问Enum类提供的方法,比如hashCode、name、valueOf等,其中valueOf方法会把一个String类型的名称转换为枚举项,也就是在枚举项中查找出字面值与参数相等的枚举项。虽然这个方法简单,但是JDK却做了一个对于开发人员来说并不简单的处理。valueOf方法先通过反射从枚举类的常量声明中查找,若找到就直接返回,若找不到则抛出无效参数异常。valueOf的本意是保护编码中的枚举安全性,使其不产生空枚举对象,简化枚举操作,但是却引入了一个我们无法避免的IllegalArgumentException异常。
建议88:用枚举实现工厂方法模式更简洁
工厂方法模式(Factory Method Pattern)是" 创建对象的接口,让子类决定实例化哪一个类,并使一个类的实例化延迟到其它子类"。工厂方法模式在开发中经常会用到。
//抽象产品
interface Car{
}
//具体产品类
class FordCar implements Car{
}
//具体产品类
class BuickCar implements Car{
}
//工厂类
class CarFactory{
//生产汽车
public static Car createCar(Class<? extends Car> c){
try {
return c.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
}
public static void main(String[] args) {
//生产车辆
Car car = CarFactory.createCar(FordCar.class);
}
(1)枚举非静态方法实现工厂方法模式。(2)通过抽象方法生成产品,枚举类型虽然不能继承,但是可以用abstract修饰其方法,此时就表示该枚举是一个抽象枚举,需要每个枚举项自行实现该方法,也就是说枚举项的类型是该枚举的一个子类。使用枚举类型的工厂方法模式有以下三个优点。1)避免错误调用的发生一般工厂方法模式中的生产方法(也就是createCar方法),可以接收三种类型的参数:类型参数(如我们的例子)、String参数(生产方法中判断String参数是需要生产什么产品)、int参数(根据int值判断需要生产什么类型的的产品),这三种参数都是宽泛的数据类型,很容易发生错误(比如边界问题、null值问题),而且出现这类错误编译器还不会报警。2)性能好,使用简洁:枚举类型的计算时以int类型的计算为基础的,这是最基本的操作,性能当然会快,至于使用便捷。3)降低类间耦合:不管生产方法接收的是Class、String还是int的参数,都会成为客户端类的负担,这些类并不是客户端需要的,而是因为工厂方法的限制必须输入的。
建议89:枚举项的数量限制在64个以内
Java提供了两个枚举集合:EnumSet和EnumMap,EnumSet表示其元素必须是某一枚举的枚举项,EnumMap表示Key值必须是某一枚举的枚举项,由于枚举类型的实例数量固定并且有限,相对来说EnumSet和EnumMap的效率会比其它Set和Map要高。虽然EnumSet很好用,但是它有一个隐藏的特点。在项目中一般会把枚举用作常量定义,可能会定义非常多的枚举项,然后通过EnumSet访问、遍历,但它对不同的枚举数量有不同的处理方式。当枚举项数量小于等于64时,创建一个RegularEnumSet实例对象,大于64时则创建一个JumboEnumSet实例对象。JumboEnumSet类把枚举项按照64个元素一组拆分成了多组,每组都映射到一个long类型的数字上,然后该数组再放置到elements数组中,简单来说JumboEnumSet类的原理与RegularEnumSet相似,只是JumboEnumSet使用了long数组容纳更多的枚举项。
建议90:小心注解继承
Java从1.5版本开始引入注解(Annotation),其目的是在不影响代码语义的情况下增强代码的可读性,并且不改变代码的执行逻辑,对于注解始终有两派争论,正方认为注解有益于数据与代码的耦合,"在有代码的周边集合数据";反方认为注解把代码和数据混淆在一起,增加了代码的易变性,消弱了程序的健壮性和稳定性。采用@Inherited元注解有利有弊,利的地方是一个注解只要标注到父类,所有的子类都会自动具有父类相同的注解,整齐,统一而且便于管理,弊的地方是单单阅读子类代码,我们无从知道为何逻辑会被改变,因为子类没有显示标注该注解。总体上来说,使用@Inherited元注解弊大于利,特别是一个类的继承层次较深时,如果注解较多,则很难判断出那个注解对子类产生了逻辑劫持。
建议91:枚举和注解结合使用威力更大
注解的写法和接口很类似,都采用了关键字interface,而且都不能有实现代码,常量定义默认都是public static final 类型的等,它们的主要不同点是:注解要在interface前加上@字符,而且不能继承,不能实现,这经常会给我们的开发带来些障碍。分析一下ACL(Access Control List,访问控制列表)设计案例,看看如何避免这些障碍,ACL有三个重要元素:资源,有哪些信息是要被控制起来的。2)权限级别,不同的访问者规划在不同的级别中。3)控制器(也叫鉴权人),控制不同的级别访问不同的资源。鉴权人是整个ACL的设计核心
interface Identifier{
//无权访问时的礼貌语
String REFUSE_WORD = "您无权访问";
//鉴权
public boolean identify();
}
这是一个鉴权人接口,定义了一个常量和一个鉴权方法。下来应该实现该鉴权方法,但问题是我们的权限级别和鉴权方法之间是紧耦合,若分拆成两个类显得有点啰嗦,怎么办?我们可以直接顶一个枚举来实现,
enum CommonIdentifier implements Identifier {
// 权限级别
Reader, Author, Admin;
@Override
public boolean identify() {
return false;
}
}
定义了一个通用鉴权者,使用的是枚举类型,并且实现了鉴权者接口。现在就剩下资源定义了,资源就是写的类、方法等,之后再通过配置来决定哪些类、方法允许什么级别的访问,这里的问题是:怎么把资源和权限级别关联起来呢?使用XML配置文件?是个方法,但对我们的示例程序来说显得太繁重了,如果使用注解会更简洁些,不过这需要我们首先定义出权限级别的注解,
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Access{
//什么级别可以访问,默认是管理员
CommonIdentifier level () default CommonIdentifier.Admin;
}
该注解释标注在类上面的并且会保留到运行期。定义一个资源类
@Access(level=CommonIdentifier.Author)
class Foo{ }
Foo类只能是作者级别的人访问。场景都定义完毕了,那如何模拟ACL实现
public static void main(String[] args) {
// 初始化商业逻辑
Foo b = new Foo();
// 获取注解
Access access = b.getClass().getAnnotation(Access.class);
// 没有Access注解或者鉴权失败
if (null == access || !access.level().identify()) {
// 没有Access注解或者鉴权失败
System.out.println(access.level().REFUSE_WORD);
}
}
通过ClassLoader类来解释该注解的,那会使我们的开发更简洁,所有的开发人员只要增加注解即可解决访问控制问题。access是一个注解类型,我们想使用Identifier接口的identity鉴权方法和REFUSE_WORD常量,但注解释不能集成的,那怎么办?此处可通过枚举类型CommonIdentifier从中间做一个委派动作(Delegate),委派?你可以然identity返回一个对象,或者在Identifier上直接定义一个常量对象,那就是“赤裸裸” 的委派了。
建议92:注意@Override不同版本的区别
@Override注解用于方法的覆写上它是在编译器有效,也就是Java编译器在编译时会根据注解检查方法是否真的是覆写,如果不是就报错拒绝编译。该注解可以很大程度地解决我们的误写问题,比如子类和父类的方法名少写一个字符,或者是数字0和字母O为区分出来等,这基本是每个程序员都曾将犯过的错误。在代码中加上@Override注解基本上可以杜绝出现此类问题,但是@Override有个版本问题。
interface Foo {
public void doSomething();
}
class FooImpl implements Foo{
@Override
public void doSomething() {
}
}
接口中定义了一个doSomething方法,实现类FooImpl实现此方法,并且在方法前加上了@Override注解。这段代码在Java1.6版本上编译没问题,虽然doSomething方法只是实现了接口的定义,严格来说并不是覆写,但@Override出现在这里可减少代码中出现的错误可如果在Java1.5版本上编译此段代码可能会出现错误:
The method doSomeThing() of type FooImpl must override a superclass method 注意这是个错误,不能继续编译,原因是Java1.5版本的@Override是严格遵守覆写的定义:子类方法与父类方法必须具有相同的方法名、输出参数、输出参数(允许子类缩小)、访问权限(允许子类扩大),父类必须是一个类,不能是接口,否则不能算是覆写。而这在Java1.6就开放了很多,实现接口的方法也可以加上@Override注解了,可以避免粗心大意导致方法名称与接口不一致的情况发生。在多环境部署应用时,需呀考虑@Override在不同版本下代表的意义,如果是Java1.6版本的程序移植到1.5版本环境中,就需要删除实现接口方法上的@Override注解。
建议93:Java的泛型是可以擦除的
泛型可以减少强制类型的转换,可以规范集合的元素类型,还可以提高代码的安全性和可读性,反射可以“看透” 程序的运行情况,可以让我们在运行期知晓一个类或实例的运行状况,可以动态的加载和调用,虽然有一定的性能忧患,但它带给我们的遍历远远大于其性能缺陷。Java泛型(Generic) 的引入加强了参数类型的安全性,减少了类型的转换,它与C++中的模板(Temeplates) 比较类似,但是有一点不同的是:Java的泛型在编译器有效,在运行期被删除,也就是说所有的泛型参数类型在编译后会被清除掉。
public class test {
//arrayMethod接收数组参数,并进行重载
public void arrayMethod(String[] intArray) {
}
public void arrayMethod(Integer[] intArray) {
}
//listMethod接收泛型List参数,并进行重载
public void listMethod(List<String> stringList) {
}
public void listMethod(List<Integer> intList) {
}
}
报错Method listMethod(List<String>) has the same erasure listMethod(List<E>) as another method in type testlistMethod(List<Integer> intList)方法在编译时擦除类型后是listMethod(List<E> intList)与另一个方法重复。这就是Java泛型擦除引起的问题:在编译后所有的泛型类型都会做相应的转化。转换规则如下:
1)List<String>、List<Integer>、List<T>擦除后的类型为List。2)List<String>[] 擦除后的类型为List[]。3)List<? extends E> 、List<? super E> 擦除后的类型为List<E>。4)List<T extends Serializable & Cloneable >擦除后的类型为List< Serializable>。Java编译后字节码中已经没有泛型的任何信息了,也就是说一个泛型类和一个普通类在经过编译后都指向了同一字节码,比如Foo<T>类,经过编译后将只有一份Foo.class类,不管是Foo<String>还是Foo<Integer>引用的都是同一字节码。Java之所以如此处理,有两个原因:1)避免JVM的大换血。C++泛型生命期延续到了运行期,而Java是在编译期擦除掉的,我们想想,如果JVM也把泛型类型延续到运行期,那么JVM就需要进行大量的重构工作了。2)版本兼容:在编译期擦除可以更好的支持原生类型(Raw Type),在Java1.5或1.6...平台上,即使声明一个List这样的原生类型也是可以正常编译通过的,只是会产生警告信息而已。
1)泛型的class对象是相同的:每个类都有一个class属性,泛型化不会改变class属性的返回值
List<String> list = new ArrayList<String>();List<Integer> list2 = new ArrayList<Integer>();
System.out.println(list.getClass()==list2.getClass());以上代码返回true,原因很简单,List<String>和List<Integer>擦除后的类型都是List,没有任何区别。2:泛型数组初始化时不能声明泛型。如下代码编译时通不过:
List<String>[] listArray = new List<String>[]; 原因很简单,可以声明一个带有泛型参数的数组,但不能初始化该数组,因为执行了类型擦除操作,List<Object>[]与List<String>[] 就是同一回事了,编译器拒绝如此声明。
3:instanceof不允许存在泛型参数以下代码不能通过编译,原因一样,泛型类型被擦除了:
List<String> list = new ArrayList<String>();
建议94:不能初始化泛型参数和数组
泛型类型在编译期被擦除,我们在类初始化时将无法获得泛型的具体参数
class Test<T> {
private T t = new T();
private T[] tArray = new T[5];
private List<T> list = new ArrayList<T>();
}
t、tArray、list都是类变量,都是通过new声明了一个类型,非常相似但这段代码是编译不过的,因为编译器在编译时需要获得T类型,但泛型在编译期类型已经被擦除了,所有new T()和 new T[5]都会报错(有人可能会有疑问,泛型类型可以擦除为顶级Object,那T类型擦除成Object不就可以编译了吗?这样也不行,泛型只是Java语言的一部分,Java语言毕竟是一个强类型、编译型的安全语言,要确保运行期的稳定性和安全性就必须要求在编译器上严格检查)。可为什么new ArrayList<T>()却不会报错呢?这是因为ArrayList表面是泛型,其实已经在编译期转为Object了,我们来看一下ArrayList的源代码就清楚了,代码如下
public class ArrayList<E> extends AbstractList<E> implements List<E>,
RandomAccess, Cloneable, java.io.Serializable {
// 容纳元素的数组
private transient Object[] elementData;
// 构造函数
public ArrayList() {
this(10);
}
// 获得一个元素
public E get(int index) {
rangeCheck(index);
// 返回前强制类型转换
return elementData(index);
}
/* 其它代码略 */
}
elementData的定义,它容纳了ArrayList的所有元素,其类型是Object数组,因为Object是所有类的父类,数组又允许协变(Covariant),因此elementData数组可以容纳所有的实例对象。元素加入时向上转型为Object类型(E类型转换为Object),取出时向下转型为E类型,如此处理而已。在某些情况下,我们需要泛型数组,那该如何处理呢
class Test<T> {
// 不再初始化,由构造函数初始化
private T t;
private T[] tArray;
private List<T> list = new ArrayList<T>();
// 构造函数初始化
public Test() {
try {
Class<?> tType = Class.forName("");
t = (T) tType.newInstance();
tArray = (T[]) Array.newInstance(tType, 5);
} catch (Exception e) {
e.printStackTrace();
}
}
}
此时运行就没有什么问题了剩下的问题就是怎么在运行期获得T的类型,也就是tType参数,一般情况下泛型类型是无法获取的,不过在客户端调用时多传输一个T类型的class就会解决问题。类的成员变量是在类初始化前初始化的,所以要求在初始化前它必须具有明确的类型,否则就只能声明不能初始化。
建议95:强制声明泛型的实际类型
Arrays工具类有一个方法asList可以把一个变长参数或数组转变为列表,但是它有一个缺点:它所生成的list长度是不可变的,而这在我们的项目开发中有时会很不方便。如果你期望生成的列表长度可变,那就需要自己来写一个数组的工具类了
class ArrayUtils {
// 把一个变长参数转化为列表,并且长度可变
public static <T> List<T> asList(T... t) {
List<T> list = new ArrayList<T>();
Collections.addAll(list, t);
return list;
}
}
public static void main(String[] args) {
// 正常用法
List<String> list1 = ArrayUtils.asList("A", "B");
// 参数为空
List list2 = ArrayUtils.asList();
// 参数为整型和浮点型的混合
List list3 = ArrayUtils.asList(1, 2, 3.1);
}
与Arrays.asList的调用方式相同,我们传入一个泛型对象,然后返回相应的List。(1)变量list1:变量list1是一个常规用法,没有任何问题,泛型实际参数类型是String,返回结果就是一个容纳String元素的List对象。(2)变量list2:变量list2它容纳的是什么元素呢?我们无法从代码中推断出list2列表到底容纳的是什么元素(因为它传递的参数是空,编译器也不知道泛型的实际参数类型是什么),不过,编译器会很聪明地推断出最顶层类Object就是其泛型类型。(3)变量list3:变量list3有两种类型的元素:整数类型和浮点类型,那它生成的List泛型化参数应该是什么呢。参照list2变量,代码修改如下:List<Number> list3 = ArrayUtils.<Number>asList(1, 2, 3.1); Number是Integer和Float的父类,先把三个输入参数、输出参数同类型,问题是我们要在什么时候明确泛型类型呢?一句话:无法从代码中推断出泛型的情况下,即可强制声明泛型类型。
建议96:不同的场景使用不同的泛型通配符
Java泛型支持通配符(Wildcard),可以单独使用一个“?”表示任意类,也可以使用extends关键字表示某一个类(接口)的子类型,还可以使用super关键字表示某一个类(接口)的父类型,但问题是什么时候该用extends,什么该用super。(1)泛型结构只参与 “读” 操作则限定上界(extends关键字)。(2)泛型结构只参与“写” 操作则限定下界(使用super关键字)。
建议97:警惕泛型是不能协变和逆变的
在编程语言的类型框架中,协变和逆变是指宽类型和窄类型在某种情况下(如参数、泛型、返回值)替换或交换的特性,简单的说,协变是一个窄类型替换宽类型,而逆变则是用宽类型覆盖窄类型。(1)泛型不支持协变:数组和泛型很相似,一个是中括号,一个是尖括号
public static void main(String[] args) {
//数组支持协变
Number [] n = new Integer[10];
//编译不通过,泛型不支持协变
List<Number> list = new ArrayList<Integer>();
}
ArrayList是List的子类型,Integer是Number的子类型,里氏替换原则在此行不通了,原因就是Java为了保证运行期的安全性,必须保证泛型参数的类型是固定的,所以它不允许一个泛型参数可以同时包含两种类型,即使是父子类关系也不行。泛型不支持协变,但可以使用通配符模拟协变, //Number子类型(包括Number类型) 都可以是泛型参数类型List<? extends Number> list = new ArrayList<Integer>();" ? extends Number " 表示的意思是,允许Number的所有子类(包括自身) 作为泛型参数类型,但在运行期只能是一个具体类型,或者是Integer类型,或者是Double类型,或者是Number类型,也就是说通配符只在编码期有效,运行期则必须是一个确定的类型。(2)泛型不支持逆变。java虽然允许逆变存在,但在对类型赋值上是不允许逆变的,你不能把一个父类实例对象赋给一个子类类型变量,泛型自然也不允许此种情况发生了。但是它可以使用super关键字来模拟实现,代码如下: //Integer的父类型(包括Integer)都可以是泛型参数类型List<? super Integer> list = new ArrayList<Number>();
" ? super Integer " 的意思是可以把所有的Integer父类型(自身、父类或接口) 作为泛型参数,这里看着就像是把一个Number类型的ArrayList赋值给了Integer类型的List,其外观类似于使用一个宽类型覆盖一个窄类型,它模拟了逆变的实现。泛型既不支持协变,也不支持逆变,带有泛型参数的子类型定义与我们经常使用的类类型也不相同,其基本类型关系如下所示:Integer是Number的子类型,ArrayList<Integer>是List<Integer> 的子类型,Integer[]是 Number[]的子类型。List<Integer>不是 List<Number> 的子类型,List<Integer> 不是 List<? extends Integer> 的子类型。List<Integer> 不是 List<? super Integer> 的子类型。Java的泛型是不支持协变和逆变的,只是能够实现逆变和协变。
建议98:建议采用顺序是List中泛型顺序依次为T、?、Object
List<T>、List<?>、List<Object>这三者都可以容纳所有的对象,但使用的顺序应该是首选List<T>,次之List<?>,最后选择List<Object>,因为(1)List<T>是确定的某一个类型List<T>表示的是List集合中的元素都为T类型,具体类型在运行期决定List<?>表示的是任意类型,与List<T>类似,而List<Object>则表示List集合中的所有元素为Object类型,因为Object是所有类的父类,所以List<Object>也可以容纳所有的类型,从这一字面意义上分析,List<T>更符合习惯:编码者知道它是某一个类型,只是在运行期才确定而已。(2)List<T>可以进行读写操作List<T>可以进行诸如add,remove等操作,因为它的类型是固定的T类型,在编码期不需要进行任何的转型操作。List<T>是只读类型的,不能进行增加、修改操作,因为编译器不知道List中容纳的是什么类型的元素,也就无法校验类型是否安全了,而且List<?>读取出的元素都是Object类型的,需要主动转型,所以它经常用于泛型方法的返回值。注意List<?>虽然无法增加,修改元素,但是却可以删除元素,比如执行remove、clear等方法,那是因为它的删除动作与泛型类型无关。
List<Object> 也可以读写操作,但是它执行写入操作时需要向上转型(Up cast),在读取数据的时候需要向下转型,而此时已经失去了泛型存在的意义了。打个比方有一个篮子用来容纳物品,如西瓜,番茄等.List<?>的意思是说,“我这里有一个篮子,可以容纳固定类别的东西,比如西瓜,番茄等”。List<?>的意思是说:“有一个篮子,我可以容纳任何东西,只要是你想得到的”。而List<Object>就是" 嘿,我也有一个篮子,我可以容纳所有物质,只要你认为是物质的东西都可以容纳进来 "。推而广之,Dao<T>应该比Dao<?>、Dao<Object>更先采用,Desc<Person>则比Desc<?>、Desc<Object>更优先采用。
建议99:严格限定泛型类型采用多重界限
interface Staff {
// 工资
public int getSalary();
}
interface Passenger {
// 是否是站立状态
public boolean isStanding();
}
//定义我这个类型的人
class Me implements Staff, Passenger {
@Override
public boolean isStanding() {
return true;
}
@Override
public int getSalary() {
return 2000;
}}
"Me"这种类型的人物有很多,比如系统分析师坐公交车,但他的工资实现就和我不同,再比如Boss级偶尔坐公交,也就是说如果我们使用“T extends Me”是限定不了需求对象的,那可以考虑使用多重限定,代码如下
public class test {
//工资低于2500的并且站立的乘客车票打8折
public static <T extends Staff & Passenger> void discount(T t) {
if (t.getSalary() < 2500 && t.isStanding()) {
System.out.println(" 恭喜您,您的车票打八折!");
}
}
public static void main(String[] args) {
discount(new Me());
}
}
使用“&”符号设定多重边界,指定泛型类型T必须是Staff和Passenger的共有子类型,此时变量t就具有了所有限定的方法和属性,要再进行判断就一如反掌了。在Java的泛型中,可以使用"&"符号关联多个上界并实现多个边界限定,而且只有上界才有此限定,下界没有多重限定的情况。多个下界,编码者可自行推断出具体的类型,比如“? super Integer” 和 “? extends Double”,可以更细化为Number类型了,或者Object类型了,无需编译器推断了。为什么要说明多重边界?是因为编码者太少使用它了,比如一个判断用户权限的方法,使用的是策略模式(Strategy Pattern)
class UserHandler<T extends User> {
// 判断用户是否有权限执行操作
public boolean permit(T user, List<Job> jobs) {
List<Class<?>> iList = Arrays.asList(user.getClass().getInterfaces());
// 判断 是否是管理员
if (iList.indexOf(Admin.class) > -1) {
Admin admin = (Admin) user;
// 判断管理员是否有此权限
} else {
// 判断普通用户是否有此权限
}
return false;
}
}
class User {}
class Job {}
class Admin extends User {}
此处进行了一次泛型参数类别判断,这里不仅仅违背了单一职责原则(Single Responsibility Principle),而且让泛型很“汗颜” :已经使用了泛型限定参数的边界了,还要进行泛型类型判断。事实上,使用多重边界可以很方便的解决此问题,而且非常优雅,建议大家 在开发中考虑使用多重限定。
建议100:数组的真实类型必须是泛型类型的子类型
List接口的toArray方法可以把一个集合转化为数组,但是使用不方便,toArray()方法返回的是一个Object数组,所以需要自行转变。toArray(T[] a)虽然返回的是T类型的数组,但是还需要传入一个T类型的数组,这也挺麻烦的,我们期望输入的是一个泛型化的List,这样就能转化为泛型数组了。
public static <T> T[] toArray(List<T> list) {
T[] t = (T[]) new Object[list.size()];
for (int i = 0, n = list.size(); i < n; i++) {
t[i] = list.get(i);
}
return t;
}
public static void main(String[] args) {
List<String> list = Arrays.asList("A","B");
for(String str :toArray(list)){
System.out.println(str);
}
}
类型转换异常,也就是说不能把一个Object数组转换为String数组,这段异常包含了两个问题:
1)为什么Object数组不能向下转型为String数组:数组是一个容器,只有确保容器内的所有元素类型与期望的类型有父子关系时才能转换,Object数组只能保证数组内的元素是Object类型,却不能确保它们都是String的父类型或子类,所以类型转换失败。
2)为什么是main方法抛出异常,而不是toArray方法:其实是在toArray方法中进行的类型向下转换,而不是main方法中。那为什么异常会在main方法中抛出,应该在toArray方法的“ T[] t = (T[]) new Object[list.size()];”这段代码才对呀?那是因为泛型是类型擦除的,toArray方法经过编译后与如下代码相同
public static Object[] toArrayTwo(List list) {
// 此处的强制类型转换没必要存在,只是为了与源代码对比
Object[] t = (Object[]) new Object[list.size()];
for (int i = 0, n = list.size(); i < n; i++) {
t[i] = list.get(i);
}
return t;
}
public static void main(String[] args) {
List<String> list = Arrays.asList("A", "B");
for (String str : (String [])toArrayTwo(list)) {
System.out.println(str);
}
}
toArray方法返回后进行一次类型转换,Object数组转换成了String数组,于是就报ClassCastException异常了。要想把一个Object数组转换为String数组,只要Object数组的实际类型也就是String就可以了。
建议101:注意Class类的特殊性
Java语言是先把Java源文件编译成后缀为class的字节码文件,然后再通过ClassLoader机制把这些类文件加载到内存中,最后生成实例执行的,这是Java处理的基本机制,但是加载到内存中的数据如何描述一个类的、如何展现。Java使用一个元类(MetaClass)来描述加载到内存中的类数据,这就是Class类,它是一个描述类的类对象,比如Dog.class文件加载到内存中后就会有一个class的实例对象描述之。因为是Class类是“类中类”,也就有预示着它有很多特殊的地方:1)无构造函数:Java中的类一般都有构造函数,用于创建实例对象,但是Class类却没有构造函数,不能实例化,Class对象是在加载类时由Java虚拟机通过调用类加载器中的difineClass方法自动构造的。2)可以描述基本类型:虽然8个基本类型在JVM中并不是一个对象,它们一般存在于栈内存中,但是Class类仍然可以描述它们,例如可以使用int.class表示int类型的类对象。3)其对象都是单例模式:一个Class的实例对象描述一个类,并且只描述一个类,反过来也成立。一个类只有一个Class实例对象,如下代码返回的结果都为true:
// 类的属性class所引用的对象与实例对象的getClass返回值相同
boolean b1=String.class.equals(new String().getClass());
boolean b2="ABC".getClass().equals(String.class);
// class实例对象不区分泛型
boolean b3=ArrayList.class.equals(new ArrayList<String>().getClass());
Class类是Java的反射入口,只有在获得了一个类的描述对象后才能动态的加载、调用,一般获得一个Class对象有三种途径:1)类属性方式:如String.class。2)对象的getClass方法,如new String().getClass()。3)forName方法加载:如Class.forName(" java.lang.String")。获得了Class对象后,就可以通过getAnnotations()获得注解,通过getMethods()获得方法,通过getConstructors()获得构造函数等,这位后续的反射代码铺平了道路。