天天看點

Java 最常見 200+ 面試題全解析:面試必備(轉)

本文分為十九個子產品,分别是: Java 基礎、容器、多線程、反射、對象拷貝、Java Web 、異常、網絡、設計模式、Spring/Spring MVC、Spring Boot/Spring Cloud、Hibernate、MyBatis、RabbitMQ、Kafka、Zookeeper、MySQL、Redis、JVM ,如下圖所示:

Java 最常見 200+ 面試題全解析:面試必備(轉)

JDK:Java Development Kit 的簡稱,Java 開發工具包,提供了 Java 的開發環境和運作環境。

JRE:Java Runtime Environment 的簡稱,Java 運作環境,為 Java 的運作提供了所需環境。

具體來說 JDK 其實包含了 JRE,同時還包含了編譯 Java 源碼的編譯器 Javac,還包含了很多 Java 程式調試和分析的工具。簡單來說:如果你需要運作 Java 程式,隻需安裝 JRE 就可以了,如果你需要編寫 Java 程式,需要安裝 JDK。

== 解讀

對于基本類型和引用類型 == 的作用效果是不同的,如下所示:

基本類型:比較的是值是否相同;

引用類型:比較的是引用是否相同;

代碼示例:

代碼解讀:因為 x 和 y 指向的是同一個引用,是以 == 也是 true,而 new String()方法則重寫開辟了記憶體空間,是以 == 結果為 false,而 equals 比較的一直是值,是以結果都為 true。

equals 解讀

equals 本質上就是 ==,隻不過 String 和 Integer 等重寫了 equals 方法,把它變成了值比較。看下面的代碼就明白了。

首先來看預設情況下 equals 比較一個有相同值的對象,代碼如下:

輸出結果出乎我們的意料,竟然是 false?這是怎麼回事,看了 equals 源碼就知道了,源碼如下:

原來 equals 本質上就是 ==。

那問題來了,兩個相同值的 String 對象,為什麼傳回的是 true?代碼如下:

同樣的,當我們進入 String 的 equals 方法,找到了答案,代碼如下:

原來是 String 重寫了 Object 的 equals 方法,把引用比較改成了值比較。

總結 :== 對于基本類型來說是值比較,對于引用類型來說是比較的是引用;而 equals 預設情況下是引用比較,隻是很多類重新了 equals 方法,比如 String、Integer 等把它變成了值比較,是以一般情況下 equals 比較的是值是否相等。

不對,兩個對象的 hashCode() 相同,equals() 不一定 true。

執行的結果:

代碼解讀:很顯然“通話”和“重地”的 hashCode() 相同,然而 equals() 則為 false,因為在散清單中,hashCode() 相等即兩個鍵值對的哈希值相等,然而哈希值相等,并不一定能得出鍵值對相等。

final 修飾的類叫最終類,該類不能被繼承。

final 修飾的方法不能被重寫。

final 修飾的變量叫常量,常量必須初始化,初始化之後值就不能被修改。

等于 -1,因為在數軸上取值時,中間值(0.5)向右取整,是以正 0.5 是往上取整,負 0.5 是直接舍棄。

String 不屬于基礎類型,基礎類型有 8 種:byte、boolean、char、short、int、float、long、double,而 String 屬于對象。

操作字元串的類有:String、StringBuffer、StringBuilder。

String 和 StringBuffer、StringBuilder 的差別在于 String 聲明的是不可變的對象,每次操作都會生成新的 String 對象,然後将指針指向新的 String 對象,而 StringBuffer、StringBuilder 可以在原有對象的基礎上進行操作,是以在經常改變字元串内容的情況下最好不要使用 String。

StringBuffer 和 StringBuilder 最大的差別在于,StringBuffer 是線程安全的,而 StringBuilder 是非線程安全的,但 StringBuilder 的性能卻高于 StringBuffer,是以在單線程環境下推薦使用 StringBuilder,多線程環境下推薦使用 StringBuffer。

不一樣,因為記憶體的配置設定方式不一樣。String str="i"的方式,Java 虛拟機會将其配置設定到常量池中;而 String str=new String("i") 則會被分到堆記憶體中。

使用 StringBuilder 或者 stringBuffer 的 reverse() 方法。

示例代碼:

indexOf():傳回指定字元的索引。

charAt():傳回指定索引處的字元。

replace():字元串替換。

trim():去除字元串兩端空白。

split():分割字元串,傳回一個分割後的字元串數組。

getBytes():傳回字元串的 byte 類型數組。

length():傳回字元串長度。

toLowerCase():将字元串轉成小寫字母。

toUpperCase():将字元串轉成大寫字元。

substring():截取字元串。

equals():字元串比較。

不需要,抽象類不一定非要有抽象方法。

上面代碼,抽象類并沒有抽象方法但完全可以正常運作。

普通類不能包含抽象方法,抽象類可以包含抽象方法。

抽象類不能直接執行個體化,普通類可以直接執行個體化。

不能,定義抽象類就是讓其他類繼承的,如果定義為 final 該類就不能被繼承,這樣彼此就會産生沖突,是以 final 不能修飾抽象類,如下圖所示,編輯器也會提示錯誤資訊:

Java 最常見 200+ 面試題全解析:面試必備(轉)

實作:抽象類的子類使用 extends 來繼承;接口必須使用 implements 來實作接口。

構造函數:抽象類可以有構造函數;接口不能有。

實作數量:類可以實作很多個接口;但是隻能繼承一個抽象類。

通路修飾符:接口中的方法預設使用 public 修飾;抽象類中的方法可以是任意通路修飾符。

按功能來分:輸入流(input)、輸出流(output)。

按類型來分:位元組流和字元流。

位元組流和字元流的差別是:位元組流按 8 位傳輸以位元組為機關輸入輸出資料,字元流按 16 位傳輸以字元為機關輸入輸出資料。

BIO:Block IO 同步阻塞式 IO,就是我們平常使用的傳統 IO,它的特點是模式簡單使用友善,并發處理能力低。

NIO:Non IO 同步非阻塞 IO,是傳統 IO 的更新,用戶端和伺服器端通過 Channel(通道)通訊,實作了多路複用。

AIO:Asynchronous IO 是 NIO 的更新,也叫 NIO2,實作了異步非堵塞 IO ,異步 IO 的操作基于事件和回調機制。

Files. exists():檢測檔案路徑是否存在。

Files. createFile():建立檔案。

Files. createDirectory():建立檔案夾。

Files. delete():删除一個檔案或目錄。

Files. copy():複制檔案。

Files. move():移動檔案。

Files. size():檢視檔案個數。

Files. read():讀取檔案。

Files. write():寫入檔案。

Java 容器分為 Collection 和 Map 兩大類,其下又有很多子類,如下所示:

Collection

List

ArrayList

LinkedList

Vector

Stack

Set

HashSet

LinkedHashSet

TreeSet

Map

HashMap

LinkedHashMap

TreeMap

ConcurrentHashMap

Hashtable

Collection 是一個集合接口,它提供了對集合對象進行基本操作的通用接口方法,所有集合都是它的子類,比如 List、Set 等。

Collections 是一個包裝類,包含了很多靜态方法,不能被執行個體化,就像一個工具類,比如提供的排序方法: Collections. sort(list)。

List、Set、Map 的差別主要展現在兩個方面:元素是否有序、是否允許元素重複。

三者之間的差別,如下表:

Java 最常見 200+ 面試題全解析:面試必備(轉)

存儲:HashMap 運作 key 和 value 為 null,而 Hashtable 不允許。

線程安全:Hashtable 是線程安全的,而 HashMap 是非線程安全的。

推薦使用:在 Hashtable 的類注釋可以看到,Hashtable 是保留類不建議使用,推薦在單線程環境下使用 HashMap 替代,如果需要多線程使用則用 ConcurrentHashMap 替代。

對于在 Map 中插入、删除、定位一個元素這類操作,HashMap 是最好的選擇,因為相對而言 HashMap 的插入會更快,但如果你要對一個 key 集合進行有序的周遊,那 TreeMap 是更好的選擇。

HashMap 基于 Hash 算法實作的,我們通過 put(key,value)存儲,get(key)來擷取。當傳入 key 時,HashMap 會根據 key. hashCode() 計算出 hash 值,根據 hash 值将 value 儲存在 bucket 裡。當計算出的 hash 值相同時,我們稱之為 hash 沖突,HashMap 的做法是用連結清單和紅黑樹存儲相同 hash 值的 value。當 hash 沖突的個數比較少時,使用連結清單否則使用紅黑樹。

HashSet 是基于 HashMap 實作的,HashSet 底層使用 HashMap 來儲存所有元素,是以 HashSet 的實作比較簡單,相關 HashSet 的操作,基本上都是直接調用底層 HashMap 的相關方法來完成,HashSet 不允許重複的值。

資料結構實作:ArrayList 是動态數組的資料結構實作,而 LinkedList 是雙向連結清單的資料結構實作。

随機通路效率:ArrayList 比 LinkedList 在随機通路的時候效率要高,因為 LinkedList 是線性的資料存儲方式,是以需要移動指針從前往後依次查找。

增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因為 ArrayList 增删操作要影響數組内的其他資料的下标。

綜合來說,在需要頻繁讀取集合中的元素時,更推薦使用 ArrayList,而在插入和删除操作較多時,更推薦使用 LinkedList。

數組轉 List:使用 Arrays. asList(array) 進行轉換。

List 轉數組:使用 List 自帶的 toArray() 方法。

線程安全:Vector 使用了 Synchronized 來實作線程同步,是線程安全的,而 ArrayList 是非線程安全的。

性能:ArrayList 在性能方面要優于 Vector。

擴容:ArrayList 和 Vector 都會根據實際的需要動态的調整容量,隻不過在 Vector 擴容每次會增加 1 倍,而 ArrayList 隻會增加 50%。

Array 可以存儲基本資料類型和對象,ArrayList 隻能存儲對象。

Array 是指定固定大小的,而 ArrayList 大小是自動擴充的。

Array 内置方法沒有 ArrayList 多,比如 addAll、removeAll、iteration 等方法隻有 ArrayList 有。

相同點:都是傳回第一個元素,并在隊列中删除傳回的對象。

不同點:如果沒有元素 poll()會傳回 null,而 remove()會直接抛出 NoSuchElementException 異常。

Vector、Hashtable、Stack 都是線程安全的,而像 HashMap 則是非線程安全的,不過在 JDK 1.5 之後随着 Java. util. concurrent 并發包的出現,它們也有了自己對應的線程安全類,比如 HashMap 對應的線程安全類就是 ConcurrentHashMap。

Iterator 接口提供周遊任何 Collection 的接口。我們可以從一個 Collection 中使用疊代器方法來擷取疊代器執行個體。疊代器取代了 Java 集合架構中的 Enumeration,疊代器允許調用者在疊代過程中移除元素。

Iterator 使用代碼如下:

Iterator 的特點是更加安全,因為它可以確定,在目前周遊的集合元素被更改的時候,就會抛出 ConcurrentModificationException 異常。

Iterator 可以周遊 Set 和 List 集合,而 ListIterator 隻能周遊 List。

Iterator 隻能單向周遊,而 ListIterator 可以雙向周遊(向前/後周遊)。

ListIterator 從 Iterator 接口繼承,然後添加了一些額外的功能,比如添加一個元素、替換一個元素、擷取前面或後面元素的索引位置。

可以使用 Collections. unmodifiableCollection(Collection c) 方法來建立一個隻讀集合,這樣改變集合的任何操作都會抛出 Java. lang. UnsupportedOperationException 異常。

示例代碼如下:

并行:多個處理器或多核處理器同時處理多個任務。

并發:多個任務在同一個 CPU 核上,按細分的時間片輪流(交替)執行,從邏輯上來看那些任務是同時執行。

如下圖:

Java 最常見 200+ 面試題全解析:面試必備(轉)

并發 = 兩個隊列和一台咖啡機。

并行 = 兩個隊列和兩台咖啡機。

一個程式下至少有一個程序,一個程序下至少有一個線程,一個程序下也可以有多個線程來增加程式的執行速度。

守護線程是運作在背景的一種特殊程序。它獨立于控制終端并且周期性地執行某種任務或等待處理某些發生的事件。在 Java 中垃圾回收線程就是特殊的守護線程。

建立線程有三種方式:

繼承 Thread 重寫 run 方法;

實作 Runnable 接口;

實作 Callable 接口。

runnable 沒有傳回值,callable 可以拿到有傳回值,callable 可以看作是 runnable 的補充。

線程的狀态:

NEW 尚未啟動

RUNNABLE 正在執行中

BLOCKED 阻塞的(被同步鎖或者IO鎖阻塞)

WAITING 永久等待狀态

TIMED_WAITING 等待指定的時間重新被喚醒的狀态

TERMINATED 執行完成

類的不同:sleep() 來自 Thread,wait() 來自 Object。

釋放鎖:sleep() 不釋放鎖;wait() 釋放鎖。

用法不同:sleep() 時間到會自動恢複;wait() 可以使用 notify()/notifyAll()直接喚醒。

notifyAll()會喚醒所有的線程,notify()之後喚醒一個線程。notifyAll() 調用後,會将全部線程由等待池移到鎖池,然後參與鎖的競争,競争成功則繼續執行,如果不成功則留在鎖池等待鎖被釋放後再次參與競争。而 notify()隻會喚醒一個線程,具體喚醒哪一個線程由虛拟機控制。

start() 方法用于啟動線程,run() 方法用于執行線程的運作時代碼。run() 可以重複調用,而 start() 隻能調用一次。

線程池建立有七種方式,最核心的是最後一種:

newSingleThreadExecutor():它的特點在于工作線程數目被限制為 1,操作一個無界的工作隊列,是以它保證了所有任務的都是被順序執行,最多會有一個任務處于活動狀态,并且不允許使用者改動線程池執行個體,是以可以避免其改變線程數目;

newCachedThreadPool():它是一種用來處理大量短時間工作任務的線程池,具有幾個鮮明特點:它會試圖緩存線程并重用,當無緩存線程可用時,就會建立新的工作線程;如果線程閑置的時間超過 60 秒,則被終止并移出緩存;長時間閑置時,這種線程池,不會消耗什麼資源。其内部使用 SynchronousQueue 作為工作隊列;

newFixedThreadPool(int nThreads):重用指定數目(nThreads)的線程,其背後使用的是無界的工作隊列,任何時候最多有 nThreads 個工作線程是活動的。這意味着,如果任務數量超過了活動隊列數目,将在工作隊列中等待空閑線程出現;如果有工作線程退出,将會有新的工作線程被建立,以補足指定的數目 nThreads;

newSingleThreadScheduledExecutor():建立單線程池,傳回 ScheduledExecutorService,可以進行定時或周期性的工作排程;

newScheduledThreadPool(int corePoolSize):和newSingleThreadScheduledExecutor()類似,建立的是個 ScheduledExecutorService,可以進行定時或周期性的工作排程,差別在于單一工作線程還是多個工作線程;

newWorkStealingPool(int parallelism):這是一個經常被人忽略的線程池,Java 8 才加入這個建立方法,其内部會建構ForkJoinPool,利用Work-Stealing算法,并行地處理任務,不保證處理順序;

ThreadPoolExecutor():是最原始的線程池建立,上面1-3建立方式都是對ThreadPoolExecutor的封裝。

RUNNING:這是最正常的狀态,接受新的任務,處理等待隊列中的任務。

SHUTDOWN:不接受新的任務送出,但是會繼續處理等待隊列中的任務。

STOP:不接受新的任務送出,不再處理等待隊列中的任務,中斷正在執行任務的線程。

TIDYING:所有的任務都銷毀了,workCount 為 0,線程池的狀态在轉換為 TIDYING 狀态時,會執行鈎子方法 terminated()。

TERMINATED:terminated()方法結束後,線程池的狀态就會變成這個。

execute():隻能執行 Runnable 類型的任務。

submit():可以執行 Runnable 和 Callable 類型的任務。

Callable 類型的任務可以擷取執行的傳回值,而 Runnable 執行無傳回值。

方法一:使用安全類,比如 Java. util. concurrent 下的類。

方法二:使用自動鎖 synchronized。

方法三:使用手動鎖 Lock。

手動鎖 Java 示例代碼如下:

synchronized 鎖更新原理:在鎖對象的對象頭裡面有一個 threadid 字段,在第一次通路的時候 threadid 為空,jvm 讓其持有偏向鎖,并将 threadid 設定為其線程 id,再次進入的時候會先判斷 threadid 是否與其線程 id 一緻,如果一緻則可以直接使用此對象,如果不一緻,則更新偏向鎖為輕量級鎖,通過自旋循環一定次數來擷取鎖,執行一定次數之後,如果還沒有正常擷取到要使用的對象,此時就會把鎖從輕量級更新為重量級鎖,此過程就構成了 synchronized 鎖的更新。

鎖的更新的目的:鎖更新是為了減低了鎖帶來的性能消耗。在 Java 6 之後優化 synchronized 的實作方式,使用了偏向鎖更新為輕量級鎖再更新到重量級鎖的方式,進而減低了鎖帶來的性能消耗。

當線程 A 持有獨占鎖a,并嘗試去擷取獨占鎖 b 的同時,線程 B 持有獨占鎖 b,并嘗試擷取獨占鎖 a 的情況下,就會發生 AB 兩個線程由于互相持有對方需要的鎖,而發生的阻塞現象,我們稱為死鎖。

盡量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),設定逾時時間,逾時可以退出防止死鎖。

盡量使用 Java. util. concurrent 并發類代替自己手寫鎖。

盡量降低鎖的使用粒度,盡量不要幾個功能用同一把鎖。

盡量減少同步的代碼塊。

ThreadLocal 為每個使用該變量的線程提供獨立的變量副本,是以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。

ThreadLocal 的經典使用場景是資料庫連接配接和 session 管理等。

synchronized 是由一對 monitorenter/monitorexit 指令實作的,monitor 對象是同步的基本實作單元。在 Java 6 之前,monitor 的實作完全是依靠作業系統内部的互斥鎖,因為需要進行使用者态到核心态的切換,是以同步操作是一個無差别的重量級操作,性能也很低。但在 Java 6 的時候,Java 虛拟機 對此進行了大刀闊斧地改進,提供了三種不同的 monitor 實作,也就是常說的三種不同的鎖:偏向鎖(Biased Locking)、輕量級鎖和重量級鎖,大大改進了其性能。

volatile 是變量修飾符;synchronized 是修飾類、方法、代碼段。

volatile 僅能實作變量的修改可見性,不能保證原子性;而 synchronized 則可以保證變量的修改可見性和原子性。

volatile 不會造成線程的阻塞;synchronized 可能會造成線程的阻塞。

synchronized 可以給類、方法、代碼塊加鎖;而 lock 隻能給代碼塊加鎖。

synchronized 不需要手動擷取鎖和釋放鎖,使用簡單,發生異常會自動釋放鎖,不會造成死鎖;而 lock 需要自己加鎖和釋放鎖,如果使用不當沒有 unLock()去釋放鎖就會造成死鎖。

通過 Lock 可以知道有沒有成功擷取鎖,而 synchronized 卻無法辦到。

synchronized 早期的實作比較低效,對比 ReentrantLock,大多數場景性能都相差較大,但是在 Java 6 中對 synchronized 進行了非常多的改進。

主要差別如下:

ReentrantLock 使用起來比較靈活,但是必須有釋放鎖的配合動作;

ReentrantLock 必須手動擷取與釋放鎖,而 synchronized 不需要手動釋放和開啟鎖;

ReentrantLock 隻适用于代碼塊鎖,而 synchronized 可用于修飾方法、代碼塊等。

volatile 标記的變量不會被編譯器優化;synchronized 标記的變量可以被編譯器優化。

atomic 主要利用 CAS (Compare And Wwap) 和 volatile 和 native 方法來保證原子操作,進而避免 synchronized 的高開銷,執行效率大為提升。

反射是在運作狀态中,對于任意一個類,都能夠知道這個類的所有屬性和方法;對于任意一個對象,都能夠調用它的任意一個方法和屬性;這種動态擷取的資訊以及動态調用對象的方法的功能稱為 Java 語言的反射機制。

Java 序列化是為了儲存各種對象在記憶體中的狀态,并且可以把儲存的對象狀态再讀出來。

以下情況需要使用 Java 序列化:

想把的記憶體中的對象狀态儲存到一個檔案中或者資料庫中時候;

想用套接字在網絡上傳送對象的時候;

想通過RMI(遠端方法調用)傳輸對象的時候。

動态代理是運作時動态生成代理類。

動态代理的應用有 spring aop、hibernate 資料查詢、測試架構的後端 mock、rpc,Java注解對象擷取等。

JDK 原生動态代理和 cglib 動态代理。JDK 原生動态代理是基于接口實作的,而 cglib 是基于繼承目前類的子類實作的。

克隆的對象可能包含一些已經修改過的屬性,而 new 出來的對象的屬性都還是初始化時候的值,是以當需要一個新的對象來儲存目前對象的“狀态”就靠克隆方法了。

實作 Cloneable 接口并重寫 Object 類中的 clone() 方法。

實作 Serializable 接口,通過對象的序列化和反序列化實作克隆,可以實作真正的深度克隆。

淺克隆:當對象被複制時隻複制它本身和其中包含的值類型的成員變量,而引用類型的成員對象并沒有複制。

深克隆:除了對象本身被複制外,對象所包含的所有成員變量也将複制。

JSP 是 servlet 技術的擴充,本質上就是 servlet 的簡易方式。servlet 和 JSP 最主要的不同點在于,servlet 的應用邏輯是在 Java 檔案中,并且完全從表示層中的 html 裡分離開來,而 JSP 的情況是 Java 和 html 可以組合成一個擴充名為 JSP 的檔案。JSP 側重于視圖,servlet 主要用于控制邏輯。

JSP 有 9 大内置對象:

request:封裝用戶端的請求,其中包含來自 get 或 post 請求的參數;

response:封裝伺服器對用戶端的響應;

pageContext:通過該對象可以擷取其他對象;

session:封裝使用者會話的對象;

application:封裝伺服器運作環境的對象;

out:輸出伺服器響應的輸出流對象;

config:Web 應用的配置對象;

page:JSP 頁面本身(相當于 Java 程式中的 this);

exception:封裝頁面抛出異常的對象。

page:代表與一個頁面相關的對象和屬性。

request:代表與用戶端發出的一個請求相關的對象和屬性。一個請求可能跨越多個頁面,涉及多個 Web 元件;需要在頁面顯示的臨時資料可以置于此作用域。

session:代表與某個使用者與伺服器建立的一次會話相關的對象和屬性。跟某個使用者相關的資料應該放在使用者自己的 session 中。

application:代表與整個 Web 應用程式相關的對象和屬性,它實質上是跨越整個 Web 應用程式,包括多個頁面、請求和會話的一個全局作用域。

存儲位置不同:session 存儲在伺服器端;cookie 存儲在浏覽器端。

安全性不同:cookie 安全性一般,在浏覽器存儲,可以被僞造和修改。

容量和個數限制:cookie 有容量限制,每個站點下的 cookie 也有個數限制。

存儲的多樣性:session 可以存儲在 Redis 中、資料庫中、應用程式中;而 cookie 隻能存儲在浏覽器中。

session 的工作原理是用戶端登入完成之後,伺服器會建立對應的 session,session 建立完之後,會把 session 的 id 發送給用戶端,用戶端再存儲到浏覽器中。這樣用戶端每次通路伺服器時,都會帶着 sessionid,伺服器拿到 sessionid 之後,在記憶體找到與之對應的 session 這樣就可以正常工作了。

可以用,session 隻是依賴 cookie 存儲 sessionid,如果 cookie 被禁用了,可以使用 url 中添加 sessionid 的方式保證 session 能正常使用。

攔截級别:struts2 是類級别的攔截;spring mvc 是方法級别的攔截。

資料獨立性:spring mvc 的方法之間基本上獨立的,獨享 request 和 response 資料,請求資料通過參數擷取,處理結果通過 ModelMap 交回給架構,方法之間不共享變量;而 struts2 雖然方法之間也是獨立的,但其所有 action 變量是共享的,這不會影響程式運作,卻給我們編碼和讀程式時帶來了一定的麻煩。

攔截機制:struts2 有以自己的 interceptor 機制,spring mvc 用的是獨立的 aop 方式,這樣導緻struts2 的配置檔案量比 spring mvc 大。

對 ajax 的支援:spring mvc 內建了ajax,所有 ajax 使用很友善,隻需要一個注解 @ResponseBody 就可以實作了;而 struts2 一般需要安裝插件或者自己寫代碼才行。

使用預處理 PreparedStatement。

使用正規表達式過濾掉字元中的特殊字元。

XSS 攻擊:即跨站腳本攻擊,它是 Web 程式中常見的漏洞。原理是攻擊者往 Web 頁面裡插入惡意的腳本代碼(css 代碼、Javascript 代碼等),當使用者浏覽該頁面時,嵌入其中的腳本代碼會被執行,進而達到惡意攻擊使用者的目的,如盜取使用者 cookie、破壞頁面結構、重定向到其他網站等。

預防 XSS 的核心是必須對輸入的資料做過濾處理。

CSRF:Cross-Site Request Forgery(中文:跨站請求僞造),可以了解為攻擊者盜用了你的身份,以你的名義發送惡意請求,比如:以你名義發送郵件、發消息、購買商品,虛拟貨币轉賬等。

防禦手段:

驗證請求來源位址;

關鍵操作添加驗證碼;

在請求位址添加 token 并驗證。

throw:是真實抛出一個異常。

throws:是聲明可能會抛出一個異常。

final:是修飾符,如果修飾類,此類不能被繼承;如果修飾方法和變量,則表示此方法和此變量不能在被改變,隻能使用。

finally:是 try{} catch{} finally{} 最後一部分,表示不論發生任何情況都會執行,finally 部分可以省略,但如果 finally 部分存在,則一定會執行 finally 裡面的代碼。

finalize: 是 Object 類的一個方法,在垃圾收集器執行的時候會調用被回收對象的此方法。

try-catch-finally 其中 catch 和 finally 都可以被省略,但是不能同時省略,也就是說有 try 的時候,必須後面跟一個 catch 或者 finally。

finally 一定會執行,即使是 catch 中 return 了,catch 中的 return 會等 finally 中的代碼執行完之後,才會執行。

NullPointerException 空指針異常

ClassNotFoundException 指定類不存在

NumberFormatException 字元串轉換為數字異常

IndexOutOfBoundsException 數組下标越界異常

ClassCastException 資料類型轉換異常

FileNotFoundException 檔案未找到異常

NoSuchMethodException 方法不存在異常

IOException IO 異常

SocketException Socket 異常

301:永久重定向。

302:暫時重定向。

它們的差別是,301 對搜尋引擎優化(SEO)更加有利;302 有被提示為網絡攔截的風險。

forward 是轉發 和 redirect 是重定向:

位址欄 url 顯示:foward url 不會發生改變,redirect url 會發生改變;

資料共享:forward 可以共享 request 裡的資料,redirect 不能共享;

效率:forward 比 redirect 效率高。

tcp 和 udp 是 OSI 模型中的運輸層中的協定。tcp 提供可靠的通信傳輸,而 udp 則常被用于讓廣播和細節控制交給應用的通信傳輸。

兩者的差別大緻如下:

tcp 面向連接配接,udp 面向非連接配接即發送資料前不需要建立連結;

tcp 提供可靠的服務(資料傳輸),udp 無法保證;

tcp 面向位元組流,udp 面向封包;

tcp 資料傳輸慢,udp 資料傳輸快;

如果采用兩次握手,那麼隻要伺服器發出确認資料包就會建立連接配接,但由于用戶端此時并未響應伺服器端的請求,那此時伺服器端就會一直在等待用戶端,這樣伺服器端就白白浪費了一定的資源。若采用三次握手,伺服器端沒有收到來自用戶端的再此确認,則就會知道用戶端并沒有要求建立請求,就不會浪費伺服器的資源。

tcp 粘包可能發生在發送端或者接收端,分别來看兩端各種産生粘包的原因:

發送端粘包:發送端需要等緩沖區滿才發送出去,造成粘包;

接收方粘包:接收方不及時接收緩沖區的包,造成多個包接收。

實體層:利用傳輸媒體為資料鍊路層提供實體連接配接,實作比特流的透明傳輸。

資料鍊路層:負責建立和管理節點間的鍊路。

網絡層:通過路由選擇算法,為封包或分組通過通信子網選擇最适當的路徑。

傳輸層:向使用者提供可靠的端到端的差錯和流量控制,保證封包的正确傳輸。

會話層:向兩個實體的表示層提供建立和使用連接配接的方法。

表示層:處理使用者資訊的表示問題,如編碼、資料格式轉換和加密解密等。

應用層:直接向使用者提供服務,完成使用者希望在網絡上完成的各種工作。

get 請求會被浏覽器主動緩存,而 post 不會。

get 傳遞參數有大小限制,而 post 沒有。

post 參數傳輸更安全,get 的參數會明文限制在 url 上,post 不會。

實作跨域有以下幾種方案:

伺服器端運作跨域 設定 CORS 等于 *;

在單個接口使用注解 @CrossOrigin 運作跨域;

使用 jsonp 跨域;

jsonp:JSON with Padding,它是利用script标簽的 src 連接配接可以通路不同源的特性,加載遠端傳回的“JS 函數”來執行的。

單例模式:保證被建立一次,節省系統開銷。

工廠模式(簡單工廠、抽象工廠):解耦代碼。

觀察者模式:定義了對象之間的一對多的依賴,這樣一來,當一個對象改變時,它的所有的依賴者都會收到通知并自動更新。

外觀模式:提供一個統一的接口,用來通路子系統中的一群接口,外觀定義了一個高層的接口,讓子系統更容易使用。

模版方法模式:定義了一個算法的骨架,而将一些步驟延遲到子類中,模版方法使得子類可以在不改變算法結構的情況下,重新定義算法的步驟。

狀态模式:允許對象在内部狀态改變時改變它的行為,對象看起來好像修改了它的類。

簡單工廠:用來生産同一等級結構中的任意産品,對于增加新的産品,無能為力。

工廠方法:用來生産同一等級結構中的固定産品,支援增加任意産品。

抽象工廠:用來生産不同産品族的全部産品,對于增加新的産品,無能為力;支援增加産品族。

spring 提供 ioc 技術,容器會幫你管理依賴的對象,進而不需要自己建立和管理依賴對象了,更輕松的實作了程式的解耦。

spring 提供了事務支援,使得事務操作變的更加友善。

spring 提供了面向切片程式設計,這樣可以更友善的處理某一類的問題。

更友善的架構內建,spring 可以很友善的內建其他架構,比如 MyBatis、hibernate 等。

aop 是面向切面程式設計,通過預編譯方式和運作期動态代理實作程式功能的統一維護的一種技術。

簡單來說就是統一處理某一“切面”(類)的問題的程式設計思想,比如統一處理日志、異常等。

ioc:Inversionof Control(中文:控制反轉)是 spring 的核心,對于 spring 架構來說,就是由 spring 來負責控制對象的生命周期和對象間的關系。

簡單來說,控制指的是目前對象對内部成員的控制權;控制反轉指的是,這種控制權不由目前對象管理了,由其他(類,第三方容器)來管理。

spring core:架構的最基礎部分,提供 ioc 和依賴注入特性。

spring context:建構于 core 封裝包基礎上的 context 封裝包,提供了一種架構式的對象通路方法。

spring dao:Data Access Object 提供了JDBC的抽象層。

spring aop:提供了面向切面的程式設計實作,讓你可以自定義攔截器、切點等。

spring Web:提供了針對 Web 開發的內建特性,例如檔案上傳,利用 servlet listeners 進行 ioc 容器初始化和針對 Web 的 ApplicationContext。

spring Web mvc:spring 中的 mvc 封裝包提供了 Web 應用的 Model-View-Controller(MVC)的實作。

setter 屬性注入

構造方法注入

注解方式注入

spring 中的 bean 預設是單例模式,spring 架構并沒有對單例 bean 進行多線程的封裝處理。

實際上大部分時候 spring bean 無狀态的(比如 dao 類),所有某種程度上來說 bean 也是安全的,但如果 bean 有狀态的話(比如 view model 對象),那就要開發者自己去保證線程安全了,最簡單的就是改變 bean 的作用域,把“singleton”變更為“prototype”,這樣請求 bean 相當于 new Bean()了,是以就可以保證線程安全了。

有狀态就是有資料存儲功能。

無狀态就是不會儲存資料。

spring 支援 5 種作用域,如下:

singleton:spring ioc 容器中隻存在一個 bean 執行個體,bean 以單例模式存在,是系統預設值;

prototype:每次從容器調用 bean 時都會建立一個新的示例,既每次 getBean()相當于執行 new Bean()操作;

Web 環境下的作用域:

request:每次 http 請求都會建立一個 bean;

session:同一個 http session 共享一個 bean 執行個體;

global-session:用于 portlet 容器,因為每個 portlet 有單獨的 session,globalsession 提供一個全局性的 http session。

注意: 使用 prototype 作用域需要慎重的思考,因為頻繁建立和銷毀 bean 會帶來很大的性能開銷。

no:預設值,表示沒有自動裝配,應使用顯式 bean 引用進行裝配。

byName:它根據 bean 的名稱注入對象依賴項。

byType:它根據類型注入對象依賴項。

構造函數:通過構造函數來注入依賴項,需要設定大量的參數。

autodetect:容器首先通過構造函數使用 autowire 裝配,如果不能,則通過 byType 自動裝配。

聲明式事務:聲明式事務也有兩種實作方式,基于 xml 配置檔案的方式和注解方式(在類上添加 @Transaction 注解)。

編碼方式:提供編碼的形式管理和維護事務。

spring 有五大隔離級别,預設值為 ISOLATION_DEFAULT(使用資料庫的設定),其他四個隔離級别和資料庫的隔離級别一緻:

ISOLATION_DEFAULT:用底層資料庫的設定隔離級别,資料庫設定的是什麼我就用什麼;

ISOLATIONREADUNCOMMITTED:未送出讀,最低隔離級别、事務未送出前,就可被其他事務讀取(會出現幻讀、髒讀、不可重複讀);

ISOLATIONREADCOMMITTED:送出讀,一個事務送出後才能被其他事務讀取到(會造成幻讀、不可重複讀),SQL server 的預設級别;

ISOLATIONREPEATABLEREAD:可重複讀,保證多次讀取同一個資料時,其值都和事務開始時候的内容是一緻,禁止讀取到别的事務未送出的資料(會造成幻讀),MySQL 的預設級别;

ISOLATION_SERIALIZABLE:序列化,代價最高最可靠的隔離級别,該隔離級别能防止髒讀、不可重複讀、幻讀。

髒讀 :表示一個事務能夠讀取另一個事務中還未送出的資料。比如,某個事務嘗試插入記錄 A,此時該事務還未送出,然後另一個事務嘗試讀取到了記錄 A。

不可重複讀 :是指在一個事務内,多次讀同一資料。

幻讀 :指同一個事務内多次查詢傳回的結果集不一樣。比如同一個事務 A 第一次查詢時候有 n 條記錄,但是第二次同等條件下查詢卻有 n+1 條記錄,這就好像産生了幻覺。發生幻讀的原因也是另外一個事務新增或者删除或者修改了第一個事務結果集裡面的資料,同一個記錄的資料内容被修改了,所有資料行的記錄就變多或者變少了。

spring mvc 先将請求發送給 DispatcherServlet。

DispatcherServlet 查詢一個或多個 HandlerMapping,找到處理請求的 Controller。

DispatcherServlet 再把請求送出到對應的 Controller。

Controller 進行業務邏輯處理後,會傳回一個ModelAndView。

Dispathcher 查詢一個或多個 ViewResolver 視圖解析器,找到 ModelAndView 對象指定的視圖對象。

視圖對象負責渲染傳回給用戶端。

前置控制器 DispatcherServlet。

映射控制器 HandlerMapping。

處理器 Controller。

模型和視圖 ModelAndView。

視圖解析器 ViewResolver。

将 http 請求映射到相應的類/方法上。

@Autowired 它可以對類成員變量、方法及構造函數進行标注,完成自動裝配的工作,通過@Autowired 的使用來消除 set/get 方法。

spring boot 是為 spring 服務的,是用來簡化新 spring 應用的初始搭建以及開發過程的。

配置簡單

獨立運作

自動裝配

無代碼生成和 xml 配置

提供應用監控

易上手

提升開發效率

spring boot 核心的兩個配置檔案:

bootstrap (. yml 或者 . properties):boostrap 由父 ApplicationContext 加載的,比 applicaton 優先加載,且 boostrap 裡面的屬性不能被覆寫;

application (. yml 或者 . properties):用于 spring boot 項目的自動化配置。

配置檔案有 . properties 格式和 . yml 格式,它們主要的差別是書法風格不同。

. properties 配置如下:

. yml 配置如下:

. yml 格式不支援 @PropertySource 注解導入。

使用 devtools 啟動熱部署,添加 devtools 庫,在配置檔案中把 spring. devtools. restart. enabled 設定為 true;

使用 Intellij Idea 編輯器,勾上自動編譯或手動重新編譯。

jpa 全稱 Java Persistence API,是 Java 持久化接口規範,hibernate 屬于 jpa 的具體實作。

spring cloud 是一系列架構的有序集合。它利用 spring boot 的開發便利性巧妙地簡化了分布式系統基礎設施的開發,如服務發現注冊、配置中心、消息總線、負載均衡、斷路器、資料監控等,都可以用 spring boot 的開發風格做到一鍵啟動和部署。

在分布式架構中,斷路器模式的作用也是類似的,當某個服務單元發生故障(類似用電器發生短路)之後,通過斷路器的故障監控(類似熔斷保險絲),向調用方傳回一個錯誤響應,而不是長時間的等待。這樣就不會使得線程因調用故障服務被長時間占用不釋放,避免了故障在分布式系統中的蔓延。

Eureka:服務注冊于發現。

Feign:基于動态代理機制,根據注解和選擇的機器,拼接請求 url 位址,發起請求。

Ribbon:實作負載均衡,從一個服務的多台機器中選擇一台。

Hystrix:提供線程池,不同的服務走不同的線程池,實作了不同服務調用的隔離,避免了服務雪崩的問題。

Zuul:網關管理,由 Zuul 網關轉發請求給對應的服務。

hibernate 是對 jdbc 的封裝,大大簡化了資料通路層的繁瑣的重複性代碼。

hibernate 是一個優秀的 ORM 實作,很多程度上簡化了 DAO 層的編碼功能。

可以很友善的進行資料庫的移植工作。

提供了緩存機制,是程式執行更改的高效。

ORM(Object Relation Mapping)對象關系映射,是把資料庫中的關系資料映射成為程式中的對象。

使用 ORM 的優點:提高了開發效率降低了開發成本、開發更簡單更對象化、可移植更強。

在 Config 裡面把 hibernate. show_SQL 設定為 true 就可以。但不建議開啟,開啟之後會降低程式的運作效率。

三種:hql、原生 SQL、條件查詢 Criteria。

實體類可以定義為 final 類,但這樣的話就不能使用 hibernate 代理模式下的延遲關聯提供性能了,是以不建議定義實體類為 final。

Integer 類型為對象,它的值允許為 null,而 int 屬于基礎資料類型,值不能為 null。

讀取并解析配置檔案。

讀取并解析映射檔案,建立 SessionFactory。

打開 Session。

建立事務。

進行持久化操作。

送出事務。

關閉 Session。

關閉 SessionFactory。

資料查詢時,沒有 OID 指定的對象,get() 傳回 null;load() 傳回一個代理對象。

load()支援延遲加載;get() 不支援延遲加載。

hibernate 常用的緩存有一級緩存和二級緩存:

一級緩存:也叫 Session 緩存,隻在 Session 作用範圍内有效,不需要使用者幹涉,由 hibernate 自身維護,可以通過:evict(object)清除 object 的緩存;clear()清除一級緩存中的所有緩存;flush()刷出緩存;

二級緩存:應用級别的緩存,在所有 Session 中都有效,支援配置第三方的緩存,如:EhCache。

臨時/瞬時狀态:直接 new 出來的對象,該對象還沒被持久化(沒儲存在資料庫中),不受 Session 管理。

持久化狀态:當調用 Session 的 save/saveOrupdate/get/load/list 等方法的時候,對象就是持久化狀态。

遊離狀态:Session 關閉之後對象就是遊離狀态。

getCurrentSession 會綁定目前線程,而 openSession 則不會。

getCurrentSession 事務是 Spring 控制的,并且不需要手動關閉,而 openSession 需要我們自己手動開啟和送出事務。

hibernate 中每個實體類必須提供一個無參構造函數,因為 hibernate 架構要使用 reflection api,通過調用 ClassnewInstance() 來建立實體類的執行個體,如果沒有無參的構造函數就會抛出異常。

<code>\#{}</code>是預編譯處理,<code>${}</code>是字元替換。 在使用 <code>#{}</code>時,MyBatis 會将 SQL 中的 <code>#{}</code>替換成“?”,配合 PreparedStatement 的 set 方法指派,這樣可以有效的防止 SQL 注入,保證程式的運作安全。

分頁方式:邏輯分頁和實體分頁。

邏輯分頁: 使用 MyBatis 自帶的 RowBounds 進行分頁,它是一次性查詢很多資料,然後在資料中再進行檢索。

實體分頁: 自己手寫 SQL 分頁或使用分頁插件 PageHelper,去資料庫查詢指定條數的分頁資料的形式。

RowBounds 表面是在“所有”資料中檢索資料,其實并非是一次性查詢出所有資料,因為 MyBatis 是對 jdbc 的封裝,在 jdbc 驅動中有一個 Fetch Size 的配置,它規定了每次最多從資料庫查詢多少條資料,假如你要查詢更多資料,它會在你執行 next()的時候,去查詢更多的資料。就好比你去自動取款機取 10000 元,但取款機每次最多能取 2500 元,是以你要取 4 次才能把錢取完。隻是對于 jdbc 來說,當你調用 next()的時候會自動幫你完成查詢工作。這樣做的好處可以有效的防止記憶體溢出。

Fetch Size 官方相關文檔:http://t. cn/EfSE2g3

邏輯分頁是一次性查詢很多資料,然後再在結果中檢索分頁的資料。這樣做弊端是需要消耗大量的記憶體、有記憶體溢出的風險、對資料庫壓力較大。

實體分頁是從資料庫查詢指定條數的資料,彌補了一次性全部查出的所有資料的種種缺點,比如需要大量的記憶體,對資料庫查詢壓力較大等問題。

MyBatis 支援延遲加載,設定 lazyLoadingEnabled=true 即可。

延遲加載的原理的是調用的時候觸發加載,而不是在初始化的時候就加載資訊。比如調用 a. getB(). getName(),這個時候發現 a. getB() 的值為 null,此時會單獨觸發事先儲存好的關聯 B 對象的 SQL,先查詢出來 B,然後再調用 a. setB(b),而這時候再調用 a. getB(). getName() 就有值了,這就是延遲加載的基本原理。

一級緩存:基于 PerpetualCache 的 HashMap 本地緩存,它的聲明周期是和 SQLSession 一緻的,有多個 SQLSession 或者分布式的環境中資料庫操作,可能會出現髒資料。當 Session flush 或 close 之後,該 Session 中的所有 Cache 就将清空,預設一級緩存是開啟的。

二級緩存:也是基于 PerpetualCache 的 HashMap 本地緩存,不同在于其存儲作用域為 Mapper 級别的,如果多個SQLSession之間需要共享緩存,則需要使用到二級緩存,并且二級緩存可自定義存儲源,如 Ehcache。預設不打開二級緩存,要開啟二級緩存,使用二級緩存屬性類需要實作 Serializable 序列化接口(可用來儲存對象的狀态)。

開啟二級緩存資料查詢流程:二級緩存 -&gt; 一級緩存 -&gt; 資料庫。

緩存更新機制:當某一個作用域(一級緩存 Session/二級緩存 Mapper)進行了C/U/D 操作後,預設該作用域下所有 select 中的緩存将被 clear。

靈活性:MyBatis 更加靈活,自己可以寫 SQL 語句,使用起來比較友善。

可移植性:MyBatis 有很多自己寫的 SQL,因為每個資料庫的 SQL 可以不相同,是以可移植性比較差。

學習和使用門檻:MyBatis 入門比較簡單,使用門檻也更低。

二級緩存:hibernate 擁有更好的二級緩存,它的二級緩存可以自行更換為第三方的二級緩存。

MyBatis 有三種基本的Executor執行器:

SimpleExecutor:每執行一次 update 或 select 就開啟一個 Statement 對象,用完立刻關閉 Statement 對象;

ReuseExecutor:執行 update 或 select,以 SQL 作為 key 查找 Statement 對象,存在就使用,不存在就建立,用完後不關閉 Statement 對象,而是放置于 Map 内供下一次使用。簡言之,就是重複使用 Statement 對象;

BatchExecutor:執行 update(沒有 select,jdbc 批處理不支援 select),将所有 SQL 都添加到批進行中(addBatch()),等待統一執行(executeBatch()),它緩存了多個 Statement 對象,每個 Statement 對象都是 addBatch()完畢後,等待逐一執行 executeBatch()批處理,與 jdbc 批處理相同。

分頁插件的基本原理是使用 MyBatis 提供的插件接口,實作自定義插件,在插件的攔截方法内攔截待執行的 SQL,然後重寫 SQL,根據 dialect 方言,添加對應的實體分頁語句和實體分頁參數。

自定義插件實作原理

MyBatis 自定義插件針對 MyBatis 四大對象(Executor、StatementHandler、ParameterHandler、ResultSetHandler)進行攔截:

Executor:攔截内部執行器,它負責調用 StatementHandler 操作資料庫,并把結果集通過 ResultSetHandler 進行自動映射,另外它還處理了二級緩存的操作;

StatementHandler:攔截 SQL 文法建構的處理,它是 MyBatis 直接和資料庫執行 SQL 腳本的對象,另外它也實作了 MyBatis 的一級緩存;

ParameterHandler:攔截參數的處理;

ResultSetHandler:攔截結果集的處理。

自定義插件實作關鍵

MyBatis 插件要實作 Interceptor 接口,接口包含的方法,如下:

setProperties 方法是在 MyBatis 進行配置插件的時候可以配置自定義相關屬性,即:接口實作對象的參數配置;

plugin 方法是插件用于封裝目标對象的,通過該方法我們可以傳回目标對象本身,也可以傳回一個它的代理,可以決定是否要進行攔截進而決定要傳回一個什麼樣的目标對象,官方提供了示例:return Plugin. wrap(target, this);

intercept 方法就是要進行攔截的時候要執行的方法。

自定義插件實作示例

官方插件實作:

搶購活動,削峰填谷,防止系統崩塌。

延遲資訊處理,比如 10 分鐘之後給下單未付款的使用者發送郵件提醒。

解耦系統,對于新增的功能可以單獨寫子產品擴充,比如使用者确認評價之後,新增了給使用者返積分的功能,這個時候不用在業務代碼裡添加新增積分的功能,隻需要把新增積分的接口訂閱确認評價的消息隊列即可,後面再添加任何功能隻需要訂閱對應的消息隊列即可。

RabbitMQ 中重要的角色有:生産者、消費者和代理:

生産者:消息的建立者,負責建立和推送資料到消息伺服器;

消費者:消息的接收方,用于處理資料和确認消息;

代理:就是 RabbitMQ 本身,用于扮演“快遞”的角色,本身不生産消息,隻是扮演“快遞”的角色。

ConnectionFactory(連接配接管理器):應用程式與Rabbit之間建立連接配接的管理器,程式代碼中使用。

Channel(信道):消息推送使用的通道。

Exchange(交換器):用于接受、配置設定消息。

Queue(隊列):用于存儲生産者的消息。

RoutingKey(路由鍵):用于把生成者的資料配置設定到交換器上。

BindingKey(綁定鍵):用于把交換器的消息綁定到隊列上。

vhost:每個 RabbitMQ 都能建立很多 vhost,我們稱之為虛拟主機,每個虛拟主機其實都是 mini 版的RabbitMQ,它擁有自己的隊列,交換器和綁定,擁有自己的權限機制。

首先用戶端必須連接配接到 RabbitMQ 伺服器才能釋出和消費消息,用戶端和 rabbit server 之間會建立一個 tcp 連接配接,一旦 tcp 打開并通過了認證(認證就是你發送給 rabbit 伺服器的使用者名和密碼),你的用戶端和 RabbitMQ 就建立了一條 amqp 信道(channel),信道是建立在“真實” tcp 上的虛拟連接配接,amqp 指令都是通過信道發送出去的,每個信道都會有一個唯一的 id,不論是釋出消息,訂閱隊列都是通過這個信道完成的。

提供了事務的功能。

通過将 channel 設定為 confirm(确認)模式。

把消息持久化磁盤,保證伺服器重新開機消息不丢失。

每個叢集中至少有一個實體磁盤,保證消息落入磁盤。

聲明隊列必須設定持久化 durable 設定為 true.

消息推送投遞模式必須設定持久化,deliveryMode 設定為 2(持久)。

消息已經到達持久化交換器。

消息已經到達持久化隊列。

以上四個條件都滿足才能保證消息持久化成功。

持久化的缺地就是降低了伺服器的吞吐量,因為使用的是磁盤而非記憶體存儲,進而降低了吞吐量。可盡量使用 ssd 硬碟來緩解吞吐量的問題。

direct(預設方式):最基礎最簡單的模式,發送方把消息發送給訂閱方,如果有多個訂閱者,預設采取輪詢的方式進行消息發送。

headers:與 direct 類似,隻是性能很差,此類型幾乎用不到。

fanout:分發模式,把消費分發給所有訂閱者。

topic:比對訂閱模式,使用正則比對到消息隊列,能比對到的都能接收到。

延遲隊列的實作有兩種方式:

通過消息過期後進入死信交換器,再由交換器轉發到延遲消費隊列,實作延遲功能;

使用 RabbitMQ-delayed-message-exchange 插件實作延遲功能。

叢集主要有以下兩個用途:

高可用:某個伺服器出現問題,整個 RabbitMQ 還可以繼續使用;

高容量:叢集可以承載更多的消息量。

磁盤節點:消息會存儲到磁盤。

記憶體節點:消息都存儲在記憶體中,重新開機伺服器消息丢失,性能高于磁盤類型。

各節點之間使用“--link”連接配接,此屬性不能忽略。

各節點使用的 erlang cookie 值必須相同,此值相當于“秘鑰”的功能,用于各節點的認證。

整個叢集中必須包含一個磁盤節點。

不是,原因有以下兩個:

存儲空間的考慮:如果每個節點都擁有所有隊列的完全拷貝,這樣新增節點不但沒有新增存儲空間,反而增加了更多的備援資料;

性能的考慮:如果每條消息都需要完整拷貝到每一個叢集節點,那新增節點并沒有提升處理消息的能力,最多是保持和單節點相同的性能甚至是更糟。

如果唯一磁盤的磁盤節點崩潰了,不能進行以下操作:

不能建立隊列

不能建立交換器

不能建立綁定

不能添加使用者

不能更改權限

不能添加和删除叢集節點

唯一磁盤節點崩潰了,叢集是可以保持運作的,但你不能更改任何東西。

RabbitMQ 對叢集的停止的順序是有要求的,應該先關閉記憶體節點,最後再關閉磁盤節點。如果順序恰好相反的話,可能會造成消息的丢失。

kafka 不能脫離 zookeeper 單獨使用,因為 kafka 使用 zookeeper 管理和協調 kafka 的節點伺服器。

kafka 有兩種資料儲存政策:按照過期時間保留和按照存儲的消息大小保留。

這個時候 kafka 會執行資料清除工作,時間和大小不論那個滿足條件,都會清空資料。

cpu 性能瓶頸

磁盤讀寫瓶頸

網絡瓶頸

叢集的數量不是越多越好,最好不要超過 7 個,因為節點越多,消息複制需要的時間就越長,整個群組的吞吐量就越低。

叢集數量最好是單數,因為超過一半故障叢集就不能用了,設定為單數容錯率更高。

zookeeper 是一個分布式的,開放源碼的分布式應用程式協調服務,是 google chubby 的開源實作,是 hadoop 和 hbase 的重要元件。它是一個為分布式應用提供一緻性服務的軟體,提供的功能包括:配置維護、域名服務、分布式同步、組服務等。

叢集管理:監控節點存活狀态、運作請求等。

主節點選舉:主節點挂掉了之後可以從備用的節點開始新一輪選主,主節點選舉說的就是這個選舉的過程,使用 zookeeper 可以協助完成這個過程。

分布式鎖:zookeeper 提供兩種鎖:獨占鎖、共享鎖。獨占鎖即一次隻能有一個線程使用資源,共享鎖是讀鎖共享,讀寫互斥,即可以有多線線程同時讀同一個資源,如果要使用寫鎖也隻能有一個線程使用。zookeeper可以對分布式鎖進行控制。

命名服務:在分布式系統中,通過使用命名服務,用戶端應用能夠根據指定名字來擷取資源或服務的位址,提供者等資訊。

zookeeper 有三種部署模式:

單機部署:一台叢集上運作;

叢集部署:多台叢集運作;

僞叢集部署:一台叢集啟動多個 zookeeper 執行個體運作。

zookeeper 的核心是原子廣播,這個機制保證了各個 server 之間的同步。實作這個機制的協定叫做 zab 協定。 zab 協定有兩種模式,分别是恢複模式(選主)和廣播模式(同步)。當服務啟動或者在上司者崩潰後,zab 就進入了恢複模式,當上司者被選舉出來,且大多數 server 完成了和 leader 的狀态同步以後,恢複模式就結束了。狀态同步保證了 leader 和 server 具有相同的系統狀态。

在分布式環境中,有些業務邏輯隻需要叢集中的某一台機器進行執行,其他的機器可以共享這個結果,這樣可以大大減少重複計算,提高性能,是以就需要主節點。

可以繼續使用,單數伺服器隻要沒超過一半的伺服器當機就可以繼續使用。

用戶端端會對某個 znode 建立一個 watcher 事件,當該 znode 發生變化時,這些用戶端會收到 zookeeper 的通知,然後用戶端可以根據 znode 變化來做出業務上的改變。

第一範式:強調的是列的原子性,即資料庫表的每一列都是不可分割的原子資料項。

第二範式:要求實體的屬性完全依賴于主關鍵字。所謂完全依賴是指不能存在僅依賴主關鍵字一部分的屬性。

第三範式:任何非主屬性不依賴于其它非主屬性。

表類型如果是 MyISAM ,那 id 就是 8。

表類型如果是 InnoDB,那 id 就是 6。

InnoDB 表隻會把自增主鍵的最大 id 記錄在記憶體中,是以重新開機之後會導緻最大 id 丢失。

使用 select version() 擷取目前 MySQL 資料庫版本。

Atomicity(原子性):一個事務(transaction)中的所有操作,或者全部完成,或者全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被恢複(Rollback)到事務開始前的狀态,就像這個事務從來沒有執行過一樣。即,事務不可分割、不可約簡。

Consistency(一緻性):在事務開始之前和事務結束以後,資料庫的完整性沒有被破壞。這表示寫入的資料必須完全符合所有的預設限制、觸發器、級聯復原等。

Isolation(隔離性):資料庫允許多個并發事務同時對其資料進行讀寫和修改的能力,隔離性可以防止多個事務并發執行時由于交叉執行而導緻資料的不一緻。事務隔離分為不同級别,包括讀未送出(Read uncommitted)、讀送出(read committed)、可重複讀(repeatable read)和串行化(Serializable)。

Durability(持久性):事務處理結束後,對資料的修改就是永久的,即便系統故障也不會丢失。

char(n) :固定長度類型,比如訂閱 char(10),當你輸入"abc"三個字元的時候,它們占的空間還是 10 個位元組,其他 7 個是空位元組。

chat 優點:效率高;缺點:占用空間;适用場景:存儲密碼的 md5 值,固定長度的,使用 char 非常合适。

varchar(n) :可變長度,存儲的值是每個值占用的位元組再加上一個用來記錄其長度的位元組的長度。

是以,從空間上考慮 varcahr 比較合适;從效率上考慮 char 比較合适,二者使用需要權衡。

float 最多可以存儲 8 位的十進制數,并在記憶體中占 4 位元組。

double 最可可以存儲 16 位的十進制數,并在記憶體中占 8 位元組。

内連接配接關鍵字:inner join;左連接配接:left join;右連接配接:right join。

内連接配接是把比對的關聯資料顯示出來;左連接配接是左邊的表全部顯示出來,右邊的表顯示出符合條件的資料;右連接配接正好相反。

索引是滿足某種特定查找算法的資料結構,而這些資料結構會以某種方式指向資料,進而實作高效查找資料。

具體來說 MySQL 中的索引,不同的資料引擎實作有所不同,但目前主流的資料庫引擎的索引都是 B+ 樹實作的,B+ 樹的搜尋效率,可以到達二分法的性能,找到資料區域之後就找到了完整的資料結構了,所有索引的性能也是更好的。

使用 explain 檢視 SQL 是如何執行查詢語句的,進而分析你的索引是否滿足需求。

explain 文法:explain select * from table where type=1。

MySQL 的事務隔離是在 MySQL. ini 配置檔案裡添加的,在檔案的最後添加:

transaction-isolation = REPEATABLE-READ

可用的配置值:READ-UNCOMMITTED、READ-COMMITTED、REPEATABLE-READ、SERIALIZABLE。

READ-UNCOMMITTED:未送出讀,最低隔離級别、事務未送出前,就可被其他事務讀取(會出現幻讀、髒讀、不可重複讀)。

READ-COMMITTED:送出讀,一個事務送出後才能被其他事務讀取到(會造成幻讀、不可重複讀)。

REPEATABLE-READ:可重複讀,預設級别,保證多次讀取同一個資料時,其值都和事務開始時候的内容是一緻,禁止讀取到别的事務未送出的資料(會造成幻讀)。

SERIALIZABLE:序列化,代價最高最可靠的隔離級别,該隔離級别能防止髒讀、不可重複讀、幻讀。

InnoDB 引擎:mysql 5.1 後預設的資料庫引擎,提供了對資料庫 acid 事務的支援,并且還提供了行級鎖和外鍵的限制,它的設計的目标就是處理大資料容量的資料庫系統。MySQL 運作的時候,InnoDB 會在記憶體中建立緩沖池,用于緩沖資料和索引。但是該引擎是不支援全文搜尋,同時啟動也比較的慢,它是不會儲存表的行數的,是以當進行 select count(*) from table 指令的時候,需要進行掃描全表。由于鎖的粒度小,寫操作是不會鎖定全表的,是以在并發度較高的場景下使用會提升效率的。

MyIASM 引擎:不提供事務的支援,也不支援行級鎖和外鍵。是以當執行插入和更新語句時,即執行寫操作的時候需要鎖定這個表,是以會導緻效率會降低。不過和 InnoDB 不同的是,MyIASM 引擎是儲存了表的行數,于是當進行 select count(*) from table 語句時,可以直接的讀取已經儲存的值而不需要進行掃描全表。是以,如果表的讀操作遠遠多于寫操作時,并且不需要事務的支援的,可以将 MyIASM 作為資料庫引擎的首選。

MyISAM 隻支援表鎖,InnoDB 支援表鎖和行鎖,預設為行鎖。

表級鎖:開銷小,加鎖快,不會出現死鎖。鎖定粒度大,發生鎖沖突的機率最高,并發量最低。

行級鎖:開銷大,加鎖慢,會出現死鎖。鎖力度小,發生鎖沖突的機率小,并發度最高。

樂觀鎖:每次去拿資料的時候都認為别人不會修改,是以不會上鎖,但是在送出更新的時候會判斷一下在此期間别人有沒有去更新這個資料。

悲觀鎖:每次去拿資料的時候都認為别人會修改,是以每次在拿資料的時候都會上鎖,這樣别人想拿這個資料就會阻止,直到這個鎖被釋放。

資料庫的樂觀鎖需要自己實作,在表裡面添加一個 version 字段,每次修改成功值加 1,這樣每次修改的時候先對比一下,自己擁有的 version 和資料庫現在的 version 是否一緻,如果不一緻就不修改,這樣就實作了樂觀鎖。

使用 show processlist 指令檢視目前所有連接配接資訊。

使用 explain 指令查詢 SQL 語句執行計劃。

開啟慢查詢日志,檢視慢查詢的 SQL。

為搜尋字段建立索引。

避免使用 select *,列出需要查詢的字段。

垂直分割分表。

選擇正确的存儲引擎。

Redis 是一個使用 C 語言開發的高速緩存資料庫。

Redis 使用場景:

記錄文章點贊數、點選數、評論數;

緩存近期熱帖;

緩存文章詳情資訊;

記錄使用者會話資訊。

資料緩存功能

分布式鎖的功能

支援資料持久化

支援事務

支援消息隊列

存儲方式不同:memcache 把資料全部存在記憶體之中,斷電後會挂掉,資料不能超過記憶體大小;Redis 有部份存在硬碟上,這樣能保證資料的持久性。

資料支援類型:memcache 對資料類型支援相對簡單;Redis 有複雜的資料類型。

使用底層模型不同:它們之間底層實作方式,以及與用戶端之間通信的應用協定不一樣,Redis 自己建構了 vm 機制,因為一般的系統調用系統函數的話,會浪費一定的時間去移動和請求。

value 值大小不同:Redis 最大可以達到 512mb;memcache 隻有 1mb。

因為 cpu 不是 Redis 的瓶頸,Redis 的瓶頸最有可能是機器記憶體或者網絡帶寬。既然單線程容易實作,而且 cpu 又不會成為瓶頸,那就順理成章地采用單線程的方案了。

關于 Redis 的性能,官方網站也有,普通筆記本輕松處理每秒幾十萬的請求。

而且單線程并不代表就慢 nginx 和 nodejs 也都是高性能單線程的代表。

緩存穿透:指查詢一個一定不存在的資料,由于緩存是不命中時需要從資料庫查詢,查不到資料則不寫入緩存,這将導緻這個不存在的資料每次請求都要到資料庫去查詢,造成緩存穿透。

解決方案:最簡單粗暴的方法如果一個查詢傳回的資料為空(不管是資料不存在,還是系統故障),我們就把這個空結果進行緩存,但它的過期時間會很短,最長不超過五分鐘。

Redis 支援的資料類型:string(字元串)、list(清單)、hash(字典)、set(集合)、zset(有序集合)。

支援的 Java 用戶端有 Redisson、jedis、lettuce 等。

jedis:提供了比較全面的 Redis 指令的支援。

Redisson:實作了分布式和可擴充的 Java 資料結構,與 jedis 相比 Redisson 的功能相對簡單,不支援排序、事務、管道、分區等 Redis 特性。

合理設定緩存的過期時間。

新增、更改、删除資料庫操作時同步更新 Redis,可以使用事物機制來保證資料的一緻性。

Redis 的持久化有兩種方式,或者說有兩種政策:

RDB(Redis Database):指定的時間間隔能對你的資料進行快照存儲。

AOF(Append Only File):每一個收到的寫指令都通過write函數追加到檔案中。

Redis 分布式鎖其實就是在系統裡面占一個“坑”,其他程式也要占“坑”的時候,占用成功了就可以繼續執行,失敗了就隻能放棄或稍後重試。

占坑一般使用 setnx(set if not exists)指令,隻允許被一個程式占有,使用完調用 del 釋放鎖。

Redis 分布式鎖不能解決逾時的問題,分布式鎖有一個逾時時間,程式的執行如果超出了鎖的逾時時間就會出現問題。

盡量使用 Redis 的散清單,把相關的資訊放到散清單裡面存儲,而不是把每個字段單獨存儲,這樣可以有效的減少記憶體使用。比如将 Web 系統的使用者對象,應該放到散清單裡面再整體存儲到 Redis,而不是把使用者的姓名、年齡、密碼、郵箱等字段分别設定 key 進行存儲。

volatile-lru:從已設定過期時間的資料集(server. db[i]. expires)中挑選最近最少使用的資料淘汰。

volatile-ttl:從已設定過期時間的資料集(server. db[i]. expires)中挑選将要過期的資料淘汰。

volatile-random:從已設定過期時間的資料集(server. db[i]. expires)中任意選擇資料淘汰。

allkeys-lru:從資料集(server. db[i]. dict)中挑選最近最少使用的資料淘汰。

allkeys-random:從資料集(server. db[i]. dict)中任意選擇資料淘汰。

no-enviction(驅逐):禁止驅逐資料。

主伺服器寫記憶體快照,會阻塞主線程的工作,當快照比較大時對性能影響是非常大的,會間斷性暫停服務,是以主伺服器最好不要寫記憶體快照。

Redis 主從複制的性能問題,為了主從複制的速度和連接配接的穩定性,主從庫最好在同一個區域網路内。

類加載器(ClassLoader)

運作時資料區(Runtime Data Area)

執行引擎(Execution Engine)

本地庫接口(Native Interface)

元件的作用: 首先通過類加載器(ClassLoader)會把 Java 代碼轉換成位元組碼,運作時資料區(Runtime Data Area)再把位元組碼加載到記憶體中,而位元組碼檔案隻是 JVM 的一套指令集規範,并不能直接交給底層作業系統去執行,是以需要特定的指令解析器執行引擎(Execution Engine),将位元組碼翻譯成底層系統指令,再交由 CPU 去執行,而這個過程中需要調用其他語言的本地庫接口(Native Interface)來實作整個程式的功能。

不同虛拟機的運作時資料區可能略微有所不同,但都會遵從 Java 虛拟機規範, Java 虛拟機規範規定的區域分為以下 5 個部分:

程式計數器(Program Counter Register):目前線程所執行的位元組碼的行号訓示器,位元組碼解析器的工作是通過改變這個計數器的值,來選取下一條需要執行的位元組碼指令,分支、循環、跳轉、異常處理、線程恢複等基礎功能,都需要依賴這個計數器來完成;

Java 虛拟機棧(Java Virtual Machine Stacks):用于存儲局部變量表、操作數棧、動态連結、方法出口等資訊;

本地方法棧(Native Method Stack):與虛拟機棧的作用是一樣的,隻不過虛拟機棧是服務 Java 方法的,而本地方法棧是為虛拟機調用 Native 方法服務的;

Java 堆(Java Heap):Java 虛拟機中記憶體最大的一塊,是被所有線程共享的,幾乎所有的對象執行個體都在這裡配置設定記憶體;

方法區(Methed Area):用于存儲已被虛拟機加載的類資訊、常量、靜态變量、即時編譯後的代碼等資料。

功能方面:堆是用來存放對象的,棧是用來執行程式的。

共享性:堆是線程共享的,棧是線程私有的。

空間大小:堆大小遠遠大于棧。

隊列和棧都是被用來預存儲資料的。

隊列允許先進先出檢索元素,但也有例外的情況,Deque 接口允許從兩端檢索元素。

棧和隊列很相似,但它運作對元素進行後進先出進行檢索。

在介紹雙親委派模型之前先說下類加載器。對于任意一個類,都需要由加載它的類加載器和這個類本身一同确立在 JVM 中的唯一性,每一個類加載器,都有一個獨立的類名稱空間。類加載器就是根據指定全限定名稱将 class 檔案加載到 JVM 記憶體,然後再轉化為 class 對象。

類加載器分類:

啟動類加載器(Bootstrap ClassLoader),是虛拟機自身的一部分,用來加載Java_HOME/lib/目錄中的,或者被 -Xbootclasspath 參數所指定的路徑中并且被虛拟機識别的類庫;

其他類加載器:

擴充類加載器(Extension ClassLoader):負責加載&lt;java_home&gt;\lib\ext目錄或Java. ext. dirs系統變量指定的路徑中的所有類庫;

應用程式類加載器(Application ClassLoader)。負責加載使用者類路徑(classpath)上的指定類庫,我們可以直接使用這個類加載器。一般情況,如果我們沒有自定義類加載器預設就是用這個加載器。

雙親委派模型:如果一個類加載器收到了類加載的請求,它首先不會自己去加載這個類,而是把這個請求委派給父類加載器去完成,每一層的類加載器都是如此,這樣所有的加載請求都會被傳送到頂層的啟動類加載器中,隻有當父加載無法完成加載請求(它的搜尋範圍中沒找到所需的類)時,子加載器才會嘗試去加載類。

類裝載分為以下 5 個步驟:

加載:根據查找路徑找到相應的 class 檔案然後導入;

檢查:檢查加載的 class 檔案的正确性;

準備:給類中的靜态變量配置設定記憶體空間;

解析:虛拟機将常量池中的符号引用替換成直接引用的過程。符号引用就了解為一個标示,而在直接引用直接指向記憶體中的位址;

初始化:對靜态變量和靜态代碼塊執行初始化工作。

一般有兩種方法來判斷:

引用計數器:為每個對象建立一個引用計數,有對象引用時計數器 +1,引用被釋放時計數 -1,當計數器為 0 時就可以被回收。它有一個缺點不能解決循環引用的問題;

可達性分析:從 GC Roots 開始向下搜尋,搜尋所走過的路徑稱為引用鍊。當一個對象到 GC Roots 沒有任何引用鍊相連時,則證明此對象是可以被回收的。

強引用:發生 gc 的時候不會被回收。

軟引用:有用但不是必須的對象,在發生記憶體溢出之前會被回收。

弱引用:有用但不是必須的對象,在下一次GC時會被回收。

虛引用(幽靈引用/幻影引用):無法通過虛引用獲得對象,用 PhantomReference 實作虛引用,虛引用的用途是在 gc 時傳回一個通知。

标記-清除算法:标記無用對象,然後進行清除回收。缺點:效率不高,無法清除垃圾碎片。

标記-整理算法:标記無用對象,讓所有存活的對象都向一端移動,然後直接清除掉端邊界以外的記憶體。

複制算法:按照容量劃分二個大小相等的記憶體區域,當一塊用完的時候将活着的對象複制到另一塊上,然後再把已使用的記憶體空間一次清理掉。缺點:記憶體使用率不高,隻有原來的一半。

分代算法:根據對象存活周期的不同将記憶體劃分為幾塊,一般是新生代和老年代,新生代基本采用複制算法,老年代采用标記整理算法。

Serial:最早的單線程串行垃圾回收器。

Serial Old:Serial 垃圾回收器的老年版本,同樣也是單線程的,可以作為 CMS 垃圾回收器的備選預案。

ParNew:是 Serial 的多線程版本。

Parallel 和 ParNew 收集器類似是多線程的,但 Parallel 是吞吐量優先的收集器,可以犧牲等待時間換取系統的吞吐量。

Parallel Old 是 Parallel 老生代版本,Parallel 使用的是複制的記憶體回收算法,Parallel Old 使用的是标記-整理的記憶體回收算法。

CMS:一種以獲得最短停頓時間為目标的收集器,非常适用 B/S 系統。

G1:一種兼顧吞吐量和停頓時間的 GC 實作,是 JDK 9 以後的預設 GC 選項。

CMS 是英文 Concurrent Mark-Sweep 的簡稱,是以犧牲吞吐量為代價來獲得最短回收停頓時間的垃圾回收器。對于要求伺服器響應速度的應用上,這種垃圾回收器非常适合。在啟動 JVM 的參數加上“-XX:+UseConcMarkSweepGC”來指定使用 CMS 垃圾回收器。

CMS 使用的是标記-清除的算法實作的,是以在 gc 的時候回産生大量的記憶體碎片,當剩餘記憶體不能滿足程式運作要求時,系統将會出現 Concurrent Mode Failure,臨時 CMS 會采用 Serial Old 回收器進行垃圾清除,此時的性能将會被降低。

新生代回收器:Serial、ParNew、Parallel Scavenge

老年代回收器:Serial Old、Parallel Old、CMS

整堆回收器:G1

新生代垃圾回收器一般采用的是複制算法,複制算法的優點是效率高,缺點是記憶體使用率低;老年代回收器一般采用的是标記-整理的算法進行垃圾回收。

分代回收器有兩個分區:老生代和新生代,新生代預設的空間占比總空間的 1/3,老生代的預設占比是 2/3。

新生代使用的是複制算法,新生代裡有 3 個分區:Eden、To Survivor、From Survivor,它們的預設占比是 8:1:1,它的執行流程如下:

把 Eden + From Survivor 存活的對象放入 To Survivor 區;

清空 Eden 和 From Survivor 分區;

From Survivor 和 To Survivor 分區交換,From Survivor 變 To Survivor,To Survivor 變 From Survivor。

每次在 From Survivor 到 To Survivor 移動時都存活的對象,年齡就 +1,當年齡到達 15(預設配置是 15)時,更新為老生代。大對象也會直接進入老生代。

老生代當空間占用到達某個值之後就會觸發全局垃圾收回,一般使用标記整理的執行算法。以上這些循環往複就構成了整個分代垃圾回收的整體執行流程。

JDK 自帶了很多監控工具,都位于 JDK 的 bin 目錄下,其中最常用的是 jconsole 和 jvisualvm 這兩款視圖監控工具。

jconsole:用于對 JVM 中的記憶體、線程和類等進行監控;

jvisualvm:JDK 自帶的全能分析工具,可以分析:記憶體快照、線程快照、程式死鎖、監控記憶體的變化、gc 變化等。

-Xms2g:初始化推大小為 2g;

-Xmx2g:堆最大記憶體為 2g;

-XX:NewRatio=4:設定年輕的和老年代的記憶體比例為 1:4;

-XX:SurvivorRatio=8:設定新生代 Eden 和 Survivor 比例為 8:2;

–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器組合;

-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器組合;

-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器組合;

-XX:+PrintGC:開啟列印 gc 資訊;

-XX:+PrintGCDetails:列印 gc 詳細資訊。

Java 最常見 200+ 面試題全解析:面試必備(轉)
Java 最常見 200+ 面試題全解析:面試必備(轉)
Java 最常見 200+ 面試題全解析:面試必備(轉)
Java 最常見 200+ 面試題全解析:面試必備(轉)