1.数组的缺点:大小一旦给定就没法更改,除非复制到一个新的数组中,开销大;而容器类均可以自动地调整本身的尺寸。
2.容器功能的多样性:容器能够实现各类不一样要求,如按不一样依据将元素进行排序或者保证容器内无重复元素等等。
关于容器的泛型参数:
当指定了某个类型做为类型参数时,则能够将该类型及其子类型的对象放入到容器当中,泛型保证了安全性。html
Collection保存单一的元素,而且Collection继承了Iterable接口,而Map中保存键值对。java
Collection中提供了判空、大小、遍历、是否包含某元素、是否包含其余Collection所有、添加某元素、添加其余Collecton所有、删除某元素、删除其余Collection所有(求差)、删除所有元素*以及、只保留与其余Collection重合部分(求交)、以及toArray方法(全部Collection实现类都能转换成数组而且如迭代器有是顺序那么数组中也是相同顺序的)。算法
Collection一个重要的做用就是做为它的具体实现集合之间相互转换的中介,比较经常使用的Collection类如ArrayList、LinkedList、HashSet、LinkedHashSet、TreeSet中除了都有无参构造函数外还所有都有一个接受Collection做为参数的构造函数(LinkedList有且仅有这两个)。编程
其中ArrayList(10)、HashSet(16,0.75)、LinkedHashSet(16,0.75)都有一个在建立时指定容量的构造函数,对于ArrayList而言是由于其底层是基于数组实现的。api
其中HashSet和LinkedHashSet(LinkedHashSet继承自HashSet)还多了一个能够同时指定容量和负载因子的构造函数,如不指定则默认是0.75。这是由于HashSet内部是以一个HashMap对象实现的(构造函数中建立赋给Map类型的成员变量map)、LinkedHashSet中是以一个LinkedHashMap对象实现的(构造函数中调用父类的一个默认访问权限级别的构造函数来建立而后一样赋给map),由于HashMap和LinkedHashMap都是用数组+(双向节点)链表来实现的,因此就有了容量和负载因子这两个参数,也相应地有了这两个构造函数。数组
其中TreeSet则有一个接受SortedSet做为参数的构造函数和一个接受比较器Comparator做为参数的构造函数。前者除了转换集合类型外还有个做用是能够按照本来SortedSet里的比较器来进行排序(若是存在),也就是说转换后新旧SortedSet里面的元素顺序是相同的。安全
//待补充...话说思否不能设置字体颜色的么
Collection中的List有三个特色:1.能够容许重复的对象。2.能够插入多个null元素。3.是一个有序容器,保持了每一个元素的插入顺序,输出的顺序就是插入的顺序。List从新规定了equals和hashCode方法的实现,这使得equals能够用来不一样类型之间的List实现类对象之间来比较所包含元素是否彻底相同,这个相同是按顺序相同的,即54321与12345是不相同的。
经常使用的实现类有ArrayList和LinkedList,当须要大量的随机访问则使用ArrayList,当须要常常从表前半部分插入和删除元素则应该根据靠前程度使用LinkedList(由于对于ArrayList而言插入或者删除元素的位置越靠前,须要复制元素的次数就越接近size(添加是size-i删除是size-i-1);对于LinkedList而言,它是根据位置位于前半部分仍是后半部分来选则是从前日后遍历找仍是从后往前找,对它而言位于插入或者删除中间的元素反而是效率最低的。因此前部分是LinkedList比ArrayList效率更高的部分)。
除了继承自Collection的方法,List接口还额外增长了如下方法:并发
新增了一个能够返回更适合List的迭代器对象的方法listIterator(Iterator的子类)。ListIterator容许从任一方向来遍历List对象,并在遍历(迭代)过程当中进行修改该List对象,还能得到迭代器的当前位置。hasNext、next是正向遍历,hasPrevious、previous是逆向遍历。listIterator有两个版本,一个是无参数的,会返回一个游标指向List开头的ListIterator,另外一个是带有一个int参数的,会返回一个游标指向指定位置的ListIterator。
混合调用next和previous会返回同一个对象,extIndex返回的是下次调用next所要返回的元素的位置,previousIndex返回的是下次调用previous所要返回的元素的位置。在同一游标位置nextIndex老是比previousIndex更大的,如下是两种边界状况:一种返回-1另外一种返回list.size() (1) a call to previousIndex when the cursor is before the initial element returns -1 and (2) a call to nextIndex when the cursor is after the final element returns list.size().oracle
subList,返回的List(banked)是由本来的List(banking)所支持的,所以对本来数组元素的更改会反映到返回的数组当中。任何能对List使用的方法对返回的subList同样可以使用该方法可获得一个banked List,但全部的方法都仍是应该推荐在baking List上使用,而不是banked List。
由于一旦你经过banking List或者另外一个由subList获得的banked List2对链表进行告终构修改(其实就是增删元素,修改元素的内容并无影响),那么该baked List的全部public方法(除了subList外和一些继承自Object的方法外),都会在运行时报ConcurrentModificationException异常。
Arrays.asList方法:可让一个数组被看作是一个List,但该List底层的实现仍是本来的数组,对List中元素的更改也就是对数组的更改。数组是没法调整大小的,也所以,这个List没法add或者remove元素。app
ArrayList:内部使用了一个名为elementData的Object数组引用变量(后面以数组变量称呼),在ArrayList对象建立以后而且尚未添加元素以前,该变量默认指向一个static(因此变量位于方法区) final的引用变量DEFAULTCAPACITY_EMPTY_ELEMENTDATA,这个引用变量指向一个空的数组对象,这是为了不咱们反复去建立未使用的数组,若是当咱们反复建立无用的数组了,那么它们其中的elementData就全都顺着引用指向着那一个空的数组对象了。
当咱们首次添加一个元素时,就会新建大小为默认值10的Object数组对象传给数组变量,而后将元素放进去。但这个时候size只会是1而不是10,由于size指代的是真正存放的元素的数量而不是容量。而当每次size刚超过容量时就会进行1.5倍的扩容,好比咱们有了10个元素装满了,如今添加第11个元素的时候就会把数组扩容成15,而后再将原数组复制过去以及第11个元素放进去,size变成11。实际上复制操做最底层都是经过System.arraycopy()来实现的,也就是直接赋值的浅拷贝(可见笔记关于Object的clone方法、浅拷贝、深拷贝),由于native方法效率比循环复制要高。
当在对ArrayList根据索引进行一次插入(复制次数size-i)或者删除(复制次数size-i-1)元素的时候,索引越靠后,须要复制的次数越少,效率越高,索引越靠前须要复制的次数越多,效率越低。
LinkedList就是一个双向链表的实现,它同时实现了List接口和Deque接口,也是说它便可以看做一个顺序链表,又能够看作一个队列(Queue),同时又能够看作一个栈(Satck)。
顺便谈下关于栈,在java中有个现成的栈类,就是java.util.Stack这个类,但这个类java官方在api中也指出再也不推荐使用。而是在须要使用这种先进后出的栈时推荐使用Deque接口下的实现类,好比这里的LinkedList,但更推荐的是ArrayDeque。
LinkedList对指定位置的增删查改,都会经过与size>>1(表示size/2,使用移位运算提高代码的运行效率)的相比较的方式来选择从前遍历仍是从后遍历。因此当要增删查改的位置恰好位于中间时,效率是最低的。
Set的特色是不接受重复元素,其中TreeSet不接受null,由于TreeSet是用TreeMap实现的,TreeMap其实就是个红黑树,而在红黑树当中是不能插入一个空节点的;其余两个HashSet和LinkedHashSet则能够接受null元素。Set从新规定了equals和hashCode方法的实现,这使得equals能够用来不一样类型之间的Set实现类对象之间来比较所包含元素是否彻底相同(与List相比不用顺序相同)。
HashSet提供最快的查询速度,而TreeSet保持元素处于排序状态,LinkedHashSet以插入顺序保存元素。其中LinkedHashSet是HashSet的子类。
HashSet底层是用一个HashMap来实现的,这个HashMap的全部键值映射的值都是同一个对象(一个Obect对象),就是下面代码当中的final修饰的PRESENT。
private transient HashMap<E,Object> map; private static final Object PRESENT = new Object(); public HashSet() { map = new HashMap<>(); } //这里的0.75还有16都是与HashMap中默认加载因子和默认容量是一致的 public HashSet(Collection<? extends E> c) { map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16)); addAll(c); }
HashSet中的大小、增、删、查、遍历等操做都是经过map来进行的,代码以下所示,值的提一下的是remove方法,由于在HashSet里面的全部元素做为键对应值都是PRESENT,在map的remove方法当中会返回删除元素的值(在这里必定就是PRESENT了)或者null,因此用返回的值与PRESENT进行比较也是能够得出是否删除成功也是能够的。
public Iterator<E> iterator() { return map.keySet().iterator(); } public int size() { return map.size(); } public boolean isEmpty() { return map.isEmpty(); } public boolean contains(Object o) { return map.containsKey(o); } public boolean add(E e) { return map.put(e, PRESENT)==null; } public boolean remove(Object o) { return map.remove(o)==PRESENT; } public void clear() { map.clear(); }
最后说下HashSet的专门为LinkedHashSet预留的一个构造函数,这是一个包访问权限的构造函数,实际上被设置为只被LinkedHashSet所调用到了,由于LinkedHashSet继承了HashSet。这个构造函数是将返回了一个LinkedHashMap对象给map,这也是LinkedHashMap的存储实现原理。
/** * Constructs a new, empty linked hash set. (This package private * constructor is only used by LinkedHashSet.) The backing * HashMap instance is a LinkedHashMap with the specified initial * capacity and the specified load factor. * * @param initialCapacity the initial capacity of the hash map * @param loadFactor the load factor of the hash map * @param dummy ignored (distinguishes this * constructor from other int, float constructor.) * @throws IllegalArgumentException if the initial capacity is less * than zero, or if the load factor is nonpositive */ HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); }
正如前面所述,LinkedHashSet是继承了HashSet的,大部分操做也是直接继承的,只有少部分本身的方法,而且构造器方法都是想上调用了HashSet的那个建立一个LinkedHashMap的构造器方法,以下所示。因此LinkedHashSet底层是用LinkedHashMap来实现的,
public LinkedHashSet(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor, true); } public LinkedHashSet(int initialCapacity) { super(initialCapacity, .75f, true); } public LinkedHashSet() { super(16, .75f, true); } public LinkedHashSet(Collection<? extends E> c) { super(Math.max(2*c.size(), 11), .75f, true); addAll(c); } @Override public Spliterator<E> spliterator() { return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED); }
TreeSet底层是用TreeMap来实现的,以下面代码所示在构造器函数中建立了一个TreeMap对象,并将其赋值给了m(NavigableMap接口类型,TreeMap也实现了该类),与HashSet一样的作法:将键做为元素,值则都指向PRESENT,一样对应的查找删除添加操做也都是调用TreeMap来完成的,这里再也不重复说明。
private transient NavigableMap<E,Object> m; private static final Object PRESENT = new Object(); TreeSet(NavigableMap<E,Object> m) { this.m = m; } public TreeSet() { this(new TreeMap<E,Object>()); } public TreeSet(Comparator<? super E> comparator) { this(new TreeMap<>(comparator)); }
Queue经常使用的实现类是ArrayDeque和LinkedList,ArrayDeque是Deque 接口的大小可变数组的实现。数组双端队列没有容量限制;它们可根据须要增长以支持使用。它们不是线程安全的;在没有外部同步时,它们不支持多个线程的并发访问。禁止 null 元素。此类极可能在用做堆栈时快于Stack,在用做队列时快于LinkedList。
Queue中的三类方法以下:
peekFirst and peekLast return NULL.
(1) HashMap:它根据键的hashCode值存储数据,大多数状况下能够直接定位到它的值,于是具备很快的访问速度,但遍历顺序倒是不肯定的。 HashMap最多只容许一条记录的键为null,容许多条记录的值为null。HashMap非线程安全,即任一时刻能够有多个线程同时写HashMap,可能会致使数据的不一致。若是须要知足线程安全,能够用 Collections的synchronizedMap方法使HashMap具备线程安全的能力,或者使用ConcurrentHashMap。
(2) Hashtable:Hashtable是遗留类,不少映射的经常使用功能与HashMap相似,不一样的是它承自Dictionary类,而且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,由于ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不须要线程安全的场合能够用HashMap替换,须要线程安全的场合能够用ConcurrentHashMap替换。
(3) LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先获得的记录确定是先插入的,也能够在构造时带参数,按照访问次序排序。
(4) TreeMap:TreeMap实现SortedMap接口,可以把它保存的记录根据键排序,默认是按键值的升序排序,也能够指定排序的比较器,当用Iterator遍历TreeMap时,获得的记录是排过序的。若是使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,不然会在运行时抛出java.lang.ClassCastException类型的异常。
对于上述四种Map类型的类,要求映射中的key是不可变对象。不可变对象是该对象在建立后它的哈希值不会被改变。若是对象的哈希值发生变化,Map对象极可能就定位不到映射的位置了。
关于HashMap主要看这篇就行了Java 8系列之从新认识HashMap,而后下面是从文中稍微摘取了自认为几个比较重要的点吧:
这也符合散列表以空间换时间的特色,小于1的负载因子的存在就是为了让数组中的链表长度尽量短,由于链表查找是更花费时间相比于数组的访问,负载因子如为1就是在键的hash值扰动取模后均匀分布的理想状况下,每一个桶内的元素个数指望是1,链表长度就只是1,这是最理想的状况了。但实际上分布达不到这么均匀,为了减小链表长度把负载因子设置成了0.75来保证链表长度尽量短。固然仅这一种减小时间的作法java官方可能还不够,若是就是出现了小几率事件某个桶内链表长度比较长怎么办,java8引入了树化桶方法。在某个桶内链表长度大于8时将其该链表转换为红黑树结构
但仅仅直接是原始的hashCode值低位信息显然是不行的,hashCode值是为了保证总体均匀的(即尽量不一样的对象对应不一样的散列码),低位可能并不怎么均匀,为了解决这种状况HashMap中会将原始的hashCode值与高16位进行一个异或(不一样为1相同为0)操做,这样就混合了原始hashCode低位和高位,加大低位随机均匀性。而后用这个混合后的hash值再去进行按位与运算。
以上也就是为何要使用扰动函数、默认容量为16以及本身设定的容量会被自动提高为最近的2次幂大小的缘由。
/** * Implements Map.put and related methods * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don't change existing value(若是值不为null,不进行覆盖,这是为putIfAbsent这种插入方法准备的) * @param evict if false, the table is in creation mode. * evict参数是由于LinkedHashMap预留了一个能够链表中长度固定,并保持最新的N的节点数据的方法afterNodeInsertion(由于removeEldestEntry始终返回false因此目前并不生效), * 能够经过重写removeEldestEntry来就能进行实现了。 * @return previous value, or null if none */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //看看对应桶上是否是已经有元素了 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);//桶中没有结点的话,就将其直接放进去 else {//桶中已有结点的话 Node<K,V> e; K k; //则先看hash值是否相同(来到这个位置是根据的是hash扰动后的值,可能hash值就不同),键的内存地址是否相同,键的内容是否相等。 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //三个条件均知足了则说明键相等,要对这个结点(多是链表的头结点,也多是红黑树的根节点)进行更新了。不知足则要进行插入了 else if (p instanceof TreeNode)//若是是红黑树结点那就调用插入到红黑树中的方法 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else {//链表结点则如下 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) {//循环后发现没有相等键的结点,则插入到最后一个节点后面 p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // 保证了在插入后链表长度为9时就进入桶树化函数 treeify是树化的意思。 //但这个函数只会在数组长度大于等于64时进行将hash肯定的这个桶内的链表转换成红黑树,对应结点也转换成了红黑树结点。 //若是数组长度小于64时就只会进行扩容操做了,而不是转换成红黑树,由于扩容后极可能链表长度就减小了 treeifyBin(tab, hash); break;//树化桶执行完毕以后结束循环,而且这个时候的e是null } //在循环中查找有无键相等的结点 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) //看hash值是否相同(来到这个位置是根据的是hash扰动后的值,可能hash值就不同),键的内存地址是否相同,键的内容是否相等。 break;//相等则从循环中跳出来,这个时候的e保存的是桶内键相等的结点的引用 p = e; } } //e!=null说明e桶内键相等的结点的引用,则进行值的覆盖 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
LinkedHashMap是HashMap的子类,相比HashMap增长的就是更换告终点为本身的内部静态类LinkedHashMap.Entry,这个Entry继承自HashMap.Node,增长了before和after指针用来记录插入顺序。以下图[via9]所示
用红黑树实现,关于红黑树实现原理已经写过了就再也不写了。
这两个List实现类都不是同步的。若是多个线程同时访问一个ArrayList或者LinkedList实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。(结构上的修改是指任何添加或删除一个或多个元素的操做,或者显式调整底层数组的大小;仅仅设置元素的值不是结构上的修改。)
这通常经过对封装该List的对象进行同步操做来完成。若是不存在这样的对象,则应该使用Collections.synchronizedList方法将该列表“包装”起来。这最好在建立时完成,以防止意外对列表进行不一样步的访问,以下所示:
List list = Collections.synchronizedList(new ArrayList(...)); List list = Collections.synchronizedList(new LinkedList(...));
注意,这三个Set实现类都不是同步的。若是多个线程同时访问HashSet、LinkedHashSet或者TreeSet,而其中至少一个线程修改了该set,则它必须保持外部同步。
这通常经过对封装该set的对象进行同步操做来完成。若是不存在这样的对象,则应该使用 Collections.synchronizedSet (对于TreeSet是Collections.synchronizedSortedSet)方法来“包装”该 set。最好在建立时完成这一操做,以防止意外的非同步访问:
Set s = Collections.synchronizedSet(new LinkedHashSet(...)); Set s = Collections.synchronizedSet(new HashSet(...)); SortedSet s = Collections.synchronizedSortedSet(new TreeSet(...));
注意,这三个Map实现类都不是同步的。若是多个线程同时访问一个HashMap、LinkedHashMap或者TreeMap,而其中至少一个线程从结构上修改了该映射,则它必须 保持外部同步。这通常经过对天然封装该映射的对象进行同步操做来完成。若是不存在这样的对象,则应该使用 Collections.synchronizedMap(对于TreeMap应该使用Collections.synchronizedSortedMap)方法来“包装”该映射。最好在建立时完成这一操做,以防止对映射进行意外的非同步访问,以下所示:
Map m = Collections.synchronizedMap(new HashMap(...)); Map m = Collections.synchronizedMap(new LinkedHashMap(...)); SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));
(结构上的修改是指添加或删除一个或多个映射关系的任何操做;仅改变与实例已经包含的键关联的值不是结构上的修改。对于LinkedHashMap而言,当按访问排序的HashMap时,结构上的修改还包括影响迭代顺序的任何操做,但此时仅利用get查询LinkedHashMapMap不是结构修改)
参考文章:
Java™ Platform Standard Ed. 8 Api
在中间位置添加元素,ArrayList比LinkedList效率更高?
arraylist add(int index) 方法时 index是处于前半部分仍是后半部分效率高
ArrayList初始化
ArrayList底层数组扩容原理
List、Set、Map的区别
Java集合框架源码剖析:LinkedHashSet 和 LinkedHashMap
浅谈java 集合框架
JDK 源码中 HashMap 的 hash 方法原理是什么?胖君的回答
编程语言中,取余和取模的区别究竟是什么?
深刻理解 hashcode 和 hash 算法