前言
只有光頭才能變強。
文本已收錄至我的GitHub精選文章,歡迎Star:https://github.com/ZhongFuCheng3y/3y
Java集合是我認為在Java基礎中最最重要的知識點了,Java集合是必須掌握的。我在實習/秋招面試的時候,只要是面到Java,那一定是少不了Java集合。
作為一個新人,最關心的其實有一點:這個技術在工作中是怎么用的。換個說法:“工作中常用到的Java集合有哪些,應用場景是什么”
如何入門Java集合以及每個常用的子類我在PDF整理好了,這就不粘貼過來了,有需要的就在PDF查看就好了。這份PDF絕對令你滿意。
List集合
List集合下最常見的集合類有兩個:ArrayList和LinkedList
在工作中,我都是無腦用ArrayList。我問了兩個同事:“你們在項目中用過LinkedList嗎?”他們都表示沒有。
眾所周知,ArrayList底層是數組,LinkedList底層是鏈表。數組遍歷速度快,LinkedList增刪元素快。
為什么在工作中一般就用ArrayList,而不用LinkedList呢?原因也很簡單:
- 在工作中,遍歷的需求比增刪多,即便是增加元素往往也只是從尾部插入元素,而ArrayList在尾部插入元素也是O(1)
- ArrayList增刪沒有想象中慢,ArrayList的增刪底層調用的
copyOf()
被優化過,加上現代CPU對內存可以塊操作,普通大小的ArrayList增刪比LinkedList更快。
所以,在開發中,想到要用集合來裝載元素,第一個想到的就是ArrayList。
那么來了,LinkedList用在什么地方呢?我們一般用在刷算法題上。把LinkedList當做一個先進先出的隊列,LinkedList本身就實現了Queue接口
如果考慮線程安全的問題,可以看看CopyWriteOnArrayList,實際開發用得不多,但我覺得可以了解一下它的思想(CopyWriteOn),這個思想在Linux/文件系統都有用到。
Set集合
Set集合下最常見的集合類有三個:HashSet、TreeSet、LinkedHashSet
List和Set都是集合,一般來說:如果我們需要保證集合的元素是唯一的,就應該想到用Set集合
比如說:現在要發送一批消息給用戶,我們為了減少「一次發送重復的內容給用戶」這樣的錯誤,我們就用Set集合來保存用戶的userId/phone
自然地,首先要保證最上游的那批用戶的userId/phone
是沒有重復的,而我們用Set集合只是為了做一個兜底來盡可能避免重復發送的問題。
一般我們在開發中最多用到的也就是HashSet。TreeSet是可以排序的Set,一般我們需要有序,從數據庫拉出來的數據就是有序的,可能往往寫order by id desc
比較多。而在開發中也很少管元素插入有序的問題,所以LinkedHashSet一般也用不上。
如果考慮線程安全的問題,可以考慮CopyOnWriteArraySet,用得就更少了(這是一個線程安全的Set,底層實際上就是CopyWriteOnArrayList)
TreeSet和LinkedHashSet更多的可能用在刷算法的時候。
Map集合
Map集合最常見的子類也有三個:HashMap、LinkedHashMap、TreeMap
如果考慮線程安全問題,應該想到的是ConcurrentHashMap,當然了Hashtable也要有一定的了解,因為面試實在是問得太多太多了。
HashMap在實際開發中用得也非常多,只要是key-value
結構的,一般我們就用HashMap
。LinkedHashMap和TreeMap用的不多,原因跟HashSet和TreeSet一樣。
ConcurrentHashMap在實際開發中也用得挺多,我們很多時候把ConcurrentHashMap用於本地緩存,不想每次都網絡請求數據,在本地做本地緩存。監聽數據的變化,如果數據有變動了,就把ConcurrentHashMap對應的值給更新了。
Queue隊列
不知道大家有沒有學過生產者和消費者模式,秋招面試的時候可能會讓你手寫一段這樣的代碼。最簡單的方式就是用阻塞隊列去寫。類似下面:
生產者:
import java.util.Random;
import java.util.Vector;
import java.util.concurrent.atomic.AtomicInteger;
public class Producer implements Runnable {
// true--->生產者一直執行,false--->停掉生產者
private volatile boolean isRunning = true;
// 公共資源
private final Vector sharedQueue;
// 公共資源的最大數量
private final int SIZE;
// 生產數據
private static AtomicInteger count = new AtomicInteger();
public Producer(Vector sharedQueue, int SIZE) {
this.sharedQueue = sharedQueue;
this.SIZE = SIZE;
}
@Override
public void run() {
int data;
Random r = new Random();
System.out.println("start producer id = " + Thread.currentThread().getId());
try {
while (isRunning) {
// 模擬延遲
Thread.sleep(r.nextInt(1000));
// 當隊列滿時阻塞等待
while (sharedQueue.size() == SIZE) {
synchronized (sharedQueue) {
System.out.println("Queue is full, producer " + Thread.currentThread().getId()
+ " is waiting, size:" + sharedQueue.size());
sharedQueue.wait();
}
}
// 隊列不滿時持續創造新元素
synchronized (sharedQueue) {
// 生產數據
data = count.incrementAndGet();
sharedQueue.add(data);
System.out.println("producer create data:" + data + ", size:" + sharedQueue.size());
sharedQueue.notifyAll();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupted();
}
}
public void stop() {
isRunning = false;
}
}
消費者:
import java.util.Random;
import java.util.Vector;
public class Consumer implements Runnable {
// 公共資源
private final Vector sharedQueue;
public Consumer(Vector sharedQueue) {
this.sharedQueue = sharedQueue;
}
@Override
public void run() {
Random r = new Random();
System.out.println("start consumer id = " + Thread.currentThread().getId());
try {
while (true) {
// 模擬延遲
Thread.sleep(r.nextInt(1000));
// 當隊列空時阻塞等待
while (sharedQueue.isEmpty()) {
synchronized (sharedQueue) {
System.out.println("Queue is empty, consumer " + Thread.currentThread().getId()
+ " is waiting, size:" + sharedQueue.size());
sharedQueue.wait();
}
}
// 隊列不空時持續消費元素
synchronized (sharedQueue) {
System.out.println("consumer consume data:" + sharedQueue.remove(0) + ", size:" + sharedQueue.size());
sharedQueue.notifyAll();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
}
}
Main方法測試:
import java.util.Vector;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test2 {
public static void main(String[] args) throws InterruptedException {
// 1.構建內存緩沖區
Vector sharedQueue = new Vector();
int size = 4;
// 2.建立線程池和線程
ExecutorService service = Executors.newCachedThreadPool();
Producer prodThread1 = new Producer(sharedQueue, size);
Producer prodThread2 = new Producer(sharedQueue, size);
Producer prodThread3 = new Producer(sharedQueue, size);
Consumer consThread1 = new Consumer(sharedQueue);
Consumer consThread2 = new Consumer(sharedQueue);
Consumer consThread3 = new Consumer(sharedQueue);
service.execute(prodThread1);
service.execute(prodThread2);
service.execute(prodThread3);
service.execute(consThread1);
service.execute(consThread2);
service.execute(consThread3);
// 3.睡一會兒然后嘗試停止生產者(結束循環)
Thread.sleep(10 * 1000);
prodThread1.stop();
prodThread2.stop();
prodThread3.stop();
// 4.再睡一會兒關閉線程池
Thread.sleep(3000);
// 5.shutdown()等待任務執行完才中斷線程(因為消費者一直在運行的,所以會發現程序無法結束)
service.shutdown();
}
}
我的項目用阻塞隊列也挺多的(我覺得跟個人編寫的代碼風格習慣有關),類似實現了上面的生產者和消費者模式。
真實場景例子:
- 運營要發一條推送消息,首先需要去用戶畫像系統圈選一個人群,填寫對應的人群ID和發送時間。
- 我通過時間調度,通過RPC拿到人群的信息。遍歷HDFS得到這個人群的每個userId
- 將遍歷的userId放到一個阻塞隊列里邊去,用多個線程while(true)取阻塞隊列的數據
好處是什么?我在取userId的時候,會有個限制:要么超出了指定的時間,要么達到BatchSize的值。這樣我就可以將相同內容的不同userId組成一個Task。
本來100個userId是100個Task,現在我將100個userId放在一個Task里邊(因為發送的內容是相同的,所以我可以這么干)。這樣再往下游傳的時候,並發量就降低了很多。
什么時候考慮線程安全
什么時候考慮線程安全的集合類,那當然是線程不安全的時候咯。那什么時候線程不安全?最常見的是:操作的對象是有狀態的
雖然說,我們經常會聽到線程不安全,但在業務開發中要我們程序員處理線程不安全的地方少之又少。比如說:你在寫Servlet的時候,加過syn/lock
鎖嗎?應該沒有吧?
因為我們的操作的對象往往是無狀態的。沒有共享變量被多個線程訪問,自然就沒有線程安全問題了。
SpringMVC是單例的,但SpringMVC都是在方法內操作數據的,每個線程進入方法都會生成棧幀,每個棧幀的數據都是線程獨有的,如果不設定共享變量,不會有線程安全問題。
上面只是簡單舉了SpringMVC的例子(只是為了更好的理解);
一句話總結:只要涉及到多個線程操作一個共享變量的時候,就要考慮是不是要用線程安全的集合類。
更多的細節,等我寫Java多線程總結的時候再說了
最后
還是想強調一下,Java集合雖然在工作中不是每個都經常用得到,但是還是得重點學習學習。
如果你學習到了源碼,可能你在創建集合的時候就會指定了集合的大小(即便我們知道它能動態擴容)
如果你想要去面試,Java集合是肯定少不了的,必問的一個知識點,你學會了就是送分題。
現在已經工作有一段時間了,為什么還來寫Java集合
呢,原因有以下幾個:
- 我是一個對排版有追求的人,如果早期關注我的同學可能會發現,我的GitHub、文章導航的
read.me
會經常更換。現在的GitHub導航也不合我心意了(太長了),並且早期的文章,說實話排版也不太行,我決定重新搞一波。 - 我的文章會分發好幾個平台,但文章發完了可能就沒人看了,並且圖床很可能因為平台的防盜鏈就掛掉了。又因為有很多的讀者問我:”你能不能把你的文章轉成PDF啊?“
- 我寫過很多系列級的文章,這些文章就幾乎不會有太大的改動了,就非常適合把它們給”持久化“。
基於上面的原因,我決定把我的系列文章匯總成一個PDF/HTML/WORD
文檔。說實話,打造這么一個文檔花了我不少的時間。為了防止白嫖,關注我的公眾號回復「888」即可獲取。
PDF的內容非常非常長,干貨非常非常的硬,有興趣的同學可以「白嫖」一波。記住:Java集合在Java的知識處於一個非常重要的知識點,建議掌握!
文檔的內容均為手打,有任何的不懂都可以直接來問我(公眾號有我的聯系方式)。
上一期的「JSP」的PDF在公眾號差點意思,目標是180個在看,雖然沒達到,但我還是帶着黑眼圈來了。
JSP的PDF有人會說:『大人,時代變了』,我就不信Java集合還有人對我說『大人,時代變了』
如果這次在看超過200,那下周再肝一個系列出來。想要看什么,可以留言告訴我
涵蓋Java后端所有知識點的開源項目(已有6 K star):https://github.com/ZhongFuCheng3y/3y
如果大家想要實時關注我更新的文章以及分享的干貨的話,微信搜索Java3y。
PDF文檔的內容均為手打,有任何的不懂都可以直接來問我(公眾號有我的聯系方式)。