(2021最新版)Java後端面試題|Java基礎部分

前言

很多朋友問,如何短時間突擊 Java 通過面試?

面試前還是很有必要針對性的刷一些題,很多朋友的實戰能力很強,但是理論比較薄弱,面試前不做準備是很喫虧的。這裏整理了很多面試常考的一些面試題,希望能幫助到你面試前的複習並且找到一個好的工作,也節省你在網上搜索資料的時間來學習。

整理的這些Java面試題,包括Java基礎、Java多線程與併發編程、spring、spring mvc、spring boot、mybatis。MySQL、Redis、消息中間件MQ、分佈式與微服務。持續更新中…

完整版Java面試題地址:105道Java面試題總結|含答案解析

內容 地址
Java基礎 本文
多線程與併發 未更新
Spring 未更新
Spring MVC、Spring Boot 未更新
MyBatis 未更新
MySQL 未更新
Redis 未更新
分佈式與微服務 未更新
MQ 未更新

1、面向對象

什麼是面向對象?

對比面向過程,是兩種不同的處理問題的角度

面向過程更注重事情的每一個步驟及順序,面向對象更注重事情有哪些參與者(對象)、及各自需要做什麼

比如:洗衣機洗衣服

面向過程會將任務拆解成一系列的步驟(函數),1、打開洗衣機----->2、放衣服----->3、放洗衣粉----->4、清洗----->5、烘乾

面向對象會拆出人和洗衣機兩個對象:

人:打開洗衣機 放衣服 放洗衣粉

洗衣機:清洗 烘乾

從以上例子能看出,面向過程比較直接高效,而面向對象更易於複用、擴展和維護

面向對象

封裝:封裝的意義,在於明確標識出允許外部使用的所有成員函數和數據項

內部細節對外部調用透明,外部調用無需修改或者關心內部實現

(1)、javabean的屬性私有,提供getset對外訪問,因爲屬性的賦值或者獲取邏輯只能由javabean本身決 定。而不能由外部胡亂修改

private String name;
public void setName(String name){
	this.name = "tuling_"+name;
}
該name有自己的命名規則,明顯不能由外部直接賦值

(2)、orm框架

操作數據庫,我們不需要關心鏈接是如何建立的、sql是如何執行的,只需要引入mybatis,調方法即可

繼承:繼承基類的方法,並做出自己的改變和/或擴展

子類共性的方法或者屬性直接使用父類的,而不需要自己再定義,只需擴展自己個性化的

多態:基於對象所屬類的不同,外部對同一個方法的調用,實際執行的邏輯不同。

繼承,方法重寫,父類引用指向子類對象

父類類型 變量名 = new 子類對象 ;
變量名.方法名();

無法調用子類特有的功能

2、 JDK、JRE、JVM

JDK:

Java Develpment Kit java 開發工具

JRE:

Java Runtime Environment java運行時環境

JVM:

java Virtual Machine java 虛擬機

3、 ==和equals比較

==對比的是棧中的值,基本數據類型是變量值,引用類型是堆中內存對象的地址

equals:object中默認也是採用==比較,通常會重寫

Object

public Boolean equals(Object obj) {
	return (this == obj);
}

String

public Boolean equals(Object anObject) {
	if (this == anObject) {
		return true;
	}
	if (anObject instanceof String) {
		String anotherString = (String)anObject;
		int n = value.length;
		if (n == anotherString.value.length) {
			char v1[] = value;
			char v2[] = anotherString.value;
			int i = 0;
			while (n-- != 0) {
				if (v1[i] != v2[i])
				return false;
				i++;
			}
			return true;
		}
	}
	return false;
}

上述代碼可以看出,String類中被複寫的equals()方法其實是比較兩個字符串的內容。

public class StringDemo {
	public static void main(String args[]) {
		String str1 = "Hello";
		String str2 = new String("Hello");
		String str3 = str2;
		// 引用傳遞
		System.out.println(str1 == str2);
		// false
		System.out.println(str1 == str3);
		// false
		System.out.println(str2 == str3);
		// true
		System.out.println(str1.equals(str2));
		// true
		System.out.println(str1.equals(str3));
		// true
		System.out.println(str2.equals(str3));
		// true
	}
}

4、hashCode與equals

hashCode介紹:

hashCode() 的作用是獲取哈希碼,也稱爲散列碼;它實際上是返回一個int整數。這個哈希碼的作用是確定該對象在哈希表中的索引位置。hashCode() 定義在JDK的Object.java中,Java中的任何類都包含有hashCode() 函數。

散列表存儲的是鍵值對(key-value),它的特點是:能根據“鍵”快速的檢索出對應的“值”。這其中就利用 到了散列碼!(可以快速找到所需要的對象)

爲什麼要有hashCode:

以HashSet如何檢查重複爲例子來說明爲什麼要有hashCode:

對象加入HashSet時,HashSet會先計算對象的hashcode值來判斷對象加入的位置,看該位置是否有值,如果沒有、HashSet會假設對象沒有重複出現。但是如果發現有值,這時會調用equals()方法來檢查兩個對象是否真的相同。如果兩者相同,HashSet就不會讓其加入操作成功。如果不同的話,就會重新散列到其他位置。這樣就大大減少了equals的次數,相應就大大提高了執行速度。

(1)如果兩個對象相等,則hashcode一定也是相同的

(2)兩個對象相等,對兩個對象分別調用equals方法都返回true

(3)兩個對象有相同的hashcode值,它們也不一定是相等的

(4)因此,equals方法被覆蓋過,則hashCode方法也必須被覆蓋

(5)hashCode()的默認行爲是對堆上的對象產生獨特值。如果沒有重寫hashCode(),則該class的兩個 對象無論如何都不會相等(即使這兩個對象指向相同的數據)

5、final

最終的

修飾類:表示類不可被繼承

修飾方法:表示方法不可被子類覆蓋,但是可以重載

修飾變量:表示變量一旦被賦值就不可以更改它的值。

(1)修飾成員變量

如果final修飾的是類變量,只能在靜態初始化塊中指定初始值或者聲明該類變量時指定初始值。 如果final修飾的是成員變量,可以在非靜態初始化塊、聲明該變量或者構造器中執行初始值。

(2)修飾局部變量

系統不會爲局部變量進行初始化,局部變量必須由程序員顯示初始化。因此使用final修飾局部變量時,即可以在定義時指定默認值(後面的代碼不能對變量再賦值),也可以不指定默認值,而在後面的代碼中對final變量賦初值(僅一次)

public class FinalVar {
	final static int a = 0;
	//再聲明的時候就需要賦值 或者靜態代碼塊賦值
	/**
static{
a = 0;
}
*/
	final int b = 0;
	//再聲明的時候就需要賦值 或者代碼塊中賦值 或者構造器賦值
	/*{
b = 0;
}*/
	public static void main(String[] args) {
		final int localA;
		//局部變量只聲明沒有初始化,不會報錯,與final無關。
		localA = 0;
		//在使用之前一定要賦值
		//localA = 1; 但是不允許第二次賦值
	}
}

(3)修飾基本類型數據和引用類型數據

如果是基本數據類型的變量,則其數值一旦在初始化之後便不能更改; 如果是引用類型的變量,則在對其初始化之後便不能再讓其指向另一個對象。但是引用的值是可變的。

public class FinalReferenceTest{
	public static void main(){
		final int[] iArr={1,2,3,4};
		iArr[2]=-3;
		//合法
		iArr=null;
		//非法,對iArr不能重新賦值
		final Person p = new Person(25);
		p.setAge(24);
		//合法
		p=null;
		//非法
	}
}

爲什麼局部內部類和匿名內部類只能訪問局部final變量?

編譯之後會生成兩個class文件,Test.class Test1.class

public class Test {
	public static void main(String[] args) {
	}
	//局部final變量a,b
	public void test(final int b) {
		//jdk8在這裏做了優化, 不用寫,語法糖,但實際上也是有
		的,也不能修改
		final int a = 10;
		//匿名內部類
		new Thread(){
			public void run() {
				System.out.println(a);
				System.out.println(b);
			}
			;
		}
		.start();
	}
}
class OutClass {
	private int age = 12;
	public void outPrint(final int x) {
		class InClass {
			public void InPrint() {
				System.out.println(x);
				System.out.println(age);
			}
		}
		new InClass().InPrint();
	}
}

首先需要知道的一點是: 內部類和外部類是處於同一個級別的,內部類不會因爲定義在方法中就會隨着 方法的執行完畢就被銷燬。

這裏就會產生問題:當外部類的方法結束時,局部變量就會被銷燬了,但是內部類對象可能還存在(只有沒有人再引用它時,纔會死亡)。這裏就出現了一個矛盾:內部類對象訪問了一個不存在的變量。爲了解決這個問題,就將局部變量複製了一份作爲內部類的成員變量,這樣當局部變量死亡後,內部類仍可以訪問它,實際訪問的是局部變量的"copy"。這樣就好像延長了局部變量的生命週期

將局部變量複製爲內部類的成員變量時,必須保證這兩個變量是一樣的,也就是如果我們在內部類中修改了成員變量,方法中的局部變量也得跟着改變,怎麼解決問題呢?

就將局部變量設置爲final,對它初始化後,我就不讓你再去修改這個變量,就保證了內部類的成員變量 和方法的局部變量的一致性。這實際上也是一種妥協。使得局部變量與內部類內建立的拷貝保持一致。

6、String、StringBuffer、StringBuilder

String是final修飾的,不可變,每次操作都會產生新的String對象

StringBuffer和StringBuilder都是在原對象上操作

StringBuffer是線程安全的,StringBuilder線程不安全的

StringBuffer方法都是synchronized修飾的

性能:StringBuilder > StringBuffer > String

場景:經常需要改變字符串內容時使用後面兩個

優先使用StringBuilder,多線程使用共享變量時使用StringBuffer

7、重載和重寫的區別

重載:

發生在同一個類中,方法名必須相同,參數類型不同、個數不同、順序不同,方法返回值和訪問 修飾符可以不同,發生在編譯時。

重寫:

發生在父子類中,方法名、參數列表必須相同,返回值範圍小於等於父類,拋出的異常範圍小於 等於父類,訪問修飾符範圍大於等於父類;如果父類方法訪問修飾符爲private則子類就不能重寫該方 法。

public int add(int a,String b)
public String add(int a,String b)
//編譯報錯

8、接口和抽象類的區別

(1)抽象類可以存在普通成員函數,而接口中只能存在public abstract 方法。

(2)抽象類中的成員變量可以是各種類型的,而接口中的成員變量只能是public static final類型的。

(3)抽象類只能繼承一個,接口可以實現多個。

接口的設計目的,是對類的行爲進行約束(更準確的說是一種“有”約束,因爲接口不能規定類不可以有什麼行爲),也就是提供一種機制,可以強制要求不同的類具有相同的行爲。它只約束了行爲的有無,但不對如何實現行爲進行限制。

而抽象類的設計目的,是代碼複用。當不同的類具有某些相同的行爲(記爲行爲集合A),且其中一部分行爲的實現方式一致時(A的非真子集,記爲B),可以讓這些類都派生於一個抽象類。在這個抽象類中實現了B,避免讓所有的子類來實現B,這就達到了代碼複用的目的。而A減B的部分,留給各個子類自己實現。正是因爲A-B在這裏沒有實現,所以抽象類不允許實例化出來(否則當調用到A-B時,無法執行)。

抽象類是對類本質的抽象,表達的是 is a 的關係,比如: BMW is a Car。抽象類包含並實現子類的通用特性,將子類存在差異化的特性進行抽象,交由子類去實現。

而接口是對行爲的抽象,表達的是 like a 的關係。比如: Bird like a Aircraft(像飛行器一樣可以飛),但其本質上 is a Bird。接口的核心是定義行爲,即實現類可以做什麼,至於實現類主體是誰、是如何實現的,接口並不關心。

使用場景:當你關注一個事物的本質的時候,用抽象類;當你關注一個操作的時候,用接口。

抽象類的功能要遠超過接口,但是,定義抽象類的代價高。因爲高級語言來說(從實際設計上來說也是)每個類只能繼承一個類。在這個類中,你必須繼承或編寫出其所有子類的所有共性。雖然接口在功能上會弱化許多,但是它只是針對一個動作的描述。而且你可以在一個類中同時實現多個接口。在設計階段會降低難度

9、List和Set的區別

List

有序,按對象進入的順序保存對象,可重複,允許多個Null元素對象,可以使用Iterator取出所有元素,在逐一遍歷,還可以使用get(int index)獲取指定下標的元素

Set

無序,不可重複,最多允許有一個Null元素對象,取元素時只能用Iterator接口取得所有元素,在逐一遍歷各個元素

10、ArrayList和LinkedList區別

ArrayList:

基於動態數組,連續內存存儲,適合下標訪問(隨機訪問),擴容機制:因爲數組長度固定,超出長度存數據時需要新建數組,然後將老數組的數據拷貝到新數組,如果不是尾部插入數據還會涉及到元素的移動(往後複製一份,插入新元素),使用尾插法並指定初始容量可以極大提升性能、甚至超過linkedList(需要創建大量的node對象)

LinkedList:

基於鏈表,可以存儲在分散的內存中,適合做數據插入及刪除操作,不適合查詢:需要逐一遍歷

遍歷LinkedList必須使用iterator不能使用for循環,因爲每次for循環體內通過get(i)取得某一元素時都需要對list重新進行遍歷,性能消耗極大。

另外不要試圖使用indexOf等返回元素索引,並利用其進行遍歷,使用indexlOf對list進行了遍歷,當結 果爲空時會遍歷整個列表。

11、HashMap和HashTable有什麼區別?其底層實現是什 麼?

區別 :

(1)HashMap方法沒有synchronized修飾,線程非安全,HashTable線程安全;

(2)HashMap允許key和value爲null,而HashTable不允許

底層實現:數組+鏈表實現

jdk8開始鏈表高度到8、數組長度超過64,鏈表轉變爲紅黑樹,元素以內部類Node節點存在

(1)計算key的hash值,二次hash然後對數組長度取模,對應到數組下標。

(2)如果沒有產生hash衝突(下標位置沒有元素),則直接創建Node存入數組

(3)如果產生hash衝突,先進行equal比較,相同則取代該元素,不同,則判斷鏈表高度插入鏈表,鏈表高度達到8,並且數組長度到64則轉變爲紅黑樹,長度低於6則將紅黑樹轉回鏈表

(4)key爲null,存在下標0的位置數組擴容

12、ConcurrentHashMap原理,jdk7和jdk8版本的區別

jdk7:

數據結構:ReentrantLock+Segment+HashEntry,一個Segment中包含一個HashEntry數組,每個HashEntry又是一個鏈表結構

元素查詢:二次hash,第一次Hash定位到Segment,第二次Hash定位到元素所在的鏈表的頭部

鎖:Segment分段鎖 Segment繼承了ReentrantLock,鎖定操作的Segment,其他的Segment不受影響,併發度爲segment個數,可以通過構造函數指定,數組擴容不會影響其他的segment

get方法無需加鎖,volatile保證

jdk8:

數據結構:synchronized+CAS+Node+紅黑樹,Node的val和next都用volatile修飾,保證可見性

查找,替換,賦值操作都使用CAS

鎖:鎖鏈表的head節點,不影響其他元素的讀寫,鎖粒度更細,效率更高,擴容時,阻塞所有的讀寫操作、併發擴容

讀操作無鎖:

Node的val和next使用volatile修飾,讀寫線程對該變量互相可見數組用volatile修飾,保證擴容時被讀線程感知

13、什麼是字節碼?採用字節碼的好處是什麼?

Java中的編譯器和解釋器:

Java中引入了虛擬機的概念,即在機器和編譯程序之間加入了一層抽象的虛擬的機器。這臺虛擬的機器 在任何平臺上都提供給編譯程序一個的共同的接口。

編譯程序只需要面向虛擬機,生成虛擬機能夠理解的代碼,然後由解釋器來將虛擬機代碼轉換爲特定系統的機器碼執行。在Java中,這種供虛擬機理解的代碼叫做 字節碼(即擴展名爲 .class的文件),它不面向任何特定的處理器,只面向虛擬機。

每一種平臺的解釋器是不同的,但是實現的虛擬機是相同的。Java源程序經過編譯器編譯後變成字節碼,字節碼由虛擬機解釋執行,虛擬機將每一條要執行的字節碼送給解釋器,解釋器將其翻譯成特定機器上的機器碼,然後在特定的機器上運行。這也就是解釋了Java的編譯與解釋並存的特點。

Java源代碼---->編譯器---->jvm可執行的Java字節碼(即虛擬指令)---->jvm---->jvm中解釋器----->機器可執行的二進制機器碼---->程序運行。

採用字節碼的好處:

Java語言通過字節碼的方式,在一定程度上解決了傳統解釋型語言執行效率低的問題,同時又保留了解釋型語言可移植的特點。所以Java程序運行時比較高效,而且,由於字節碼並不專對一種特定的機器,因此,Java程序無須重新編譯便可在多種不同的計算機上運行。

14、Java中的異常體系

Java中的所有異常都來自頂級父類Throwable。

Throwable下有兩個子類Exception和Error。

Error是程序無法處理的錯誤,一旦出現這個錯誤,則程序將被迫停止運行。

Exception不會導致程序停止,又分爲兩個部分RunTimeException運行時異常和CheckedException檢查異常。

RunTimeException常常發生在程序運行過程中,會導致程序當前線程執行失敗。CheckedException常 常發生在程序編譯過程中,會導致程序編譯不通過。

15、Java類加載器

JDK自帶有三個類加載器:bootstrap ClassLoader、ExtClassLoader、AppClassLoader。

BootStrapClassLoader是ExtClassLoader的父類加載器,默認負責加載%JAVA_HOME%lib下的jar包和class文件。

ExtClassLoader是AppClassLoader的父類加載器,負責加載%JAVA_HOME%/lib/ext文件夾下的jar包和 class類。

AppClassLoader是自定義類加載器的父類,負責加載classpath下的類文件。系統類加載器,線程上下文加載器繼承ClassLoader實現自定義類加載器

16、雙親委託模型

雙親委派模型的好處:

主要是爲了安全性,避免用戶自己編寫的類動態替換 Java的一些核心類,比如 String。同時也避免了類的重複加載,因爲 JVM中區分不同類,不僅僅是根據類名,相同的 class文件被不同的 ClassLoader加載就是不同的兩個類

17、GC如何判斷對象可以被回收

引用計數法:每個對象有一個引用計數屬性,新增一個引用時計數加1,引用釋放時計數減1,計數爲0時可以回收,

可達性分析法:從 GC Roots 開始向下搜索,搜索所走過的路徑稱爲引用鏈。當一個對象到 GCRoots 沒有任何引用鏈相連時,則證明此對象是不可用的,那麼虛擬機就判斷是可回收對象。

引用計數法,可能會出現A 引用了 B,B 又引用了 A,這時候就算他們都不再使用了,但因爲相互引用 計數器=1 永遠無法被回收。

GC Roots的對象有:

虛擬機棧(棧幀中的本地變量表)中引用的對象 方法區中類靜態屬性引用的對象 方法區中常量引用的對象 本地方法棧中JNI(即一般說的Native方法)引用的對象

可達性算法中的不可達對象並不是立即死亡的,對象擁有一次自我拯救的機會。對象被系統宣告死亡至少要經歷兩次標記過程:第一次是經過可達性分析發現沒有與GC Roots相連接的引用鏈,第二次是在由虛擬機自動建立的Finalizer隊列中判斷是否需要執行finalize()方法。

當對象變成(GC Roots)不可達時,GC會判斷該對象是否覆蓋了finalize方法,若未覆蓋,則直接將其回收。否則,若對象未執行過finalize方法,將其放入F-Queue隊列,由一低優先級線程執行該隊列中對象的finalize方法。執行finalize方法完畢後,GC會再次判斷該對象是否可達,若不可達,則進行回收,否則,對象“復活”每個對象只能觸發一次finalize()方法

由於finalize()方法運行代價高昂,不確定性大,無法保證各個對象的調用順序,不推薦大家使用,建議遺忘它。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章