目录算法
2、List数组
2.1.2 删除元素数据结构
2.3 抽象类AbstractSequentialListspa
3、Set线程
Java.util中的容器又被称为Java Collections framework。虽然被称为框架,可是其主要目的是提供一组接口尽可能简单并且相同、而且尽可能高效、以便于开发人员按照场景选用,而不是本身重复实现的类。容器按接口能够分为两大类:Collection和Map。本文主要关注Collection,之后会将Map这块也进行研究。
先从Collection提及。能够看出:
1.Collection接口并非一个根接口,它的超级接口是Iterator,须要提供其遍历、移除元素(可选操做)的能力。
2.Collection接口定义了基本的容器操做方法。
除此之外,
1.remove()和contains()判断元素是否相等的依据是相似的。
对于remove(Object o),若Collection中包含的元素e,知足(o==null ? e==null : o.equals(e)),移除其中的一个;
对于contains(Object o),若Collection中包含至少一个或多个元素e,知足(o==null ? e==null : o.equals(e)),则返回true。
2.AbstractCollection抽象类实现了一部分Collection接口的方法,主要是基于iterator实现的,如remove()、toArray(),以及利用自己的属性size实现的size()。若是读一下源码,能够发现虽然AbstractCollection利用add()实现了addAll(),可是add()自己的实现是直接抛UnsupportedOperationException异常的。实际上add()是一种“可选操做”,目的是延迟到须要时再实现。
了解了通用的Collection后,接下来,看看三大类的Collection:List、Set、Queue。首先从List提及。List中的元素是有序的,于是咱们能够按序访问List中的元素,以及访问指定位置上的元素。对于“按顺序遍历访问元素”的需求,使用List的超级接口Iterator便可以作到,这也是对应抽象类AbstractList中的实现;而访问特定位置的元素(也即按索引访问)、元素的增长和删除涉及到了List中各个元素的链接关系,并无在AbstractList中提供。
ArrayList是最经常使用的List的实现,其包装了一个用于存放元素的数组,并用size属性来标识该容器里的元素个数,而非这个被包装数组的大小。若是对数组有所了解,很容易理解ArrayList的元素是怎么编排的,各个数组的元素如何随机访问(经过索引)、元素之间如何跳转(索引增减)。阅读源码能够发现,这个数组用transient关键字修饰,表示其不会被序列化。固然,ArrayList的元素最终仍是会被序列化的,要否则,这个最经常使用的List之一,不能持久化、不能网络传输,简直不可想象。在序列化/反序列化时,会调用ArrayList的writeObject()/readObject()方法,将该ArrayList中的元素(即0...size-1下标对应的元素)写入流/从流读出。这样作的好处是,只保存/传输有实际意义的元素,最大限度的节约了存储、传输和处理的开销。
提到序列化,有个问题是,ArrayList的writeObject()/readObject()是如何被调用的?它们并不属于ArrayList的任何一个接口,甚至是Serializabe!其实,序列化是ObjectOutputStream对象调用自身的writeObject()方法时,由它经过反射检查入参——也即待序列化的对象——是否有writeObject()方法,并进行调用,这和接口无关,确实很古怪(能够参考《Java编程思想·第四版(中文)》第581页)。
ArrayList在删除元素时,不只要将其余元素前移来占用被移除的元素并缩小size,对于原来位置的元素,如(size-1)位置的元素前移至(size-2)位,那么(size-1)位置是要设置为null的,这样才能让垃圾回收机制发挥做用。这种数据的用法在Java中比较常见,好比利用Vector实现的Stack,也是这样。而在C语言中,一种利用数组实现的栈是能够在pop()后只修改当前栈对应的数组下标而不做清理的。
利用Arrays.copyOf()方法作数组的调整。
虽然Vector通过了改造,但这么作只是为了兼容Java2以前的代码,不建议继续使用。
Java1.6的源码中,和ArrayList相似,Vector底层也是数组,可是这个数组并无transient修饰,其序列化要低效很多。
Stack是继承Vector实现的,而不是包装一个Vector。这并非一个很好的设计,若是要使用栈行为,应该使用LinkedList。Java1.6源码中,Stack每次扩大都须要new新的数组并做拷贝,效率并很差。
新代码中误用这两个容器的缘由,多是以前在C++中使用过STL的Vector和Stack。我刚接触Java时,总觉得这两个类在Java中的地位相似C++。
知足“连续访问”数据存储而非“随机访问”需求的List,对于指定index元素的操做,都须要利用抽象方法listIterator()得到一个迭代器。其惟一实现是LinkedList,对其的讨论放在Queue这部分。
Set接口模仿了数学概念上的set,各个元素不要求重复。除了这一点,几乎和Collection自己是同样的。
Set接口有一个直接子接口SortedSet,该接口要求Set中全部元素有序。和经过Iterable接口依次访问全部元素的“有序”不一样,这个“有序”要求Set包括一个比较器,能够判断两个元素的大于、小于或等于关系。此外,该接口提供了访问第一个元素、访问最后一个元素、访问必定范围内元素的方法。SortedSet的子接口NavigableSet进一步扩展了这个系列的方法,提供了诸如返回大于/小于元素E的第一个元素的方法。
因为Set的操做与底层的实现关联性很强,AbstractSet中实现的方法有限,在Java1.6中只有equals()、hashCode()、removeAll()进行了实现。
HashSet之因此命名中包含了“Hash”,是由于其底层是用HashMap实现的。Map有个特色,各个Key是惟一的,这和Set的元素惟一很相似。对HashSet的元素E进行的操做,其实是对其包装的HashMap中对应的<E,PRESENT>的操做,其中PRESENT是一个private static final的Object。所以,HashSet的原理,放到HashMap那一块来研究。
HashSet有一个很特别的构造方法:HashSet(int initialCapacity, float loadFactor, boolean dummy)。这个方法第三个参数的惟一做用是,与其余两个参数的构造方法相区分。使用这个构造方法,在底层使用的是HashMap的子类LinkedHashMap。而LinkedHashSet,正是使用了这个构造方法,在内部建立并封装了一个LinkedHashMap而非通常的HashMap。
假如先有HashSet,后有HashMap,用HashSet实现HashMap,是不是一个好的主意?这也放在HashMap处研究。
HashSet的包装的HashMap也使用transient关键字修饰,采用了和ArrayList同样的序列化策略。
TreeSet是SortedSet的一个实现,也是其子接口NavigableSet的实现。
与HashSet/LinkedHashSet相似,TreeSet底层封装了一个NavigableMap,一样使用transient修饰,以及序列化策略。
Queue和List有两个区别:前者有“队头”的概念,取元素、移除元素、均为对“队头”的操做(一般但不老是FIFO,即先进先出),然后者只有在插入时须要保证在尾部进行;前者对元素的一些同一种操做提供了两种方法,在特定状况下抛异常/返回特殊值——add()/offer()、remove()/poll()、element()/peek()。不难想到,在所谓的两种方法中,抛异常的方法彻底能够经过包装不抛异常的方法来实现,这也是AbstractQueue所作的。
Deque接口继承了Queue,可是和AbstractQueue没有关系。Deque同时提供了在队头和队尾进行插入和删除的操做。
PriorityQueue用于存放含有优先级的元素,插入的对象必须能够比较。该类内部一样封装了一个数组。与其抽象父类AbstractQueue不一样,PriorityQueue的offer()方法在插入null时会抛空指针异常——null是没法与其余元素比较一般意义下的优先级的;此外,add()方法是直接包装了offer(),没有附加的行为。
因为其内部的数据结构是数组的缘故,不少操做都须要先把元素经过indexOf()转化成对应的数组下标,再进行进一步的操做,如remove()、removeEq()、contains()等。其实这个数组保持优先级队列的方式,是采用堆(Heap)的方式,具体能够参考任意一本算法书籍,好比《算法导论》等,这里就不展开解释了。和堆的特性有关,在寻找指定元素时,必须从头到尾遍历,而不能使用二分查找。
颇有趣的是,LinkedList既是List,也是Queue(Deque),其缘由是它是双向的,内部的元素(Entry)同时保留了上一个和下一个元素的引用。使用头部的引用header,取其previous,就能够得到尾部的引用。经过这一转换,能够很容易实现Deque所须要的行为。也正所以,能够支持栈的行为,天生就有push()和pop()方法。简而言之,是Java中的双向链表,其支持的操做和普通的双向链表同样。
和数组不一样,根据下标查找特定元素时,只能遍历地获取了,于是在随机访问时效率不如ArrayList。尽管如此,做者仍是尽量地利用了LinkedList的特性作了点优化,尽可能减小了访问次数:
private Entry<E> entry(int index) { if (index < 0 || index >= size) throw new IndexOutOfBoundsException("Index: "+index+ ", Size: "+size); Entry<E> e = header; if (index < (size >> 1)) { for (int i = 0; i <= index; i++) e = e.next; } else { for (int i = size; i > index; i--) e = e.previous; } return e; }
LinkedList对首部和尾部的插入都支持,但继承自Collection接口的add()方法是在尾部进行插入。
ArrayList、HashSet/LinkedHashSet、PriorityQueue、LinkedList是线程不安全的,可使用synchronized关键字,或者相似下面的方法解决:
List list = Collections.synchronizedList(new ArrayList(...));
ArrayList、LinkedList、HashMap/LinkedHashMap、TreeSet的clone()是浅拷贝,元素的引用和拷贝前相同;PriorityQueue的clone()继承自Object。
在for(Element e : collection)中:
collection == null,直接抛异常;
容器内容为空,即刚刚被new出来,里面什么也没有,直接跳过循环;
容器中放了null(若是容许的话),则将这个null取出并赋值给e,执行循环中的语句。
List能够放无限多个,set只能放一个。EnumSet、PriorityQueue是不能放null的。这个null也在计数中。因此放进去null用foreach取出来时须要判空。