1. 程式人生 > >BTA 常問的 Java基礎40道常見面試題及詳細答案

BTA 常問的 Java基礎40道常見面試題及詳細答案

最近看到網上流傳著,各種面試經驗及面試題,往往都是一大堆技術題目貼上去,而沒有答案。

為此我業餘時間整理了,Java基礎常見的40道常見面試題,及詳細答案,望各路大牛,發現不對的地方,不吝賜教,留言即可。

  1. 八種基本資料型別的大小,以及他們的封裝類
  2. 引用資料型別
  3. Switch能否用string做引數
  4. equals與==的區別
  5. 自動裝箱,常量池
  6. Object有哪些公用方法
  7. Java的四種引用,強弱軟虛,用到的場景
  8. Hashcode的作用
  9. HashMap的hashcode的作用
  10. 為什麼過載hashCode方法?
  11. ArrayList、LinkedList、Vector的區別
  12. String、StringBuffer與StringBuilder的區別
  13. Map、Set、List、Queue、Stack的特點與用法
  14. HashMap和HashTable的區別
  15. JDK7與JDK8中HashMap的實現
  16. HashMap和ConcurrentHashMap的區別,HashMap的底層原始碼
  17. ConcurrentHashMap能完全替代HashTable嗎
  18. 為什麼HashMap是執行緒不安全的
  19. 如何執行緒安全的使用HashMap
  20. 多併發情況下HashMap是否還會產生死迴圈
  21. TreeMap、HashMap、LindedHashMap的區別
  22. Collection包結構,與Collections的區別
  23. try?catch?finally,try裡有return,finally還執行麼
  24. Excption與Error包結構,OOM你遇到過哪些情況,SOF你遇到過哪些情況
  25. Java(OOP)面向物件的三個特徵與含義
  26. Override和Overload的含義去區別
  27. Interface與abstract類的區別
  28. Static?class?與non?static?class的區別
  29. java多型的實現原理
  30. foreach與正常for迴圈效率對比
  31. Java?IO與NIO
  32. java反射的作用於原理
  33. 泛型常用特點
  34. 解析XML的幾種方式的原理與特點:DOM、SAX
  35. Java1.7與1.8,1.9,10 新特性
  36. 設計模式:單例、工廠、介面卡、責任鏈、觀察者等等
  37. JNI的使用
  38. AOP是什麼
  39. OOP是什麼
  40. AOP與OOP的區別

八種基本資料型別的大小,以及他們的封裝類

八種基本資料型別:int、short、float、double、long、boolean、byte、char。

封裝類分別是:Integer、Short、Float、Double、Long、Boolean、Byte、Character。

引用資料型別

引用資料型別是由類的編輯器定義的,他們是用於訪問物件的。這些變數被定義為不可更改的特定型別。

例如:Employee, Puppy 等等
- 類物件和陣列變數就是這種引用資料型別。
- 任何引用資料型別的預設值都為空。
- 一個引用資料型別可以被用於任何宣告型別和相容型別的物件。

Switch能否用string做引數

jdk7之前
switch 只能支援 byte、short、char、int 這幾個基本資料型別和其對應的封裝型別。

switch後面的括號裡面只能放int型別的值,但由於byte,short,char型別,它們會?自動?轉換為int型別(精精度小的向大的轉化),所以它們也支援

jdk1.7後
整形,列舉型別,boolean,字串都可以。

原理

switch (expression)  // 括號裡是一個表示式,結果是個整數{
  case constant1:   // case 後面的標號,也是個整數
     group of statements 1;
     break;
  case constant2:
     group of statements 2;
     break;
  ...
  default:
     default group of statements
}

jdk1.7後,整形,列舉型別,boolean,字串都可以。

public class TestString {

    static String string = "123";
    public static void main(String[] args) {
        switch (string) {
        case "123":
            System.out.println("123");
            break;
        case "abc":
            System.out.println("abc");
            break;
        default:
            System.out.println("defauls");
            break;
        }
    }
}

為什麼jdk1.7後又可以用string型別作為switch引數呢?

其實,jdk1.7並沒有新的指令來處理switch string,而是通過呼叫switch中string.hashCode,將string轉換為int從而進行判斷

equals與==的區別

使用==比較原生型別如:boolean、int、char等等,使用equals()比較物件。

1、==是判斷兩個變數或例項是不是指向同一個記憶體空間。
equals是判斷兩個變數或例項所指向的記憶體空間的值是不是相同。

2、==是指對記憶體地址進行比較。
equals()是對字串的內容進行比較。

3、==指引用是否相同。
equals()指的是值是否相同。

public static void main(String[] args) {

        String a = new String("ab"); // a 為一個引用
        String b = new String("ab"); // b為另一個引用,物件的內容一樣
        String aa = "ab"; // 放在常量池中
        String bb = "ab"; // 從常量池中查詢

        System.out.println(aa == bb); // true
        System.out.println(a == b); // false,非同一物件
        System.out.println(a.equals(b)); // true
        System.out.println(42 == 42.0);  // true
    }

public static void main(String[] args) {
    Object obj1 = new Object();
    Object obj2 = new Object();
    System.out.println(obj1.equals(obj2));//false
    System.out.println(obj1==obj2);//false
    obj1=obj2;
    System.out.println(obj1==obj2);//true
    System.out.println(obj2==obj1);//true
}

自動裝箱,常量池

自動裝箱 在jdk?1.5之前,如果你想要定義一個value為100的Integer物件,則需要如下定義:

Integer i = new Integer(100);

int intNum1 = 100; //普通變數
Integer intNum2 = intNum1; //自動裝箱
int intNum3 = intNum2; //自動拆箱
Integer intNum4 = 100; //自動裝箱

上面的程式碼中,intNum2為一個Integer型別的例項,intNum1為Java中的基礎資料型別,將intNum1賦值給intNum2便是自動裝箱;而將intNum2賦值給intNum3則是自動拆箱。

八種基本資料型別: boolean byte char shrot int long float double ,所生成的變數相當於常量。

基本型別包裝類:Boolean Byte Character Short Integer Long Float Double。

自動拆箱和自動裝箱定義:

自動裝箱是將一個java定義的基本資料型別賦值給相應封裝類的變數。
拆箱與裝箱是相反的操作,自動拆箱則是將一個封裝類的變數賦值給相應基本資料型別的變數。

Object有哪些公用方法

Object是所有類的父類,任何類都預設繼承Object

clone
保護方法,實現物件的淺複製,只有實現了Cloneable接口才可以呼叫該方法,否則丟擲CloneNotSupportedException異常。

equals
在Object中與==是一樣的,子類一般需要重寫該方法。

hashCode
該方法用於雜湊查詢,重寫了equals方法一般都要重寫hashCode方法。這個方法在一些具有雜湊功能的Collection中用到。

getClass
final方法,獲得執行時型別

wait
使當前執行緒等待該物件的鎖,當前執行緒必須是該物件的擁有者,也就是具有該物件的鎖。
wait() 方法一直等待,直到獲得鎖或者被中斷。
wait(long timeout) 設定一個超時間隔,如果在規定時間內沒有獲得鎖就返回。

呼叫該方法後當前執行緒進入睡眠狀態,直到以下事件發生

1、其他執行緒呼叫了該物件的notify方法。
2、其他執行緒呼叫了該物件的notifyAll方法。
3、其他執行緒呼叫了interrupt中斷該執行緒。
4、時間間隔到了。
5、此時該執行緒就可以被排程了,如果是被中斷的話就丟擲一個InterruptedException異常。

notify
喚醒在該物件上等待的某個執行緒。

notifyAll
喚醒在該物件上等待的所有執行緒。

toString
轉換成字串,一般子類都有重寫,否則列印控制代碼。

Java的四種引用,強弱軟虛,用到的場景

從JDK1.2版本開始,把物件的引用分為四種級別,從而使程式能更加靈活的控制物件的生命週期。這四種級別由高到低依次為:強引用、軟引用、弱引用和虛引用。

1、強引用

最普遍的一種引用方式,如String s = “abc”,變數s就是字串“abc”的強引用,只要強引用存在,則垃圾回收器就不會回收這個物件。

2、軟引用(SoftReference)

用於描述還有用但非必須的物件,如果記憶體足夠,不回收,如果記憶體不足,則回收。一般用於實現記憶體敏感的快取記憶體,軟引用可以和引用佇列ReferenceQueue聯合使用,如果軟引用的物件被垃圾回收,JVM就會把這個軟引用加入到與之關聯的引用佇列中。

3、弱引用(WeakReference)

弱引用和軟引用大致相同,弱引用與軟引用的區別在於:只具有弱引用的物件擁有更短暫的生命週期。在垃圾回收器執行緒掃描它所管轄的記憶體區域的過程中,一旦發現了只具有弱引用的物件,不管當前記憶體空間足夠與否,都會回收它的記憶體。

4、虛引用(PhantomReference)

就是形同虛設,與其他幾種引用都不同,虛引用並不會決定物件的生命週期。如果一個物件僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。 虛引用主要用來跟蹤物件被垃圾回收器回收的活動。

虛引用與軟引用和弱引用的一個區別在於:

虛引用必須和引用佇列 (ReferenceQueue)聯合使用。當垃圾回收器準備回收一個物件時,如果發現它還有虛引,就會在回收物件的記憶體之前,把這個虛引用加入到與之關聯的引用佇列中。

Hashcode的作用

1、HashCode的特性

(1)HashCode的存在主要是用於查詢的快捷性,如Hashtable,HashMap等,HashCode經常用於確定物件的儲存地址

(2)如果兩個物件相同,?equals方法一定返回true,並且這兩個物件的HashCode一定相同

(3)兩個物件的HashCode相同,並不一定表示兩個物件就相同,即equals()不一定為true,只能夠說明這兩個物件在一個雜湊儲存結構中。

(4)如果物件的equals方法被重寫,那麼物件的HashCode也儘量重寫

2、HashCode作用

Java中的集合有兩類,一類是List,再有一類是Set。前者集合內的元素是有序的,元素可以重複;後者元素無序,但元素不可重複

equals方法可用於保證元素不重複,但如果每增加一個元素就檢查一次,若集合中現在已經有1000個元素,那麼第1001個元素加入集合時,就要呼叫1000次equals方法。這顯然會大大降低效率。?於是,Java採用了雜湊表的原理

雜湊演算法也稱為雜湊演算法,是將資料依特定演算法直接指定到一個地址上。

這樣一來,當集合要新增新的元素時,先呼叫這個元素的HashCode方法,就一下子能定位到它應該放置的物理位置上

(1)如果這個位置上沒有元素,它就可以直接儲存在這個位置上,不用再進行任何比較了。

(2)如果這個位置上已經有元素了,就呼叫它的equals方法與新元素進行比較,相同的話就不存了。

(3)不相同的話,也就是發生了Hash key相同導致衝突的情況,那麼就在這個Hash key的地方產生一個連結串列,將所有產生相同HashCode的物件放到這個單鏈表上去,串在一起(很少出現)。

這樣一來實際呼叫equals方法的次數就大大降低了,幾乎只需要一兩次。

如何理解HashCode的作用:

從Object角度看,JVM每new一個Object,它都會將這個Object丟到一個Hash表中去,這樣的話,下次做Object的比較或者取這個物件的時候(讀取過程),它會根據物件的HashCode再從Hash表中取這個物件。這樣做的目的是提高取物件的效率。若HashCode相同再去呼叫equal。

3、HashCode實踐(如何用來查詢)

HashCode是用於查詢使用的,而equals是用於比較兩個物件是否相等的

(1)例如記憶體中有這樣的位置

0  1  2  3  4  5  6  7

而我有個類,這個類有個欄位叫ID,我要把這個類存放在以上8個位置之一,如果不用HashCode而任意存放,那麼當查詢時就需要到這八個位置裡挨個去找,或者用二分法一類的演算法。

但以上問題如果用HashCode就會使效率提高很多
定義我們的HashCode為ID%8,比如我們的ID為9,9除8的餘數為1,那麼我們就把該類存在1這個位置,如果ID是13,求得的餘數是5,那麼我們就把該類放在5這個位置。依此類推。

(2)但是如果兩個類有相同的HashCode,例如9除以8和17除以8的餘數都是1,也就是說,我們先通過?HashCode來判斷兩個類是否存放某個桶裡,但這個桶裡可能有很多類,那麼我們就需要再通過equals在這個桶裡找到我們要的類

請看下面這個例子

public class HashTest {
    private int i;

    public int getI() {
        return i;
    }

    public void setI(int i) {
        this.i = i;
    }

    public int hashCode() {
        return i % 10;
    }

    public final static void main(String[] args) {
        HashTest a = new HashTest();
        HashTest b = new HashTest();
        a.setI(1);
        b.setI(1);
        Set<HashTest> set = new HashSet<HashTest>();
        set.add(a);
        set.add(b);
        System.out.println(a.hashCode() == b.hashCode());
        System.out.println(a.equals(b));
        System.out.println(set);
    }
}

輸出結果為:

true
False
[HashTest@1, HashTest@1]

以上這個示例,我們只是重寫了HashCode方法,從上面的結果可以看出,雖然兩個物件的HashCode相等,但是實際上兩個物件並不是相等因為我們沒有重寫equals方法,那麼就會呼叫Object預設的equals方法,顯示這是兩個不同的物件。

這裡我們將生成的物件放到了HashSet中,而HashSet中只能夠存放唯一的物件,也就是相同的(適用於equals方法)的物件只會存放一個,但是這裡實際上是兩個物件ab都被放到了HashSet中,這樣HashSet就失去了他本身的意義了。

下面我們繼續重寫equals方法:

public class HashTest {
    private int i;

    public int getI() {
        return i;
    }

    public void setI(int i) {
        this.i = i;
    }

    public boolean equals(Object object) {
        if (object == null) {
            return false;
        }
        if (object == this) {
            return true;
        }
        if (!(object instanceof HashTest)) {
            return false;
        }
        HashTest other = (HashTest) object;
        if (other.getI() == this.getI()) {
            return true;
        }
        return false;
    }

    public int hashCode() {
        return i % 10;
    }

    public final static void main(String[] args) {
        HashTest a = new HashTest();
        HashTest b = new HashTest();
        a.setI(1);
        b.setI(1);
        Set<HashTest> set = new HashSet<HashTest>();
        set.add(a);
        set.add(b);
        System.out.println(a.hashCode() == b.hashCode());
        System.out.println(a.equals(b));
        System.out.println(set);
    }
}

輸出結果如下所示。

從結果我們可以看出,現在兩個物件就完全相等了,HashSet中也只存放了一份物件。

注意:

hashCode()只是簡單示例寫的,真正的生產換將不是這樣的

true
true
[HashTest@1]

HashMap的hashcode的作用

hashCode的存在主要是用於查詢的快捷性,如Hashtable,HashMap等,hashCode是用來在雜湊儲存結構中確定物件的儲存地址的。

如果兩個物件相同,就是適用於equals(java.lang.Object) 方法,那麼這兩個物件的hashCode一定要相同。

如果物件的equals方法被重寫,那麼物件的hashCode也儘量重寫,並且產生hashCode使用的物件,一定要和equals方法中使用的一致,否則就會違反上面提到的第2點。

兩個物件的hashCode相同,並不一定表示兩個物件就相同,也就是不一定適用於equals(java.lang.Object) 方法,只能夠說明這兩個物件在雜湊儲存結構中,如Hashtable,他們“存放在同一個籃子裡”。

什麼時候需要重寫?

一般的地方不需要過載hashCode,只有當類需要放在HashTable、HashMap、HashSet等等hash結構的集合時才會過載hashCode,那麼為什麼要過載hashCode呢?

要比較兩個類的內容屬性值,是否相同時候,根據hashCode 重寫規則,重寫類的 指定欄位的hashCode(),equals()方法。

例如

public class EmpWorkCondition{

    /**
     * 員工ID
     */
    private Integer empId;

    /**
     * 員工服務總單數
     */
    private Integer orderSum;

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        EmpWorkCondition that = (EmpWorkCondition) o;
        return Objects.equals(empId, that.empId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(empId);
    }

    // 省略 getter setter
}
public static void main(String[] args) {

    List<EmpWorkCondition> list1 = new ArrayList<EmpWorkCondition>();

    EmpWorkCondition emp1 = new EmpWorkCondition();
    emp1.setEmpId(100);
    emp1.setOrderSum(90000);
    list1.add(emp1);

    List<EmpWorkCondition> list2 = new ArrayList<EmpWorkCondition>();

    EmpWorkCondition emp2 = new EmpWorkCondition();
    emp2.setEmpId(100);
    list2.add(emp2);

    System.out.println(list1.contains(emp2));

}

輸出結果:true

上面的方法,做的事情就是,比較兩個集合中的,實體類物件屬性值,是否一致

OrderSum 不在比較範圍內,因為沒有重寫它的,equals()和hashCode()方法

為什麼要過載equal方法?

因為Object的equal方法預設是兩個物件的引用的比較,意思就是指向同一記憶體,地址則相等,否則不相等;如果你現在需要利用物件裡面的值來判斷是否相等,則過載equal方法。

為什麼過載hashCode方法?

一般的地方不需要過載hashCode,只有當類需要放在HashTable、HashMap、HashSet等等hash結構的集合時才會過載hashCode,那麼為什麼要過載hashCode呢?

如果你重寫了equals,比如說是基於物件的內容實現的,而保留hashCode的實現不變,那麼很可能某兩個物件明明是“相等”,而hashCode卻不一樣。

這樣,當你用其中的一個作為鍵儲存到hashMap、hasoTable或hashSet中,再以“相等的”找另一個作為鍵值去查詢他們的時候,則根本找不到。

為什麼equals()相等,hashCode就一定要相等,而hashCode相等,卻不要求equals相等?

1、因為是按照hashCode來訪問小記憶體塊,所以hashCode必須相等。
2、HashMap獲取一個物件是比較key的hashCode相等和equal為true。

之所以hashCode相等,卻可以equal不等,就比如ObjectA和ObjectB他們都有屬性name,那麼hashCode都以name計算,所以hashCode一樣,但是兩個物件屬於不同型別,所以equal為false。

為什麼需要hashCode?

1、通過hashCode可以很快的查到小記憶體塊。
2、通過hashCode比較比equal方法快,當get時先比較hashCode,如果hashCode不同,直接返回false。

ArrayList、LinkedList、Vector的區別

List的三個子類的特點

ArrayList:
- 底層資料結構是陣列,查詢快,增刪慢。
- 執行緒不安全,效率高。

Vector:
- 底層資料結構是陣列,查詢快,增刪慢。
- 執行緒安全,效率低。
- Vector相對ArrayList查詢慢(執行緒安全的)。
- Vector相對LinkedList增刪慢(陣列結構)。

LinkedList
- 底層資料結構是連結串列,查詢慢,增刪快。
- 執行緒不安全,效率高。

Vector和ArrayList的區別
- Vector是執行緒安全的,效率低。
- ArrayList是執行緒不安全的,效率高。
- 共同點:底層資料結構都是陣列實現的,查詢快,增刪慢。

ArrayList和LinkedList的區別
- ArrayList底層是陣列結果,查詢和修改快。
- LinkedList底層是連結串列結構的,增和刪比較快,查詢和修改比較慢。

共同點:都是執行緒不安全的

List有三個子類使用

  • 查詢多用ArrayList。
  • 增刪多用LinkedList。
  • 如果都多ArrayList。

String、StringBuffer與StringBuilder的區別

String:適用於少量的字串操作的情況。
StringBuilder:適用於單執行緒下在字元緩衝區進行大量操作的情況。
StringBuffer:適用多執行緒下在字元緩衝區進行大量操作的情況。
StringBuilder:是執行緒不安全的,而StringBuffer是執行緒安全的。

這三個類之間的區別主要是在兩個方面,即執行速度和執行緒安全這兩方面。
首先說執行速度,或者說是執行速度,在這方面執行速度快慢為:StringBuilder > StringBuffer > String

String最慢的原因

String為字串常量,而StringBuilder和StringBuffer均為字串變數,即String物件一旦建立之後該物件是不可更改的,但後兩者的物件是變數,是可以更改的。

再來說執行緒安全

線上程安全上,StringBuilder是執行緒不安全的,而StringBuffer是執行緒安全的

如果一個StringBuffer物件在字串緩衝區被多個執行緒使用時,StringBuffer中很多方法可以帶有synchronized關鍵字,所以可以保證執行緒是安全的,但StringBuilder的方法則沒有該關鍵字,所以不能保證執行緒安全,有可能會出現一些錯誤的操作。所以如果要進行的操作是多執行緒的,那麼就要使用StringBuffer,但是在單執行緒的情況下,還是建議使用速度比較快的StringBuilder。

Map、Set、List、Queue、Stack的特點與用法

Map

  • Map是鍵值對,鍵Key是唯一不能重複的,一個鍵對應一個值,值可以重複。
  • TreeMap可以保證順序。
  • HashMap不保證順序,即為無序的。
  • Map中可以將Key和Value單獨抽取出來,其中KeySet()方法可以將所有的keys抽取正一個Set。而Values()方法可以將map中所有的values抽取成一個集合。

Set

  • 不包含重複元素的集合,set中最多包含一個null元素。
  • 只能用Lterator實現單項遍歷,Set中沒有同步方法。

List

  • 有序的可重複集合。
  • 可以在任意位置增加刪除元素。
  • 用Iterator實現單向遍歷,也可用ListIterator實現雙向遍歷。

Queue

  • Queue遵從先進先出原則。
  • 使用時儘量避免add()和remove()方法,而是使用offer()來新增元素,使用poll()來移除元素,它的優點是可以通過返回值來判斷是否成功。
  • LinkedList實現了Queue介面。
  • Queue通常不允許插入null元素。

Stack

  • Stack遵從後進先出原則。
  • Stack繼承自Vector。
  • 它通過五個操作對類Vector進行擴充套件,允許將向量視為堆疊,它提供了通常的push和pop操作,以及取堆疊頂點的peek()方法、測試堆疊是否為空的empty方法等。

用法

  • 如果涉及堆疊,佇列等操作,建議使用List。
  • 對於快速插入和刪除元素的,建議使用LinkedList。
  • 如果需要快速隨機訪問元素的,建議使用ArrayList。

更為精煉的總結

Collection 是物件集合, Collection 有兩個子介面 List 和 Set

List 可以通過下標 (1,2..) 來取得值,值可以重複。
Set 只能通過遊標來取值,並且值是不能重複的。

ArrayList , Vector , LinkedList 是 List 的實現類

  • ArrayList 是執行緒不安全的, Vector 是執行緒安全的,這兩個類底層都是由陣列實現的。
  • LinkedList 是執行緒不安全的,底層是由連結串列實現的。

Map 是鍵值對集合

  • HashTable 和 HashMap 是 Map 的實現類。
  • HashTable 是執行緒安全的,不能儲存 null 值。
  • HashMap 不是執行緒安全的,可以儲存 null 值。

Stack類:繼承自Vector,實現一個後進先出的棧。提供了幾個基本方法,push、pop、peak、empty、search等。

Queue介面:提供了幾個基本方法,offer、poll、peek等。已知實現類有LinkedList、PriorityQueue等。

HashMap和HashTable的區別

Hashtable是基於陳舊的Dictionary類的,HashMap是Java 1.2引進的Map介面的一個實現,它們都是集合中將資料無序存放的。

1、hashMap去掉了HashTable?的contains方法,但是加上了containsValue()和containsKey()方法

HashTable Synchronize同步的,執行緒安全,HashMap不允許空鍵值為空?,效率低。
HashMap 非Synchronize執行緒同步的,執行緒不安全,HashMap允許空鍵值為空?,效率高。
Hashtable是基於陳舊的Dictionary類的,HashMap是Java 1.2引進的Map介面的一個實現,它們都是集合中將資料無序存放的

Hashtable的方法是同步的,HashMap未經同步,所以在多執行緒場合要手動同步HashMap這個區別就像Vector和ArrayList一樣。

檢視Hashtable的原始碼就可以發現,除建構函式外,Hashtable的所有 public 方法宣告中都有 synchronized 關鍵字,而HashMap的原始碼中則連 synchronized 的影子都沒有,當然,註釋除外。

2、Hashtable不允許 null 值(key 和 value 都不可以),HashMap允許 null 值(key和value都可以)

3、兩者的遍歷方式大同小異,Hashtable僅僅比HashMap多一個elements方法

Hashtable table = new Hashtable();
table.put("key", "value");
Enumeration em = table.elements();
while (em.hasMoreElements()) {
   String obj = (String) em.nextElement();
   System.out.println(obj);
}

4、HashTable使用Enumeration,HashMap使用Iterator

從內部機制實現上的區別如下:

  1. 雜湊值的使用不同,Hashtable直接使用物件的hashCode
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;

而HashMap重新計算hash值,而且用與代替求模:

int hash = hash(k);
int i = indexFor(hash, table.length);

static int hash(Object x) {
  int h = x.hashCode();

  h += ~(h << 9);
  h ^= (h >>> 14);
  h += (h << 4);
  h ^= (h >>> 10);
  return h;
}
static int indexFor(int h, int length) {
  return h & (length-1);
  1. Hashtable中hash陣列預設大小是11,增加的方式是 old*2+1。HashMap中hash陣列的預設大小是16,而且一定是2的指數。

JDK7與JDK8中HashMap的實現

JDK7中的HashMap

HashMap底層維護一個數組,陣列中的每一項都是一個Entry。

transient Entry<K,V>[] table;

我們向 HashMap 中所放置的物件實際上是儲存在該陣列當中。
而Map中的key,value則以Entry的形式存放在陣列中。

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;

總結一下map.put後的過程

當向 HashMap 中 put 一對鍵值時,它會根據 key的 hashCode 值計算出一個位置, 該位置就是此物件準備往陣列中存放的位置。

如果該位置沒有物件存在,就將此物件直接放進陣列當中;如果該位置已經有物件存在了,則順著此存在的物件的鏈開始尋找(為了判斷是否是否值相同,map不允許

transient Node<K,V>[] table;

當衝突節點數不小於8-1時,轉換成紅黑樹。

static final int TREEIFY_THRESHOLD = 8;

HashMap和ConcurrentHashMap的區別,HashMap的底層原始碼

為了執行緒安全從ConcurrentHashMap程式碼中可以看出,它引入了一個“分段鎖”的概念,具體可以理解為把一個大的Map拆分成N個小的HashTable,根據key.hashCode()來決定把key放到哪個HashTable中。

Hashmap本質是陣列加連結串列。根據key取得hash值,然後計算出陣列下標,如果多個key對應到同一個下標,就用連結串列串起來,新插入的在前面。

ConcurrentHashMap:在hashMap的基礎上,ConcurrentHashMap將資料分為多個segment,預設16個(concurrency level),然後每次操作對一個segment加鎖,避免多執行緒鎖的機率,提高併發效率

總結

JDK6,7中的ConcurrentHashmap主要使用Segment來實現減小鎖粒度,把HashMap分割成若干個Segment,在put的時候需要鎖住Segment,get時候不加鎖,使用volatile來保證可見性,當要統計全域性時(比如size),首先會嘗試多次計算modcount來確定,這幾次嘗試中,是否有其他執行緒進行了修改操作,如果沒有,則直接返回size。如果有,則需要依次鎖住所有的Segment來計算。

jdk7中ConcurrentHashmap中,當長度過長碰撞會很頻繁,連結串列的增改刪查操作都會消耗很長的時間,影響效能

jdk8 中完全重寫了concurrentHashmap,程式碼量從原來的1000多行變成了 6000多 行,實現上也和原來的分段式儲存有很大的區別

JDK8中採用的是位桶+連結串列/紅黑樹(有關紅黑樹請檢視紅黑樹)的方式,也是非執行緒安全的。當某個位桶的連結串列的長度達到某個閥值的時候,這個連結串列就將轉換成紅黑樹。

JDK8中,當同一個hash值的節點數不小於8時,將不再以單鏈表的形式儲存了,會被調整成一顆紅黑樹(上圖中null節點沒畫)。這就是JDK7與JDK8中HashMap實現的最大區別。

主要設計上的變化有以下幾點

1.jdk8不採用segment而採用node,鎖住node來實現減小鎖粒度。
2.設計了MOVED狀態 當resize的中過程中 執行緒2還在put資料,執行緒2會幫助resize。
3.使用3個CAS操作來確保node的一些操作的原子性,這種方式代替了鎖。
4.sizeCtl的不同值來代表不同含義,起到了控制的作用。

至於為什麼JDK8中使用synchronized而不是ReentrantLock,我猜是因為JDK8中對synchronized有了足夠的優化吧。

ConcurrentHashMap能完全替代HashTable嗎

hashTable雖然效能上不如ConcurrentHashMap,但並不能完全被取代,兩者的迭代器的一致性不同的,hash table的迭代器是強一致性的,而concurrenthashmap是弱一致的。

ConcurrentHashMap的get,clear,iterator 都是弱一致性的。 Doug Lea 也將這個判斷留給使用者自己決定是否使用ConcurrentHashMap。

ConcurrentHashMap與HashTable都可以用於多執行緒的環境,但是當Hashtable的大小增加到一定的時候,效能會急劇下降,因為迭代時需要被鎖定很長的時間。因為ConcurrentHashMap引入了分割(segmentation),不論它變得多麼大,僅僅需要鎖定map的某個部分,而其它的執行緒不需要等到迭代完成才能訪問map。簡而言之,在迭代的過程中,ConcurrentHashMap僅僅鎖定map的某個部分,而Hashtable則會鎖定整個map。

那麼既然ConcurrentHashMap那麼優秀,為什麼還要有Hashtable的存在呢?ConcurrentHashMap能完全替代HashTable嗎?

HashTable雖然效能上不如ConcurrentHashMap,但並不能完全被取代,兩者的迭代器的一致性不同的,HashTable的迭代器是強一致性的,而ConcurrentHashMap是弱一致的
ConcurrentHashMap的get,clear,iterator 都是弱一致性的。 Doug Lea 也將這個判斷留給使用者自己決定是否使用ConcurrentHashMap。

那麼什麼是強一致性和弱一致性呢?

get方法是弱一致的,是什麼含義?可能你期望往ConcurrentHashMap底層資料結構中加入一個元素後,立馬能對get可見,但ConcurrentHashMap並不能如你所願。換句話說,put操作將一個元素加入到底層資料結構後,get可能在某段時間內還看不到這個元素,若不考慮記憶體模型,單從程式碼邏輯上來看,卻是應該可以看得到的。

下面將結合程式碼和java記憶體模型相關內容來分析下put/get方法。put方法我們只需關注Segment#put,get方法只需關注Segment#get,在繼續之前,先要說明一下Segment裡有兩個volatile變數:count和table;HashEntry裡有一個volatile變數:value。

總結

ConcurrentHashMap的弱一致性主要是為了提升效率,是一致性與效率之間的一種權衡。要成為強一致性,就得到處使用鎖,甚至是全域性鎖,這就與Hashtable和同步的HashMap一樣了。

為什麼HashMap是執行緒不安全的

HashMap 在併發執行 put 操作時會引起死迴圈,導致 CPU 利用率接近100%。因為多執行緒會導致 HashMap 的 Node 連結串列形成環形資料結構,一旦形成環形資料結構,Node 的 next 節點永遠不為空,就會在獲取 Node 時產生死迴圈。

如何執行緒安全的使用HashMap

瞭解了 HashMap 為什麼執行緒不安全,那現在看看如何執行緒安全的使用 HashMap。這個無非就是以下三種方式:

Hashtable
ConcurrentHashMap
Synchronized Map

Hashtable

例子

//Hashtable
Map<String, String> hashtable = new Hashtable<>();
//synchronizedMap
Map<String, String> synchronizedHashMap = Collections.synchronizedMap(new HashMap<String, String>());
//ConcurrentHashMap
Map<String, String> concurrentHashMap = new ConcurrentHashMap<>();
Hashtable

先稍微吐槽一下,為啥命名不是 HashTable 啊,看著好難受不管了就裝作它叫HashTable 吧。這貨已經不常用了,就簡單說說吧。HashTable 原始碼中是使用?synchronized?來保證執行緒安全的,比如下面的 get 方法和 put 方法:

public synchronized V get(Object key) {
   // 省略實現
}
public synchronized V put(K key, V value) {
// 省略實現
}

所以當一個執行緒訪問 HashTable 的同步方法時,其他執行緒如果也要訪問同步方法,會被阻塞住。舉個例子,當一個執行緒使用 put 方法時,另一個執行緒不但不可以使用 put 方法,連 get 方法都不可以,好霸道啊!!!so~~,效率很低,現在基本不會選擇它了。

ConcurrentHashMap

ConcurrentHashMap 於 Java 7 的,和8有區別,在8中 CHM 摒棄了 Segment(鎖段)的概念,而是啟用了一種全新的方式實現,利用 CAS 演算法,有時間會重新總結一下。

SynchronizedMap

synchronizedMap() 方法後會返回一個 SynchronizedMap 類的物件,而在 SynchronizedMap 類中使用了 synchronized 同步關鍵字來保證對 Map 的操作是執行緒安全的。

效能對比

這是要靠資料說話的時代,所以不能只靠嘴說 CHM 快,它就快了。寫個測試用例,實際的比較一下這三種方式的效率(原始碼來源),下面的程式碼分別通過三種方式建立 Map 物件,使用 ExecutorService 來併發執行5個執行緒,每個執行緒新增/獲取500K個元素。

Test started for: class java.util.Hashtable
2500K entried added/retrieved in 2018 ms
2500K entried added/retrieved in 1746 ms
2500K entried added/retrieved in 1806 ms
2500K entried added/retrieved in 1801 ms
2500K entried added/retrieved in 1804 ms
For class java.util.Hashtable the average time is 1835 ms

Test started for: class java.util.Collections$SynchronizedMap
2500K entried added/retrieved in 3041 ms
2500K entried added/retrieved in 1690 ms
2500K entried added/retrieved in 1740 ms
2500K entried added/retrieved in 1649 ms
2500K entried added/retrieved in 1696 ms
For class java.util.Collections$SynchronizedMap the average time is 1963 ms

Test started for: class java.util.concurrent.ConcurrentHashMap
2500K entried added/retrieved in 738 ms
2500K entried added/retrieved in 696 ms
2500K entried added/retrieved in 548 ms
2500K entried added/retrieved in 1447 ms
2500K entried added/retrieved in 531 ms
For class java.util.concurrent.ConcurrentHashMap the average time is 792 ms

ConcurrentHashMap 效能是明顯優於 Hashtable 和 SynchronizedMap 的,CHM 花費的時間比前兩個的一半還少。

多併發情況下HashMap是否還會產生死迴圈

今天本來想看下了ConcurrentHashMap的原始碼,ConcurrentHashMap是Java 5中支援高併發、高吞吐量的執行緒安全HashMap實現。

在看很多部落格在介紹ConcurrentHashMap之前,都說HashMap適用於單執行緒訪問,這是因為HashMap的所有方法都沒有進行鎖同步,因此是執行緒不安全的,不僅如此,當多執行緒訪問的時候還容易產生死迴圈。

雖然自己在前幾天的時候看過HashMap的原始碼,感覺思路啥啥的都還清楚,對於多執行緒訪問只知道HashMap是執行緒不安全的,但是不知道HashMap在多執行緒併發的情況下會產生死迴圈呢,為什麼會產生,何種情況下才會產生死迴圈呢???

《Java困惑》:多併發情況下HashMap是否還會產生死迴圈。

既然會產生死迴圈,為什麼併發情況下,還是用ConcurrentHashMap。
jdk 好像有,但是Jdk8 已經修復了這個問題。

TreeMap、HashMap、LindedHashMap的區別

LinkedHashMap可以保證HashMap集合有序,存入的順序和取出的順序一致。

TreeMap實現SortMap介面,能夠把它儲存的記錄根據鍵排序,預設是按鍵值的升序排序,也可以指定排序的比較器,當用Iterator遍歷TreeMap時,得到的記錄是排過序的。

HashMap不保證順序,即為無序的,具有很快的訪問速度。
HashMap最多隻允許一條記錄的鍵為Null;允許多條記錄的值為 Null。
HashMap不支援執行緒的同步。

我們在開發的過程中使用HashMap比較多,在Map中在Map 中插入、刪除和定位元素,HashMap 是最好的選擇

但如果您要按自然順序或自定義順序遍歷鍵,那麼TreeMap會更好

如果需要輸出的順序和輸入的相同,那麼用LinkedHashMap 可以實現,它還可以按讀取順序來排列

Collection包結構,與Collections的區別

Collection 是集合類的上級介面,子介面主要有Set、List 、Map。

Collecions 是針對集合類的一個幫助類, 提供了操作集合的工具方法,一系列靜態方法實現對各種集合的搜尋、排序線性、執行緒安全化等操作。

例如

Map<String, Object> map4 = Collections.synchronizedMap(new HashMap<String, Object>()); 執行緒安全 的HashMap
Collections.sort(List<T> list, Comparator<? super T> c); 排序 List

Collection

Collection 是單列集合

List

元素是有序的、可重複。
有序的 collection,可以對列表中每個元素的插入位置進行精確地控制。
可以根據元素的整數索引(在列表中的位置)訪問元素,並搜尋列表中的元素。
可存放重複元素,元素存取是有序的。

List介面中常用類

Vector:執行緒安全,但速度慢,已被ArrayList替代。底層資料結構是陣列結構。
ArrayList:執行緒不安全,查詢速度快。底層資料結構是陣列結構。
LinkedList:執行緒不安全。增刪速度快。底層資料結構是列表結構。

Set

Set介面中常用的類

Set(集) 元素無序的、不可重複。
取出元素的方法只有迭代器。不可以存放重複元素,元素存取是無序的。

HashSet:執行緒不安全,存取速度快。它是如何保證元素唯一性的呢?依賴的是元素的hashCode方法和euqals方法。
TreeSet:執行緒不安全,可以對Set集合中的元素進行排序。它的排序是如何進行的呢?通過compareTo或者compare方法中的來保證元素的唯一性。元素是以二叉樹的形式存放的。

Map

map是一個雙列集合

Hashtable:執行緒安全,速度快。底層是雜湊表資料結構。是同步的。不允許null作為鍵,null作為值。

Properties:用於配置檔案的定義和操作,使用頻率非常高,同時鍵和值都是字串。是集合中可以和IO技術相結合的物件。

HashMap:執行緒不安全,速度慢。底層也是雜湊表資料結構。是不同步的。允許null作為鍵,null作為值,替代了Hashtable。

LinkedHashMap: 可以保證HashMap集合有序。存入的順序和取出的順序一致。

TreeMap:可以用來對Map集合中的鍵進行排序

try?catch?finally,try裡有return,finally還執行麼

肯定會執行。finally{}塊的程式碼。
只有在try{}塊中包含遇到System.exit(0)
之類的導致Java虛擬機器直接退出的語句才會不執行。

當程式執行try{}遇到return時,程式會先執行return語句,但並不會立即返回——也就是把return語句要做的一切事情都準備好,也就是在將要返回、但並未返回的時候,程式把執行流程轉去執行finally塊,當finally塊執行完成後就直接返回剛才return語句已經準備好的結果。

Excption與Error包結構。OOM你遇到過哪些情況,SO F你遇到過哪些情況

Throwable是 Java 語言中所有錯誤或異常的超類。
Throwable包含兩個子類: Error 和 Exception 。它們通常用於指示發生了異常情況。
Throwable包含了其執行緒建立時執行緒執行堆疊的快照,它提供了printStackTrace()等介面用於獲取堆疊跟蹤資料等資訊。

Java將可丟擲(Throwable)的結構分為三種類型:

被檢查的異常(Checked Exception)。
執行時異常(RuntimeException)。
錯誤(Error)。

執行時異常RuntimeException

定義 : RuntimeException及其子類都被稱為執行時異常。
特點 : Java編譯器不會檢查它 也就是說,當程式中可能出現這類異常時,倘若既”沒有通過throws宣告丟擲它”,也”沒有用try-catch語句捕獲它”,還是會編譯通過。

例如,除數為零時產生的ArithmeticException異常,陣列越界時產生的IndexOutOfBoundsException異常,fail-fail機制產生的ConcurrentModificationException異常等,都屬於執行時異常。

堆記憶體溢位 OutOfMemoryError(OOM)

除了程式計數器外,虛擬機器記憶體的其他幾個執行時區域都有發生OutOfMemoryError(OOM)異常的可能。

Java Heap 溢位。
一般的異常資訊:java.lang.OutOfMemoryError:Java heap spacess。
java堆用於儲存物件例項,我們只要不斷的建立物件,並且保證GC Roots到物件之間有可達路徑來避免垃圾回收機制清除這些物件,就會在物件數量達到最大堆容量限制後產生記憶體溢位異常。

堆疊溢位 StackOverflow (SOF)

StackOverflowError 的定義:
當應用程式遞迴太深而發生堆疊溢位時,丟擲該錯誤。
因為棧一般預設為1-2m,一旦出現死迴圈或者是大量的遞迴呼叫,在不斷的壓棧過程中,造成棧容量超過1m而導致溢位。

棧溢位的原因:

遞迴呼叫。
大量迴圈或死迴圈。
全域性變數是否過多。
陣列、List、map資料過大。

Java(OOP)面向物件的三個特徵與含義

封裝(高內聚低耦合 –>解耦)

封裝是指將某事物的屬性和行為包裝到物件中,這個物件只對外公佈需要公開的屬性和行為,而這個公佈也是可以有選擇性的公佈給其它物件。在java中能使用private、protected、public三種修飾符或不用(即預設defalut)對外部物件訪問該物件的屬性和行為進行限制。

java的繼承(重用父類的程式碼)

繼承是子物件可以繼承父物件的屬性和行為,亦即父物件擁有的屬性和行為,其子物件也就擁有了這些屬性和行為。

java中的多型(父類引用指向子類物件)

多型是指父物件中的同一個行為能在其多個子物件中有不同的表現

有兩種多型的機制:編譯時多型、執行時多型

1、方法的過載:過載是指同一類中有多個同名的方法,但這些方法有著不同的引數。,因此在編譯時就可以確定到底呼叫哪個方法,它是一種編譯時多型。
2、方法的重寫:子類可以覆蓋父類的方法,因此同樣的方法會在父類中與子類中有著不同的表現形式。

Override和Overload的含義去區別

過載 Overload方法名相同,引數列表不同(個數、順序、型別不同)與返回型別無關。
重寫 Override 覆蓋。 將父類的方法覆蓋。
重寫方法重寫:方法名相同,訪問修飾符只能大於被重寫的方法訪問修飾符,方法簽名個數,順序個數型別相同。

Override(重寫)

  • 方法名、引數、返回值相同。
  • 子類方法不能縮小父類方法的訪問許可權。
  • 子類方法不能丟擲比父類方法更多的異常(但子類方法可以不丟擲異常)。
  • 存在於父類和子類之間。
  • 方法被定義為final不能被重寫。

Overload(過載)

  • 引數型別、個數、順序至少有一個不相同。
  • 不能過載只有返回值不同的方法名。
  • 存在於父類和子類、同類中。

而過載的規則

1、必須具有不同的引數列表。
2、可以有不同的返回型別,只要引數列表不同就可以了。
3、可以有不同的訪問修飾符。
4、可以丟擲不同的異常。

重寫方法的規則

1、引數列表必須完全與被重寫的方法相同,否則不能稱其為重寫而是過載。
2、返回的型別必須一直與被重寫的方法的返回型別相同,否則不能稱其為重寫而是過載。
3、訪問修飾符的限制一定要大於被重寫方法的訪問修飾符(public>protected>default>private)。
4、重寫方法一定不能丟擲新的檢查異常或者比被重寫方法申明更加寬泛的檢查型異常。

例如:
父類的一個方法申明瞭一個檢查異常IOException,在重寫這個方法是就不能丟擲Exception,只能丟擲IOException的子類異常,可以丟擲非檢查異常。

Interface與abstract類的區別

Interface 只能有成員常量,只能是方法的宣告。
Abstract class可以有成員變數,可以宣告普通方法和抽象方法。

interface是介面,所有的方法都是抽象方法,成員變數是預設的public static final 型別。介面不能例項化自己

abstract class是抽象類,至少包含一個抽象方法的累叫抽象類,抽象類不能被自身例項化,並用abstract關鍵字來修飾

Static?class?與non?static?class的區別

static class(內部靜態類)

1、用static修飾的是內部類,此時這個內部類變為靜態內部類;對測試有用。
2、內部靜態類不需要有指向外部類的引用。
3、靜態類只能訪問