2w字長文!手擼一套 Java 基礎面試題

程式設計師cxuan發表於2020-06-12

Java 基礎篇

Java 有哪些特點

  • 併發性的: 你可以在其中執行許多語句,而不必一次執行它
  • 物件導向的:基於類和麵向物件的程式語言。
  • 獨立性的: 支援一次編寫,到處執行的獨立程式語言,即編譯後的程式碼可以在支援 Java 的所有平臺上執行。

Java 的特性

Java 的特性有如下這幾點

  • 簡單,Java 會讓你的工作變得更加輕鬆,使你把關注點放在主要業務邏輯上,而不必關心指標、運算子過載、記憶體回收等與主要業務無關的功能。

  • 便攜性,Java 是平臺無關性的,這意味著在一個平臺上編寫的任何應用程式都可以輕鬆移植到另一個平臺上。

  • 安全性, 編譯後會將所有的程式碼轉換為位元組碼,人類無法讀取。它使開發無病毒,無篡改的系統/應用成為可能。

  • 動態性,它具有適應不斷變化的環境的能力,它能夠支援動態記憶體分配,從而減少了記憶體浪費,提高了應用程式的效能。

  • 分散式,Java 提供的功能有助於建立分散式應用。使用遠端方法呼叫(RMI),程式可以通過網路呼叫另一個程式的方法並獲取輸出。您可以通過從網際網路上的任何計算機上呼叫方法來訪問檔案。這是革命性的一個特點,對於當今的網際網路來說太重要了。

  • 健壯性,Java 有強大的記憶體管理功能,在編譯和執行時檢查程式碼,它有助於消除錯誤。

  • 高效能,Java 最黑的科技就是位元組碼程式設計,Java 程式碼編譯成的位元組碼可以輕鬆轉換為本地機器程式碼。通過 JIT 即時編譯器來實現高效能。

  • 解釋性,Java 被編譯成位元組碼,由 Java 執行時環境解釋。

  • 多執行緒性,Java支援多個執行執行緒(也稱為輕量級程式),包括一組同步原語。這使得使用執行緒程式設計更加容易,Java 通過管程模型來實現執行緒安全性。

描述一下值傳遞和引用傳遞的區別

要想真正理解的話,可以參考這篇文章 : https://www.zhihu.com/question/31203609

簡單理解的話就是

值傳遞是指在呼叫函式時將實際引數複製一份到函式中,這樣的話如果函式對其傳遞過來的形式引數進行修改,將不會影響到實際引數

引用傳遞 是指在呼叫函式時將物件的地址直接傳遞到函式中,如果在對形式引數進行修改,將影響到實際引數的值。

== 和 equals 區別是什麼

== 是 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。

String 中的 equals 是如何重寫的

String 代表的是 Java 中的字串,String 類比較特殊,它整個類都是被 final 修飾的,也就是說,String 不能被任何類繼承,任何 修改 String 字串的方法都是建立了一個新的字串。

equals 方法是 Object 類定義的方法,Object 是所有類的父類,當然也包括 String,String 重寫了 equals 方法,下面我們來看看是怎麼重寫的

  • 首先會判斷要比較的兩個字串它們的引用是否相等。如果引用相等的話,直接返回 true ,不相等的話繼續下面的判斷
  • 然後再判斷被比較的物件是否是 String 的例項,如果不是的話直接返回 false,如果是的話,再比較兩個字串的長度是否相等,如果長度不想等的話也就沒有比較的必要了;長度如果相同,會比較字串中的每個 字元 是否相等,一旦有一個字元不相等,就會直接返回 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 方法

equals 方法和 hashCode 都是 Object 中定義的方法,它們經常被一起重寫。

equals 方法是用來比較物件大小是否相等的方法,hashcode 方法是用來判斷每個物件 hash 值的一種方法。如果只重寫 equals 方法而不重寫 hashcode 方法,很可能會造成兩個不同的物件,它們的 hashcode 也相等,造成衝突。比如

String str1 = "通話";
String str2 = "重地";

它們兩個的 hashcode 相等,但是 equals 可不相等。

我們來看一下 hashCode 官方的定義

總結起來就是

  • 如果在 Java 執行期間對同一個物件呼叫 hashCode 方法後,無論呼叫多少次,都應該返回相同的 hashCode,但是在不同的 Java 程式中,執行 hashCode 方法返回的值可能不一致。
  • 如果兩個物件的 equals 相等,那麼 hashCode 必須相同
  • 如果兩個物件 equals 不相等,那麼 hashCode 也有可能相同,所以需要重寫 hashCode 方法,因為你不知道 hashCode 的底層構造(反正我是不知道,有大牛可以傳授傳授),所以你需要重寫 hashCode 方法,來為不同的物件生成不同的 hashCode 值,這樣能夠提高不同物件的訪問速度。
  • hashCode 通常是將地址轉換為整數來實現的。

String s1 = new String("abc") 在記憶體中建立了幾個物件

一個或者兩個,String s1 是宣告瞭一個 String 型別的 s1 變數,它不是物件。使用 new 關鍵字會在堆中建立一個物件,另外一個物件是 abc ,它會在常量池中建立,所以一共建立了兩個物件;如果 abc 在常量池中已經存在的話,那麼就會建立一個物件。

詳細請翻閱筆者的另外一篇文章 一篇與眾不同的 String、StringBuffer、StringBuilde 詳解

String 為什麼是不可變的、jdk 原始碼中的 String 如何定義的、為什麼這麼設計。

首先了解一下什麼是不可變物件,不可變物件就是一經建立後,其物件的內部狀態不能被修改,啥意思呢?也就是說不可變物件需要遵守下面幾條原則

  • 不可變物件的內部屬性都是 final 的
  • 不可變物件的內部屬性都是 private 的
  • 不可變物件不能提供任何可以修改內部狀態的方法、setter 方法也不行
  • 不可變物件不能被繼承和擴充套件

與其說問 String 為什麼是不可變的,不如說如何把 String 設計成不可變的。

String 類是一種物件,它是獨立於 Java 基本資料型別而存在的,String 你可以把它理解為字串的集合,String 被設計為 final 的,表示 String 物件一經建立後,它的值就不能再被修改,任何對 String 值進行修改的方法就是重新建立一個字串。String 物件建立後會存在於執行時常量池中,執行時常量池是屬於方法區的一部分,JDK1.7 後把它移到了堆中。

不可變物件不是真的不可變,可以通過反射來對其內部的屬性和值進行修改,不過一般我們不這樣做。

static 關鍵字是幹什麼用的?談談你的理解

static 是 Java 中非常重要的關鍵字,static 表示的概念是 靜態的,在 Java 中,static 主要用來

  • 修飾變數,static 修飾的變數稱為靜態變數、也稱為類變數,類變數屬於類所有,對於不同的類來說,static 變數只有一份,static 修飾的變數位於方法區中;static 修飾的變數能夠直接通過 類名.變數名 來進行訪問,不用通過例項化類再進行使用。
  • 修飾方法,static 修飾的方法被稱為靜態方法,靜態方法能夠直接通過 類名.方法名 來使用,在靜態方法內部不能使用非靜態屬性和方法
  • static 可以修飾程式碼塊,主要分為兩種,一種直接定義在類中,使用 static{},這種被稱為靜態程式碼塊,一種是在類中定義靜態內部類,使用 static class xxx 來進行定義。
  • static 可以用於靜態導包,通過使用 import static xxx 來實現,這種方式一般不推薦使用
  • static 可以和單例模式一起使用,通過雙重檢查鎖來實現執行緒安全的單例模式。

詳情請參考這篇文章 一篇 static 還能難得住我?

final 關鍵字是幹什麼用的?談談你的理解

final 是 Java 中的關鍵字,它表示的意思是 不可變的,在 Java 中,final 主要用來

  • 修飾類,final 修飾的類不能被繼承,不能被繼承的意思就是不能使用 extends 來繼承被 final 修飾的類。
  • 修飾變數,final 修飾的變數不能被改寫,不能被改寫的意思有兩種,對於基本資料型別來說,final 修飾的變數,其值不能被改變,final 修飾的物件,物件的引用不能被改變,但是物件內部的屬性可以被修改。final 修飾的變數在某種程度上起到了不可變的效果,所以,可以用來保護只讀資料,尤其是在併發程式設計中,因為明確的不能再為 final 變數進行賦值,有利於減少額外的同步開銷。
  • 修飾方法,final 修飾的方法不能被重寫。
  • final 修飾符和 Java 程式效能優化沒有必然聯絡

抽象類和介面的區別是什麼

抽象類和介面都是 Java 中的關鍵字,抽象類和介面中都允許進行方法的定義,而不用具體的方法實現。抽象類和介面都允許被繼承,它們廣泛的應用於 JDK 和框架的原始碼中,來實現多型和不同的設計模式。

不同點在於

  • 抽象級別不同:類、抽象類、介面其實是三種不同的抽象級別,抽象程度依次是 介面 > 抽象類 > 類。在介面中,只允許進行方法的定義,不允許有方法的實現,抽象類中可以進行方法的定義和實現;而類中只允許進行方法的實現,我說的方法的定義是不允許在方法後面出現 {}
  • 使用的關鍵字不同:類使用 class 來表示;抽象類使用 abstract class 來表示;介面使用 interface 來表示
  • 變數:介面中定義的變數只能是公共的靜態常量,抽象類中的變數是普通變數。

重寫和過載的區別

在 Java 中,重寫和過載都是對同一方法的不同表現形式,下面我們針對重寫和過載做一下簡單的區分

  • 子父級關係不同,重寫是針對子級和父級的不同表現形式,而過載是在同一類中的不同表現形式;
  • 概念不同,子類重寫父類的方法一般使用 @override 來表示;重寫後的方法其方法的宣告和引數型別、順序必須要與父類完全一致;過載是針對同一類中概念,它要求過載的方法必須滿足下面任何一個要求:方法引數的順序,引數的個數,引數的型別任意一個保持不同即可。

byte的取值範圍是多少,怎麼計算出來的

byte 的取值範圍是 -128 -> 127 之間,一共是 256 位。一個 byte 型別在計算機中佔據一個位元組,那麼就是 8 bit,所以最大就是 2^8 = 1111 1111。

Java 中用補碼來表示二進位制數,補碼的最高位是符號位,最高位用 0 表示正數,最高位 1 表示負數,正數的補碼就是其本身,由於最高位是符號位,所以正數表示的就是 0111 1111 ,也就是 127。最大負數就是 1111 1111,這其中會涉及到兩個 0 ,一個 +0 ,一個 -0 ,+0 歸為正數,也就是 0 ,-0 歸為負數,也就是 -128,所以 byte 的範圍就是 -128 - 127。

HashMap 和 HashTable 的區別

相同點

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 重複。

  • 執行緒安全性:HashMap 不是執行緒安全的,如果多個外部操作同時修改 HashMap 的資料結構比如 add 或者是 delete,必須進行同步操作,僅僅對 key 或者 value 的修改不是改變資料結構的操作。可以選擇構造執行緒安全的 Map 比如 Collections.synchronizedMap 或者是 ConcurrentHashMap。而 HashTable 本身就是執行緒安全的容器。
  • 效能方面:雖然 HashMap 和 HashTable 都是基於單連結串列的,但是 HashMap 進行 put 或者 get? 操作,可以達到常數時間的效能;而 HashTable 的 put 和 get 操作都是加了 synchronized 鎖的,所以效率很差。

  • 初始容量不同:HashTable 的初始長度是11,之後每次擴充容量變為之前的 2n+1(n為上一次的長度)而 HashMap 的初始長度為16,之後每次擴充變為原來的兩倍。建立時,如果給定了容量初始值,那麼HashTable 會直接使用你給定的大小,而 HashMap 會將其擴充為2的冪次方大小。

HashMap 和 HashSet 的區別

HashSet 繼承於 AbstractSet 介面,實現了 Set、Cloneable,、java.io.Serializable 介面。HashSet 不允許集合中出現重複的值。HashSet 底層其實就是 HashMap,所有對 HashSet 的操作其實就是對 HashMap 的操作。所以 HashSet 也不保證集合的順序,也不是執行緒安全的容器。

HashMap 的底層結構

JDK1.7 中,HashMap 採用位桶 + 連結串列的實現,即使用連結串列來處理衝突,同一 hash 值的連結串列都儲存在一個陣列中。但是當位於一個桶中的元素較多,即 hash 值相等的元素較多時,通過 key 值依次查詢的效率較低。

所以,與 JDK 1.7 相比,JDK 1.8 在底層結構方面做了一些改變,當每個桶中元素大於 8 的時候,會轉變為紅黑樹,目的就是優化查詢效率。

HashMap 的長度為什麼是 2 的冪次方

這道題我想了幾天,之前和群裡小夥伴們探討每日一題的時候,問他們為什麼 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 多執行緒操作導致死迴圈問題

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 執行緒安全的實現有哪些

因為 HashMap 不是一個執行緒安全的容器,所以併發場景下推薦使用 ConcurrentHashMap ,或者使用執行緒安全的 HashMap,使用 Collections 包下的執行緒安全的容器,比如說

Collections.synchronizedMap(new HashMap());

還可以使用 HashTable ,它也是執行緒安全的容器,基於 key-value 儲存,經常用 HashMap 和 HashTable 做比較就是因為 HashTable 的資料結構和 HashMap 相同。

上面效率最高的就是 ConcurrentHashMap。

講一下 HashMap put 的過程

首先會使用 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 中是否是新構建的,如果是的話會首先進行 resize
  • 然後判斷需要插入的元素在 HashMap 中是否已經存在(說明出現了碰撞情況),如果不存在,直接生成新的k-v 節點存放,再判斷是否需要擴容。
  • 如果要插入的元素已經存在的話,說明發生了衝突,這就會轉換成連結串列或者紅黑樹來解決衝突,首先判斷連結串列中的 hash,key 是否相等,如果相等的話,就用新值替換舊值,如果節點是屬於 TreeNode 型別,會直接在紅黑樹中進行處理,如果 hash ,key 不相等也不屬於 TreeNode 型別,會直接轉換為連結串列處理,進行連結串列遍歷,如果連結串列的 next 節點是 null,判斷是否轉換為紅黑樹,如果不轉換的話,在遍歷過程中找到 key 完全相等的節點,則用新節點替換老節點

ConcurrentHashMap 底層實現

ConcurrentHashMap 是執行緒安全的 Map,它也是高併發場景下的首選資料結構,ConcurrentHashMap 底層是使用分段鎖來實現的。

Integer 快取池

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 的私有系統屬性中。

UTF-8 和 Unicode 的關係

由於每個國家都有自己獨有的字元編碼,所以Unicode 的發展旨在建立一個新的標準,用來對映當今使用的大多數語言中的字元,這些字元有一些不是必要的,但是對於建立文字來說卻是不可或缺的。Unicode 統一了所有字元的編碼,是一個 Character Set,也就是字符集,字符集只是給所有的字元一個唯一編號,但是卻沒有規定如何儲存,不同的字元其儲存空間不一樣,有的需要一個位元組就能儲存,有的則需要2、3、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 中的所有字元,又能有效減少儲存傳輸過程中佔用的空間。

專案為 UTF-8 環境,char c = '中',是否合法

可以,因為 Unicode 編碼採用 2 個位元組的編碼,UTF-8 是 Unicode 的一種實現,它使用可變長度的字符集進行編碼,char c = '中' 是兩個位元組,所以能夠儲存。合法。

Arrays.asList 獲得的 List 應該注意什麼

Arrays.asList 是 Array 中的一個靜態方法,它能夠實現把陣列轉換成為 List 序列,需要注意下面幾點

  • Arrays.asList 轉換完成後的 List 不能再進行結構化的修改,什麼是結構化的修改?就是不能再進行任何 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 方法,所以支援其可以對元素進行修改。

  • Arrays.asList 不支援基礎型別的轉換

Java 中的基礎資料型別(byte,short,int,long,float,double,boolean)是不支援使用 Arrays.asList 方法去轉換的

Collection 和 Collections 的區別

Collection 和 Collections 都是位於 java.util 包下的類

Collection 是集合類的父類,它是一個頂級介面,大部分抽象類比如說 AbstractListAbstractSet 都繼承了 Collection 類,Collection 類只定義一節標準方法比如說 add、remove、set、equals 等,具體的方法由抽象類或者實現類去實現。

Collections 是集合類的工具類,Collections 提供了一些工具類的基本使用

  • sort 方法,對當前集合進行排序, 實現 Comparable 介面的類,只能使用一種排序方案,這種方案叫做自然比較
  • 比如實現執行緒安全的容器 Collections.synchronizedListCollections.synchronizedMap
  • reverse 反轉,使用 reverse 方法可以根據元素的自然順序 對指定列表按降序進行排序。
  • fill,使用指定元素替換指定列表中的所有元素。

有很多用法,讀者可以翻閱 Collections 的原始碼檢視,Collections 不能進行例項化,所以 Collections 中的方法都是由 Collections.方法 直接呼叫。

你知道 fail-fast 和 fail-safe 嗎

fail-fast 是 Java 中的一種快速失敗機制,java.util 包下所有的集合都是快速失敗的,快速失敗會丟擲 ConcurrentModificationException 異常,fail-fast 你可以把它理解為一種快速檢測機制,它只能用來檢測錯誤,不會對錯誤進行恢復,fail-fast 不一定只在多執行緒環境下存在,ArrayList 也會丟擲這個異常,主要原因是由於 modCount 不等於 expectedModCount

fail-safe 是 Java 中的一種 安全失敗 機制,它表示的是在遍歷時不是直接在原集合上進行訪問,而是先複製原有集合內容,在拷貝的集合上進行遍歷。 由於迭代時是對原集合的拷貝進行遍歷,所以在遍歷過程中對原集合所作的修改並不能被迭代器檢測到,所以不會觸發 ConcurrentModificationException。java.util.concurrent 包下的容器都是安全失敗的,可以在多執行緒條件下使用,併發修改。

ArrayList、LinkedList 和 Vector 的區別

這也是一道老生常談的問題了

ArrayList、LinkedList、Vector 都是位於 java.util 包下的工具類,它們都實現了 List 介面。

  • ArrayList 的底層是動態陣列,它是基於陣列的特性而演變出來的,所以ArrayList 遍歷訪問非常快,但是增刪比較慢,因為會涉及到陣列的拷貝。ArrayList 是一個非執行緒安全的容器,在併發場景下會造成問題,如果想使用執行緒安全的容器的話,推薦使用 Collections.synchronizedList;ArrayList 在擴容時會增加 50% 的容量。
  • LinkedList 的底層是雙向連結串列,所以 LinkedList 的增加和刪除非常快,只需把元素刪除,把各自的指標指向新的元素即可。但是 LinkedList 遍歷比較慢,因為只有每次訪問一個元素才能知道下一個元素的值。LinkedList 也是一個非執行緒安全的容器,推薦使用 Collections.synchronizedList
  • Vector 向量是最早出現的集合容器,Vector 是一個執行緒安全的容器,它的每個方法都粗暴的加上了 synchronized 鎖,所以它的增刪、遍歷效率都很低。Vector 在擴容時,它的容量會增加一倍。

Exception 和 Error 有什麼區別

Exception 泛指的是 異常,Exception 主要分為兩種異常,一種是編譯期出現的異常,稱為 checkedException ,一種是程式執行期間出現的異常,稱為 uncheckedException,常見的 checkedException 有 IOException,uncheckedException 統稱為 RuntimeException,常見的 RuntimeException 主要有NullPointerExceptionIllegalArgumentExceptionArrayIndexOutofBoundException等,Exception 可以被捕獲。

Error 是指程式執行過程中出現的錯誤,通常情況下會造成程式的崩潰,Error 通常是不可恢復的,Error 不能被捕獲。

詳細可以參考這篇文章 看完這篇 Exception 和 Error ,和麵試官扯皮就沒問題了

String、StringBuilder 和 StringBuffer 有什麼區別

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 的常規操作。

動態代理是基於什麼原理

代理一般分為靜態代理動態代理,它們都是代理模式的一種應用,靜態代理指的是在程式執行前已經編譯好,程式知道由誰來執行代理方法。

而動態代理只有在程式執行期間才能確定,相比於靜態代理, 動態代理的優勢在於可以很方便的對代理類的函式進行統一的處理,而不用修改每個代理類中的方法。可以說動態代理是基於 反射 實現的。通過反射我們可以直接操作類或者物件,比如獲取類的定義,獲取宣告的屬性和方法,呼叫方法,在執行時可以修改類的定義。

動態代理是一種在執行時構建代理、動態處理方法呼叫的機制。動態代理的實現方式有很多,Java 提供的代理被稱為 JDK 動態代理,JDK 動態代理是基於類的繼承。

int 和 Integer 的區別

int 和 Integer 區別可就太多了

  • int 是 Java 中的基本資料型別,int 代表的是 整型,一個 int 佔 4 位元組,也就是 32 位,int 的初始值是預設值是 0 ,int 在 Java 記憶體模型中被分配在棧中,int 沒有方法。
  • Integer 是 Java 中的基本資料型別的包裝類,Integer 是一個物件,Integer 可以進行方法呼叫,Integer 的預設值是 null,Integer 在 Java 記憶體模型中被分配在堆中。int 和 Integer 在計算時可以進行相互轉換,int -> Integer 的過程稱為 裝箱,Integer -> int 的過程稱為 拆箱,Integer 還有 IntegerCache ,會自動快取 -128 - 127 中的值

Java 提供了哪些 I/O 方式

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 解決完了。

談談你知道的設計模式

一張思維導圖鎮場

比如全域性唯一性可以用 單例模式

可以使用 策略模式 優化過多的 if...else...

制定標準用 模版模式

接手其他人的鍋,但不想改原來的類用 介面卡模式

使用 組合 而不是繼承

使用 裝飾器可以製作加糖、加乳酪的咖啡

代理 可以用於任何中間商......

Comparator 和 Comparable 有什麼不同

  • Comparable 更像是自然排序

  • Comparator 更像是定製排序

同時存在時採用 Comparator(定製排序)的規則進行比較。

對於一些普通的資料型別(比如 String, Integer, Double…),它們預設實現了Comparable 介面,實現了 compareTo 方法,我們可以直接使用。

而對於一些自定義類,它們可能在不同情況下需要實現不同的比較策略,我們可以新建立 Comparator 介面,然後使用特定的 Comparator 實現進行比較。

Object 類中一般都有哪些方法

Object 類是所有物件的父類,它裡面包含一些所有物件都能夠使用的方法

  • hashCode():用於計算物件的雜湊碼
  • equals():用於物件之間比較值是否相等
  • toString(): 用於把物件轉換成為字串
  • clone(): 用於物件之間的拷貝
  • wait(): 用於實現物件之間的等待
  • notify(): 用於通知物件釋放資源
  • notifyAll(): 用於通知所有物件釋放資源
  • finalize(): 用於告知垃圾回收器進行垃圾回收
  • getClass(): 用於獲得物件類

Java 泛型和型別擦除

關於 Java 泛型和擦除看著一篇就夠了。

反射的基本原理,反射建立類例項的三種方式是什麼

反射機制就是使 Java 程式在執行時具有自省(introspect) 的能力,通過反射我們可以直接操作類和物件,比如獲取某個類的定義,獲取類的屬性和方法,構造方法等。

建立類例項的三種方式是

  • 物件例項.getClass();
  • 通過 Class.forName() 建立
  • 物件例項.newInstance() 方法建立

強引用、若引用、虛引用和幻象引用的區別

我們說的不同的引用型別其實都是邏輯上的,而對於虛擬機器來說,主要體現的是物件的不同的可達性(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、finally 和 finalize() 的區別

這三者可以說是沒有任何關聯之處,我們上面談到了,final 可以用來修飾類、變數和方法,可以參考上面 final 的那道面試題。

finally 是一個關鍵字,它經常和 try 塊一起使用,用於異常處理。使用 try...finally 的程式碼塊種,finally 部分的程式碼一定會被執行,所以我們經常在 finally 方法中用於資源的關閉操作。

JDK1.7 中,推薦使用 try-with-resources 優雅的關閉資源,它直接使用 try(){} 進行資源的關閉即可,就不用寫 finally 關鍵字了。

finalize 是 Object 物件中的一個方法,用於物件的回收方法,這個方法我們一般不推薦使用,finalize 是和垃圾回收關聯在一起的,在 Java 9 中,將 finalize 標記為了 deprecated, 如果沒有特別原因,不要實現 finalize 方法,也不要指望他來進行垃圾回收。

內部類有哪些分類,分別解釋一下

在 Java 中,可以將一個類的定義放在另外一個類的定義內部,這就是內部類。內部類本身就是類的一個屬性,與其他屬性定義方式一致。

內部類的分類一般主要有四種

  • 成員內部類
  • 區域性內部類
  • 匿名內部類
  • 靜態內部類

靜態內部類就是定義在類內部的靜態類,靜態內部類可以訪問外部類所有的靜態變數,而不可訪問外部類的非靜態變數;

成員內部類 就是定義在類內部,成員位置上的非靜態類,就是成員內部類。成員內部類可以訪問外部類所有的變數和方法,包括靜態和非靜態,私有和公有。

定義在方法中的內部類,就是區域性內部類。定義在例項方法中的區域性類可以訪問外部類的所有變數和方法,定義在靜態方法中的區域性類只能訪問外部類的靜態變數和方法。

匿名內部類 就是沒有名字的內部類,除了沒有名字,匿名內部類還有以下特點:

  • 匿名內部類必須繼承一個抽象類或者實現一個介面
  • 匿名內部類不能定義任何靜態成員和靜態方法。
  • 當所在的方法的形參需要被匿名內部類使用時,必須宣告為 final。
  • 匿名內部類不能是抽象的,它必須要實現繼承的類或者實現的介面的所有抽象方法。

說出幾種常用的異常

  • NullPointerException: 空指標異常
  • NoSuchMethodException:找不到方法
  • IllegalArgumentException:不合法的引數異常
  • IndexOutOfBoundException: 陣列下標越界異常
  • IOException:由於檔案未找到、未開啟或者I/O操作不能進行而引起異常
  • ClassNotFoundException :找不到檔案所丟擲的異常
  • NumberFormatException: 字元的UTF程式碼資料格式有錯引起異常;
  • InterruptedException: 執行緒中斷丟擲的異常

靜態繫結和動態繫結的區別

一個Java 程式要經過編寫、編譯、執行三個步驟,其中編寫程式碼不在我們討論的範圍之內,那麼我們的重點自然就放在了編譯執行這兩個階段,由於編譯和執行階段過程相當繁瑣,下面就我的理解來進行解釋:

Java 程式從原始檔建立到程式執行要經過兩大步驟:

1、編譯時期是由編譯器將原始檔編譯成位元組碼的過程

2、位元組碼檔案由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() ,觸發了動態繫結機制。

動態繫結的過程

  1. 虛擬機器提取物件的實際型別的方法表;
  2. 虛擬機器搜尋方法簽名;
  3. 呼叫方法。

動態繫結和靜態繫結的特點

靜態繫結

靜態繫結在編譯時期觸發,那麼它的主要特點是

1、編譯期觸發,能夠提早知道程式碼錯誤

2、提高程式執行效率

動態繫結

1、使用動態繫結的前提條件能夠提高程式碼的可用性,使程式碼更加靈活。

2、多型是設計模式的基礎,能夠降低耦合性。

我發起了一個 Github 專案,地址是 https://github.com/crisxuan/bestJavaer,歡迎各位 star,下面是具體介紹

相關文章