Java集合框架源码分析html
本次源码分析对Java JDK中的集合框架部分展开分析,采用的是JDK 1.8.0_171版本的源码。java
Java集合框架(Java Collections Framework,JCF)也称容器,便可以容纳其余Java对象的对象。JCF为开发者提供了通用的容器,数据持有对象的方式和对数据集合的操做,优势是:程序员
1) 下降编程难度编程
2) 提升程序性能设计模式
3) 提升API间的互操做性数组
4) 下降学习难度缓存
5) 下降设计和实现相关API的难度安全
6) 增长程序的可重用性网络
Java容器中只能存放对象,对于基本类型(int,double,float,long等),须要将其包装成对象类型后(Integer,Double,Float,Long等)才能放到容器里。不少时候装箱和拆箱都可以自动完成。这虽然会致使额外的性能和空间开销,但简化了设计和编程。多线程
1.整体架构分析
为了规范容器的行为,统一设计,JCF定义了14种容器接口(Collection interface),它们的关系以下图所示:
Map接口没有继承自Collection接口,由于Map表示的是关联式的容器而不是集合,但Java提供了从Map转换到Collection的方法,能够方便地将Map切换到集合视图。上图中提供了Queue接口,但没有Stack,由于Stack的功能已被JDK 1.6版本引入的Deque取代。上述接口的通用实现以下表:
|
Implementations |
|||||
Hash Table |
Resizable Array |
Balanced Tree |
Linked List |
Hash Table +Linked List |
||
Inter faces |
Set |
HashSet |
|
TreeSet |
|
LinkedHashSet |
List |
|
ArrayList |
|
LinkedList |
|
|
Deque |
|
ArrayDeque |
|
LinkedList |
|
|
Map |
HashMap |
|
TreeMap |
|
LinkedHashMap |
整体上来讲,从下面的框架图能够看出,集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合,另外一种是图(Map),存储键/值对映射。Collection接口又有三种子类型,List,Set和Queue。再下面是一些抽象类,最后是一些具体实现类。
下面对部分具体实现类的实现作简单描述:
1) ArrayList:线程不一样步,默认初始容量为10,当数组大小不足时容量扩大为1.5倍。为追求效率,ArrayList没有实现同步(synchronized),若是须要多个线程并发访问,用户须要手动实现同步,或者用Vector代替。
2) LinkedList:线程不一样步,双向链表实现。LinkedList同时实现了List接口和Deque接口,因此既能够当作一个顺序容器,又能够当作一个队列,或者也能够看成一个栈。官方声明不建议使用Stack类,且也没有Queue的实现(只是接口),因此能够考虑用LinkedList来看成栈使用。首选的栈或队列实现,仍是ArrayDeque,性能更好。
3) Vector:线程同步,默认初始容量为10,当数组大小不足时容量扩大为2倍。他的同步是经过Iterator方法加synchronized实现的。
4) TreeSet:线程不一样步,内部使用NavigableMap操做。默认元素天然顺序排列,能够经过Comparator改变排序。TreeSet里面有一个TreeMap(适配器模式)。
5) HashSet:线程不一样步,内部使用HashMap进行数据存储,提供的方法基本都是调用HashMap的方法,二者本质上是相同的。集合元素能够为null。
6) Set:Set是一种不包含重复元素的集合,最多只有一个null元素。且Set集合一般能够经过Map集合经过适配器模式获得。
7) PriorityQueue:PriorityQueue实现了Queue接口,不容许放入null元素,其经过堆实现,即经过彻底二叉树实现小顶堆(任意一个非叶子节点的权值,都不大于其左右子节点的权值),也就意味着能够经过数组来做为其底层实现。
8) TreeMap:线程不一样步,基于红黑树的NavigableMap实现,可以把它保存的记录根据键排序,默认是按照键值的升序排序,也能够指定排序的比较器。当用Iterator遍历TreeMap时,获得的记录是排过序的。
9) HashMap:线程不一样步,根据key的hashcode进行存储,内部使用静态内部类Node的数组进行存储,默认初始大小为16,每次扩容一倍。当发生Hash冲突时,采用拉链法来解决。在1.8版本的JDK中,当单个桶中元素个数大于等于8时,链表改成红黑树实现;当元素个数小于6时,变回链表实现,由此来防止hashcode攻击。HashMap是HashTable的轻量级实现,能够接受null的key和value,而HashTable是不容许的。
10) LinkedHashMap:保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先获得的记录确定是先插入的。也能够在构造时带上参数,按照使用的次数排序,在遍历的时候会比HashMap慢。不过有例外状况,当HashMap容量很大,实际数据较少时,遍历起来可能会比LinkedHashMap慢,由于后者遍历速度只与实际数据量有关,和容量无关。
11) HashTable:线程安全,HashTable的迭代器是fail-fast迭代器。HashTable不能存储null的key和value。
12) Collections、Arrays:集合类的工具帮助类,提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各类操做。
13) Comparable、Comparator:通常是用于对象的比较来实现排序,略有区别。
2.源码分析
2.1 ArrayList
ArrayList实现了List接口,是顺序容器,即元素存放的数据与放入的顺序相同。容许放入null元素,底层经过数组实现。除未实现同步外,其他和Vector大体相同。每一个ArrayList都有一个容量(capacity),表示底层数组的实际大小,容器内存储元素的个数不能多于当前容量,容量不足自动扩容1.5倍。Size(),isEmpty(),get(),set()方法均能在常数时间内完成,add()方法的时间开销和插入位置有关,addAll()方法的时间开销跟添加元素的个数成正比,其他方法大可能是线性时间。
2.1.1 set()
因为底层是一个数组,set()方法也就变得很是简单,直接对数组的指定位置赋值便可。RangeCheck(index)用于检查下标是否越界,须要注意赋值语句仅仅是引用的复制。
1 public E set(int index, E element) { 2 rangeCheck(index); 3 E oldValue = elementData(index); 4 elementData[index] = element; 5 return oldValue; 6 }
2.1.2 get()
Get()方法也很简单,去对应下标位置获取元素便可,惟一要注意的是因为底层数组是Object[],获得元素后须要进行类型转换。
1 public E get(int index) { 2 rangeCheck(index); 3 return elementData(index); 4 }
2.1.3 add(int index,E element)
和C++的vector不一样,ArrayList没有push_back()方法,对应的是add(E e),也没有insert()方法,对应的是add(int index,E element)。这两个方法都是向容器中添加新元素,可能会致使capacity不足,所以在添加元素以前,都须要进行剩余空间检查。经过ensureCapacityInternal()方法判断,该方法内部继续嵌套调用,计算容量时,若全局数组为空,则返回传入参数和default capacity(为10)中较大的那个,若minCapacity较大,而后根据二者的差值调用grow()方法完成扩容。由源码中看出,对数组的扩容首先经过扩展为原来的1.5倍,而后和minCapacity比较大小,以后也会判断容量是否过大,设置超大容量,最后扩展和复制。因为Java GC自动管理了内存,也就不须要考虑源数组释放的问题。空间扩容后,插入过程也就变得容易,须要先对元素进行移动,而后完成插入操做,因此该方法为线性复杂度。
1 public void add(int index, E element) { 2 rangeCheckForAdd(index); 3 ensureCapacityInternal(size + 1); // Increments modCount!! 4 System.arraycopy(elementData, index, elementData, index + 1, size - index); 5 elementData[index] = element; 6 size++; 7 }
1 private void ensureCapacityInternal(int minCapacity) { 2 3 ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); 4 5 } 6 7 8 private void ensureExplicitCapacity(int minCapacity) { 9 10 modCount++; 11 12 // overflow-conscious code 13 14 if (minCapacity - elementData.length > 0) 15 16 grow(minCapacity); 17 18 } 19 20 21 private void grow(int minCapacity) { 22 23 // overflow-conscious code 24 25 int oldCapacity = elementData.length; 26 27 int newCapacity = oldCapacity + (oldCapacity >> 1); 28 29 if (newCapacity - minCapacity < 0) 30 31 newCapacity = minCapacity; 32 33 if (newCapacity - MAX_ARRAY_SIZE > 0) 34 35 newCapacity = hugeCapacity(minCapacity); 36 37 // minCapacity is usually close to size, so this is a win: 38 39 elementData = Arrays.copyOf(elementData, newCapacity); 40 41 }
2.1.4 remove()
Remove()方法有两个版本,一个以下所示,从指定位置删除元素,另外一个是remove(Object o),删除第一个知足o.equals(elementData[index])的元素。删除操做是add()操做的逆过程,须要将删除节点以后的元素向前移动一个位置。须要注意的是为了让GC起做用,必须显式地为最后一个位置赋null值。
1 public E remove(int index) { 2 3 rangeCheck(index); 4 5 modCount++; 6 7 E oldValue = elementData(index); 8 9 int numMoved = size - index - 1; 10 11 if (numMoved > 0) 12 13 System.arraycopy(elementData, index+1, elementData, index,numMoved); 14 15 elementData[--size] = null; // clear to let GC do its work 16 17 return oldValue; 18 19 }
因为各个不一样的集合类和结构大部分都是底层实现方式不一样,而功能基本相同,因此这里仅分析ArrayList一例。
2.2 容器中的设计模式
2.2.1 迭代器模式
Collection实现了Iterable接口,其中的iterator()方法可以产生一个Iterator对象,经过这个对象就能够迭代遍历Collection中的元素。从JDK1.5以后可使用foreach方法来遍历实现了Iterable接口的聚合对象。
1 List<String> list = new ArrayList<>(); 2 3 list.add("a"); 4 5 list.add("b"); 6 7 for (String item : list) { 8 9 System.out.println(item); 10 11 }
2.2.2 适配器模式
Java.util.Arrays类中的asList()方法能够把数组类型转换为List类型,若是要将数组类型转换为List类型,应该注意的是asList()的参数为泛型的变长参数,所以不能使用基本类型做为参数,只能使用相应的包装类型数组。这里数组类型和List类型的转换就是一种典型的适配器模式,做为两个不兼容的接口之间的桥梁,结合了其功能。
1 public static <T> List<T> asList(T... a) { 2 3 return new ArrayList<>(a); 4 5 }
一般状况下,客户端能够经过目标类的接口访问它所提供的服务。有时,现有的类能够知足客户类的功能须要,可是它所提供的接口不必定是客户类所指望的,这多是由于现有类中方法名与目标类中定义的方法名不一致等缘由所致使的。
在这种状况下,现有的接口须要转化为客户类指望的接口,这样保证了对现有类的重用。若是不进行这样的转化,客户类就不能利用现有类所提供的功能,适配器模式能够完成这样的转化。
在适配器模式中能够定义一个包装类,包装不兼容接口的对象,这个包装类指的就是适配器(Adapter),它所包装的对象就是适配者(Adaptee),即被适配的类。
适配器提供客户类须要的接口,适配器的实现就是把客户类的请求转化为对适配者的相应接口的调用。也就是说:当客户类调用适配器的方法时,在适配器类的内部将调用适配者类的方法,而这个过程对客户类是透明的,客户类并不直接访问适配者类。所以,适配器可使因为接口不兼容而不能交互的类能够一块儿工做。这就是适配器模式的模式动机。
3.违反/符合高质量代码案例
3.1 使用静态内部类提升封装性(符合)
LinkedList.java 970-980行
1 private static class Node<E> { 2 3 E item; 4 5 Node<E> next; 6 7 Node<E> prev; 8 9 10 11 Node(Node<E> prev, E element, Node<E> next) { 12 13 this.item = element; 14 15 this.next = next; 16 17 this.prev = prev; 18 19 } 20 21 }
静态内部类的优势是增强了类的封装和提升代码的可读性。Node类封装了链表中该节点先后节点的信息,不须要在LinkedList中所有定义全部节点,只需声明first和last Node,便可将全部节点相互链接,造成链表,提升了封装性。静态内部类体现了Node和LinkedList之间的强关联关系,加强了语意和可读性。从代码结构看,静态内部类放置在外部类内,在这里表示Node类是LinkedList类的子行为或自属性。与普通内部类相比,静态内部类不持有外部类的引用:在普通内部类中,咱们能够直接访问外部类的属性,方法,即便是private类型也能够访问,这是由于持有引用,能够自由访问。而静态内部类,只能够访问外部类的静态方法和静态属性,其余则不能。其次,静态内部类不依赖外部类,内部实例能够脱离外部类实例单独存在。而内部类则与外部类共同生死,一块儿声明,一块儿被垃圾回收。第三,普通内部类不能声明static方法和变量,而静态内部类则没有任何限制。方便静态方法和变量扩展,具备优越性。
3.2 equals应该考虑null值状况(违反)
LinkedList.java 595-610行
1 public int indexOf(Object o) { 2 3 int index = 0; 4 5 if (o == null) { 6 7 for (Node<E> x = first; x != null; x = x.next) { 8 9 if (x.item == null) 10 11 return index; 12 13 index++; 14 15 } 16 17 } else { 18 19 for (Node<E> x = first; x != null; x = x.next) { 20 21 if (o.equals(x.item)) 22 23 return index; 24 25 index++; 26 27 } 28 29 } 30 31 return -1; 32 33 }
若是传入的Object对象o是null,且未通过检查判断,则在调用o.equals()方法时,就会出现空指针异常。出现这种状况的缘由是由于equals()方法或者对其的覆写未遵循对称性原则,对于任何引用x和y的情形,若是x.equals(y)返回true,则y.equals(x)也应该返回true。因此最好的解决办法就是在覆写equals()方法时,加上对null值的判断,保证对称性原则,不然就须要像上述方法同样在调用equals()方法以前完成对null值的判断。
3.3 使用序列化类的私有方法解决部分属性持久化问题(符合)
ArayList.java 135行,755-800行
1 transient Object[] elementData; 2 3 4 private void writeObject(java.io.ObjectOutputStream s) 5 6 throws java.io.IOException{ 7 8 // Write out element count, and any hidden stuff 9 10 int expectedModCount = modCount; 11 12 s.defaultWriteObject(); 13 14 15 16 // Write out size as capacity for behavioural compatibility with clone() 17 18 s.writeInt(size); 19 20 21 22 // Write out all elements in the proper order. 23 24 for (int i=0; i<size; i++) { 25 26 s.writeObject(elementData[i]); 27 28 } 29 30 31 32 if (modCount != expectedModCount) { 33 34 throw new ConcurrentModificationException(); 35 36 } 37 38 } 39 40 41 private void readObject(java.io.ObjectInputStream s) 42 43 throws java.io.IOException, ClassNotFoundException { 44 45 elementData = EMPTY_ELEMENTDATA; 46 47 48 49 // Read in size, and any hidden stuff 50 51 s.defaultReadObject(); 52 53 54 55 // Read in capacity 56 57 s.readInt(); // ignored 58 59 60 61 if (size > 0) { 62 63 // be like clone(), allocate array based upon size not capacity 64 65 int capacity = calculateCapacity(elementData, size); 66 67 SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity); 68 69 ensureCapacityInternal(size); 70 71 72 73 Object[] a = elementData; 74 75 // Read in all elements in the proper order. 76 77 for (int i=0; i<size; i++) { 78 79 a[i] = s.readObject(); 80 81 } 82 83 } 84 85 }
序列化是将Java对象以一种形式持久化,如存放到硬盘,或者用于传输,反序列化是其逆过程。ArrayList也实现了Serializable接口来保证序列化。通过源码分析可知其数据存储都依赖于elementData数组,但注意该数组被transient关键字修饰。说明设计者认为该数组不须要持久化,一般部分持久化的缘由是有一些属性为敏感信息,为了安全起见,不但愿在网络操做中传输或本地序列化缓存。即elementData数组的生命周期仅存于调用者的内存中而不会写到磁盘中。
那么数组元素怎样序列化呢?即遵循序列化的流程,经过调用ObjectOutputStream对象输出流的writeObject()方法写入对象状态信息,而后就能够经过readObject()方法读取信息来反序列化。而对于elementData,defaultWriteObject()并不会去持久化被transient修饰的它,这里须要重写writeObject()和readObject()方法来手动持久化,经过循环将其中元素的值取出来,而后依次写入输出流。虽然重写的两个方法为private,看起来不能调用,但实际上ObjectOutputStream会调用这个类的writeObject()方法,read也同理,经过反射机制来完成判断一个类是否重写了方法,根据传入的ArrayList对象获得class,而后包装成ObjectStreamClass,在writeSerialData方法里,会调用ObjectStreamClass的invokeWriteObject方法。
defaultReadObject和defaultWriteObject应该是readObject(ObjectInputStream o)和writeObject(ObjectOutputStream o)内部的第一个方法调用。它分别读取和写入该类的全部非瞬态字段。这些方法还有助于向后和向前的兼容性。若是未来在类中添加一些非瞬态字段,并尝试经过旧版本的类对它进行反序列化,则defaultReadObject()方法将忽略新添加的字段,相似地,若是您使用新的序列化对旧的序列化对象进行反序列化版本,则新的非瞬态字段将采用JVM的默认值,即,若是其对象为null,则为null,不然将其从boolean设置为false,将int设置为0,等等。
那么最终也对elementData元素进行了序列化,为何要将其设置为瞬态字段呢?由于ArrayList的自动扩容机制中,elementData数组至关于容器,容器不足时就会扩容,容量每每大于等于所存元素的个数。直接序列化会形成元素空间的浪费,特别是元素个数不少的状况,这种浪费很是不划算。由此,这样作的目的是为了只序列化实际存储的元素,节省资源。
3.4 在接口中存在实现代码(违反)
List.java 475-484行
1 @SuppressWarnings({"unchecked", "rawtypes"}) 2 3 default void sort(Comparator<? super E> c) { 4 5 Object[] a = this.toArray(); 6 7 Arrays.sort(a, (Comparator) c); 8 9 ListIterator<E> i = this.listIterator(); 10 11 for (Object e : a) { 12 13 i.next(); 14 15 i.set((E) e); 16 17 } 18 19 }
通常而言,接口是一种契约,一种框架性协议,他能够声明常量,声明抽象方法,也能够继承接口,但不能有具体实现。这代表其实现类都是同一种类型,或者是具有类似特征的一个集合体,其约束着实现者,保证提供的服务是稳定的、可靠的。若是把实现代码写到接口中,那接口就绑定了可能变化的因素,随时都有可能被抛弃,被更改,被重构。因此,接口中虽然能够有实现,但应避免使用。因此合理的修改方式是,在List的子类中分别去实现sort方法。
但Java 8的新特性中,推出了默认方法(default methods),简单来讲,就是能够在接口中定义一个已实现的方法,且该接口的实现类不须要实现该方法。这么作的好处是为了方便扩展已有接口。若是没有默认方法,假如给JDK中的某个接口添加一个新的抽象方法,那么全部实现了该接口的类都得修改,影响很大。
但使用默认方法,能够给已有接口添加新方法,而不用修改该接口的实现类。固然,接口中新添加的默认方法,全部实现类也会继承。这样下降了接口与实现类之间的耦合度。
这样来看,Java 8的新特性和高质量代码规范产生了矛盾,因此仍是要根据实际状况来分析,默认方法实现存在的必要性,使得工做更加高效。
3.5 警戒数组的浅拷贝(违反)
ArrayList.java 353-363行
1 public Object clone() { 2 3 try { 4 5 ArrayList<?> v = (ArrayList<?>) super.clone(); 6 7 v.elementData = Arrays.copyOf(elementData, size); 8 9 v.modCount = 0; 10 11 return v; 12 13 } catch (CloneNotSupportedException e) { 14 15 // this shouldn't happen, since we are Cloneable 16 17 throw new InternalError(e); 18 19 } 20 21 } 22 23 24 class Person implements Serializable{ 25 26 private int age; 27 28 private String name; 29 30 31 32 public Person(){}; 33 34 public Person(int age,String name){ 35 36 this.age=age; 37 38 this.name=name; 39 40 } 41 42 43 44 public String toString(){ 45 46 return this.name+"-->"+this.age; 47 48 } 49 50 }
举例来讲,对于上面这个JavaBean,经过new Person()构建3个对象,而后添加到第一个ArrayList,而后经过遍历循环复制,添加到另外一个ArrayList,在调用add方法时,并无new Person()操做。所以,经过set方法修改属性时,会破坏源数据,两个ArrayList都会收到影响,缘由是浅拷贝;一样,使用ArrayList的构造方法来复制几个内容,一样是浅拷贝。在clone()方法中的Arrays.copy()和System.arraycopy()也是不能对集合进行深拷贝的。
1 public static <T> List<T> deepCopy(List<T> src) throws IOException, ClassNotFoundException { 2 3 ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); 4 5 ObjectOutputStream out = new ObjectOutputStream(byteOut); 6 7 out.writeObject(src); 8 9 10 11 ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray()); 12 13 ObjectInputStream in = new ObjectInputStream(byteIn); 14 15 @SuppressWarnings("unchecked") 16 17 List<T> dest = (List<T>) in.readObject(); 18 19 return dest; 20 21 }
那么经过实现Serializable接口后,使用序列化的方式能够实现深拷贝,如上所示。由于在序列化的过程当中,咱们取出了原List中的值,并将其传给了新的对象,那么两个List所含的对象指向的地址空间就不一样了,因此是深拷贝,能够解决上述例子中Person()类属性修改则所有同时修改的问题。
3.6 显示声明UID(符合)
ArrayList.java 107-110行
1 public class ArrayList<E> extends AbstractList<E> 2 3 implements List<E>, RandomAccess, Cloneable, java.io.Serializable 4 5 { 6 private static final long serialVersionUID = 8683452581122892189L;
7 }
类实现Serializable接口的目的是为了可持久化,好比网络传输或本地存储。须要考虑的一个问题是,若是消息的生产者和消息的消费者所参考的类有差别,好比生产者中的类增长一个年龄属性,而消费者没有增长属性。由于这是一个分布式部署的应用,你甚至都不知道这个应用部署在何处,特别是经过广播(broadcast)方式发消息的状况,漏掉一两个订阅者很是正常。此时,不一致的状况会致使InvalidClassException异常,缘由是序列化和反序列化所对应的类版本发生了变化,JVM不能把数据流转换为实例对象。JVM是经过SerialVersionUID来标识类的版本定义的,显示声明后,在反序列化时,就会比较UID是否相同,来确保类没有发生改变,不然不执行反序列化过程。这是一个很好的校验机制,保证类和对象数据的一致性。
3.7 基本类型数组转换列表陷阱(违反)
Arrays.java 3799-3801行
1 public static <T> List<T> asList(T... a) { 2 3 return new ArrayList<>(a); 4 5 }
开发过程当中常常会使用Arrays和Collections这两个工具类在数组和列表之间转换,但也会有一些奇怪的问题。
1 public class Client65 { 2 3 public static void main(String[] args) { 4 5 int data [] = {1,2,3,4,5}; 6 7 List list= Arrays.asList(data); 8 9 System.out.println("列表中的元素数量是:"+list.size()); 10 11 } 12 13 }
如上代码所示,理所固然地认为list元素数量是5,但实际打印为1。为何经过asList方法转换后就只有一个元素了呢?从该方法源码可得,输入一个变长参数,返回一个固定长度的列表。咱们知道基本类型是不能泛型化的,即8个基本类型不能做为该方法的泛型参数,必须使用其包装类型。但例子中传入一个int类型的数组却没有报错。由于Java中,数组能够做为一个对象,能够泛型化,因此这里咱们把int数组做为了一个T类型,转换时就只有一个int数组类型的元素了。打印list.get(0).getClass()发现,元素类型是class[I。JVM不可能输出Array类型,由于其属于java.lang.reflect包,是经过反射访问数组元素的工具类。在java中任何一个一位数组的类型都是[I,缘由是Java并无定义数组这一个类,是编译器编译时产生的。
因此解决方案,一是经过程序员在调用asList方法前,封装基本对象类型数组,二则是在该方法内部,首先判断是否为基本类型,对基本类型进行装箱处理,而后传给new ArrayList()返回给用户。因此说asList这个方法的陷阱,不太优雅,容易致使程序逻辑混乱。
3.8 数组的真实类型必须为泛型类型的子类型
ArrayList.java 408-416行
1 public <T> T[] toArray(T[] a) { 2 3 int size = size(); 4 5 if (a.length < size) 6 7 return Arrays.copyOf(this.a, size, 8 9 (Class<? extends T[]>) a.getClass()); 10 11 System.arraycopy(this.a, 0, a, 0, size); 12 13 if (a.length > size) 14 15 a[size] = null; 16 17 return a; 18 19 }
List接口的toArray方法能够把一个集合转化为数组,可是使用不方便,返回的是一个Object数组,因此须要自行转变。ToArray(T[] a)虽然返回的是T类型的数组,但还须要传入一个T类型的数组,至关麻烦。咱们指望输入的是一个泛型化的List,这样就能够转化为泛型数组。
1 public static <T> T[] toArray(List<T> list) { 2 3 T[] t = (T[]) new Object[list.size()]; 4 5 for (int i = 0, n = list.size(); i < n; i++) { 6 7 t[i] = list.get(i); 8 9 } 10 11 return t; 12 13 }
如上所示,对输出的Object数组转型为T类型数组,以后遍历List赋值给数组的每一个元素。使用List<String>做为传入参数,调用该方法时,编译能够经过,但运行异常,显示类型转换异常,也就是说不能把一个Object数组转化为String类型数组。问题在于,为何Object数组不能向下转型为String数组,由于数组是容器,只有确保容器内元素类型和指望的类型有父子关系时才能转换,Object数组只能保证数组内的元素是Object类型,不能保证他们都是String的父类型或子类型,因此转换失败。另外一个问题是,抛出异常的位置在main方法,而不是toArray方法,按理来讲在toArray方法中进行类型的向下转换,而不是main方法,但却在main方法抛异常。缘由是编译时泛型擦除,转化时并非把Object转换为String类型,而是Object转Object,彻底没有必要。因此在main方法中,为了可以实现对String数组的遍历,就须要类型转换,此时出现异常。
1 public static <T> T[] toArray(List<T> list,Class<T> tClass) { 2 3 //声明并初始化一个T类型的数组 4 5 T[] t = (T[])Array.newInstance(tClass, list.size()); 6 7 for (int i = 0, n = list.size(); i < n; i++) { 8 9 t[i] = list.get(i); 10 11 } 12 13 return t; 14 15 }
因此为了能实现上述需求,把泛型数组声明为泛型的子类型便可。经过反射类Array声明了一个T类型的数组,因为我么你没法在运行期得到泛型类型参数,只能经过调用者主动传入T参数类型。
在这里咱们看到,当一个泛型类(特别是泛型集合)转变为泛型数组时,泛型数组的真实类型不能是泛型的父类型(好比顶层类Object),只能是泛型类型的子类型(固然包括自身类型),不然就会出现类型转换异常。源码的设计给使用者形成了必定的困扰。
3.9 受检异常尽量转化为非受检异常(遵照)
ArrayList.java 655-666行
1 private void rangeCheck(int index) { 2 3 if (index >= size) 4 5 throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); 6 7 } 8 9 10 11 /** 12 13 * A version of rangeCheck used by add and addAll. 14 15 */ 16 17 private void rangeCheckForAdd(int index) { 18 19 if (index > size || index < 0) 20 21 throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); 22 23 }
受检异常是正常逻辑的一种补偿手段,特别是对于可靠性要求较高的系统。在某些条件下必须抛出受检异常以便由城西进行补偿处理。
但受检异常使接口声明脆弱,OOP要求尽可能多地面向接口编程,可提升代码的扩展性、稳定性,可是涉及异常问题就不同了。随着系统的开发,会有不少的实现类,他们一般须要抛出不一样的异常。异常是对主逻辑的补充,但修改一个补充逻辑,会致使主逻辑也被修改,即实现类逆影响接口的情景。咱们知道实现类是不稳定的,而接口是稳定的,一旦定义了异常,则增长了接口的不稳定性,实现的变动会最终影响到调用者,破坏了封装性,是违背迪米特法则的。
受检异常也使得代码的可读性下降,一个方法增长受检异常,则必须有一个调用者对异常进行处理。多个catch块捕获处理多个异常,大大下降代码的可读性。
当受检异常威胁到了系统的安全性,稳定性,可靠性、正确性时,则必须处理,不能转化为非受检异常,其它状况则能够转化为非受检异常。
3.10 合理使用显示锁Lock(遵照)
CopyOnWriteArrayList.java 434-447行
1 public boolean add(E e) { 2 3 final ReentrantLock lock = this.lock; 4 5 lock.lock(); 6 7 try { 8 9 Object[] elements = getArray(); 10 11 int len = elements.length; 12 13 Object[] newElements = Arrays.copyOf(elements, len + 1); 14 15 newElements[len] = e; 16 17 setArray(newElements); 18 19 return true; 20 21 } finally { 22 23 lock.unlock(); 24 25 } 26 27 }
不少程序员会认为,Lock类和synchronized关键字在代码块的并发性和内存上时语义是同样的,都是保持代码块同时只有一个线程具备执行权。但实际状况是,经过一个例子模拟,发现synchronized内部锁保证了只有一个线性的运行权,其余等待执行;而Lock显示锁未出现互斥状况。在例子中同步资源是代码块,前者是类级别的锁,然后者是对象级别的锁。简单来讲,把Lock定义为多线程类的私有属性是起不到资源互斥做用的,除非把Lock定义为全部线程共享变量。
因此,这样来看,Lock支持更细粒度的锁控制,假设读写锁分离,写操做不容许有读写操做同时存在,而读操做时读写可并发执行,这样的需求内部锁很难实现,而Lock能够。
为了得到线程安全的ArrayList,可使用concurrent并发包下的CopyOnWriteArrayList类,这是一个CopyOnWrite容器,读取元素是从原数组读取的,添加元素是在复制的新数组上。读写分离,于是能够在并发条件下进行不加锁的读取,读取效率高,适用于读操做远大于写操做的场景。这里Lock显示锁的优越性就体现出来了,因此合理地使用Lock和synchronized能够提升程序性能,实现不一样的需求,也可使得代码具备高质量。
参考:
[1] http://www.javashuo.com/article/p-sdwwwaum-mq.html
[3]https://www.cnblogs.com/selene/default.html?page=3 编写高质量代码:改善Java程序的151个建议
[4]https://www.cnblogs.com/chenpi/p/5897713.html Java 8默认方法
[5]https://www.cnblogs.com/aoguren/p/4767309.html ArrayList序列化技术细节详解