【備戰春招/秋招系列】美團Java面經總結終結篇 (附詳解答案)

SnailClimb發表於2019-01-19

該文已加入開源專案:JavaGuide(一份涵蓋大部分Java程式設計師所需要掌握的核心知識的文件類專案,Star 數接近 14 k)。地址:github.com/Snailclimb/….

系列文章:

下面只是我從很多份美團面經中總結的在面試中一些常見的問題。不同於個人面經,這份面經具有普適性。每次面試必備的自我介紹、專案介紹這些東西,大家可以自己私下好好思考。我在前面的文章中也提到了應該怎麼做自我介紹與專案介紹,詳情可以檢視這篇文章:【備戰春招/秋招系列2】初出茅廬的程式設計師該如何準備面試?

下面這個問題,面試中經常出現。我覺得不論是出於應付面試還是說更好地掌握Java這門程式語言,大家都要掌握!

一. Object類有哪些方法?

1.1 Object類的常見方法總結

Object類是一個特殊的類,是所有類的父類。它主要提供了以下11個方法:


public final native Class<?> getClass()//native方法,用於返回當前執行時物件的Class物件,使用了final關鍵字修飾,故不允許子類重寫。

public native int hashCode() //native方法,用於返回物件的雜湊碼,主要使用在雜湊表中,比如JDK中的HashMap。
public boolean equals(Object obj)//用於比較2個物件的記憶體地址是否相等,String類對該方法進行了重寫使用者比較字串的值是否相等。

protected native Object clone() throws CloneNotSupportedException//naitive方法,用於建立並返回當前物件的一份拷貝。一般情況下,對於任何物件 x,表示式 x.clone() != x 為true,x.clone().getClass() == x.getClass() 為true。Object本身沒有實現Cloneable介面,所以不重寫clone方法並且進行呼叫的話會發生CloneNotSupportedException異常。

public String toString()//返回類的名字@例項的雜湊碼的16進位制的字串。建議Object所有的子類都重寫這個方法。

public final native void notify()//native方法,並且不能重寫。喚醒一個在此物件監視器上等待的執行緒(監視器相當於就是鎖的概念)。如果有多個執行緒在等待只會任意喚醒一個。

public final native void notifyAll()//native方法,並且不能重寫。跟notify一樣,唯一的區別就是會喚醒在此物件監視器上等待的所有執行緒,而不是一個執行緒。

public final native void wait(long timeout) throws InterruptedException//native方法,並且不能重寫。暫停執行緒的執行。注意:sleep方法沒有釋放鎖,而wait方法釋放了鎖 。timeout是等待時間。

public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos引數,這個參數列示額外時間(以毫微秒為單位,範圍是 0-999999)。 所以超時的時間還需要加上nanos毫秒。

public final void wait() throws InterruptedException//跟之前的2個wait方法一樣,只不過該方法一直等待,沒有超時時間這個概念

protected void finalize() throws Throwable { }//例項被垃圾回收器回收的時候觸發的操作

複製程式碼

問完上面這個問題之後,面試官很可能緊接著就會問你“hashCode與equals”相關的問題。

1.2 hashCode與equals

面試官可能會問你:“你重寫過 hashcode 和 equals 麼,為什麼重寫equals時必須重寫hashCode方法?”

1.2.1 hashCode()介紹

hashCode() 的作用是獲取雜湊碼,也稱為雜湊碼;它實際上是返回一個int整數。這個雜湊碼的作用是確定該物件在雜湊表中的索引位置。hashCode() 定義在JDK的Object.java中,這就意味著Java中的任何類都包含有hashCode() 函式。另外需要注意的是: Object 的 hashcode 方法是本地方法,也就是用 c 語言或 c++ 實現的,該方法通常用來將物件的 記憶體地址 轉換為整數之後返回。

    public native int hashCode();
複製程式碼

雜湊表儲存的是鍵值對(key-value),它的特點是:能根據“鍵”快速的檢索出對應的“值”。這其中就利用到了雜湊碼!(可以快速找到所需要的物件)

1.2.2 為什麼要有hashCode

我們以“HashSet如何檢查重複”為例子來說明為什麼要有hashCode:

當你把物件加入HashSet時,HashSet會先計算物件的hashcode值來判斷物件加入的位置,同時也會與其他已經加入的物件的hashcode值作比較,如果沒有相符的hashcode,HashSet會假設物件沒有重複出現。但是如果發現有相同hashcode值的物件,這時會呼叫equals()方法來檢查hashcode相等的物件是否真的相同。如果兩者相同,HashSet就不會讓其加入操作成功。如果不同的話,就會重新雜湊到其他位置。(摘自我的Java啟蒙書《Head fist java》第二版)。這樣我們就大大減少了equals的次數,相應就大大提高了執行速度。

1.2.3 hashCode()與equals()的相關規定

  1. 如果兩個物件相等,則hashcode一定也是相同的
  2. 兩個物件相等,對兩個物件分別呼叫equals方法都返回true
  3. 兩個物件有相同的hashcode值,它們也不一定是相等的
  4. 因此,equals方法被覆蓋過,則hashCode方法也必須被覆蓋
  5. hashCode()的預設行為是對堆上的物件產生獨特值。如果沒有重寫hashCode(),則該class的兩個物件無論如何都不會相等(即使這兩個物件指向相同的資料)

1.2.4 為什麼兩個物件有相同的hashcode值,它們也不一定是相等的?

在這裡解釋一位小夥伴的問題。以下內容摘自《Head Fisrt Java》。

因為hashCode() 所使用的雜湊演算法也許剛好會讓多個物件傳回相同的雜湊值。越糟糕的雜湊演算法越容易碰撞,但這也與資料值域分佈的特性有關(所謂碰撞也就是指的是不同的物件得到相同的 hashCode)。

我們剛剛也提到了 HashSet,如果 HashSet 在對比的時候,同樣的 hashcode 有多個物件,它會使用 equals() 來判斷是否真的相同。也就是說 hashcode 只是用來縮小查詢成本。

==與equals 的對比也是比較常問的基礎問題之一!

1.3 ==與equals

== : 它的作用是判斷兩個物件的地址是不是相等。即,判斷兩個物件是不是同一個物件。(基本資料型別==比較的是值,引用資料型別==比較的是記憶體地址)

equals() : 它的作用也是判斷兩個物件是否相等。但它一般有兩種使用情況:

  • 情況1:類沒有覆蓋equals()方法。則通過equals()比較該類的兩個物件時,等價於通過“==”比較這兩個物件。
  • 情況2:類覆蓋了equals()方法。一般,我們都覆蓋equals()方法來兩個物件的內容相等;若它們的內容相等,則返回true(即,認為這兩個物件相等)。

舉個例子:

public class test1 {
    public static void main(String[] args) {
        String a = new String("ab"); // a 為一個引用
        String b = new String("ab"); // b為另一個引用,物件的內容一樣
        String aa = "ab"; // 放在常量池中
        String bb = "ab"; // 從常量池中查詢
        if (aa == bb) // true
            System.out.println("aa==bb");
        if (a == b) // false,非同一物件
            System.out.println("a==b");
        if (a.equals(b)) // true
            System.out.println("aEQb");
        if (42 == 42.0) { // true
            System.out.println("true");
        }
    }
}
複製程式碼

說明:

  • String中的equals方法是被重寫過的,因為object的equals方法是比較的物件的記憶體地址,而String的equals方法比較的是物件的值。
  • 當建立String型別的物件時,虛擬機器會在常量池中查詢有沒有已經存在的值和要建立的值相同的物件,如果有就把它賦給當前引用。如果沒有就在常量池中重新建立一個String物件。

【備戰春招/秋招系列5】美團面經總結進階篇 (附詳解答案) 這篇文章中,我們已經提到了一下關於 HashMap 在面試中常見的問題:HashMap 的底層實現、簡單講一下自己對於紅黑樹的理解、紅黑樹這麼優秀,為何不直接使用紅黑樹得了、HashMap 和 Hashtable 的區別/HashSet 和 HashMap 區別。HashMap 和 ConcurrentHashMap 這倆兄弟在一般只要面試中問到集合相關的問題就一定會被問到,所以各位務必引起重視!

二 ConcurrentHashMap 相關問題

2.1 ConcurrentHashMap 和 Hashtable 的區別

ConcurrentHashMap 和 Hashtable 的區別主要體現在實現執行緒安全的方式上不同。

  • 底層資料結構: JDK1.7的 ConcurrentHashMap 底層採用 分段的陣列+連結串列 實現,JDK1.8 採用的資料結構跟HashMap1.8的結構一樣,陣列+連結串列/紅黑二叉樹。Hashtable 和 JDK1.8 之前的 HashMap 的底層資料結構類似都是採用 陣列+連結串列 的形式,陣列是 HashMap 的主體,連結串列則是主要為了解決雜湊衝突而存在的;
  • 實現執行緒安全的方式(重要):在JDK1.7的時候,ConcurrentHashMap(分段鎖) 對整個桶陣列進行了分割分段(Segment),每一把鎖只鎖容器其中一部分資料,多執行緒訪問容器裡不同資料段的資料,就不會存在鎖競爭,提高併發訪問率。(預設分配16個Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的時候已經摒棄了Segment的概念,而是直接用 Node 陣列+連結串列+紅黑樹的資料結構來實現,併發控制使用 synchronized 和 CAS 來操作。(JDK1.6以後 對 synchronized鎖做了很多優化) 整個看起來就像是優化過且執行緒安全的 HashMap,雖然在JDK1.8中還能看到 Segment 的資料結構,但是已經簡化了屬性,只是為了相容舊版本;② Hashtable(同一把鎖) :使用 synchronized 來保證執行緒安全,效率非常低下。當一個執行緒訪問同步方法時,其他執行緒也訪問同步方法,可能會進入阻塞或輪詢狀態,如使用 put 新增元素,另一個執行緒不能使用 put 新增元素,也不能使用 get,競爭會越來越激烈效率越低。

兩者的對比圖:

圖片來源:www.cnblogs.com/chengxiao/p…

HashTable:

【備戰春招/秋招系列】美團Java面經總結終結篇 (附詳解答案)

JDK1.7的ConcurrentHashMap:

【備戰春招/秋招系列】美團Java面經總結終結篇 (附詳解答案)
JDK1.8的ConcurrentHashMap(TreeBin: 紅黑二叉樹節點 Node: 連結串列節點):
【備戰春招/秋招系列】美團Java面經總結終結篇 (附詳解答案)

2.2 ConcurrentHashMap執行緒安全的具體實現方式/底層具體實現

JDK1.7(上面有示意圖)

首先將資料分為一段一段的儲存,然後給每一段資料配一把鎖,當一個執行緒佔用鎖訪問其中一個段資料時,其他段的資料也能被其他執行緒訪問。

ConcurrentHashMap 是由 Segment 陣列結構和 HashEntry 陣列結構組成

Segment 實現了 ReentrantLock,所以 Segment 是一種可重入鎖,扮演鎖的角色。HashEntry 用於儲存鍵值對資料。

static class Segment<K,V> extends ReentrantLock implements Serializable {
}
複製程式碼

一個 ConcurrentHashMap 裡包含一個 Segment 陣列。Segment 的結構和HashMap類似,是一種陣列和連結串列結構,一個 Segment 包含一個 HashEntry 陣列,每個 HashEntry 是一個連結串列結構的元素,每個 Segment 守護著一個HashEntry陣列裡的元素,當對 HashEntry 陣列的資料進行修改時,必須首先獲得對應的 Segment的鎖。

JDK1.8 (上面有示意圖)

ConcurrentHashMap取消了Segment分段鎖,採用CAS和synchronized來保證併發安全。資料結構跟HashMap1.8的結構類似,陣列+連結串列/紅黑二叉樹。

synchronized只鎖定當前連結串列或紅黑二叉樹的首節點,這樣只要hash不衝突,就不會產生併發,效率又提升N倍。

三 談談 synchronized 和 ReenTrantLock 的區別

① 兩者都是可重入鎖

兩者都是可重入鎖。“可重入鎖”概念是:自己可以再次獲取自己的內部鎖。比如一個執行緒獲得了某個物件的鎖,此時這個物件鎖還沒有釋放,當其再次想要獲取這個物件的鎖的時候還是可以獲取的,如果不可鎖重入的話,就會造成死鎖。同一個執行緒每次獲取鎖,鎖的計數器都自增1,所以要等到鎖的計數器下降為0時才能釋放鎖。

② synchronized 依賴於 JVM 而 ReenTrantLock 依賴於 API

synchronized 是依賴於 JVM 實現的,前面我們也講到了 虛擬機器團隊在 JDK1.6 為 synchronized 關鍵字進行了很多優化,但是這些優化都是在虛擬機器層面實現的,並沒有直接暴露給我們。ReenTrantLock 是 JDK 層面實現的(也就是 API 層面,需要 lock() 和 unlock 方法配合 try/finally 語句塊來完成),所以我們可以通過檢視它的原始碼,來看它是如何實現的。

③ ReenTrantLock 比 synchronized 增加了一些高階功能

相比synchronized,ReenTrantLock增加了一些高階功能。主要來說主要有三點:①等待可中斷;②可實現公平鎖;③可實現選擇性通知(鎖可以繫結多個條件)

  • ReenTrantLock提供了一種能夠中斷等待鎖的執行緒的機制,通過lock.lockInterruptibly()來實現這個機制。也就是說正在等待的執行緒可以選擇放棄等待,改為處理其他事情。
  • ReenTrantLock可以指定是公平鎖還是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的執行緒先獲得鎖。 ReenTrantLock預設情況是非公平的,可以通過 ReenTrantLock類的ReentrantLock(boolean fair)構造方法來制定是否是公平的。
  • synchronized關鍵字與wait()和notify/notifyAll()方法相結合可以實現等待/通知機制,ReentrantLock類當然也可以實現,但是需要藉助於Condition介面與newCondition() 方法。Condition是JDK1.5之後才有的,它具有很好的靈活性,比如可以實現多路通知功能也就是在一個Lock物件中可以建立多個Condition例項(即物件監視器),執行緒物件可以註冊在指定的Condition中,從而可以有選擇性的進行執行緒通知,在排程執行緒上更加靈活。 在使用notify/notifyAll()方法進行通知時,被通知的執行緒是由 JVM 選擇的,用ReentrantLock類結合Condition例項可以實現“選擇性通知” ,這個功能非常重要,而且是Condition介面預設提供的。而synchronized關鍵字就相當於整個Lock物件中只有一個Condition例項,所有的執行緒都註冊在它一個身上。如果執行notifyAll()方法的話就會通知所有處於等待狀態的執行緒這樣會造成很大的效率問題,而Condition例項的signalAll()方法 只會喚醒註冊在該Condition例項中的所有等待執行緒。

如果你想使用上述功能,那麼選擇ReenTrantLock是一個不錯的選擇。

④ 兩者的效能已經相差無幾

在JDK1.6之前,synchronized 的效能是比 ReenTrantLock 差很多。具體表示為:synchronized 關鍵字吞吐量歲執行緒數的增加,下降得非常嚴重。而ReenTrantLock 基本保持一個比較穩定的水平。我覺得這也側面反映了, synchronized 關鍵字還有非常大的優化餘地。後續的技術發展也證明了這一點,我們上面也講了在 JDK1.6 之後 JVM 團隊對 synchronized 關鍵字做了很多優化。JDK1.6 之後,synchronized 和 ReenTrantLock 的效能基本是持平了。所以網上那些說因為效能才選擇 ReenTrantLock 的文章都是錯的!JDK1.6之後,效能已經不是選擇synchronized和ReenTrantLock的影響因素了!而且虛擬機器在未來的效能改進中會更偏向於原生的synchronized,所以還是提倡在synchronized能滿足你的需求的情況下,優先考慮使用synchronized關鍵字來進行同步!優化後的synchronized和ReenTrantLock一樣,在很多地方都是用到了CAS操作。

四 執行緒池瞭解嗎?

4.1 為什麼要用執行緒池?

執行緒池提供了一種限制和管理資源(包括執行一個任務)。 每個執行緒池還維護一些基本統計資訊,例如已完成任務的數量。

這裡借用《Java併發程式設計的藝術》提到的來說一下使用執行緒池的好處:

  • 降低資源消耗。 通過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗。
  • 提高響應速度。 當任務到達時,任務可以不需要的等到執行緒建立就能立即執行。
  • 提高執行緒的可管理性。 執行緒是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一的分配,調優和監控。

4.2 Java 提供了哪幾種執行緒池?他們各自的使用場景是什麼?

Java 主要提供了下面4種執行緒池

  • FixedThreadPool : 該方法返回一個固定執行緒數量的執行緒池。該執行緒池中的執行緒數量始終不變。當有一個新的任務提交時,執行緒池中若有空閒執行緒,則立即執行。若沒有,則新的任務會被暫存在一個任務佇列中,待有執行緒空閒時,便處理在任務佇列中的任務。
  • SingleThreadExecutor: 方法返回一個只有一個執行緒的執行緒池。若多餘一個任務被提交到該執行緒池,任務會被儲存在一個任務佇列中,待執行緒空閒,按先入先出的順序執行佇列中的任務。
  • CachedThreadPool: 該方法返回一個可根據實際情況調整執行緒數量的執行緒池。執行緒池的執行緒數量不確定,但若有空閒執行緒可以複用,則會優先使用可複用的執行緒。若所有執行緒均在工作,又有新的任務提交,則會建立新的執行緒處理任務。所有執行緒在當前任務執行完畢後,將返回執行緒池進行復用。
  • **ScheduledThreadPoolExecutor:**主要用來在給定的延遲後執行任務,或者定期執行任務。ScheduledThreadPoolExecutor又分為:ScheduledThreadPoolExecutor(包含多個執行緒)和SingleThreadScheduledExecutor (只包含一個執行緒)兩種。

各種執行緒池的適用場景介紹

  • FixedThreadPool: 適用於為了滿足資源管理需求,而需要限制當前執行緒數量的應用場景。它適用於負載比較重的伺服器;
  • SingleThreadExecutor: 適用於需要保證順序地執行各個任務並且在任意時間點,不會有多個執行緒是活動的應用場景。
  • CachedThreadPool: 適用於執行很多的短期非同步任務的小程式,或者是負載較輕的伺服器;
  • ScheduledThreadPoolExecutor: 適用於需要多個後臺執行週期任務,同時為了滿足資源管理需求而需要限制後臺執行緒的數量的應用場景,
  • SingleThreadScheduledExecutor: 適用於需要單個後臺執行緒執行週期任務,同時保證順序地執行各個任務的應用場景。

4.3 建立的執行緒池的方式

(1) 使用 Executors 建立

我們上面剛剛提到了 Java 提供的幾種執行緒池,通過 Executors 工具類我們可以很輕鬆的建立我們上面說的幾種執行緒池。但是實際上我們一般都不是直接使用Java提供好的執行緒池,另外在《阿里巴巴Java開發手冊》中強制執行緒池不允許使用 Executors 去建立,而是通過 ThreadPoolExecutor 建構函式 的方式,這樣的處理方式讓寫的同學更加明確執行緒池的執行規則,規避資源耗盡的風險。

Executors 返回執行緒池物件的弊端如下:

FixedThreadPool 和 SingleThreadExecutor : 允許請求的佇列長度為 Integer.MAX_VALUE,可能堆積大量的請求,從而導致OOM。
CachedThreadPool 和 ScheduledThreadPool : 允許建立的執行緒數量為 Integer.MAX_VALUE ,可能會建立大量執行緒,從而導致OOM。

複製程式碼

(2) ThreadPoolExecutor的建構函式建立

我們可以自己直接呼叫 ThreadPoolExecutor 的建構函式來自己建立執行緒池。在建立的同時,給 BlockQueue 指定容量就可以了。示例如下:

private static ExecutorService executor = new ThreadPoolExecutor(13, 13,
        60L, TimeUnit.SECONDS,
        new ArrayBlockingQueue(13));
複製程式碼

這種情況下,一旦提交的執行緒數超過當前可用執行緒數時,就會丟擲java.util.concurrent.RejectedExecutionException,這是因為當前執行緒池使用的佇列是有邊界佇列,佇列已經滿了便無法繼續處理新的請求。但是異常(Exception)總比發生錯誤(Error)要好。

(3) 使用開源類庫

Hollis 大佬之前在他的文章中也提到了:“除了自己定義ThreadPoolExecutor外。還有其他方法。這個時候第一時間就應該想到開源類庫,如apache和guava等。”他推薦使用guava提供的ThreadFactoryBuilder來建立執行緒池。下面是參考他的程式碼示例:

public class ExecutorsDemo {

    private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
        .setNameFormat("demo-pool-%d").build();

    private static ExecutorService pool = new ThreadPoolExecutor(5, 200,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) {

        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            pool.execute(new SubThread());
        }
    }
}
複製程式碼

通過上述方式建立執行緒時,不僅可以避免OOM的問題,還可以自定義執行緒名稱,更加方便的出錯的時候溯源。

五 Nginx

5.1 簡單介紹一下Nginx

Nginx是一款輕量級的Web 伺服器/反向代理伺服器及電子郵件(IMAP/POP3)代理伺服器。 Nginx 主要提供反向代理、負載均衡、動靜分離(靜態資源服務)等服務。下面我簡單地介紹一下這些名詞。

反向代理

談到反向代理,就不得不提一下正向代理。無論是正向代理,還是反向代理,說到底,就是代理模式的衍生版本罷了

  • **正向代理:**某些情況下,代理我們使用者去訪問伺服器,需要使用者手動的設定代理伺服器的ip和埠號。正向代理比較常見的一個例子就是 VPN了。
  • 反向代理: 是用來代理伺服器的,代理我們要訪問的目標伺服器。代理伺服器接受請求,然後將請求轉發給內部網路的伺服器,並將從伺服器上得到的結果返回給客戶端,此時代理伺服器對外就表現為一個伺服器。

通過下面兩幅圖,大家應該更好理解(圖源:blog.720ui.com/2016/nginx_…

正向代理

反向代理

所以,簡單的理解,就是正向代理是為客戶端做代理,代替客戶端去訪問伺服器,而反向代理是為伺服器做代理,代替伺服器接受客戶端請求。

負載均衡

在高併發情況下需要使用,其原理就是將併發請求分攤到多個伺服器執行,減輕每臺伺服器的壓力,多臺伺服器(叢集)共同完成工作任務,從而提高了資料的吞吐量。

Nginx支援的weight輪詢(預設)、ip_hash、fair、url_hash這四種負載均衡排程演算法,感興趣的可以自行查閱。

負載均衡相比於反向代理更側重的時將請求分擔到多臺伺服器上去,所以談論負載均衡只有在提供某服務的伺服器大於兩臺時才有意義。

動靜分離

動靜分離是讓動態網站裡的動態網頁根據一定規則把不變的資源和經常變的資源區分開來,動靜資源做好了拆分以後,我們就可以根據靜態資源的特點將其做快取操作,這就是網站靜態化處理的核心思路。

5.2 為什麼要用 Nginx ?

這部分內容參考極客時間—Nginx核心知識100講的內容

如果面試官問你這個問題,就一定想看你知道 Nginx 伺服器的一些優點嗎。

Nginx 有以下5個優點:

  1. 高併發、高效能(這是其他web伺服器不具有的)
  2. 可擴充套件性好(模組化設計,第三方外掛生態圈豐富)
  3. 高可靠性(可以在伺服器行持續不間斷的執行數年)
  4. 熱部署(這個功能對於 Nginx 來說特別重要,熱部署指可以在不停止 Nginx服務的情況下升級 Nginx)
  5. BSD許可證(意味著我們可以將原始碼下載下來進行修改然後使用自己的版本)

5.3 Nginx 的四個主要組成部分了解嗎?

這部分內容參考極客時間—Nginx核心知識100講的內容

  • Nginx 二進位制可執行檔案:由各模組原始碼編譯出一個檔案
  • Nginx.conf 配置檔案:控制Nginx 行為
  • acess.log 訪問日誌: 記錄每一條HTTP請求資訊
  • error.log 錯誤日誌:定位問題

你若盛開,清風自來。 歡迎關注我的微信公眾號:“Java面試通關手冊”,一個有溫度的微信公眾號。公眾號後臺回覆關鍵字“1”,可以免費獲取一份我精心準備的小禮物哦!

【備戰春招/秋招系列】美團Java面經總結終結篇 (附詳解答案)

相關文章