Java集合教程之Collection實(shí)例詳解
前言
集合就是一組數(shù)的集合,就像是一個(gè)容器,但是我們應(yīng)該清楚的是集合中存放的都是對(duì)象的引用,而不是真正的實(shí)體。而我們常說的集合中的對(duì)象其實(shí)指的就是對(duì)象的引用。
我們可以把集合理解為一個(gè)小型數(shù)據(jù)庫,用于存放數(shù)據(jù),我們對(duì)集合的操作也就是數(shù)據(jù)的增刪改查,在 Java 中有兩個(gè)頂層接口 Collection 和 Map 用于定義和規(guī)范集合的相關(guān)操作。這篇文章主要說一下集合框架中的 Collection 部分。
Collection 表示一組對(duì)象,這些對(duì)象可以是有序也可以是無序的,它提供了不同的子接口滿足我們的需求。我們主要看看 List 和 Set 。
List 整體的特征就是有序可重復(fù)。我們需要研究的是上圖中具體的實(shí)現(xiàn)類都有什么特性。底層的實(shí)現(xiàn)原理是什么,首先來看一看 List 的古老的實(shí)現(xiàn)類 Vector ,說是古老是因?yàn)樵?JDK 1.0 的時(shí)候就出現(xiàn)了,都走開,我要開始看源碼了!這些源碼來自于 JDK 1.7。
public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { /** * The array buffer into which the components of the vector are * stored. */ protected Object[] elementData; /** * The number of valid components in this {@code Vector} object. */ protected int elementCount; /** * The amount by which the capacity of the vector is automatically * incremented when its size becomes greater than its capacity. If * the capacity increment is less than or equal to zero, the capacity * of the vector is doubled each time it needs to grow. */ protected int capacityIncrement; public Vector() { this(10); } // 添加元素 public synchronized boolean add(E e) { modCount++; ensureCapacityHelper(elementCount + 1); elementData[elementCount++] = e; return true; } // 刪除元素 public synchronized E remove(int index) { modCount++; if (index >= elementCount) throw new ArrayIndexOutOfBoundsException(index); E oldValue = elementData(index); int numMoved = elementCount - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--elementCount] = null; // Let gc do its work return oldValue; } // 修改元素 public synchronized E set(int index, E element) { if (index >= elementCount) throw new ArrayIndexOutOfBoundsException(index); E oldValue = elementData(index); elementData[index] = element; return oldValue; } // 查找元素 public synchronized E get(int index) { if (index >= elementCount) throw new ArrayIndexOutOfBoundsException(index); return elementData(index); } ...
就以上源碼分析就可以知道關(guān)于 Vector 的特征。1 底層實(shí)現(xiàn)是使用數(shù)組來存儲(chǔ)數(shù)據(jù),所以相應(yīng)的查找元素和添加元素速度較快,刪除和插入元素較慢。2 數(shù)組的初始長度為 10 ,當(dāng)長度不夠時(shí),增長量也為 10 使用變量 capacityIncrement 來表示。3 方法的聲明中都加入了 synchronized 關(guān)鍵字,線程安全的,所以效率相應(yīng)降低了。 4 沒分析出來的再看一遍。
下面開始看 ArrayList 的源碼。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { private static final long serialVersionUID = 8683452581122892189L; /** * Default initial capacity. */ private static final int DEFAULT_CAPACITY = 10; /** * Shared empty array instance used for empty instances. */ private static final Object[] EMPTY_ELEMENTDATA = {}; /** * The array buffer into which the elements of the ArrayList are stored. * DEFAULT_CAPACITY when the first element is added. */ private transient Object[] elementData; /** * Constructs an empty list with an initial capacity of ten. */ public ArrayList() { super(); this.elementData = EMPTY_ELEMENTDATA; } // 添加元素 public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } // 增加數(shù)組的長度 private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); } ...
因?yàn)樵创a和 Vector 類似,所以有些就不貼了,但是不耽誤我們繼續(xù)分析 ArrayList 。1 底層存儲(chǔ)數(shù)據(jù)使用的還是數(shù)組,長度依舊為 10 ,但是進(jìn)步了,沒有在剛開始創(chuàng)建的時(shí)候就初始化,而是在添加第一個(gè)元素的時(shí)候才初始化的。2 方法的聲明少了 synchronized 關(guān)鍵字,線程不安全,但性能提高了。3 數(shù)組長度不夠時(shí),會(huì)自動(dòng)增加為原長度的 1.5 倍。
以上分析也能體現(xiàn)出 Vector 和 ArrayList 的差別。主要就是想說 Vector 已經(jīng)不用了。使用 ArrayList 即可,關(guān)于線程安全問題,后面再說。
接著看 LinkedList 的實(shí)現(xiàn),上源碼 ~
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable { transient int size = 0; /** * Pointer to first node. */ transient Node<E> first; /** * Pointer to last node. */ transient Node<E> last; /** * Constructs an empty list. */ public LinkedList() { } // 每一個(gè)元素即為一個(gè)節(jié)點(diǎn),節(jié)點(diǎn)的結(jié)構(gòu)如下(這是一個(gè)內(nèi)部類?。? private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } } // 添加元素 public boolean add(E e) { linkLast(e); return true; } void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; } // 刪除某個(gè)節(jié)點(diǎn)的邏輯 E unlink(Node<E> x) { // assert x != null; final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev; if (prev == null) { first = next; } else { prev.next = next; x.prev = null; } if (next == null) { last = prev; } else { next.prev = prev; x.next = null; } x.item = null; size--; modCount++; return element; } ...
重點(diǎn)就是 LinkedList 的底層實(shí)現(xiàn)是雙鏈表。這樣就會(huì)有以下特性,1 查找元素較慢,但是添加和刪除較快。2 占內(nèi)存,因?yàn)槊恳粋€(gè)節(jié)點(diǎn)都要維護(hù)兩個(gè)索引。3 線程不安全 。4 對(duì)集合長度沒有限制。
以上,List 的幾個(gè)實(shí)現(xiàn)已經(jīng)分析完成,以后再談到 Vector ,ArrayList ,LinkedList 之間的區(qū)別應(yīng)該不會(huì)不知所云了吧!還要接著看 Collection 的另一個(gè)子接口 Set 。首先有個(gè)大前提,Set 中存儲(chǔ)的元素是無序不可重復(fù)的。然后我們再來看實(shí)現(xiàn)類是如何實(shí)現(xiàn)的。下面開始 HashSet 的表演。
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable { private transient HashMap<E,Object> map; // Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object(); /** * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has * default initial capacity (16) and load factor (0.75). */ public HashSet() { map = new HashMap<>(); } // 添加元素,其實(shí)就是像 Map 中添加主鍵,所以添加的元素不能重復(fù) public boolean add(E e) { return map.put(e, PRESENT)==null; } // 實(shí)現(xiàn) Iterable 接口中的 Iterator 方法。 public Iterator<E> iterator() { return map.keySet().iterator(); } ...
看了源碼才發(fā)現(xiàn),1 原來 HashSet 就是對(duì) HashMap 的封裝啊,底層實(shí)現(xiàn)是基于 hash 表的,回頭有必要再好好的介紹一下 hash 相關(guān)的知識(shí)。2 Set 集合中的值,都會(huì)以 key 的形式存放在 Map 中,所以說 Set 中的元素不能重復(fù)。3 線程不安全。4 允許存放空值,因?yàn)?Map 的鍵允許為空。
今天要說的最后一個(gè)實(shí)現(xiàn)類終于出現(xiàn)啦,他就是 TreeSet ,這個(gè)實(shí)現(xiàn)類中的元素是有序的!注意這里說的有序是指按照一定的規(guī)則排序,而我們說 Set 集合中的元素?zé)o序是因?yàn)樘砑舆M(jìn)集合的順序和輸出的順序不保證一致。TreeSet 是怎么保證有序我們待會(huì)再說,還是一樣的套路,是對(duì) TreeMap 的封裝,線程依舊不安全。
public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>, Cloneable, java.io.Serializable { /** * The backing map. */ private transient NavigableMap<E,Object> m; // Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object(); /** * Constructs a set backed by the specified navigable map. */ TreeSet(NavigableMap<E,Object> m) { this.m = m; } /** * Constructs a new, empty tree set, sorted according to the * natural ordering of its elements. All elements inserted into * the set must implement the Comparable interface. */ public TreeSet() { this(new TreeMap<E,Object>()); } /** * Constructs a new, empty tree set, sorted according to the specified * comparator. All elements inserted into the set must be mutually * comparable by the specified comparator * If the Comparable natural ordering of the elements will be used. */ public TreeSet(Comparator<? super E> comparator) { this(new TreeMap<>(comparator)); } // 添加元素方法 public boolean add(E e) { return m.put(e, PRESENT)==null; } ...
我們可以看到 TreeSet 中構(gòu)造函數(shù)上方的注釋,TreeSet 要保證元素有序,保證有序的思路是在添加元素的時(shí)候進(jìn)行比較。
... // 這是 TreeMap 的 put 方法的節(jié)選,為了看到比較的過程。 public V put(K key, V value) { ... // split comparator and comparable paths Comparator<? super K> cpr = comparator; if (cpr != null) { do { parent = t; cmp = cpr.compare(key, t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else return t.setValue(value); } while (t != null); } else { if (key == null) throw new NullPointerException(); Comparable<? super K> k = (Comparable<? super K>) key; do { parent = t; cmp = k.compareTo(t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else return t.setValue(value); } while (t != null); } ... }
Java 中提供了兩種方式,第一種方法,需要我們所添加對(duì)象的類實(shí)現(xiàn) Comparable 接口,進(jìn)而實(shí)現(xiàn) compareTo 方法,這種方式也叫自然排序,我們并沒有傳入什么排序規(guī)則。這種方式對(duì)應(yīng) TreeSet 的空參構(gòu)造器。而另一種方式就是定制排序,即我們自己定義兩個(gè)元素的排序規(guī)則,在實(shí)例化 TreeSet 的時(shí)候傳入對(duì)應(yīng)的排序規(guī)則即可,對(duì)應(yīng)于 TreeSet 中帶有 Comparator 接口的構(gòu)造器,這里面需要實(shí)現(xiàn) compare 方法 。有點(diǎn)迷糊了是吧,舉個(gè)例子看看 ~
public class Person implements Comparable<Person>{ public String name; public Integer age; public Person(String name,Integer age) { this.name = name; this.age = age; } /* 自定義的比較的邏輯: * 首先按照對(duì)象的 name 屬性排序 * 其次按照 age 屬性排序 * 方法的返回值為 0 ,大于 0,小于 0 ,分別對(duì)應(yīng)于 相等,大于,和小于 */ @Override public int compareTo(Person o) { int i = this.name.compareTo(o.name); if(i == 0){ return this.age.compareTo(o.age); }else { return i; } } @Override public String toString() { return "[Person] name:"+this.name+" age:"+this.age; } } // 以下是測試代碼 public static void main(String[] args) { TreeSet<Person> set = new TreeSet<>(); set.add(new Person("AJK923",20)); set.add(new Person("BJK923",20)); set.add(new Person("AJK923",21)); set.add(new Person("BJK923",21)); for (Person person : set) { System.out.println(person.toString()); } /* [Person] name:AJK923 age:20 [Person] name:AJK923 age:21 [Person] name:BJK923 age:20 [Person] name:BJK923 age:21 */ ----以下為定制排序的部分----匿名內(nèi)部類實(shí)現(xiàn) Comparator 接口---- TreeSet<Person> set2 = new TreeSet<>(new Comparator<Person>() { @Override public int compare(Person o1, Person o2) { int i = o1.age.compareTo(o2.age); if(i == 0){ return o1.name.compareTo(o2.name); }else { return i; } } }); set2.add(new Person("AJK923",20)); set2.add(new Person("BJK923",20)); set2.add(new Person("AJK923",21)); set2.add(new Person("BJK923",21)); for (Person person : set2) { System.out.println(person.toString()); } /* [Person] name:AJK923 age:20 [Person] name:BJK923 age:20 [Person] name:AJK923 age:21 [Person] name:BJK923 age:21 */ }
上面就是自然排序和定制排序的應(yīng)用。要說明幾點(diǎn):
1 定制排序和自然排序同時(shí)存在的話,優(yōu)先執(zhí)行定制排序??梢钥纯瓷厦娴?put 方法的實(shí)現(xiàn) 。
2 自然排序?qū)?yīng) Comparable 接口,定制排序?qū)?yīng) Comparator 接口。
3 String ,包裝類,日期類都已經(jīng)實(shí)現(xiàn)了 Comparable 接口的 comparaTo 方法,所以我上面的例子中都偷懶了,沒有自己實(shí)現(xiàn)具體比較,而是直接調(diào)用現(xiàn)成的。
看到這里,也算是對(duì) Collection 有了整體的認(rèn)識(shí),但是這里我并沒有說到具體的 API ,我現(xiàn)在日常也用不到幾個(gè)方法,就放一張 Collection 接口的結(jié)構(gòu)圖吧,方法也比較簡單,看個(gè)名字就大概知道什么意思了。
還是要重點(diǎn)說一下 iterator 方法。這個(gè)方法定義在 Iterable 接口中(Collection 繼承了 Iterable 接口),方法返回的是 iterator 接口對(duì)象。iterator 中定義了迭代器的操作規(guī)則,而 Collection 的實(shí)現(xiàn)類中是具體的實(shí)現(xiàn)。Iterator 接口中定義了 next ,hasNext 和 remove 方法??匆幌略贏rrayList 中是如何實(shí)現(xiàn)的吧。
private class Itr implements Iterator<E> { int cursor; // index of next element to return int lastRet = -1; // index of last element returned; -1 if no such int expectedModCount = modCount; public boolean hasNext() { return cursor != size; } @SuppressWarnings("unchecked") public E next() { checkForComodification(); int i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1; return (E) elementData[lastRet = i]; } public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }
應(yīng)用起來還是比較簡單的,使用一個(gè) while 循環(huán)即可??聪旅孢@個(gè)例子。
public static void main(String[] args) { List<Integer> list = new ArrayList<>(); list.add(1); list.add(2); list.add(3); Iterator<Integer> it = list.iterator(); while (it.hasNext()) { Integer i = it.next(); System.out.println(i); } }
不知道你有沒有發(fā)現(xiàn),除了 Vector 以外,保存集合元素的那個(gè)變量都定義為 transient 不管你是數(shù)組,Node 或是 Map ,不由得我又要想一想為什么這樣設(shè)計(jì)?
先看一下 transient 的作用吧!Java 的 serialization 提供了一種持久化對(duì)象實(shí)例的機(jī)制。當(dāng)持久化對(duì)象時(shí),可能有一個(gè)特殊的對(duì)象數(shù)據(jù)成員,我們不想用 serialization 機(jī)制來保存它。為了在一個(gè)特定對(duì)象的一個(gè)域上關(guān)閉 serialization,可以在這個(gè)域前加上關(guān)鍵字 transient 。當(dāng)一個(gè)對(duì)象被序列化的時(shí)候,transient 型變量的值不包括在序列化的表示中,非 transient 型的變量是被包括進(jìn)去的。
那么為什么要這么做呢,好好的標(biāo)準(zhǔn)序列化不用,原因如下:
對(duì)于 ArrayList 來說,底層實(shí)現(xiàn)是數(shù)組,而數(shù)組的長度和容量不一定相等,可能容量為 10 ,而我們的元素只有 5 。此時(shí)序列化的時(shí)候就有點(diǎn)浪費(fèi)資源,序列化和反序列化還是要的,故 ArrayList 自己實(shí)現(xiàn)了兩個(gè)方法,分別是 writeObject 和 readObject ,用于序列化和反序列化。
對(duì)于 HashSet 和 HashMap 來說,底層實(shí)現(xiàn)都是依賴于 hash 表,而不同的 JVM 可能算出的 hashCode 值不一致,這樣在跨平臺(tái)的時(shí)候就會(huì)導(dǎo)致序列化紊亂。故也重寫了那兩個(gè)方法。借用一句似懂非懂的話:
當(dāng)一個(gè)對(duì)象的物理表示方法與它的邏輯數(shù)據(jù)內(nèi)容有實(shí)質(zhì)性差別時(shí),使用默認(rèn)序列化形式有 N 種缺陷,所以應(yīng)該盡可能的根據(jù)實(shí)際情況重寫序列化方法。
對(duì)應(yīng)于 Collection ,有一個(gè) Collections 工具類,其中提供很多方法,比如說集合的排序,求子集合,最大值,最小值,交換,填充,打亂集合等等,還記得上面說到的實(shí)現(xiàn)類中存在線程不安全的情況吧,這個(gè)工具類中提供很多對(duì)應(yīng)的 synchronized 的方法。
后記 :不知不覺中擴(kuò)展了這么多知識(shí)點(diǎn),實(shí)話說,肯定還有遺漏的地方,就我現(xiàn)在能想到的依然還有很多,除去 Hash 表和 Hash 算法這一部分之外,我還沒有對(duì)底層的數(shù)據(jù)結(jié)構(gòu)進(jìn)一步分析,數(shù)組,鏈表,二叉樹等等,現(xiàn)在是分析不動(dòng)了,本文已經(jīng)很長了。
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
idea不能自動(dòng)補(bǔ)全yml配置文件的原因分析
這篇文章主要介紹了idea不能自動(dòng)補(bǔ)全yml配置文件的原因,通過添加yml文件為配置文件能夠很快的解決,具體解決步驟跟隨小編一起通過本文學(xué)習(xí)下吧2021-06-06合并有序數(shù)組的實(shí)現(xiàn)(java與C語言)
這篇文章主要介紹了合并有序數(shù)組的實(shí)現(xiàn)(java與C語言)的相關(guān)資料,這里對(duì)有序數(shù)組的合并分享了java版本和C語言版本的示例,需要的朋友可以參考下2017-08-08關(guān)于maven依賴 ${xxx.version}報(bào)錯(cuò)問題
這篇文章主要介紹了關(guān)于maven依賴 ${xxx.version}報(bào)錯(cuò)問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01Java ByteBuffer網(wǎng)絡(luò)編程用法實(shí)例解析
這篇文章主要介紹了Java ByteBuffer網(wǎng)絡(luò)編程用法實(shí)例解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-10-10springboot中關(guān)于classpath:路徑使用及說明
這篇文章主要介紹了springboot中關(guān)于classpath:路徑使用及說明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-09-09Java基于Javafaker生成測試數(shù)據(jù)
這篇文章主要介紹了Java基于Javafaker生成測試數(shù)據(jù)的方法,幫助大家更好的理解和使用Java,感興趣的朋友可以了解下2020-12-12