hey guys ,這不是也到了面試季了麼,cxuan 又打算從新寫一下 Java 相關的面試題,先從基礎的開始吧,這些面試題屬於基礎系列,不包含多線程相關面試題和 JVM 相關面試題,多線程和 JVM 的我放在後面了,下面很少說,搞起!java
併發性的
: 你能夠在其中執行許多語句,而沒必要一次執行它面向對象的
:基於類和麪向對象的編程語言。獨立性的
: 支持一次編寫,處處運行的獨立編程語言,即編譯後的代碼能夠在支持 Java 的全部平臺上運行。Java 的特性有以下這幾點git
簡單
,Java 會讓你的工做變得更加輕鬆,使你把關注點放在主要業務邏輯上,而沒必要關心指針、運算符重載、內存回收等與主要業務無關的功能。便攜性
,Java 是平臺無關性的,這意味着在一個平臺上編寫的任何應用程序均可以輕鬆移植到另外一個平臺上。安全性
, 編譯後會將全部的代碼轉換爲字節碼,人類沒法讀取。它使開發無病毒,無篡改的系統/應用成爲可能。動態性
,它具備適應不斷變化的環境的能力,它可以支持動態內存分配,從而減小了內存浪費,提升了應用程序的性能。分佈式
,Java 提供的功能有助於建立分佈式應用。使用遠程方法調用(RMI)
,程序能夠經過網絡調用另外一個程序的方法並獲取輸出。您能夠經過從互聯網上的任何計算機上調用方法來訪問文件。這是革命性的一個特色,對於當今的互聯網來講過重要了。健壯性
,Java 有強大的內存管理功能,在編譯和運行時檢查代碼,它有助於消除錯誤。高性能
,Java 最黑的科技就是字節碼編程,Java 代碼編譯成的字節碼能夠輕鬆轉換爲本地機器代碼。經過 JIT 即時編譯器來實現高性能。解釋性
,Java 被編譯成字節碼,由 Java 運行時環境解釋。多線程性
,Java支持多個執行線程(也稱爲輕量級進程),包括一組同步原語。這使得使用線程編程更加容易,Java 經過管程模型來實現線程安全性。面向對象的特徵主要有三點程序員
封裝:封裝是面向對象的特徵之一,是對象和類概念的主要特性。封裝,也就是把客觀事物封裝成抽象的類,而且類能夠把本身的數據和方法只讓可信的類或者對象操做,對不可信的進行信息隱藏。github
繼承:繼承指的是使用現有類的全部功能,並在無需從新編寫原來的類的狀況下對這些功能進行擴展。面試
多態:多態是容許你將父對象設置成爲和一個或更多的他的子對象相等的技術,賦值以後,父對象就能夠根據當前賦值給它的子對象的特性以不一樣的方式運做。簡單的說,就是一句話:容許將子類類型的指針賦值給父類類型的指針。算法
這裏還須要解釋一下 JVM 是什麼編程
大體來講,JRE、JDK 和 JVM 的關係以下設計模式
要想真正理解的話,能夠參考這篇文章 : https://www.zhihu.com/question/31203609數組
簡單理解的話就是緩存
值傳遞
是指在調用函數時將實際參數複製一份到函數中,這樣的話若是函數對其傳遞過來的形式參數進行修改,將不會影響到實際參數
引用傳遞
是指在調用函數時將對象的地址直接傳遞到函數中,若是在對形式參數進行修改,將影響到實際參數的值。
==
是 Java 中一種操做符,它有兩種比較方式
基本數據類型
來講, == 判斷的是兩邊的值
是否相等public class DoubleCompareAndEquals { Person person1 = new Person(24,"boy"); Person person2 = new Person(24,"girl"); int c = 10; private void doubleCompare(){ int a = 10; int b = 10; System.out.println(a == b); System.out.println(a == c); System.out.println(person1.getId() == person2.getId()); } }
引用類型
來講, == 判斷的是兩邊的引用
是否相等,也就是判斷兩個對象是否指向了同一塊內存區域。private void equals(){ System.out.println(person1.getName().equals(person2.getName())); }
equals
是 Java 中全部對象的父類,即 Object
類定義的一個方法。它只能比較對象,它表示的是引用雙方的值是否相等。因此記住,並非說 == 比較的就是引用是否相等,equals 比較的就是值,這須要區分來講的。
equals 用做對象之間的比較具備以下特性
自反性
:對於任何非空引用 x 來講,x.equals(x) 應該返回 true。對稱性
:對於任何非空引用 x 和 y 來講,若x.equals(y)爲 true,則y.equals(x)也爲 true。傳遞性
:對於任何非空引用的值來講,有三個值,x、y 和 z,若是x.equals(y) 返回true,y.equals(z) 返回true,那麼x.equals(z) 也應該返回true。一致性
:對於任何非空引用 x 和 y 來講,若是 x.equals(y) 相等的話,那麼它們必須始終相等。非空性
:對於任何非空引用的值 x 來講,x.equals(null) 必須返回 false。在 Java 中,數據類型只有四類八種
byte 也就是字節,1 byte = 8 bits,byte 的默認值是 0 ;
short 佔用兩個字節,也就是 16 位,1 short = 16 bits,它的默認值也是 0 ;
int 佔用四個字節,也就是 32 位,1 int = 32 bits,默認值是 0 ;
long 佔用八個字節,也就是 64 位,1 long = 64 bits,默認值是 0L;
因此整數型的佔用字節大小空間爲 long > int > short > byte
浮點型有兩種數據類型:float 和 double
float 是單精度浮點型,佔用 4 位,1 float = 32 bits,默認值是 0.0f;
double 是雙精度浮點型,佔用 8 位,1 double = 64 bits,默認值是 0.0d;
字符型就是 char,char 類型是一個單一的 16 位 Unicode 字符,最小值是 \u0000 (也就是 0 )
,最大值是 \uffff (即爲 65535)
,char 數據類型能夠存儲任何字符,例如 char a = 'A'。
布爾型指的就是 boolean,boolean 只有兩種值,true 或者是 false,只表示 1 位,默認值是 false。
以上 x 位
都指的是在內存中的佔用。
String 表明的是 Java 中的字符串
,String 類比較特殊,它整個類都是被 final
修飾的,也就是說,String 不能被任何類繼承,任何 修改
String 字符串的方法都是建立了一個新的字符串。
equals 方法是 Object 類定義的方法,Object 是全部類的父類,固然也包括 String,String 重寫了 equals
方法,下面咱們來看看是怎麼重寫的
引用
是否相等。若是引用相等的話,直接返回 true ,不相等的話繼續下面的判斷字符
是否相等,一旦有一個字符不相等,就會直接返回 false。下面是它的流程圖
這裏再提示一下,你可能有疑惑何時是
if (this == anObject) { return true; }
這個判斷語句如何才能返回 true?由於都是字符串啊,字符串比較的不都是堆空間嗎,猛然一看發現好像永遠也不會走,可是你忘記了 String.intern()
方法,它表示的概念在不一樣的 JDK 版本有不一樣的區分
在 JDK1.7 及之後調用 intern 方法是判斷運行時常量池中是否有指定的字符串,若是沒有的話,就把字符串添加到常量池中,並返回常量池中的對象。
驗證過程以下
private void StringOverrideEquals(){ String s1 = "aaa"; String s2 = "aa" + new String("a"); String s3 = new String("aaa"); System.out.println(s1.intern().equals(s1)); System.out.println(s1.intern().equals(s2)); System.out.println(s3.intern().equals(s1)); }
首先 s1.intern.equals(s1) 這個不管如何都返回 true,由於 s1 字符串建立出來就已經在常量池中存在了。
而後第二條語句返回 false,由於 s1 返回的是常量池中的對象,而 s2 返回的是堆中的對象
第三條語句 s3.intern.equals(s1),返回 true ,由於 s3 對象雖然在堆中建立了一個對象,可是 s3 中的 "aaa" 返回的是常量池中的對象。
equals 方法和 hashCode 都是 Object 中定義的方法,它們常常被一塊兒重寫。
equals 方法是用來比較對象大小是否相等的方法,hashcode 方法是用來判斷每一個對象 hash 值的一種方法。若是隻重寫 equals 方法而不重寫 hashcode 方法,極可能會形成兩個不一樣的對象,它們的 hashcode 也相等,形成衝突。好比
String str1 = "通話"; String str2 = "重地";
它們兩個的 hashcode 相等,可是 equals 可不相等。
咱們來看一下 hashCode 官方的定義
總結起來就是
這個確定是不必定的,舉個很是簡單的例子,你重寫了 hashcode 方法,來算取餘數,那麼兩個對象的 hashcode 極可能重複,可是兩個對象的 equals 卻不必定相同。
就算你不重寫 hashcode 方法,我給你一段代碼示例
String str1 = "通話"; String str2 = "重地"; System. out. println(String. format("str1:%d | str2:%d", str1. hashCode(),str2. hashCode())); System. out. println(str1. equals(str2));
上面兩段代碼的輸出結果是
str1:1179395 | str2:1179395
false
這兩個字符串的 equals 並不相同。也就是說,就算是 hashcode 相同的字符串,equals 也有可能不一樣。
一個或者兩個,String s1 是聲明瞭一個 String 類型的 s1 變量,它不是對象。使用 new
關鍵字會在堆中建立一個對象,另一個對象是 abc
,它會在常量池中建立,因此一共建立了兩個對象;若是 abc 在常量池中已經存在的話,那麼就會建立一個對象。
詳細請翻閱筆者的另一篇文章 一篇不同凡響的 String、StringBuffer、StringBuilder 詳解
首先了解一下什麼是不可變對象
,不可變對象就是一經建立後,其對象的內部狀態不能被修改,啥意思呢?也就是說不可變對象須要遵照下面幾條原則
與其說問 String 爲何是不可變的,不如說如何把 String 設計成不可變的。
String 類是一種對象,它是獨立於 Java 基本數據類型而存在的,String 你能夠把它理解爲字符串的集合,String 被設計爲 final 的,表示 String 對象一經建立後,它的值就不能再被修改,任何對 String 值進行修改的方法就是從新建立一個字符串。String 對象建立後會存在於運行時常量池中,運行時常量池是屬於方法區的一部分,JDK1.7 後把它移到了堆中。
不可變對象不是真的不可變,能夠經過反射
來對其內部的屬性和值進行修改,不過通常咱們不這樣作。
static 是 Java 中很是重要的關鍵字,static 表示的概念是 靜態的
,在 Java 中,static 主要用來
靜態變量
、也稱爲類變量
,類變量屬於類全部,對於不一樣的類來講,static 變量只有一份,static 修飾的變量位於方法區中;static 修飾的變量可以直接經過 類名.變量名 來進行訪問,不用經過實例化類再進行使用。靜態方法
,靜態方法可以直接經過 類名.方法名 來使用,在靜態方法內部不能使用非靜態屬性和方法static{}
,這種被稱爲靜態代碼塊
,一種是在類中定義靜態內部類
,使用 static class xxx
來進行定義。import static xxx
來實現,這種方式通常不推薦使用深刻理解請參考這篇文章 一個小小的 static 還能可貴住我?
final 是 Java 中的關鍵字,它表示的意思是 不可變的
,在 Java 中,final 主要用來
extends
來繼承被 final 修飾的類。不可變
的效果,因此,能夠用來保護只讀數據,尤爲是在併發編程中,由於明確的不能再爲 final 變量進行賦值,有利於減小額外的同步開銷。抽象類和接口都是 Java 中的關鍵字,抽象類和接口中都容許進行方法的定義,而不用具體的方法實現。抽象類和接口都容許被繼承,它們普遍的應用於 JDK 和框架的源碼中,來實現多態和不一樣的設計模式。
不一樣點在於
抽象級別不一樣
:類、抽象類、接口實際上是三種不一樣的抽象級別,抽象程度依次是 接口 > 抽象類 > 類。在接口中,只容許進行方法的定義,不容許有方法的實現,抽象類中能夠進行方法的定義和實現;而類中只容許進行方法的實現,我說的方法的定義是不容許在方法後面出現 {}
使用的關鍵字不一樣
:類使用 class
來表示;抽象類使用 abstract class
來表示;接口使用 interface
來表示變量
:接口中定義的變量只能是公共的靜態常量,抽象類中的變量是普通變量。在 Java 中,重寫和重載都是對同一方法的不一樣表現形式,下面咱們針對重寫和重載作一下簡單的區分
子父級關係不一樣
,重寫是針對子級和父級的不一樣表現形式,而重載是在同一類中的不一樣表現形式;概念不一樣
,子類重寫父類的方法通常使用 @override
來表示;重寫後的方法其方法的聲明和參數類型、順序必需要與父類徹底一致;重載是針對同一類中概念,它要求重載的方法必須知足下面任何一個要求:方法參數的順序,參數的個數,參數的類型任意一個保持不一樣便可。這道題考到你對於構造器的理解和認識。
咱們 Java 中建立一個對象其實就是調用了該對象的構造方法,好比下面代碼
InstanceObject IO = new InstanceObject() ; // 調用了無參構造方法 InstanceObject IO = new InstanceObject(xxx) ; // 調用了有參數的構造方法
而重載的概念是什麼呢?
它是指咱們能夠定義一些名稱相同的方法,經過定義不一樣的輸入參數來區分這些方法,而後再調用時,JVM 就會根據不一樣的參數樣式,來選擇合適的方法執行。
也就是說,重載的概念更多描述的是對相同
命名的方法的不一樣描述。那麼咱們上面這段代碼很顯然就是重載了,由於名稱相同,我能夠經過有無參數來判斷調用不一樣的構造方法來進行初始化。
那麼構造器可否被重寫呢?這裏咱們先來看一下什麼是重寫
重寫是子類對父類的容許訪問的方法的實現過程進行從新編寫, 返回值和形參都不能改變。從重寫的概念定義來講咱們就知道構造器不能被重寫了。
首先,構造器沒有返回值,第二點,構造器的名稱必須和類名一致。
你總不能在類 A 中寫了 public A();在類 B 中也寫 public A() 吧,這顯然是不能經過編譯的。
byte 的取值範圍是 -128 -> 127 之間,一共是 256 個。一個 byte 類型在計算機中佔據一個字節,那麼就是 8 bit,因此最大就是 2^7 = 1111 1111。
Java 中用補碼
來表示二進制數,補碼的最高位是符號位,最高位用 0 表示正數,最高位 1 表示負數,正數的補碼就是其自己
,因爲最高位是符號位,因此正數表示的就是 0111 1111 ,也就是 127。最大負數就是 1111 1111,這其中會涉及到兩個 0 ,一個 +0 ,一個 -0 ,+0 歸爲正數,也就是 0 ,-0 歸爲負數,也就是 -128,因此 byte 的範圍就是 -128 - 127。
相同點
HashMap 和 HashTable 都是基於哈希表實現的,其內部每一個元素都是 key-value
鍵值對,HashMap 和 HashTable 都實現了 Map、Cloneable、Serializable 接口。
不一樣點
父類不一樣:HashMap 繼承了 AbstractMap
類,而 HashTable 繼承了 Dictionary
類
空值不一樣:HashMap 容許空的 key 和 value 值,HashTable 不容許空的 key 和 value 值。HashMap 會把 Null key 當作普通的 key 對待。不容許 null key 重複。
Collections.synchronizedMap
或者是 ConcurrentHashMap
。而 HashTable 自己就是線程安全的容器。單鏈表
的,可是 HashMap 進行 put 或者 get???? 操做,能夠達到常數時間的性能;而 HashTable 的 put 和 get 操做都是加了 synchronized
鎖的,因此效率不好。HashSet 繼承於 AbstractSet 接口,實現了 Set、Cloneable,、java.io.Serializable 接口。HashSet 不容許集合中出現重複的值。HashSet 底層其實就是 HashMap,全部對 HashSet 的操做其實就是對 HashMap 的操做。因此 HashSet 也不保證集合的順序,也不是線程安全的容器。
JDK1.7 中,HashMap 採用位桶 + 鏈表
的實現,即便用鏈表
來處理衝突,同一 hash 值的鏈表都存儲在一個數組中。可是當位於一個桶中的元素較多,即 hash 值相等的元素較多時,經過 key 值依次查找的效率較低。
因此,與 JDK 1.7 相比,JDK 1.8 在底層結構方面作了一些改變,當每一個桶中元素大於 8 的時候,會轉變爲紅黑樹,目的就是優化查詢效率。
這道題我想了幾天,以前和羣裏小夥伴們探討每日一題的時候,問他們爲何 length%hash == (n - 1) & hash,它們說相等的前提是 length 的長度 2 的冪次方,而後我回了一句難道 length 還能不是 2 的冪次方嗎?實際上是我沒有搞懂因果關係,由於 HashMap 的長度是 2 的冪次方,因此使用餘數來判斷在桶中的下標。若是 length 的長度不是 2 的冪次方,小夥伴們能夠舉個例子來試試
例如長度爲 9 時候,3 & (9-1) = 0,2 & (9-1) = 0 ,都在 0 上,碰撞了;
這樣會增大 HashMap 碰撞的概率。
HashMap 不是一個線程安全的容器,在高併發場景下,應該使用 ConcurrentHashMap
,在多線程場景下使用 HashMap 會形成死循環問題(基於 JDK1.7),出現問題的位置在 rehash
處,也就是
do { Entry<K,V> next = e.next; // <--假設線程一執行到這裏就被調度掛起了 int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null);
這是 JDK1.7 的 rehash 代碼片斷,在併發的場景下會造成環。
JDK1.8 也會形成死循環問題。
由於 HashMap 不是一個線程安全的容器,因此併發場景下推薦使用 ConcurrentHashMap
,或者使用線程安全的 HashMap,使用 Collections
包下的線程安全的容器,好比說
Collections.synchronizedMap(new HashMap());
還可使用 HashTable ,它也是線程安全的容器,基於 key-value 存儲,常常用 HashMap 和 HashTable 作比較就是由於 HashTable 的數據結構和 HashMap 相同。
上面效率最高的就是 ConcurrentHashMap。
首先會使用 hash 函數來計算 key,而後執行真正的插入方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 若是table 爲null 或者沒有爲table分配內存,就resize一次 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 指定hash值節點爲空則直接插入,這個(n - 1) & hash纔是表中真正的哈希 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // 若是不爲空 else { Node<K,V> e; K k; // 計算表中的這個真正的哈希值與要插入的key.hash相比 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 若不一樣的話,而且當前節點已經在 TreeNode 上了 else if (p instanceof TreeNode) // 採用紅黑樹存儲方式 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // key.hash 不一樣而且也再也不 TreeNode 上,在鏈表上找到 p.next==null else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { // 在表尾插入 p.next = newNode(hash, key, value, null); // 新增節點後若是節點個數到達閾值,則進入 treeifyBin() 進行再次判斷 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } // 若是找到了同hash、key的節點,那麼直接退出循環 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; // 更新 p 指向下一節點 p = e; } } // map中含有舊值,返回舊值 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } // map調整次數 + 1 ++modCount; // 鍵值對的數量達到閾值,須要擴容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
HashMap put 方法的核心就是在 putval
方法,它的插入過程以下
關於 HashMap 的深刻理解請參考這篇文章 看完這篇 HashMap ,和麪試官扯皮就沒問題了
ConcurrentHashMap 是線程安全的 Map,它也是高併發場景下的首選數據結構,ConcurrentHashMap 底層是使用分段鎖
來實現的。
Integer 緩存池也就是 IntegerCache
,它是 Integer 的靜態內部類。
它的默認值用於緩存 -128 - 127 之間的數字,若是有 -128 - 127 之間的數字的話,使用 new Integer 不用建立對象,會直接從緩存池中取,此操做會減小堆中對象的分配,有利於提升程序的運行效率。
例如建立一個 Integer a = 24,實際上是調用 Integer 的 valueOf
,能夠經過反編譯得出這個結論
而後咱們看一下 valueOf 方法
若是在指定緩存池範圍內的話,會直接返回緩存的值而不用建立新的 Integer 對象。
緩存的大小可使用 XX:AutoBoxCacheMax
來指定,在 VM 初始化時,java.lang.Integer.IntegerCache.high
屬性會設置和保存在 sun.misc.VM
的私有系統屬性中。
因爲每一個國家都有本身獨有的字符編碼,因此Unicode 的發展旨在建立一個新的標準,用來映射當今使用的大多數語言中的字符,這些字符有一些不是必要的,可是對於建立文原本說倒是不可或缺的。Unicode 統一了全部字符的編碼,是一個 Character Set,也就是字符集,字符集只是給全部的字符一個惟一編號,可是卻沒有規定如何存儲,不一樣的字符其存儲空間不同,有的須要一個字節就能存儲,有的則須要二、三、4個字節。
UTF-8 只是衆多可以對文本字符進行解碼
的一種方式,它是一種變長的方式。UTF-8 表明 8 位一組表示 Unicode 字符的格式,使用 1 - 4 個字節來表示字符。
U+ 0000 ~ U+ 007F: 0XXXXXXX U+ 0080 ~ U+ 07FF: 110XXXXX 10XXXXXX U+ 0800 ~ U+ FFFF: 1110XXXX 10XXXXXX 10XXXXXX U+10000 ~ U+1FFFF: 11110XXX 10XXXXXX 10XXXXXX 10XXXXXX
能夠看到,UTF-8 經過開頭的標誌位位數實現了變長。對於單字節字符,只佔用一個字節,實現了向下兼容 ASCII,而且能和 UTF-32 同樣,包含 Unicode 中的全部字符,又能有效減小存儲傳輸過程當中佔用的空間。
能夠,由於 Unicode 編碼採用 2 個字節的編碼,UTF-8 是 Unicode 的一種實現,它使用可變長度的字符集進行編碼,char c = '中' 是兩個字節,因此可以存儲。合法。
Arrays.asList
是 Array 中的一個靜態方法,它可以實現把數組轉換成爲 List 序列,須要注意下面幾點
public static void main(String[] args) { Integer[] integer = new Integer[] { 1, 2, 3, 4 }; List integetList = Arrays.asList(integer); integetList.add(5); }
結果會直接拋出
Exception in thread "main" java.lang.UnsupportedOperationException
咱們看一下源碼就能發現問題
// 這是 java.util.Arrays 的內部類,而不是 java.util.ArrayList private static class ArrayList<E> extends AbstractList<E> implements RandomAccess, java.io.Serializable
繼承 AbstractList 中對 add、remove、set 方法是直接拋異常的,也就是說若是繼承的子類沒有去重寫這些方法,那麼子類的實例去調用這些方法是會直接拋異常的。
下面是AbstractList中方法的定義,咱們能夠看到具體拋出的異常:
public void add(int index, E element) { throw new UnsupportedOperationException(); } public E remove(int index) { throw new UnsupportedOperationException(); } public E set(int index, E element) { throw new UnsupportedOperationException(); }
雖然 set 方法也拋出了一場,可是因爲 內部類 ArrayList 重寫了 set 方法,因此支持其能夠對元素進行修改。
Java 中的基礎數據類型(byte,short,int,long,float,double,boolean)是不支持使用 Arrays.asList 方法去轉換的
Collection 和 Collections 都是位於 java.util
包下的類
Collection 是集合類的父類,它是一個頂級接口,大部分抽象類好比說 AbstractList
、AbstractSet
都繼承了 Collection 類,Collection 類只定義一節標準方法好比說 add、remove、set、equals 等,具體的方法由抽象類或者實現類去實現。
Collections 是集合類的工具類,Collections 提供了一些工具類的基本使用
Collections.synchronizedList
、 Collections.synchronizedMap
等有不少用法,讀者能夠翻閱 Collections 的源碼查看,Collections 不能進行實例化,因此 Collections 中的方法都是由 Collections.方法
直接調用。
fail-fast
是 Java 中的一種快速失敗
機制,java.util 包下全部的集合都是快速失敗的,快速失敗會拋出 ConcurrentModificationException
異常,fail-fast 你能夠把它理解爲一種快速檢測機制,它只能用來檢測錯誤,不會對錯誤進行恢復,fail-fast 不必定只在多線程
環境下存在,ArrayList 也會拋出這個異常,主要緣由是因爲 modCount 不等於 expectedModCount。
fail-safe
是 Java 中的一種 安全失敗
機制,它表示的是在遍歷時不是直接在原集合上進行訪問,而是先複製原有集合內容,在拷貝的集合上進行遍歷。 因爲迭代時是對原集合的拷貝進行遍歷,因此在遍歷過程當中對原集合所做的修改並不能被迭代器檢測到,因此不會觸發 ConcurrentModificationException。java.util.concurrent
包下的容器都是安全失敗的,能夠在多線程條件下使用,併發修改。
這也是一道老生常談的問題了
ArrayList、LinkedList、Vector 都是位於 java.util
包下的工具類,它們都實現了 List 接口。
Collections.synchronizedList
;ArrayList 在擴容時會增長 50% 的容量。Collections.synchronizedList
synchronized
鎖,因此它的增刪、遍歷效率都很低。Vector 在擴容時,它的容量會增長一倍。Exception 泛指的是 異常
,Exception 主要分爲兩種異常,一種是編譯期出現的異常,稱爲 checkedException
,一種是程序運行期間出現的異常,稱爲 uncheckedException
,常見的 checkedException 有 IOException
,uncheckedException 統稱爲 RuntimeException
,常見的 RuntimeException 主要有NullPointerException
、 IllegalArgumentException
、ArrayIndexOutofBoundException
等,Exception 能夠被捕獲。
Error 是指程序運行過程當中出現的錯誤,一般狀況下會形成程序的崩潰,Error 一般是不可恢復的,Error 不能被捕獲。
詳細能夠參考這篇文章 看完這篇 Exception 和 Error ,和麪試官扯皮就沒問題了
String 特指的是 Java 中的字符串,String 類位於 java.lang
包下,String 類是由 final 修飾的,String 字符串一旦建立就不能被修改,任何對 String 進行修改的操做都至關於從新建立了一個字符串。String 字符串的底層使用 StringBuilder 來實現的
StringBuilder 位於 java.util
包下,StringBuilder 是一非線程安全的容器,StringBuilder 的 append 方法經常使用於字符串拼接,它的拼接效率要比 String 中 +
號的拼接效率高。StringBuilder 通常不用於併發環境
StringBuffer 位於 java.util
包下,StringBuffer 是一個線程安全的容器,多線程場景下通常使用 StringBuffer 用做字符串的拼接
StringBuilder 和 StringBuffer 都是繼承於AbstractStringBuilder 類,AbstractStringBuilder 類實現了 StringBuffer 和 StringBuilder 的常規操做。
深刻理解能夠參考一下這篇文章 深刻理解 String、StringBuilder 和 StringBuffer
代理通常分爲靜態代理
和 動態代理
,它們都是代理模式的一種應用,靜態代理指的是在程序運行前已經編譯好,程序知道由誰來執行代理方法。
而動態代理只有在程序運行期間才能肯定,相比於靜態代理, 動態代理的優點在於能夠很方便的對代理類的函數進行統一的處理,而不用修改每一個代理類中的方法。能夠說動態代理是基於 反射
實現的。經過反射咱們能夠直接操做類或者對象,好比獲取類的定義,獲取聲明的屬性和方法,調用方法,在運行時能夠修改類的定義。
動態代理是一種在運行時構建代理、動態處理方法調用的機制。動態代理的實現方式有不少,Java 提供的代理被稱爲 JDK 動態代理
,JDK 動態代理是基於類的繼承。
理解動態代理能夠參考這篇文章 動態代理深刻理解
動態代理的實現方式我給你羅列四種,分別是
Javassist
是在 Java 中編輯字節碼的類庫;它使 Java 程序可以在運行時定義一個新類, 並在 JVM 加載時修改類文件。關於這四種字節碼的實現能夠參考 動態代理居然如此簡單!
Serialization 表示的是序列化,實現序列化和反序列化的對象必須實現 serializable
接口,序列化是將對象變成字節流,存儲到磁盤或網絡中。反序列化是序列化的反過程。
我在學到序列化的時候就常常有個疑問,爲何對象須要序列化呢?
爲了數據傳輸的目的,由於這個對象在你代碼中是對象格式,可是它在傳輸的過程當中可不是對象格式,因此,當其餘進程或者其餘機器想要和你通訊時,大家雙方會發送各類類型的數據,也有可能會把這個對象發送過去,因此這個對象勢必會轉換爲可以在網絡中或者磁盤中可以識別的格式,也就是二進制。
這也就是說,發送方須要把這個 Java 對象轉換爲字節序列,才能在網絡上傳送;接收方則須要把字節序列再恢復爲 Java 對象,這也是 Deserialization 接口所作的工做。
int 和 Integer 區別可就太多了
整型
,一個 int 佔 4 字節,也就是 32 位,int 的初始值是默認值是 0 ,int 在 Java 內存模型中被分配在棧中,int 沒有方法。裝箱
,Integer -> int 的過程稱爲 拆箱
,Integer 還有 IntegerCache ,會自動緩存 -128 - 127 中的值深刻理解拆箱和裝箱能夠參考這篇文章 詳解 Java 中的自動裝箱與拆箱
Java I/O 方式有不少種,傳統的 I/O 也稱爲 BIO
,主要流有以下幾種。
Java I/O 包的實現比較簡單,可是容易出現性能瓶頸,傳統的 I/O 是基於同步阻塞的。
JDK 1.4 以後提供了 NIO
,也就是位於 java.nio
包下,提供了基於 channel、Selector、Buffer的抽象,能夠構建多路複用、同步非阻塞 I/O 程序。
JDK 1.7 以後對 NIO 進行了進一步改進,引入了 異步非阻塞
的方式,也被稱爲 AIO(Asynchronous IO)
。能夠用生活中的例子來講明:項目經理交給手下員工去改一個 bug,那麼項目經理不會一直等待員工解決 bug,他確定在員工解決 bug 的期間給其餘手下分配 bug 或者作其餘事情,員工解決完 bug 以後再告訴項目經理 bug 解決完了。
深刻理解 IO 能夠參考這篇文章 深刻理解 IO 。
一張思惟導圖鎮場
好比全局惟一性能夠用 單例模式
。
可使用 策略模式
優化過多的 if...else...
制定標準用 模版模式
接手其餘人的鍋,但不想改原來的類用 適配器模式
使用 組合
而不是繼承
使用 裝飾器
能夠製做加糖、加奶酪的咖啡
代理
能夠用於任何中間商......
這也是一道老生常談的面試題了,通常不會讓你手寫單例模式。就算是手寫的話,只須要寫出關鍵代碼來就能夠了。這道題其實想問的是單例模式的幾種實現方式,而且每種實現方式有沒有什麼問題。
通常能夠這樣回答下
若是想要深刻了解這道面試題,能夠查閱 我向面試官講解了單例模式,他對我豎起了大拇指
Comparable 更像是天然排序
Comparator 更像是定製排序
同時存在時採用 Comparator(定製排序)的規則進行比較。
對於一些普通的數據類型(好比 String, Integer, Double…),它們默認實現了Comparable 接口,實現了 compareTo 方法,咱們能夠直接使用。
而對於一些自定義類,它們可能在不一樣狀況下須要實現不一樣的比較策略,咱們能夠新建立 Comparator 接口,而後使用特定的 Comparator 實現進行比較。
關於 Comparator 和 Comparable 的深刻理解,能夠參考這篇文章 Comparable 和 Comparator的理解。
Object 類是全部對象的父類,它裏面包含一些全部對象都可以使用的方法
反射機制就是使 Java 程序在運行時具備自省(introspect)
的能力,經過反射咱們能夠直接操做類和對象,好比獲取某個類的定義,獲取類的屬性和方法,構造方法等。
建立類實例的三種方式是
深刻理解反射的文章,請查閱 學會反射後,我被錄取了!(乾貨)
咱們說的不一樣的引用類型其實都是邏輯上的,而對於虛擬機來講,主要體現的是對象的不一樣的可達性(reachable)
狀態和對垃圾收集(garbage collector)
的影響。
能夠經過下面的流程來對對象的生命週期作一個總結
對象被建立並初始化,對象在運行時被使用,而後離開對象的做用域,對象會變成不可達並會被垃圾收集器回收。圖中用紅色標明的區域表示對象處於強可達階段。
JDK1.2 介紹了 java.lang.ref
包,對象的生命週期有四個階段:????強可達????(Strongly Reachable????)
、軟可達(Soft Reachable????)
、弱可達(Weak Reachable????)
、 幻象可達(Phantom Reachable????)
。
若是隻討論符合垃圾回收條件的對象,那麼只有三種:軟可達、弱可達和幻象可達。
軟可達:軟可達就是????咱們只能經過軟引用????才能訪問的狀態,軟可達的對象是由 SoftReference
引用的對象,而且沒有強引用的對象。軟引用是用來描述一些還有用可是非必須的對象。垃圾收集器會盡量長時間的保留軟引用的對象,可是會在發生 OutOfMemoryError
以前,回收軟引用的對象。若是回收完軟引用的對象,內存仍是不夠分配的話,就會直接拋出 OutOfMemoryError。
弱可達:弱可達的對象是 WeakReference
引用的對象。垃圾收集器能夠隨時收集弱引用的對象,不會嘗試保留軟引用的對象。
幻象可達:幻象可達是由 PhantomReference
引用的對象,幻象可達就是沒有強、軟、弱引用進行關聯,而且已經被 finalize 過了,只有幻象引用指向這個對象的時候。
除此以外,還有強可達和不可達的兩種可達性判斷條件
不可達(unreachable)
:處於不可達的對象就意味着對象能夠被清除了。下面是一個不一樣可達性狀態的轉換圖
判斷可達性條件,也是 JVM 垃圾收集器決定如何處理對象的一部分考慮因素。
全部的對象可達性引用都是 java.lang.ref.Reference
的子類,它裏面有一個get()
方法,返回引用對象。 若是已經過程序或垃圾收集器清除了此引用對象,則此方法返回 null 。也就是說,除了幻象引用外,軟引用和弱引用都是能夠獲得對象的。並且這些對象能夠人爲拯救
,變爲強引用,例如把 this 關鍵字賦值給對象,只要從新和引用鏈上的任意一個對象創建關聯便可。
深刻理解引用問題,能夠看看這一篇文章 深刻理解各類引用問題。
這三者能夠說是沒有任何關聯之處,咱們上面談到了,final 能夠用來修飾類、變量和方法,能夠參考上面 final 的那道面試題。
finally 是一個關鍵字,它常常和 try 塊一塊兒使用,用於異常處理。使用 try...finally 的代碼塊種,finally 部分的代碼必定會被執行,因此咱們常常在 finally 方法中用於資源的關閉操做。
JDK1.7 中,推薦使用 try-with-resources
優雅的關閉資源,它直接使用 try(){} 進行資源的關閉便可,就不用寫 finally 關鍵字了。
finalize 是 Object 對象中的一個方法,用於對象的回收方法,這個方法咱們通常不推薦使用,finalize 是和垃圾回收關聯在一塊兒的,在 Java 9 中,將 finalize 標記爲了 deprecated
, 若是沒有特別緣由,不要實現 finalize 方法,也不要期望他來進行垃圾回收。
深刻理解 final、finally 和 finalize ,能夠看看這一篇 看完這篇 final、finally 和 finalize 和麪試官扯皮就沒問題了。
在 Java 中,能夠將一個類的定義放在另一個類的定義內部,這就是內部類。內部類自己就是類的一個屬性,與其餘屬性定義方式一致。
內部類的分類通常主要有四種
靜態內部類
就是定義在類內部的靜態類,靜態內部類能夠訪問外部類全部的靜態變量,而不可訪問外部類的非靜態變量;
成員內部類
就是定義在類內部,成員位置上的非靜態類,就是成員內部類。成員內部類能夠訪問外部類全部的變量和方法,包括靜態和非靜態,私有和公有。
定義在方法中的內部類,就是局部內部類
。定義在實例方法中的局部類能夠訪問外部類的全部變量和方法,定義在靜態方法中的局部類只能訪問外部類的靜態變量和方法。
匿名內部類
就是沒有名字的內部類,除了沒有名字,匿名內部類還有如下特色:
一個Java 程序要通過編寫、編譯、運行三個步驟,其中編寫代碼不在咱們討論的範圍以內,那麼咱們的重點天然就放在了編譯
和 運行
這兩個階段,因爲編譯和運行階段過程至關繁瑣,下面就個人理解來進行解釋:
Java 程序從源文件建立到程序運行要通過兩大步驟:
一、編譯時期是由編譯器將源文件編譯成字節碼的過程
二、字節碼文件由Java虛擬機解釋執行
綁定就是一個方法的調用與調用這個方法的類鏈接在一塊兒的過程被稱爲綁定。
綁定主要分爲兩種:
靜態綁定 和 動態綁定
綁定的其餘叫法
靜態綁定 == 前期綁定 == 編譯時綁定
動態綁定 == 後期綁定 == 運行時綁定
爲了方便區分: 下面統一稱呼爲靜態綁定和動態綁定
在程序運行前,也就是編譯時期 JVM 就可以肯定方法由誰調用,這種機制稱爲靜態綁定
識別靜態綁定的三個關鍵字以及各自的理解
若是一個方法由 private、static、final 任意一個關鍵字所修飾,那麼這個方法是前期綁定的
構造方法也是前期綁定
private:private 關鍵字是私有的意思,若是被 private 修飾的方法是沒法由本類以外的其餘類所調用的,也就是本類所特有的方法,因此也就由編譯器識別此方法是屬於哪一個類的
public class Person { private String talk; private String canTalk(){ return talk; } } class Animal{ public static void main(String[] args) { Person p = new Person(); // private 修飾的方法是Person類獨有的,因此Animal類沒法訪問(動物原本就不能說話) // p.canTalk(); } }
final:final 修飾的方法不能被重寫,可是能夠由子類進行調用,若是將方法聲明爲 final 能夠有效的關閉動態綁定
public class Fruit { private String fruitName; final String eatingFruit(String name){ System.out.println("eating " + name); return fruitName; } } class Apple extends Fruit{ // 不能重寫final方法,eatingFruit方法只屬於Fruit類,Apple類沒法調用 // String eatingFruit(String name){ // super.eatingFruit(name); // } String eatingApple(String name){ return super.eatingFruit(name); } }
static: static 修飾的方法比較特殊,不用經過 new 出某個類來調用,由類名.變量名
直接調用該方法,這個就很關鍵了,new 很關鍵,也能夠認爲是開啓多態的導火索,而由類名.變量名直接調用的話,此時的類名是肯定的,並不會產生多態,以下代碼:
public class SuperClass { public static void sayHello(){ System.out.println("由 superClass 說你好"); } } public class SubClass extends SuperClass{ public static void sayHello(){ System.out.println("由 SubClass 說你好"); } public static void main(String[] args) { SuperClass.sayHello(); SubClass.sayHello(); } }
SubClass 繼承 SuperClass 後,在
是沒法重寫 sayHello 方法的,也就是說 sayHello() 方法是對子類隱藏的,可是你能夠編寫本身的 sayHello() 方法,也就是子類 SubClass 的sayHello() 方法,因而可知,方法由 static 關鍵詞所修飾,也是編譯時綁定
在運行時根據具體對象的類型進行綁定
除了由 private、final、static 所修飾的方法和構造方法外,JVM 在運行期間決定方法由哪一個對象調用的過程稱爲動態綁定
若是把編譯、運行當作一條時間線的話,在運行前必需要進行程序的編譯過程,那麼在編譯期進行的綁定是前期綁定,在程序運行了,發生的綁定就是後期綁定。
public class Father { void drinkMilk(){ System.out.println("父親喜歡喝牛奶"); } } public class Son extends Father{ @Override void drinkMilk() { System.out.println("兒子喜歡喝牛奶"); } public static void main(String[] args) { Father son = new Son(); son.drinkMilk(); } }
Son 類繼承 Father 類,並重寫了父類的 dringMilk() 方法,在輸出結果得出的是兒子喜歡喝牛奶。那麼上面的綁定方式是什麼呢?
上面的綁定方式稱之爲動態綁定
,由於在你編寫 Father son = new Son() 的時候,編譯器並不知道 son 對象真正引用的是誰,在程序運行時期才知道,這個 son 是一個 Father 類的對象,可是卻指向了 Son 的引用,這種概念稱之爲多態,那麼咱們就可以整理出來多態的三個原則:
繼承
重寫
父類引用指向子類對象
也就是說,在 Father son = new Son() ,觸發了動態綁定機制。
動態綁定的過程
靜態綁定
靜態綁定在編譯時期觸發,那麼它的主要特色是
一、編譯期觸發,可以提前知道代碼錯誤
二、提升程序運行效率
動態綁定
一、使用動態綁定的前提條件可以提升代碼的可用性,使代碼更加靈活。
二、多態是設計模式的基礎,可以下降耦合性。
語法糖指的是計算機語言中添加的某種語法,這種語法對語言的功能並無影響,可是更方便程序員使用。由於 Java 代碼須要運行在 JVM 中,JVM 是並不支持語法糖的,語法糖在程序編譯階段就會被還原成簡單的基礎語法結構,這個過程就是解語法糖
。
下面我就列出來 Java 中有哪些語法糖
泛型:泛型是一種語法糖。在 JDK1.5 中,引入了泛型機制,可是泛型機制的自己是經過類型擦除
來實現的,在 JVM 中沒有泛型,只有普通類型和普通方法,泛型類的類型參數,在編譯時都會被擦除。
自動拆箱和自動裝箱:自動拆箱和自動裝箱是一種語法糖,它說的是八種基本數據類型的包裝類和其基本數據類型之間的自動轉換。簡單的說,裝箱就是自動將基本數據類型轉換爲包裝器
類型;拆箱就是自動將包裝器類型轉換爲基本數據類型。
內部類:內部類其實也是一個語法糖,由於其只是一個編譯時的概念,一旦編譯完成,編譯器就會爲內部類生成一個單獨的class 文件,名爲 outer$innter.class。
變長參數:變長參數也是一個比較小衆的用法,所謂變長參數,就是方法能夠接受長度不定肯定的參數。通常咱們開發不會使用到變長參數,並且變長參數也不推薦使用,它會使咱們的程序變的難以處理。
加強 for 循環:加強 for 循環與普通 for 循環相比,功能更強而且代碼更加簡潔,你無需知道遍歷的次數和數組的索引便可進行遍歷;加強 for 循環的對象要麼是一個數組,要麼實現了 Iterable 接口。這個語法糖主要用來對數組或者集合進行遍歷,其在循環過程當中不能改變集合的大小。
Switch 支持字符串和枚舉:switch
關鍵字原生只能支持整數
類型。若是 switch 後面是 String 類型的話,編譯器會將其轉換成 String 的hashCode
的值,因此其實 switch 語法比較的是 String 的 hashCode 。
條件編譯:通常狀況下,源程序中全部的行都參加編譯。但有時但願對其中一部份內容只在知足必定條件下才進行編譯,即對一部份內容指定編譯條件,這就是 條件編譯(conditional compile)
。
斷言:也就是所謂的 assert
關鍵字,是 jdk 1.4 後加入的新功能。它主要使用在代碼開發和測試時期,用於對某些關鍵數據的判斷,若是這個關鍵數據不是你程序所預期的數據,程序就提出警告或退出。
try-with-resources :JDK 1.7 開始,java引入了 try-with-resources 聲明,將 try-catch-finally 簡化爲 try-catch,這實際上是一種語法糖
,在編譯時會進行轉化爲 try-catch-finally 語句。新的聲明包含三部分:try-with-resources 聲明、try 塊、catch 塊。它要求在 try-with-resources 聲明中定義的變量實現了 AutoCloseable 接口,這樣在系統能夠自動調用它們的 close 方法,從而替代了 finally 中關閉資源的功能。
字符串相加:這個想必你們應該都知道,字符串的拼接有兩種,若是可以在編譯時期肯定拼接的結果,那麼使用 +
號鏈接的字符串會被編譯器直接優化爲相加的結果,若是編譯期不能肯定拼接的結果,底層會直接使用 StringBuilder
的 append
進行拼接
深刻理解 Java 語法糖,能夠參考這篇文章 Java 中的語法糖,真甜。
這也是一道老生常談的問題了。
Java 集合中總共有三種類型,分別是 List、Set 和 Map,它們都位於 java.util 包下,並且都是接口,它們有各自的實現類。
List 的實現類有ArrayList、LinkedList、Vector、CopyOnWriteArrayList 等
Set 的實現類有HashSet、TreeSet、LinkedHashSet、SortedSet 等
它們一樣有本身的抽象類,List 的抽象類是 AbstractList、Set 的抽象類是 AbstractSet。抽象類主要用做頂級接口也就是 List、Set 這類接口的拓展,實現了一些頂級接口功能的同時也定義了一些模版方法,這用到了模版設計模式。
List 和 Set 都繼承了 Collection
接口,都屬於一種 集合
。
下面來聊一聊 List 和 Set 的不一樣點
有序性
List 表示的是一種 有序集合
,也就是說,List 中的元素都是有序排列的,List 接口可以準確的控制每一個元素的插入順序,能夠經過索引來查找元素。
Set 對集合的順序沒有要求,不一樣的實現類能夠定義各自的順序,好比 HashSet 就是一種無序的數據結構,HashSet 中的元素是亂序的;TreeSet 則會按照元素的天然順序進行排序;LinkedHashSet 會按照插入 Set 中的元素順序進行排序;SortedSet 會根據 Comparable 或者 Comparator 的比較順序進行排序,也就是說你能夠自定義排序規則。
元素是否重複
List 容許重複元素存在的,而且 List 可以容許插入 null 元素,而 Set 是不容許重複元素的,因此 Set 也只能容許一個 null 元素存在。
Map 一樣也是咱們平常開發中使用頻率很是高的集合類。
Map 是一個支持 key-value
也就是 鍵值對
存儲的對象。其中,鍵對象不容許重複,而值對象能夠重複,而且值對象還能夠是 Map 類型的,就像數組中的元素還能夠是數組同樣。
Map 接口的實現類有:HashMap、TreeMap、LinkedHashMap 類。
HashMap 是咱們最經常使用的 Map 實現了,HashMap 的底層構造就是 數組 + 鏈表,它會根據 key 的 hashCode 值存儲數據,可使用 get(key)
來獲取鍵對應的值,HashMap 不是線程安全的容器,而且只能容許一條 null 鍵值對。在 HashMap 中元素比較少的時候,HashMap 是以鏈表的形式存儲元素的,等到數據量到達必定程度後,鏈表會轉爲紅黑樹進行存儲。
TreeMap 是一個紅黑樹的 Map,它會對鍵按照指定的順序進行排序,TreeMap 不是一個線程安全的 Map,TreeMap 中不能容許 null 值,不然會拋出空指針異常。
LinkedHashMap 保證了元素的插入順序,在查詢遍歷時會比 HashMap 慢,一樣也不是線程安全的容器。LinkedHashMap 能夠容許空元素。
值得注意的是,Map 的實現還有 Hashtable
和 ConcurrentHashMap
,這兩個元素都是線程安全的 Map。
Hashtable 咱們通常不經常使用,由於它的線程安全都只是使用了簡單暴力的 synchronized
同步鎖,同步鎖的開銷比較大,不推薦使用。
還有最後一個就是 ConcurrentHashMap 了,ConcurrentHashMap 是一個基於多線程高併發的 Map,它常常用於多線程場景,ConcurrentHashMap 的底層使用了分段鎖
,分段鎖對每一個段進行加鎖,在保證線程安全的同時下降了鎖的粒度。
ArrayList 是實現了基於動態數組的數據結構,LinkedList 基於鏈表的數據結構。
對於隨機訪問 get 和 set 操做,ArrayList 要優於 LinkedList,由於 LinkedList 要移動指針。
對於新增和刪除操做 add 和 remove,LinedList 比較佔優點,由於 ArrayList 要移動數據。
ArrayList 和 LinkedList 都不是線程安全的容易,它們都須要手動加鎖,或者使用 Collections 中的線程安全的實現 Collections.synchronizedList 來構造。它們都具備 fail-fast 快速失敗機制的。
這個時候可能還會問你和 Vector
的區別
主要有兩點區別:線程安全性和擴容方面
Vector 與 ArrayList同樣,也是經過數組實現的,不一樣的是它支持線程的同步
,即某一時刻只有一個線程可以寫Vector,避免多線程同時寫而引發的不一致性,但實現同步須要很高的花費,所以,訪問它比訪問 ArrayList 慢。
當 Vector 或 ArrayList 中的元素超過它的初始大小時,Vector 會將它的容量翻倍,而 ArrayList 只增長 50% 的大小。這樣,ArrayList 就有利於節約內存空間。
先呈現一下 Collections.sort 和 Arrays.sort 的源碼
從這三段代碼可知,Collections.sort 最終調用的是 Arrays.sort 中的方法,那麼 Collections.sort 不用看了,直接看 Arrays.sort 便可。
Arrays.sort 源碼中有三個分支判斷,若是沒有提供外部 Comparator 比較器的話,會直接調用 sort 方法,調用後的 sort 方法以下
首先會進行LegacyMergeSort.userRequested 的判斷,那麼這個判斷是什麼意思呢?
是這樣,在 JDK1.6 中使用的是 LegacyMergeSort
,在 JDK1.7 中使用的是 TimeSort
,若是想要使用原來的 LegacyMergeSort 的話,就須要在系統變量中加上
-Djava.util.Arrays.useLegacyMergeSort=true
這個配置。
若是沒有使用這個配置的話,默認調用的是 ComparableTimSort.sort 方法,這實際上是一種 TimSort
的優化,而 TimSort 的排序算法就是歸併排序。
咱們從 Arrays.sort 中的第二個判斷也能夠看到這一點,ComparableTimSort 和 TimSort 最大的區別就是看你有沒有提供外部的比較器了。
咱們平常開發過程當中使用 Iterator 用的很是多,ListIterator 不多接觸,這道題主要考察你對 List 源碼的熟悉程度以及對 fail-fast 機制是否瞭解。
通常來說,Iterator 和 ListIterator 的主要區別有
Iterator 在遍歷過程當中,不能修改集合中元素的數量操做,不然會拋出異常。
使用範圍不一樣,Iterator 能夠在全部集合中使用,而 ListIterator 只能用在 List 類型與子類型
listIterator 有 add 方法,能夠向集合中添加元素,而 iterator 不能。
listiterator 和 iterator 都有 hasnext 和 next 方法能夠順序遍歷元素, 可是 listiterator 有 hasprevious 和 previous 方法,能夠逆向遍歷元素
listiterator 能夠定位當前的索引位置 nextIndex 和 previousIndex 能夠實現,iterator 沒有此功能
listiterator 能夠實現對對象的修改 set() 方法能夠實現,iterator 僅能夠遍歷,不能修改。
今天面試官又問到你 ArrayList 的擴容機制了。。。。。。那就直接看源碼唄
話說何時會擴容啊,確定是調用 add 方法添加元素的時候呀
ArrayList 添加元素會進行判斷,這也是ensureCapacityInternal
方法乾的事情,也就是判斷並肯定容量大小的一個方法,咱們直接跟進去
這個方法會進行一個判斷,咱們上面註釋也給出來了,就是說你這個 ArrayList 在構造的時候是否指定了初始容量,若是 List 中沒有元素,就會取你設置初始容量和 ArrayList 中最大容量判斷,取大的爲準。
而後調用 ensureExplicitCapacity
方法,判斷是否須要擴容
能夠看到,若是此時 ArrayList 數組元素的 size 要比存儲的元素大的話,就會調用 grow
方法,這個 grow 方法其實就是擴容方法。
這段代碼就是 ArrayList 擴容機制了。咱們來分析一下
首先會獲取 ArrayList 元素的數量,而後把 ArrayList 的容量擴容 50%,把擴容後的容量和參數容量進行比較,若是比參數小的話,取值更大的容量進行元素拷貝。若是擴容後的容量比 ArrayList 的最大容量還要大,就會進行最大容量的取值
hugeCapacity
方法首先判斷參數是否小於 0 ,若是是的話那麼就會拋出 OutOfMemoryError 異常,若是不是的話就判斷參數和 ArrayList 最大容量到底誰大,若是參數大的話,就會取值爲 Integer.MAX_VALUE,若是小的話就會取 MAX_ARRAY_SIZE。
簡單來講,BIO(Blocking I/O)
,它是一種同步阻塞式的 IO,數據的讀寫必須在一個線程中進行完成,也就是說它只有一個執行順序流,若是遇到 IO 流的讀取操做,它必須等到 IO 流的讀取或者寫入完成後,才能進行接下來的工做。
NIO(non-blocking IO)
其實也被稱爲 new io
,它是 JDK 1.4 及其以上版本中新增長的一種 IO 流,它是一種同步非阻塞的 IO 模型。在 NIO 中,線程發起請求後會馬上返回,同步指的是必須等待 IO 緩衝區內的數據就緒,而非阻塞指的是,用戶線程不原地等待 IO 緩衝區,能夠先作一些其餘操做,可是要定時輪詢檢查 IO 緩衝區數據是否就緒。Java 中 的 NIO 是 new IO 的意思。實際上是 NIO 加上 IO 多路複用技術。普通的 NIO 是線程輪詢查看一個 IO 緩衝區是否就緒,而 Java 中的 new IO 指的是線程輪詢地去查看一堆 IO 緩衝區中哪些就緒,這是一種 IO 多路複用的思想。IO 多路複用模型中,將檢查 IO 數據是否就緒的任務,交給系統級別的 select 或 epoll 模型,由系統進行監控,減輕用戶線程負擔。
AIO
是真正意義上的異步非阻塞 IO 模型。 上述 NIO 實現中,須要用戶線程定時輪詢,去檢查 IO 緩衝區數據是否就緒,佔用應用程序線程資源,其實輪詢至關於仍是阻塞的,並不是真正解放當前線程,由於它仍是須要去查詢哪些 IO 就緒。而真正的理想的異步非阻塞 IO 應該讓內核系統完成,用戶線程只須要告訴內核,當緩衝區就緒後,通知我或者執行我交給你的回調函數。
在 Java 中,根據對對象屬性的拷貝程度(基本數據類和引用類型),會分爲兩種:
Shallow Copy
Deep Copy
淺拷貝:淺拷貝會從新建立一個對象,這個對象有着原始對象屬性值的一份拷貝。若是屬性是基本類型,拷貝的就是基本類型的值;若是屬性是內存地址(引用類型),拷貝的就是內存地址 ,所以若是其中一個對象改變了這個地址,就會影響到另外一個對象。即默認拷貝構造函數只是對對象進行淺拷貝複製,即逐個成員依次拷貝,也就是隻複製對象空間而不復制資源。
實現淺拷貝,須要實現 Cloneable
接口,並覆寫 clone()
方法。
深拷貝:對於基本數據類型的成員對象,由於基礎數據類型是值傳遞的,因此是直接將屬性值賦值給新的對象。基礎類型的拷貝,其中一個對象修改該值,不會影響另一個(和淺拷貝同樣)。對於引用類型,好比數組或者類對象,深拷貝會新建一個對象空間,而後拷貝里面的內容,因此它們指向了不一樣的內存空間。改變其中一個,不會對另一個也產生影響。對於有多層對象的,每一個對象都須要實現 Cloneable
並重寫 clone()
方法,進而實現了對象的串行層層拷貝。深拷貝相比於淺拷貝速度較慢而且花銷較大。
Java 建立對象的方式主要有五種
Object obj = new Object();
User user = (User)Class.forName("com.cxuan.test.User").newInstance(); // 或者使用 User user = User.class.newInstance();
Constructor<User> constructor = User.class.getConstructor(); User user = constructor.newInstance();
Constructor<User> constructor = User.class.getConstructor(); User user = constructor.newInstance(); user.setName("cxuan"); User user2 = (User)user.clone(); System.out.println(user2.getName());
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("xxx")); out.writeObject(user2); out.close(); //Deserialization ObjectInputStream in = new ObjectInputStream(new FileInputStream("xxx")); User user3 = (User) in.readObject(); in.close(); user3.setName("cxuan003"); System.out.println(user3 + ", hashcode : " + user3.hashCode());
深刻理解這幾種建立對象的方式,能夠參考 盤點 Java 建立對象的 n 種操做
這篇寫了很長時間,微信公衆號的統計是 3w 字,31 張圖,閱讀時間大概是 77 分鐘。
我知道這個閱讀時間能夠坐地鐵 10 號線繞北京一圈了,也會有不少人產生反感,文章寫這麼長,誰有時間看完啊?這種快餐式的疑問我已經聽到過無數遍了,可是我每次都會看成耳旁風。是啊,咱們天天,刷抖音看美女,刷頭條看美女,刷公衆號看一羣大佬們每天吹牛逼,而後罵道:"臥槽,又特麼浪費老子 1 個小時",而後僞裝認真的看了 15 分鐘代碼,繼續浪費。
我知道這篇文章仍舊沒多少人看,由於立刻就要被信息洪流淹沒了,可能你看完了就會放在收藏夾中,甚至你以爲我寫的小兒科,也可能這篇文章在你的編程生涯中泛不起一點漣漪,我不想過多的描述一種現象。我只說一下我作了哪些事情吧。這篇文章中大概有不到 20 條連接,其中有 14 條都是我公衆號的內容。
一篇不同凡響的 String、StringBuffer、StringBuilder 詳解
看完這篇 Exception 和 Error ,和麪試官扯皮就沒問題了
看完這篇 final、finally 和 finalize 和麪試官扯皮就沒問題了
我想,這可以對的起我在 github https://github.com/crisxuan/bestJavaer 上面說過的話。
好了,就這樣,至於點贊在看什麼的,隨意就好,但願這篇文章可以幫到你。