hey guys ,這不是也到了面試季了么,cxuan 又打算重新寫一下 Java 相關的面試題,先從基礎的開始吧,這些面試題屬于基礎系列,不包含多執行緒相關面試題和 JVM 相關面試題,多執行緒和 JVM 的我放在后面了,下面不多說,搞起!
Java 基礎篇
Java 有哪些特點
并發性的: 你可以在其中執行許多陳述句,而不必一次執行它面向物件的:基于類和面向物件的編程語言,獨立性的: 支持一次撰寫,到處運行的獨立編程語言,即編譯后的代碼可以在支持 Java 的所有平臺上運行,

Java 的特性
Java 的特性有如下這幾點
簡單,Java 會讓你的作業變得更加輕松,使你把關注點放在主要業務邏輯上,而不必關心指標、運算子多載、記憶體回收等與主要業務無關的功能,便攜性,Java 是平臺無關性的,這意味著在一個平臺上撰寫的任何應用程式都可以輕松移植到另一個平臺上,安全性, 編譯后會將所有的代碼轉換為位元組碼,人類無法讀取,它使開發無病毒,無篡改的系統/應用成為可能,動態性,它具有適應不斷變化的環境的能力,它能夠支持動態記憶體分配,從而減少了記憶體浪費,提高了應用程式的性能,分布式,Java 提供的功能有助于創建分布式應用,使用遠程方法呼叫(RMI),程式可以通過網路呼叫另一個程式的方法并獲取輸出,您可以通過從互聯網上的任何計算機上呼叫方法來訪問檔案,這是革命性的一個特點,對于當今的互聯網來說太重要了,健壯性,Java 有強大的記憶體管理功能,在編譯和運行時檢查代碼,它有助于消除錯誤,高性能,Java 最黑的科技就是位元組碼編程,Java 代碼編譯成的位元組碼可以輕松轉換為本地機器代碼,通過 JIT 即時編譯器來實作高性能,解釋性,Java 被編譯成位元組碼,由 Java 運行時環境解釋,多執行緒性,Java支持多個執行執行緒(也稱為輕量級行程),包括一組同步原語,這使得使用執行緒編程更加容易,Java 通過管程模型來實作執行緒安全性,
面向物件的特征有哪些
面向物件的特征主要有三點
-
封裝:封裝是面向物件的特征之一,是物件和類概念的主要特性,封裝,也就是把客觀事物封裝成抽象的類,并且類可以把自己的資料和方法只讓可信的類或者物件操作,對不可信的進行資訊隱藏,
-
繼承:繼承指的是使用現有類的所有功能,并在無需重新撰寫原來的類的情況下對這些功能進行擴展,
-
多型:多型是允許你將父物件設定成為和一個或更多的他的子物件相等的技術,賦值之后,父物件就可以根據當前賦值給它的子物件的特性以不同的方式運作,簡單的說,就是一句話:允許將子型別別的指標賦值給父型別別的指標,
JDK 和 JRE 有什么區別
- JRE 的英文名稱是 Java Runtime Environment,Java 運行時環境,它主要包含兩個部分,jvm 的標準實作和 Java 的一些基本類別庫,它相對于 jvm 來說,多出來的是一部分的 Java 類別庫,
- JDK 的英文名稱是 Java Development Kit,Java 開發工具包,jdk 是整個 Java 開發的核心,它集成了 jre 和一些好用的小工具,例如:javac.exe,java.exe,jar.exe 等,
這里還需要解釋一下 JVM 是什么
- JVM 的英文名稱是 Java Virtual Machine,指的就是 Java 虛擬機,Java 虛擬機是跨平臺實作的核心
大致來說,JRE、JDK 和 JVM 的關系如下

描述一下值傳遞和參考傳遞的區別
要想真正理解的話,可以參考這篇文章 : 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,
Java 中的基本資料型別有哪些,各自占用多少位元組
在 Java 中,資料型別只有四類八種
- 整數型:byte、short、int、long
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 中的 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 通常是將地址轉換為整數來實作的,
兩個物件的 hashcode 相同,那么 equals 是否也一定為 true
這個肯定是不一定的,舉個非常簡單的例子,你重寫了 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 = new String(“abc”) 在記憶體中創建了幾個物件
一個或者兩個,String s1 是宣告了一個 String 型別的 s1 變數,它不是物件,使用 new 關鍵字會在堆中創建一個物件,另外一個物件是 abc ,它會在常量池中創建,所以一共創建了兩個物件;如果 abc 在常量池中已經存在的話,那么就會創建一個物件,
詳細請翻閱筆者的另外一篇文章 一篇與眾不同的 String、StringBuffer、StringBuilder 詳解
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來表示;重寫后的方法其方法的宣告和引數型別、順序必須要與父類完全一致;多載是針對同一類中概念,它要求多載的方法必須滿足下面任何一個要求:方法引數的順序,引數的個數,引數的型別任意一個保持不同即可,
構造器能否被多載,能否被重寫?
這道題考到你對于構造器的理解和認識,
我們 Java 中創建一個物件其實就是呼叫了該物件的構造方法,比如下面代碼
InstanceObject IO = new InstanceObject() ; // 呼叫了無參構造方法
InstanceObject IO = new InstanceObject(xxx) ; // 呼叫了有引數的構造方法
而多載的概念是什么呢?
它是指我們可以定義一些名稱相同的方法,通過定義不同的輸入引數來區分這些方法,然后再呼叫時,JVM 就會根據不同的引數樣式,來選擇合適的方法執行,
也就是說,多載的概念更多描述的是對相同命名的方法的不同描述,那么我們上面這段代碼很顯然就是多載了,因為名稱相同,我可以通過有無引數來判斷呼叫不同的構造方法來進行初始化,
那么構造器能否被重寫呢?這里我們先來看一下什么是重寫
重寫是子類對父類的允許訪問的方法的實作程序進行重新撰寫, 回傳值和形參都不能改變,從重寫的概念定義來說我們就知道構造器不能被重寫了,
首先,構造器沒有回傳值,第二點,構造器的名稱必須和類名一致,
你總不能在類 A 中寫了 public A();在類 B 中也寫 public A() 吧,這顯然是不能通過編譯的,
byte的取值范圍是多少,怎么計算出來的
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 的區別
相同點
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 完全相等的節點,則用新節點替換老節點
關于 HashMap 的深入理解請參考這篇文章 看完這篇 HashMap ,和面試官扯皮就沒問題了
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 是集合類的父類,它是一個頂級介面,大部分抽象類比如說 AbstractList、AbstractSet 都繼承了 Collection 類,Collection 類只定義一節標準方法比如說 add、remove、set、equals 等,具體的方法由抽象類或者實作類去實作,
Collections 是集合類的工具類,Collections 提供了一些工具類的基本使用
- sort 方法,對當前集合進行排序, 實作 Comparable 介面的類,只能使用一種排序方案,這種方案叫做自然比較
- 比如實作執行緒安全的容器
Collections.synchronizedList、Collections.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 主要有NullPointerException、 IllegalArgumentException、ArrayIndexOutofBoundException等,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 的常規操作,
深入理解可以參考一下這篇文章 深入理解 String、StringBuilder 和 StringBuffer
動態代理是基于什么原理
代理一般分為靜態代理和 動態代理,它們都是代理模式的一種應用,靜態代理指的是在程式運行前已經編譯好,程式知道由誰來執行代理方法,
而動態代理只有在程式運行期間才能確定,相比于靜態代理, 動態代理的優勢在于可以很方便的對代理類的函式進行統一的處理,而不用修改每個代理類中的方法,可以說動態代理是基于 反射 實作的,通過反射我們可以直接操作類或者物件,比如獲取類的定義,獲取宣告的屬性和方法,呼叫方法,在運行時可以修改類的定義,
動態代理是一種在運行時構建代理、動態處理方法呼叫的機制,動態代理的實作方式有很多,Java 提供的代理被稱為 JDK 動態代理,JDK 動態代理是基于類的繼承,
理解動態代理可以參考這篇文章 動態代理深入理解
動態代理的幾種實作方式
動態代理的實作方式我給你羅列四種,分別是
- JDK 動態代理:JDK 動態代理也是 Java 動態代理,它是基于介面的動態代理,它通常代理介面下的所有類,
- CGLIB 動態代理:CGLIB 動態代理是針對類實作代理,主要是對指定的類生成一個子類,覆寫其中的方法 ,也就是說 CGLIB 動態代理采用類繼承 -> 方法重寫的方式進行的,
- Javassist 代理:
Javassist是在 Java 中編輯位元組碼的類別庫;它使 Java 程式能夠在運行時定義一個新類, 并在 JVM 加載時修改類檔案, - ASM 代理:ASM 是一套 Java 位元組碼生成架構,它可以動態生成二進制格式的子類或其它代理類,或者在類被 Java 虛擬機裝入記憶體之前,動態修改類,
關于這四種位元組碼的實作可以參考 動態代理竟然如此簡單!
解釋下 Serialization 和 Deserialization
Serialization 表示的是序列化,實作序列化和反序列化的物件必須實作 serializable 介面,序列化是將物件變成位元組流,存盤到磁盤或網路中,反序列化是序列化的反程序,
我在學到序列化的時候就經常有個疑問,為什么物件需要序列化呢?
為了資料傳輸的目的,因為這個物件在你代碼中是物件格式,但是它在傳輸的程序中可不是物件格式,所以,當其他行程或者其他機器想要和你通信時,你們雙方會發送各種型別的資料,也有可能會把這個物件發送過去,所以這個物件勢必會轉換為能夠在網路中或者磁盤中能夠識別的格式,也就是二進制,
這也就是說,發送方需要把這個 Java 物件轉換為位元組序列,才能在網路上傳送;接收方則需要把位元組序列再恢復為 Java 物件,這也是 Deserialization 介面所做的作業,
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 中的自動裝箱與拆箱
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 解決完了,
深入理解 IO 可以參考這篇文章 深入理解 IO ,
談談你知道的設計模式
一張思維導圖鎮場

比如全域唯一性可以用 單例模式,
可以使用 策略模式 優化過多的 if…else…
制定標準用 模版模式
接手其他人的鍋,但不想改原來的類用 配接器模式
使用 組合 而不是繼承
使用 裝飾器可以制作加糖、加奶酪的咖啡
代理 可以用于任何中間商…
寫出幾種單例模式實作
這也是一道老生常談的面試題了,一般不會讓你手寫單例模式,就算是手寫的話,只需要寫出關鍵代碼來就可以了,這道題其實想問的是單例模式的幾種實作方式,并且每種實作方式有沒有什么問題,
一般可以這樣回答下
- 餓漢式:執行緒安全,呼叫效率高,但是不能延時加載
- 懶漢式:執行緒安全,呼叫效率不高,但是能延時加載
- Double CheckLock 實作單例:DCL 也就是雙重鎖判斷機制(由于JVM底層模型原因,偶爾會出問題,不建議使用
- 靜態內部類實作模式:執行緒安全,呼叫效率高,可以延時加載
- 列舉類:執行緒安全,呼叫效率高,不能延時加載,可以天然的防止反射和反序列化呼叫
如果想要深入了解這道面試題,可以查閱 我向面試官講解了單例模式,他對我豎起了大拇指
Comparator 和 Comparable 有什么不同
-
Comparable 更像是自然排序
-
Comparator 更像是定制排序
同時存在時采用 Comparator(定制排序)的規則進行比較,
對于一些普通的資料型別(比如 String, Integer, Double…),它們默認實作了Comparable 介面,實作了 compareTo 方法,我們可以直接使用,
而對于一些自定義類,它們可能在不同情況下需要實作不同的比較策略,我們可以新創建 Comparator 介面,然后使用特定的 Comparator 實作進行比較,
關于 Comparator 和 Comparable 的深入理解,可以參考這篇文章 Comparable 和 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 方法,也不要指望他來進行垃圾回收,
深入理解 final、finally 和 finalize ,可以看看這一篇 看完這篇 final、finally 和 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、提高程式運行效率
動態系結
1、使用動態系結的前提條件能夠提高代碼的可用性,使代碼更加靈活,
2、多型是設計模式的基礎,能夠降低耦合性,
Java 中有哪些語法糖
語法糖指的是計算機語言中添加的某種語法,這種語法對語言的功能并沒有影響,但是更方便程式員使用,因為 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 中的語法糖,真甜,
List 和 Set 的異同
這也是一道老生常談的問題了,
相同點
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 同樣也是我們日常開發中使用頻率非常高的集合類,
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 和 Vector 的區別
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 中的方法,那么 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 有什么區別
我們日常開發程序中使用 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 的擴容機制
今天面試官又問到你 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、NIO、AIO 的區別
簡單來說,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 創建物件的五種方式
Java 創建物件的方式主要有五種
- 使用 new 來創建物件
Object obj = new Object();
- 使用 newInstance 方法來創建
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 詳解
一個小小的 static 還能難得住我?
看完這篇 HashMap ,和面試官扯皮就沒問題了
看完這篇 Exception 和 Error ,和面試官扯皮就沒問題了
動態代理深入理解
動態代理竟然如此簡單!
深入理解 IO
我向面試官講解了單例模式,他對我豎起了大拇指
Comparable 和 Comparator的理解
學會反射后,我被錄取了!(干貨)
深入理解各種參考問題
看完這篇 final、finally 和 finalize 和面試官扯皮就沒問題了
Java 中的語法糖,真甜,
盤點 Java 創建物件的 n 種操作
我想,這能夠對的起我在 github https://github.com/crisxuan/bestJavaer 上面說過的話,

好了,就這樣,至于點贊在看什么的,隨意就好,希望這篇文章能夠幫到你,
我自己肝了六本 PDF,微信搜索「程式員cxuan」關注公眾號后,在后臺回復 cxuan ,領取全部 PDF,這些 PDF 如下
六本 PDF 鏈接

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/274051.html
標籤:java
上一篇:Java并發編程(十):ReentrantLock-NonfairSync原始碼逐行深度分析(中)
下一篇:Python垃圾回識訓制
