Java八股文純享版——篇①:Java基礎
註:
1.筆記為個人歸納整理,儘力保證準確性,如有錯誤,懇請指正
2.寫文不易,轉載請註明出處
3.本文首發地址 //blog.leapmie.com/archives/b8fe0da9/
4.本系列文章目錄詳見《Java八股文純享版——目錄》
5.文末可關注公眾號,內容更精彩
JDK8對比JDK7的差別
1.HashMap的實現差別
2.支援Lambda表達式語法(如創建執行緒,對於介面只有一個方法需要重寫的類可以用lambda方式簡潔創建對象)
3.支援Stream流操作。Stream提供一種對 Java 集合的流式操作,比如filter, map, reduce, find, match, sorted等。創建Stream有兩種方式:stream() 創建串列流、parallelStream() 創建可以並行計算的並行流。
List<String> stringList = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
List<String> filtered = stringList .parallelStream()
.filter(string > !string.isEmpty()) //過濾
.map(i -> i*i) // 映射
.sorted() // 排序
.limit(10) // 分頁
.collect(Collectors.toList()); // 返回結果集
4.介面支援默認方法(如果實現多個介面同時都定義了相同的默認方法,則實現類必須重寫該方法)
public interface Interface1{
default void helloWorld() {
System.out.println("hi i'm from Interface1");
}
}
public class MyImplement implements Interface1{
public static void main(String[] args) {
MyImplement myImplement = new MyImplement();
myImplement.helloWorld();
}
}
HashMap結構
Jdk7的實現
數組+鏈表組成,數組是HashMap的主體,鏈表用於解決Hash衝突。
Jdk8的實現
數組+紅黑樹。JDK8中當HashMap鏈表長度大於8的時候,改為紅黑樹結構,解決鏈表過長的問題,當小於6時會轉換回鏈表。
轉換閾值為什麼是8
Java源碼的貢獻者在進行大量實驗分析,hashcode碰撞次數符合泊松分布,在負載因子0.75(HashMap默認值)的情況下,單個hash槽內元素個數為8的概率為0.00000006,概率小於百萬分之一,所以發生紅黑樹轉換的情況其實並不多,設置為8可以大幅減少轉換的代價。
從紅黑樹轉換為鏈表的閾值為6,是為了避免元素數量在臨界點來回變化導致的結構頻繁轉換。
以下為源碼注釋中的概率說明:
0: 0.60653066
1: 0.30326533
2: 0.07581633
3: 0.01263606
4: 0.00157952
5: 0.00015795
6: 0.00001316
7: 0.00000094
8: 0.00000006
為什麼是紅黑樹而不是其他樹?
普通二叉樹可能會出現單邊長度過長的問題,紅黑樹屬於平衡二叉樹,保證樹的合理高度,而相比AVL平衡二叉樹具備更好的插入、刪除效率。(紅黑樹允許局部少量的不完全平衡,這樣對於效率影響不大,但省去了很多沒有必要的調平衡操作,avl樹調平衡有時候代價較大,所以效率不如紅黑樹)。
HashMap的擴容機制
當HashMap中的元素越來越多的時候,碰撞的幾率也就越來越高,為了提高查詢的效率,就要對HashMap的數組進行擴容(resize)。
當hashmap中的元素個數超過數組大小*loadFactor時,就會進行數組擴容,loadFactor的默認值為0.75。
擴容的大小為原數組長度的一倍。
ConcurrentHashmap實現原理
Jdk7的實現
HashTable是一個執行緒安全的類,它使用synchronized來鎖住整張Hash表來實現執行緒安全,性能低下。
ConcurrentHashMap內部分為很多個Segment,每一個Segment擁有一把鎖,每個段相當於一個小的Hashtable。當一個執行緒佔用鎖訪問其中一個數據段時不影響其他段的訪問,提高並發效率。
Jdk8的實現
table數組+單向鏈表+紅黑樹的結構
jdk8中取消segments欄位,直接採用transient volatile HashEntry<K,V>[] table 保存數據,採用 table 數組元素作為鎖,從而實現了對每一行數據進行加鎖,進一步減少並發衝突的概率,代替原來的每一段加鎖。
因為段的隔離級別不太容易確定,默認是16,但是很多情況下並不合適,如果太大很多空間就浪費了,如果太小每個段中可能元素過於多,所以取消segments,改成了CAS演算法
ArrayList與LinkedArrayList的區別
- Array(動態數組)的數據結構,一個是Link(鏈表)的數據結構
- 當隨機訪問List時(get和set操作),ArrayList比LinkedList的效率更高
- 當對數據進行增加和刪除的操作時(add和remove操作),LinkedList比ArrayList的效率更高
List的安全實現
ArrayList不是執行緒安全的,有以下幾種方案實List的現執行緒安全:
1. Vector類
Vector實現方式比較笨重,add等每個方法使用Synchronized修飾
Vector v = new Vector(3, 2);
v.addElement(new Integer(1));
v.addElement(new Integer(2));
Enumeration en=v.elements();
while(en.hasMoreElements()){
Object object=en.nextElement();
System.out.println(object);
}
2. Collections.synchronizedList
Collections.synchronizedList(List
List<String> list = Collections.synchronizedList(new ArrayList<>());
3.CopyOnWriteArrayList
List<String> list =new CopyOnWriteArrayList<String>();
list.add("1");
list.add("2");
Iterator<String> iter = list.iterator();
while(iter.hasNext()){
String o = iter.next();
System.out.println(o);
}
內部在add等方法通過ReentrantLock加鎖實現。
缺點:
1.因為CopyOnWrite的寫是複製機制,所以在進行寫操作的時候,記憶體里會同時駐紮兩個對象的記憶體,舊的對象和新寫入的對象。
2.CopyOnWrite容器只能保證數據的最終一致性,不能保證數據的實時一致性。所以如果你希望寫入的的數據,馬上能讀到,請不要使用CopyOnWrite容器。
Java的異常類別
異常分為Error和exception,其中exception分為CheckedException和RuntimeException
Error
error表示系統級的錯誤,是java運行環境內部錯誤或硬體問題,由Java虛擬機拋出,除了退出運行別無選擇,如OOM(OutOfMemoryError)。
CheckedException(檢查異常)
檢查異常主要是指IO異常、SQL異常等。對於這種異常,JVM要求我們必須對其進行catch處理,如FileNotFoundException。
RuntimeException(運行時異常)
運行時異常一般不處理,比如NullPointerException,對於運行時異常,程式會將異常一直向上拋,一直拋到處理程式碼,如果沒有catch塊進行處理,到了最上層,如果是多執行緒就有Thread.run()拋出,如果不是多執行緒就由main.run拋出,拋出異常後執行緒終止。
Iterator
如有ArrayList a,內容為[“a”,”b”,”c”,”d”]
在for 循環里遍歷List,刪除元素會怎樣?
for (int i = 0; i < a.size(); i++) {
if (i == 1) {
a.remove(i);
} else {
System.out.println(i + a.get(i));
}
}
最終輸出0a,2d,因為元素b被刪除,然後c往前移位對應i=1,所以c也被跳過輸出。
在iterator 循環里遍歷List,刪除元素會怎樣?
Iterator<String> iterator = a.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
if ("b".equals(s)) {
a.remove(1);
} else {
System.out.println(s);
}
}
拋出異常ConcurrentModificationException,要避免拋異常應該使用iterator.remove()進行刪除。
Iterator實現原理
Iterator的實現中主要有幾個變數cursor,lastRest, expectedModCount三個變數,其中cursor將記錄下一個位置,lastRet記錄當前位置,expectedModCount記錄沒有修改的List的版本號。
ArrayList作了添加或刪除操作都會增加modCount版本號,這樣的意思是在迭代期間,會不斷檢查modCount和迭代器持有的expectedModCount兩者是不是相等,如果不想等就拋出異常了
Java的繼承有什麼缺點
- 父類向子類暴露了實現細節
- 父類更改之後子類也要同時更改
- 子類覆蓋了一些方法,可能會導致其他調用了該方法的方法錯誤
包裝類
《阿里巴巴Java手冊》規定如下
【強制】所有整型包裝類對象之間值的比較,全部使用 equals 方法比較。
說明:對於 Integer var = ? 在-128 至 127 範圍內的賦值,Integer 對象是在 IntegerCache.cache 產 生,會復用已有對象,這個區間內的 Integer 值可以直接使用==進行判斷,但是這個區間之外的所有數 據,都會在堆上產生,並不會復用已有對象,這是一個大坑,推薦使用 equals 方法進行判斷。
對於以下語句:
Integer i01 = 59;
int i02 = 59;
Integer i03 =Integer.valueOf(59);
Integer i04 = new Integer(59);
以下輸出結果為false的是:
A System.out.println(i01 == i02);
B System.out.println(i01 == i03);
C System.out.println(i03 == i04);
D System.out.println(i02 == i04);
答案為C
JVM中一個位元組以下的整型數據會在JVM啟動的時候載入進記憶體,除非用new Integer()顯式的創建對象,否則都是同一個對象
所以只有i04是一個新對象,其他都是同一個對象。所有A,B選項為true
C選項i03和i04是兩個不同的對象,返回false
D選項i02是基本數據類型,會觸發i04自動拆箱,比較的時候比較的是數值,返回true
重寫hashCode方法
為什麼重寫equals方法要重寫hashCode方法?
當equals方法被重寫時,通常有必要重寫hashCode方法,以維護hashCode方法的常規約定:值相同的對象必須有相同的hashCode。
- hashCode不同時,object1.equals(object2)為false;
- hashCode相同時,object1.equals(object2)不一定為true
因為hashCode效率更高(僅為一個int值),比較起來更快,對於HashMap等很多結構是先通過對象的hashCode方法判斷是否一致,然後再繼續操作。
例如類Person中有屬性name、idcard等欄位,如果重寫equals方法希望通過name、idcard欄位值一致則代表該對象相等,必須同時重寫hashCode方法。
class Person {
String name;
String idcard;
String sex;
@Override
public int hashCode() {
int result = 17; //任意素數
// 31 有個很好的性能,即用移位和減法來代替乘法,通常*31
result = 31*result +name.hashCode();
result = 31*result +idcard.hashCode();
return result;
}
摘自《Effective Java》中關於重寫hashCode方法的習慣步驟如下:
「之所以選擇31,是因為它是一個奇素數。如果乘數是偶數,並且乘法溢出的話,資訊就會丟失,因為與2相乘等價於位移運算。使用素數的好處並不很明顯,但是習慣上都使用素數來計算三列結果。31有個很好的特性,即用移位和減法來代替乘法,可以得到更好的性能:31 * i 等於 (i << 5) – i」。
對象引用類型及回收時機
從JDK 1.2版本開始,把對象的引用分為4種級別,從而使程式能更加靈活地控制對象的生命周期。這4種級別由高到低依次為:強引用、軟引用、弱引用和虛引用。
(1)強引用(StrongReference)
強引用是我們使用的最廣泛,也是最普遍的一種引用類型。即
A a = new A();
只要某個對象有強引用與之關聯,JVM必定不會回收這個對象,即使在記憶體不足的情況下,JVM寧願拋出OutOfMemory錯誤也不會回收這種對象。
如果想中斷強引用和某個對象之間的關聯,可以顯示地將引用賦值為null,這樣一來的話,JVM在合適的時間就會回收該對象。
⑵軟引用(SoftReference)
軟引用是用來描述一些有用但並不是必需的對象,在Java中用java.lang.ref.SoftReference類來表示。
軟引用是用來描述一些有用但並不是必需的對象,在Java中用java.lang.ref.SoftReference類來表示。
軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被JVM回收,這個軟引用就會被加入到與之關聯的引用隊列中。
對於軟引用關聯著的對象,只有在記憶體不足的時候JVM才會回收該對象。因此,這一點可以很好地用來解決OOM的問題,並且這個特性很適合用來實現快取:比如網頁快取、圖片快取等。
⑶弱引用(WeakReference)
弱引用也是用來描述非必需對象的,當JVM進行垃圾回收時,無論記憶體是否充足,都會回收被弱引用關聯的對象。
在java中,用java.lang.ref.WeakReference類來表示。
WeakReference<String> sr = new WeakReference<String>(new String("aaa"));
不過要注意的是,這裡所說的被弱引用關聯的對象是指只有弱引用與之關聯,如果存在強引用同時與之關聯,則進行垃圾回收時也不會回收該對象(軟引用也是如此)。弱引用也可以和一個引用隊列(ReferenceQueue)聯合使用。
⑷虛引用(PhantomReference)
如果一個對象與虛引用關聯,則跟沒有引用與之關聯一樣,在任何時候都可能被垃圾回收器回收。在java中用java.lang.ref.PhantomReference類表示。
ReferenceQueue<String> queue = new ReferenceQueue<String>();
PhantomReference<String> pr = new PhantomReference<String>(new String("aaa"), queue);
虛引用必須和引用隊列關聯使用,當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會把這個虛引用加入到與之關聯的引用隊列中。