夯實Java基礎系列19:一文搞懂Java集合類框架,以及常見面試題

a724888發表於2019-10-08

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫裡檢視

https://github.com/h2pl/Java-Tutorial

喜歡的話麻煩點下Star哈

文章首發於我的個人部落格:

www.how2playlife.com

本文參考 https://www.cnblogs.com/chenssy/p/3495238.html

在編寫java程式中,我們最常用的除了八種基本資料型別,String物件外還有一個集合類,在我們的的程式中到處充斥著集合類的身影!

image

java中集合大家族的成員實在是太豐富了,有常用的ArrayList、HashMap、HashSet,也有不常用的Stack、Queue,有執行緒安全的Vector、HashTable,也有執行緒不安全的LinkedList、TreeMap等等!

上面的圖展示了整個集合大家族的成員以及他們之間的關係。下面就上面的各個介面、基類做一些簡單的介紹(主要介紹各個集合的特點。區別)。

下面幾張圖更清晰地介紹了結合類介面間的關係:

Collections和Collection。
Arrays和Collections。

Collection的子介面

map的實現類

Collection介面

Collection介面是最基本的集合介面,它不提供直接的實現,Java SDK提供的類都是繼承自Collection的“子介面”如List和Set。Collection所代表的是一種規則,它所包含的元素都必須遵循一條或者多條規則。如有些允許重複而有些則不能重複、有些必須要按照順序插入而有些則是雜湊,有些支援排序但是有些則不支援。

在Java中所有實現了Collection介面的類都必須提供兩套標準的建構函式,一個是無參,用於建立一個空的Collection,一個是帶有Collection引數的有參建構函式,用於建立一個新的Collection,這個新的Collection與傳入進來的Collection具備相同的元素。
//要求實現基本的增刪改查方法,並且需要能夠轉換為陣列型別

public class Collection介面 {
    class collect implements Collection {
        @Override
        public int size() {
            return 0;
        }
        @Override
        public boolean isEmpty() {
            return false;
        }
        @Override
        public boolean contains(Object o) {
            return false;
        }
        @Override
        public Iterator iterator() {
            return null;
        }
        @Override
        public Object[] toArray() {
            return new Object[0];
        }
        @Override
        public boolean add(Object o) {
            return false;
        }
        @Override
        public boolean remove(Object o) {
            return false;
        }
        @Override
        public boolean addAll(Collection c) {
            return false;
        }
        @Override
        public void clear() {
        }
//省略部分程式碼  
        @Override
        public Object[] toArray(Object[] a) {
            return new Object[0];
        }
    }
}

List介面

List介面為Collection直接介面。List所代表的是有序的Collection,即它用某種特定的插入順序來維護元素順序。使用者可以對列表中每個元素的插入位置進行精確地控制,同時可以根據元素的整數索引(在列表中的位置)訪問元素,並搜尋列表中的元素。實現List介面的集合主要有:ArrayList、LinkedList、Vector、Stack。

2.1、ArrayList

ArrayList是一個動態陣列,也是我們最常用的集合。它允許任何符合規則的元素插入甚至包括null。每一個ArrayList都有一個初始容量(10),該容量代表了陣列的大小。隨著容器中的元素不斷增加,容器的大小也會隨著增加。在每次向容器中增加元素的同時都會進行容量檢查,當快溢位時,就會進行擴容操作。所以如果我們明確所插入元素的多少,最好指定一個初始容量值,避免過多的進行擴容操作而浪費時間、效率。

size、isEmpty、get、set、iterator 和 listIterator 操作都以固定時間執行。add 操作以分攤的固定時間執行,也就是說,新增 n 個元素需要 O(n) 時間(由於要考慮到擴容,所以這不只是新增元素會帶來分攤固定時間開銷那樣簡單)。

ArrayList擅長於隨機訪問。同時ArrayList是非同步的。

2.2、LinkedList

同樣實現List介面的LinkedList與ArrayList不同,ArrayList是一個動態陣列,而LinkedList是一個雙向連結串列。所以它除了有ArrayList的基本操作方法外還額外提供了get,remove,insert方法在LinkedList的首部或尾部。

由於實現的方式不同,LinkedList不能隨機訪問,它所有的操作都是要按照雙重連結串列的需要執行。在列表中索引的操作將從開頭或結尾遍歷列表(從靠近指定索引的一端)。這樣做的好處就是可以通過較低的代價在List中進行插入和刪除操作。

與ArrayList一樣,LinkedList也是非同步的。如果多個執行緒同時訪問一個List,則必須自己實現訪問同步。一種解決方法是在建立List時構造一個同步的List:
List list = Collections.synchronizedList(new LinkedList(…));

2.3、Vector
與ArrayList相似,但是Vector是同步的。所以說Vector是執行緒安全的動態陣列。它的操作與ArrayList幾乎一樣。

2.4、Stack
Stack繼承自Vector,實現一個後進先出的堆疊。Stack提供5個額外的方法使得Vector得以被當作堆疊使用。基本的push和pop 方法,還有peek方法得到棧頂的元素,empty方法測試堆疊是否為空,search方法檢測一個元素在堆疊中的位置。Stack剛建立後是空棧。。

public class List介面 {
    //下面是List的繼承關係,由於List介面規定了包括諸如索引查詢,迭代器的實現,所以實現List介面的類都會有這些方法。
    //所以不管是ArrayList和LinkedList底層都可以使用陣列操作,但一般不提供這樣外部呼叫方法。
    //    public interface Iterable<T>
//    public interface Collection<E> extends Iterable<E>
//    public interface List<E> extends Collection<E>
    class MyList implements List {
        @Override
        public int size() {
            return 0;
        }
        @Override
        public boolean isEmpty() {
            return false;
        }
        @Override
        public boolean contains(Object o) {
            return false;
        }
        @Override
        public Iterator iterator() {
            return null;
        }
        @Override
        public Object[] toArray() {
            return new Object[0];
        }
        @Override
        public boolean add(Object o) {
            return false;
        }
        @Override
        public boolean remove(Object o) {
            return false;
        }
        @Override
        public void clear() {
        }
       //省略部分程式碼
        @Override
        public Object get(int index) {
            return null;
        }
        @Override
        public ListIterator listIterator() {
            return null;
        }
        @Override
        public ListIterator listIterator(int index) {
            return null;
        }
        @Override
        public List subList(int fromIndex, int toIndex) {
            return null;
        }
        @Override
        public Object[] toArray(Object[] a) {
            return new Object[0];
        }
    }
}

Set介面

Set是一種不包括重複元素的Collection。它維持它自己的內部排序,所以隨機訪問沒有任何意義。與List一樣,它同樣執行null的存在但是僅有一個。由於Set介面的特殊性,所有傳入Set集合中的元素都必須不同,同時要注意任何可變物件,如果在對集合中元素進行操作時,導致e1.equals(e2)==true,則必定會產生某些問題。實現了Set介面的集合有:EnumSet、HashSet、TreeSet。

3.1、EnumSet
是列舉的專用Set。所有的元素都是列舉型別。

3.2、HashSet
HashSet堪稱查詢速度最快的集合,因為其內部是以HashCode來實現的。它內部元素的順序是由雜湊碼來決定的,所以它不保證set 的迭代順序;特別是它不保證該順序恆久不變。

public class Set介面 {
    // Set介面規定將set看成一個集合,並且使用和陣列類似的增刪改查方式,同時提供iterator迭代器
    //    public interface Set<E> extends Collection<E>
    //    public interface Collection<E> extends Iterable<E>
    //    public interface Iterable<T>
    class MySet implements Set {
        @Override
        public int size() {
            return 0;
        }
        @Override
        public boolean isEmpty() {
            return false;
        }
        @Override
        public boolean contains(Object o) {
            return false;
        }
        @Override
        public Iterator iterator() {
            return null;
        }
        @Override
        public Object[] toArray() {
            return new Object[0];
        }
        @Override
        public boolean add(Object o) {
            return false;
        }
        @Override
        public boolean remove(Object o) {
            return false;
        }
        @Override
        public boolean addAll(Collection c) {
            return false;
        }
        @Override
        public void clear() {
        }
        @Override
        public boolean removeAll(Collection c) {
            return false;
        }
        @Override
        public boolean retainAll(Collection c) {
            return false;
        }
        @Override
        public boolean containsAll(Collection c) {
            return false;
        }
        @Override
        public Object[] toArray(Object[] a) {
            return new Object[0];
        }
    }
}

Map介面

Map與List、Set介面不同,它是由一系列鍵值對組成的集合,提供了key到Value的對映。同時它也沒有繼承Collection。在Map中它保證了key與value之間的一一對應關係。也就是說一個key對應一個value,所以它不能存在相同的key值,當然value值可以相同。實現map的有:HashMap、TreeMap、HashTable、Properties、EnumMap。

4.1、HashMap
以雜湊表資料結構實現,查詢物件時通過雜湊函式計算其位置,它是為快速查詢而設計的,其內部定義了一個hash表陣列(Entry[] table),元素會通過雜湊轉換函式將元素的雜湊地址轉換成陣列中存放的索引,如果有衝突,則使用雜湊連結串列的形式將所有相同雜湊地址的元素串起來,可能通過檢視HashMap.Entry的原始碼它是一個單連結串列結構。

4.2、TreeMap
鍵以某種排序規則排序,內部以red-black(紅-黑)樹資料結構實現,實現了SortedMap介面

4.3、HashTable
也是以雜湊表資料結構實現的,解決衝突時與HashMap也一樣也是採用了雜湊連結串列的形式,不過效能比HashMap要低

public class Map介面 {
    //Map介面是最上層介面,Map介面實現類必須實現put和get等雜湊操作。
    //並且要提供keyset和values,以及entryset等查詢結構。
    //public interface Map<K,V>
    class MyMap implements Map {
        @Override
        public int size() {
            return 0;
        }
        @Override
        public boolean isEmpty() {
            return false;
        }
        @Override
        public boolean containsKey(Object key) {
            return false;
        }
        @Override
        public boolean containsValue(Object value) {
            return false;
        }
        @Override
        public Object get(Object key) {
            return null;
        }
        @Override
        public Object put(Object key, Object value) {
            return null;
        }
        @Override
        public Object remove(Object key) {
            return null;
        }
        @Override
        public void putAll(Map m) {
        }
        @Override
        public void clear() {
        }
        @Override
        public Set keySet() {
            return null;
        }
        @Override
        public Collection values() {
            return null;
        }
        @Override
        public Set<Entry> entrySet() {
            return null;
        }
    }
}

Queue

佇列,它主要分為兩大類,一類是阻塞式佇列,佇列滿了以後再插入元素則會丟擲異常,主要包括ArrayBlockQueue、PriorityBlockingQueue、LinkedBlockingQueue。另一種佇列則是雙端佇列,支援在頭、尾兩端插入和移除元素,主要包括:ArrayDeque、LinkedBlockingDeque、LinkedList。

public class Queue介面 {
    //queue介面是對佇列的一個實現,需要提供佇列的進隊出隊等方法。一般使用linkedlist作為實現類
    class MyQueue implements Queue {
        @Override
        public int size() {
            return 0;
        }
        @Override
        public boolean isEmpty() {
            return false;
        }
        @Override
        public boolean contains(Object o) {
            return false;
        }
        @Override
        public Iterator iterator() {
            return null;
        }
        @Override
        public Object[] toArray() {
            return new Object[0];
        }
        @Override
        public Object[] toArray(Object[] a) {
            return new Object[0];
        }
        @Override
        public boolean add(Object o) {
            return false;
        }
        @Override
        public boolean remove(Object o) {
            return false;
        }
        //省略部分程式碼
        @Override
        public boolean offer(Object o) {
            return false;
        }
        @Override
        public Object remove() {
            return null;
        }
        @Override
        public Object poll() {
            return null;
        }
        @Override
        public Object element() {
            return null;
        }
        @Override
        public Object peek() {
            return null;
        }
    }
}

關於Java集合的小抄

這部分內容轉自我偶像 江南白衣 的部落格: http://calvin1978.blogcn.com/articles/collection.html
在儘可能短的篇幅裡,將所有集合與併發集合的特徵、實現方式、效能捋一遍。適合所有”精通Java”,其實還不那麼自信的人閱讀。

期望能不止用於面試時,平時選擇資料結構,也能考慮一下其成本與效率,不要看著API合適就用了。

List

ArrayList

以陣列實現。節約空間,但陣列有容量限制。超出限制時會增加50%容量,用System.arraycopy()複製到新的陣列。因此最好能給出陣列大小的預估值。預設第一次插入元素時建立大小為10的陣列。

按陣列下標訪問元素-get(i)、set(i,e) 的效能很高,這是陣列的基本優勢。

如果按下標插入元素、刪除元素-add(i,e)、 remove(i)、remove(e),則要用System.arraycopy()來複制移動部分受影響的元素,效能就變差了。

越是前面的元素,修改時要移動的元素越多。直接在陣列末尾加入元素-常用的add(e),刪除最後一個元素則無影響。

LinkedList

以雙向連結串列實現。連結串列無容量限制,但雙向連結串列本身使用了更多空間,每插入一個元素都要構造一個額外的Node物件,也需要額外的連結串列指標操作。

按下標訪問元素-get(i)、set(i,e) 要悲劇的部分遍歷連結串列將指標移動到位 (如果i>陣列大小的一半,會從末尾移起)。

插入、刪除元素時修改前後節點的指標即可,不再需要複製移動。但還是要部分遍歷連結串列的指標才能移動到下標所指的位置。

只有在連結串列兩頭的操作-add()、addFirst()、removeLast()或用iterator()上的remove()倒能省掉指標的移動。

Apache Commons 有個TreeNodeList,裡面是棵二叉樹,可以快速移動指標到位。

CopyOnWriteArrayList

併發優化的ArrayList。基於不可變物件策略,在修改時先複製出一個陣列快照來修改,改好了,再讓內部指標指向新陣列。

因為對快照的修改對讀操作來說不可見,所以讀讀之間不互斥,讀寫之間也不互斥,只有寫寫之間要加鎖互斥。但複製快照的成本昂貴,典型的適合讀多寫少的場景。

雖然增加了addIfAbsent(e)方法,會遍歷陣列來檢查元素是否已存在,效能可想像的不會太好。

遺憾

無論哪種實現,按值返回下標contains(e), indexOf(e), remove(e) 都需遍歷所有元素進行比較,效能可想像的不會太好。

沒有按元素值排序的SortedList。

除了CopyOnWriteArrayList,再沒有其他執行緒安全又併發優化的實現如ConcurrentLinkedList。湊合著用Set與Queue中的等價類時,會缺少一些List特有的方法如get(i)。如果更新頻率較高,或陣列較大時,還是得用Collections.synchronizedList(list),對所有操作用同一把鎖來保證執行緒安全。

Map

HashMap

以Entry[]陣列實現的雜湊桶陣列,用Key的雜湊值取模桶陣列的大小可得到陣列下標。

插入元素時,如果兩條Key落在同一個桶(比如雜湊值1和17取模16後都屬於第一個雜湊桶),我們稱之為雜湊衝突。

JDK的做法是連結串列法,Entry用一個next屬性實現多個Entry以單向連結串列存放。查詢雜湊值為17的key時,先定位到雜湊桶,然後連結串列遍歷桶裡所有元素,逐個比較其Hash值然後key值。

在JDK8裡,新增預設為8的閾值,當一個桶裡的Entry超過閥值,就不以單向連結串列而以紅黑樹來存放以加快Key的查詢速度。

當然,最好還是桶裡只有一個元素,不用去比較。所以預設當Entry數量達到桶數量的75%時,雜湊衝突已比較嚴重,就會成倍擴容桶陣列,並重新分配所有原來的Entry。擴容成本不低,所以也最好有個預估值。

取模用與操作(hash & (arrayLength-1))會比較快,所以陣列的大小永遠是2的N次方, 你隨便給一個初始值比如17會轉為32。預設第一次放入元素時的初始值是16。

iterator()時順著雜湊桶陣列來遍歷,看起來是個亂序。

LinkedHashMap

擴充套件HashMap,每個Entry增加雙向連結串列,號稱是最佔記憶體的資料結構。

支援iterator()時按Entry的插入順序來排序(如果設定accessOrder屬性為true,則所有讀寫訪問都排序)。

插入時,Entry把自己加到Header Entry的前面去。如果所有讀寫訪問都要排序,還要把前後Entry的before/after拼接起來以在連結串列中刪除掉自己,所以此時讀操作也是執行緒不安全的了。

3 TreeMap

以紅黑樹實現,紅黑樹又叫自平衡二叉樹:

對於任一節點而言,其到葉節點的每一條路徑都包含相同數目的黑結點。
上面的規定,使得樹的層數不會差的太遠,使得所有操作的複雜度不超過 O(lgn),但也使得插入,修改時要複雜的左旋右旋來保持樹的平衡。

支援iterator()時按Key值排序,可按實現了Comparable介面的Key的升序排序,或由傳入的Comparator控制。可想象的,在樹上插入/刪除元素的代價一定比HashMap的大。

支援SortedMap介面,如firstKey(),lastKey()取得最大最小的key,或sub(fromKey, toKey), tailMap(fromKey)剪取Map的某一段。

EnumMap

EnumMap的原理是,在建構函式裡要傳入列舉類,那它就構建一個與列舉的所有值等大的陣列,按Enum. ordinal()下標來訪問陣列。效能與記憶體佔用俱佳。

美中不足的是,因為要實現Map介面,而 V get(Object key)中key是Object而不是泛型K,所以安全起見,EnumMap每次訪問都要先對Key進行型別判斷,在JMC裡錄得不低的取樣命中頻率。

ConcurrentHashMap

併發優化的HashMap。

在JDK5裡的經典設計,預設16把寫鎖(可以設定更多),有效分散了阻塞的概率。資料結構為Segment[],每個Segment一把鎖。Segment裡面才是雜湊桶陣列。Key先算出它在哪個Segment裡,再去算它在哪個雜湊桶裡。

也沒有讀鎖,因為put/remove動作是個原子動作(比如put的整個過程是一個對陣列元素/Entry 指標的賦值操作),讀操作不會看到一個更新動作的中間狀態。

但在JDK8裡,Segment[]的設計被拋棄了,改為精心設計的,只在需要鎖的時候加鎖。

支援ConcurrentMap介面,如putIfAbsent(key,value)與相反的replace(key,value)與以及實現CAS的replace(key, oldValue, newValue)。

ConcurrentSkipListMap

JDK6新增的併發優化的SortedMap,以SkipList結構實現。Concurrent包選用它是因為它支援基於CAS的無鎖演算法,而紅黑樹則沒有好的無鎖演算法。

原理上,可以想象為多個連結串列組成的N層樓,其中的元素從稀疏到密集,每個元素有往右與往下的指標。從第一層樓開始遍歷,如果右端的值比期望的大,那就往下走一層,繼續往前走。

典型的空間換時間。每次插入,都要決定在哪幾層插入,同時,要決定要不要多蓋一層樓。

它的size()同樣不能隨便調,會遍歷來統計。

Set

所有Set幾乎都是內部用一個Map來實現, 因為Map裡的KeySet就是一個Set,而value是假值,全部使用同一個Object即可。

Set的特徵也繼承了那些內部的Map實現的特徵。

HashSet:內部是HashMap。

LinkedHashSet:內部是LinkedHashMap。

TreeSet:內部是TreeMap的SortedSet。

ConcurrentSkipListSet:內部是ConcurrentSkipListMap的併發優化的SortedSet。

CopyOnWriteArraySet:內部是CopyOnWriteArrayList的併發優化的Set,利用其addIfAbsent()方法實現元素去重,如前所述該方法的效能很一般。

好像少了個ConcurrentHashSet,本來也該有一個內部用ConcurrentHashMap的簡單實現,但JDK偏偏沒提供。Jetty就自己簡單封了一個,Guava則直接用java.util.Collections.newSetFromMap(new ConcurrentHashMap()) 實現。

Queue

Queue是在兩端出入的List,所以也可以用陣列或連結串列來實現。

普通佇列

LinkedList
是的,以雙向連結串列實現的LinkedList既是List,也是Queue。

ArrayDeque
以迴圈陣列實現的雙向Queue。大小是2的倍數,預設是16。

為了支援FIFO,即從陣列尾壓入元素(快),從陣列頭取出元素(超慢),就不能再使用普通ArrayList的實現了,改為使用迴圈陣列。

有隊頭隊尾兩個下標:彈出元素時,隊頭下標遞增;加入元素時,隊尾下標遞增。如果加入元素時已到陣列空間的末尾,則將元素賦值到陣列[0],同時隊尾下標指向0,再插入下一個元素則賦值到陣列[1],隊尾下標指向1。如果隊尾的下標追上隊頭,說明陣列所有空間已用完,進行雙倍的陣列擴容。

PriorityQueue

用平衡二叉最小堆實現的優先順序佇列,不再是FIFO,而是按元素實現的Comparable介面或傳入Comparator的比較結果來出隊,數值越小,優先順序越高,越先出隊。但是注意其iterator()的返回不會排序。

平衡最小二叉堆,用一個簡單的陣列即可表達,可以快速定址,沒有指標什麼的。最小的在queue[0] ,比如queue[4]的兩個孩子,會在queue[2 4+1] 和 queue[2(4+1)],即queue[9]和queue[10]。

入隊時,插入queue[size],然後二叉地往上比較調整堆。

出隊時,彈出queue[0],然後把queque[size]拿出來二叉地往下比較調整堆。

初始大小為11,空間不夠時自動50%擴容。

執行緒安全的佇列

ConcurrentLinkedQueue/Deque
無界的併發優化的Queue,基於連結串列,實現了依賴於CAS的無鎖演算法。

ConcurrentLinkedQueue的結構是單向連結串列和head/tail兩個指標,因為入隊時需要修改隊尾元素的next指標,以及修改tail指向新入隊的元素兩個CAS動作無法原子,所以需要的特殊的演算法。

執行緒安全的阻塞佇列

BlockingQueue,一來如果佇列已空不用重複的檢視是否有新資料而會阻塞在那裡,二來佇列的長度受限,用以保證生產者與消費者的速度不會相差太遠。當入隊時佇列已滿,或出隊時佇列已空,不同函式的效果見下表

ArrayBlockingQueue
定長的併發優化的BlockingQueue,也是基於迴圈陣列實現。有一把公共的鎖與notFull、notEmpty兩個Condition管理佇列滿或空時的阻塞狀態。

LinkedBlockingQueue/Deque
可選定長的併發優化的BlockingQueue,基於連結串列實現,所以可以把長度設為Integer.MAX_VALUE成為無界無等待的。

利用連結串列的特徵,分離了takeLock與putLock兩把鎖,繼續用notEmpty、notFull管理佇列滿或空時的阻塞狀態。

PriorityBlockingQueue
無界的PriorityQueue,也是基於陣列儲存的二叉堆(見前)。一把公共的鎖實現執行緒安全。因為無界,空間不夠時會自動擴容,所以入列時不會鎖,出列為空時才會鎖。

DelayQueue
內部包含一個PriorityQueue,同樣是無界的,同樣是出列時才會鎖。一把公共的鎖實現執行緒安全。元素需實現Delayed介面,每次呼叫時需返回當前離觸發時間還有多久,小於0表示該觸發了。

pull()時會用peek()檢視隊頭的元素,檢查是否到達觸發時間。ScheduledThreadPoolExecutor用了類似的結構。

同步佇列

SynchronousQueue同步佇列本身無容量,放入元素時,比如等待元素被另一條執行緒的消費者取走再返回。JDK執行緒池裡用它。

JDK7還有個LinkedTransferQueue,在普通執行緒安全的BlockingQueue的基礎上,增加一個transfer(e) 函式,效果與SynchronousQueue一樣。

參考文章

https://blog.csdn.net/zzw1531439090/article/details/87872424

https://blog.csdn.net/weixin_40374341/article/details/86496343

https://www.cnblogs.com/uodut/p/7067162.html

https://www.jb51.net/article/135672.htm

https://www.cnblogs.com/suiyue-/p/6052456.html

微信公眾號

Java技術江湖

如果大家想要實時關注我更新的文章以及分享的乾貨的話,可以關注我的公眾號【Java技術江湖】一位阿里 Java 工程師的技術小站,作者黃小斜,專注 Java 相關技術:SSM、SpringBoot、MySQL、分散式、中介軟體、叢集、Linux、網路、多執行緒,偶爾講點Docker、ELK,同時也分享技術乾貨和學習經驗,致力於Java全棧開發!

Java工程師必備學習資源: 一些Java工程師常用學習資源,關注公眾號後,後臺回覆關鍵字 “Java” 即可免費無套路獲取。

我的公眾號

個人公眾號:黃小斜

作者是 985 碩士,螞蟻金服 JAVA 工程師,專注於 JAVA 後端技術棧:SpringBoot、MySQL、分散式、中介軟體、微服務,同時也懂點投資理財,偶爾講點演算法和計算機理論基礎,堅持學習和寫作,相信終身學習的力量!

程式設計師3T技術學習資源: 一些程式設計師學習技術的資源大禮包,關注公眾號後,後臺回覆關鍵字 “資料” 即可免費無套路獲取。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69906029/viewspace-2659075/,如需轉載,請註明出處,否則將追究法律責任。

相關文章