帶你見識一下,Java中的方法爆炸!
本文轉(zhuǎn)載自微信公眾號「小姐姐味道」,作者小姐姐養(yǎng)的狗。轉(zhuǎn)載本文請聯(lián)系小姐姐味道公眾號。
要想了解Java的API有多變態(tài),就不得不提一下隊列這個接口,許多工作多年的人,依然是對此非常迷惑。雖然隊列是計算機(jī)算法中的一個基本結(jié)構(gòu),但它并不僅僅只有add這個方法。
讀完本文,再看到add、offer、put,不要再犯暈了!
1. 一段小代碼
猜猜下面的代碼會輸出啥?
- void run(Callable<Object> c){
- try{
- System.out.println(c.call());
- }catch (Exception ex){
- System.out.println(ex);
- }
- }
- void testSynchronousQueue(){
- Queue<Integer> q1 = new SynchronousQueue();
- run(()-> q1.add(1));
- Queue<Integer> q2 = new SynchronousQueue();
- run(()-> q1.offer(1));
- }
實在是讓人非常失望,兩次執(zhí)行都失敗了。
- java.lang.IllegalStateException: Queue full
- false
第一次,使用add方法,程序拋出了異常,表示隊列滿了;第二次,程序返回了false,證明添加失敗。既然無法向隊列中添加元素,又沒有指定隊列大小的地方。那這個隊列,有什么鳥用!
2. Queue的方法
在了解這個隊列的使用之前,我們來看一下Queue接口所定義的方法。
- add(E e) 插入一個元素到隊列的尾部。如果無法插入,則拋出異常
- offer(E e) 插入一個元素到隊列的為
- E remove() 從隊列頭移除一個元素,如果隊列為空,則拋出異常
- E poll() 從隊列頭移除一個元素,如果隊列為空,則返回null
- E element() 查看對頭元素,如果隊列為空,則拋出異常
- E peek() 查看對頭元素,如果隊列為空,則返回null
可以看到,對隊列的基本操作,只有三個:插入新元素、查看隊頭、隊頭出對。根據(jù)是否拋出異常,又分為了兩類。3x2=6,共6個方法。
喜歡刷題的同學(xué),常用的肯定是offer、poll、peek,這樣可以免去惱人的異常處理。平常的編碼,也推薦使用非異常的api,但Java為什么提供了兩套方法,來供我們使用呢?
原因就是,Queue接口繼承了Collection接口,而add和remove等方法,是屬于Collection接口的,Queue不得不實現(xiàn)一套。事實上,add方法直接調(diào)用了offer方法,為什么多出這么一套api來,真的是個謎。
- public boolean add(E e) {
- if (offer(e))
- return true;
- else
- throw new IllegalStateException("Queue full");
- }
不拋異常,就容易被遺忘處理,確實是個比較牽強(qiáng)的原因。就憑這,能讓人在這么重要的基礎(chǔ)類庫里面,創(chuàng)造出這么多不同名稱的方法么?
3. Put和Take
相比較上面讓人糾結(jié)的add和offer,put和take方法就確實有用了。但put和take是不屬于Queue接口的,它的歸屬是BlockingQueue。不好意思,一不小心就跳到concurrent包了。
put和take,意味著阻塞。如果操作不成功,它就一直在那里阻塞。想要它們能夠正常運行下去,就需要有多個線程的配合。下面的代碼會往隊列里發(fā)送一個1,然后take方法拿出它,進(jìn)行打印。
- void testBlockingSynchronousQueue() throws InterruptedException {
- BlockingQueue<Integer> q1 = new SynchronousQueue();
- new Thread(()-> {
- try {
- q1.put(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }).start();
- new Thread(()-> {
- try {
- System.out.println(q1.take());
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }).start();
- }
所以,我們來看一下這對方法。
- put(E e) 插入元素,如果隊列滿了,它會一直阻塞等待
- E take() 獲取隊頭元素,如果隊列為空則一直等待
可以看到put和take配合起來,很容易實現(xiàn)一個線程安全的生產(chǎn)者消費者模型。相比較使用Queue的接口方法,我們只能通過死循環(huán)去檢測,這樣阻塞的方式就特別節(jié)省資源。
但是還沒完。阻塞的take和put方法,只能被interrupt,如何讓程序阻塞等待一段時間,然后恢復(fù)運行呢?那就只有加入一個帶時間戳的阻塞方法。
BlockingQueue選擇了offer和poll方法,而不是take和put,咱也搞不懂到底是為什么。
- E poll(long timeout, TimeUnit unit)
- boolean offer(E e, long timeout, TimeUnit unit) 依然是有返回值的
4. 你以為這樣就完了?
你以為這樣就完了?并沒有。我們需要把目光投向LinkedList,傳說中幾行代碼實現(xiàn)LRU緩存的類。
ArrayList是一個比較純凈的List,僅僅實現(xiàn)了List接口,但LinkedList就胃口大了一些。由于API設(shè)計者,盡最大可能想讓這個鏈表功能更強(qiáng)大一些,它繼承了Deque接口。由于Deque繼承了Queue,所以這個鏈表不僅僅是個隊列,還是個雙向隊列。
所以,它們又多了一堆API,分別來描述到底是在隊頭還是隊尾進(jìn)行操作。
- addFirst 操作隊頭,加入元素
- addLast 操作隊尾,加入元素
- offerFirst 操作隊頭,加入元素
- offerLast 操作隊尾,加入元素
- removeFirst 操作隊頭,刪除元素
- removeLast 操作隊尾,刪除元素
- pollFirst 操作隊頭,刪除元素
- pollLast 操作隊尾,刪除元素
- getFirst 獲取隊頭元素,類似element。TMD,這里為什么不用element?
- getLast 獲取隊尾元素
- peekFirst 獲取隊頭元素
- peekLast 獲取隊尾元素
當(dāng)然,這里還有pop和push,pop=removeFirst,push=addFirst。//建議不要用,太難記了。
很好很好,由于有了頭和尾的概念,api的大小變成了3x2x2=12個!加上原來的那6個,共18個(直接把pop和push忽略)。
你要說,怎么沒有take和put這種阻塞的方法啊。原因就是LinkedList并不是并發(fā)的集合,你要找的功能,在LinkedBlockingDeque中,肯定會有takeFirst、takeLast、putLast、putFirst等。
5. 隊列大小
反過頭來再看我們剛開始的SynchronousQueue,為什么無論向里面添加元素,還是提取元素,都會返回失?。克娜萘康降资嵌嗌??
這是一個非常奇葩的類,它的內(nèi)部容量是0!已經(jīng)被硬編碼進(jìn)代碼里了。
- public int size() {
- return 0;
- }
它僅僅建立了一個通道,一旦有生產(chǎn),消費者就能立馬拿到它,它本身是不不存任何數(shù)據(jù)的。Executors.newCachedThreadPool()就使用了SynchronousQueue。
常用的LinkedBlockingQueue、ArrayBlockingQueue,都是有界的。
但這里還有一個比較奇葩的類,那就是ConcurrentLinkedQueue,從名字可以看出來,它并不是一個阻塞的并發(fā)類,所以并沒有take和put等方法。另外,它是無界的,使用時要特別小心。你或許說,我每次判斷它的size()方法來看一下是否越界不就行了。
- public int size() {
- int count = 0;
- for (Node<E> p = first(); p != null; p = succ(p))
- if (p.item != null)
- // Collection.size() spec says to max out
- if (++count == Integer.MAX_VALUE)
- break;
- return count;
- }
如上代碼所示,這就是比較坑的地方,size方法,并不是O(1)時間級別的。xjjdog就曾在上面吃過大虧,最后還是不敢再亂用了。
End
從上面的描述可以看出來。對于一個隊列,有三套接口:插入、彈出、檢測;根據(jù)是否拋異常,又分為兩套,一套會拋出異常,另外一套直接返回值,刷題黨自然喜歡后者了;如果再加上雙向的隊列,就需要再區(qū)分對頭隊尾;如果是阻塞隊列,還要再加上一個維度。
所以,對于一個阻塞的雙向隊列,它的基本操作方法有:(3[基本]x2[異常與返回值]+4[阻塞加超時])x3[隊頭隊尾]=5x2x3=30個方法,這就是王者LinkedBlockingDeque。
這樣的代碼,我反正是寫吐了。你呢?
作者簡介:小姐姐味道 (xjjdog),一個不允許程序員走彎路的公眾號。聚焦基礎(chǔ)架構(gòu)和Linux。十年架構(gòu),日百億流量,與你探討高并發(fā)世界,給你不一樣的味道。我的個人微信xjjdog0,歡迎添加好友,進(jìn)一步交流。