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),內部使用同步程式碼塊的方式實現同步,用SynchronizedCollection這個靜態內部類作為鎖。

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的繼承有什麼缺點

  1. 父類向子類暴露了實現細節
  2. 父類更改之後子類也要同時更改
  3. 子類覆蓋了一些方法,可能會導致其他調用了該方法的方法錯誤

包裝類

《阿里巴巴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);

虛引用必須和引用隊列關聯使用,當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會把這個虛引用加入到與之關聯的引用隊列中。


[目錄]《Java八股文純享版——目錄》