面試Java必定會問到SE部分的基礎知識,我也被問過很多次,這篇文章記錄一些常問的問題和答案。
一、理解JDK、JRE、JVM
-
JDK(Java Development Kit)Java開發工具包,是整個Java開發的核心,其中包含了JRE,即Java運行時環境,擁有編譯器和工具(javadoc、jdb)等。如果是開發Java程序只需安裝JDK即可。
-
JRE(Java Runtime Environment):Java運行時環境,其中包含了JVM標準實現、Java類庫和一些基礎構件。JRE適用於運行Java程序,而不能創建和開發Java程序,但是如果運行的程序含有需編譯的程序(例如JSP需轉換爲Servlet)就需要安裝jdk。
-
JVM(Java Virtual Machine):Java虛擬機,它能夠將 class 文件中的字節碼指令進行識別成機器碼並調用操作系統上的 API 完成動作。JVM有針對不同操作系統的具體實現,這是Java跨平臺的關鍵所在。
所有Java開發和運行環境不一定都是同一個廠商提供的,大部分是Sun公司(現被Oracle收購)提供的,JDK、JRE、JVM等都有不同的實現。比如JDK有Oracle JDK、Open JDK以及其他公司提供的JDK等,JVM有Sun HotSpot VM、IBM J9 VM、Google Android Dalvik VM以及其他VM等。一般我們使用的是Oracle JDK + HotSpot VM。
二、重載和重寫
-
重載(Overload):發生在同一個類中,方法名必須相同,參數類型、參數個數、參數順序、返回值、訪問權限修飾符可以不同。
-
重寫(Override):發生在父子類中,方法名、參數必須相同,返回值必須是父類返回值或者其子類,異常必須是父類異常或其子類,訪問修飾符權限必須大於等於父類,除了private,被private修飾的方法無法被重寫。
私有(private)方法和構造方法無法被重寫,但是可以被重載,一個類可以有多個被重載的構造方法。
三、Java的三大特性
-
封裝:提供訪問權限修飾符來控制屬性和方法的訪問可見性,Java中有四大訪問修飾符:private、default、protect、public。其中特別注意:外部類可以訪問內部類的private/protected變量,在編譯時,外部類和內部類不再是嵌套結構,而是變爲一個包中的兩個類,然後對於private變量的訪問,編譯器會生成一個accessor函數。
- private:同一個類可訪問
- default:同一個類和同一個包可訪問
- protect:同一個類、同一個包、子類可訪問
- public:同一個類、同一個包、子類、其他包可訪問
-
繼承:子類繼承父類,子類擁有父類所有的屬性和方法,但是隻能夠訪問非private的屬性和方法,子類的屬性和方法對父類不可見,子類可以重寫父類的非private和非final的方法。在開發中合理使用繼承可以方便地複用代碼,重構時繼承提供了很大的方便。
-
多態:程序中定義的引用變量所指向的具體類型和通過該引用變量發出的方法調用在編程時並不確定,而是在程序運行期間才確定,即一個引用變量倒底會指向哪個類的實例對象,該引用變量發出的方法調用到底是哪個類中實現的方法,必須在由程序運行期間才能決定。多態有三種實現形式:
- 繼承:在多態中必須存在有繼承關係的子類和父類
- 重寫/實現接口:子類對父類中某些方法進行重新定義,在調用這些方法時就會調用子類的方法
- 向上轉型:在多態中需要將子類的引用賦給父類對象,只有這樣該引用才能夠具備技能調用父類的方法和子類的方法
四、接口(interface)和抽象類(abstract)的區別
- 抽象類可以有構造方法,接口中不能有構造方法。
- 接口中的抽象方法默認是public,但也只能是public,抽象方法不能具體實現,jdk8之後可以有default方法實現,而抽象類既可以有抽象方法又可以有非抽象方法。從某種意義上講接口是一種特殊的抽象類。
- 抽象類中可以包含靜態方法,在 JDK1.8 之前接口中不能不包含靜態方法,JDK1.8 以後可以包含。
- 接口的實例變量默認是final類型,不可修改,而抽象類不一定。
- 一個類可以實現多個接口,但只能繼承一個抽象類,接口不可以實現接口,但可以繼承接口,並且可以繼承多個接口。
- 接口不能用new實例化,但是可以聲明變量,其變量必須引用該接口實例化的一個對象。
五、==和equals區別
==:如果比較的是基本數據類型,判斷其值是否相等;如果比較的是引用類型,判斷兩個對象的地址是否相等。
equals:equals是在Object類中定義的方法,在Object類中僅比較兩個對象的地址是否相同。
public boolean equals (Object x){
return this == x;
}
大家都知道所有類都是Object的子類,所以可以選擇是否重寫equals方法,以String類的equals方法爲例,equals方法用於判斷字符串的值是否相同。
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;
}
在重寫equals方法時需要滿足幾點規則:
- 自反性。對於任何非null的引用值x,x.equals(x)應返回true。
- 對稱性。對於任何非null的引用值x與y,當且僅當:y.equals(x)返回true時,x.equals(y)才返回true。
- 傳遞性。對於任何非null的引用值x、y與z,如果y.equals(x)返回true,y.equals(z)返回true,那麼x.equals(z)也應返回true。
- 一致性。對於任何非null的引用值x與y,假設對象上equals比較中的信息沒有被修改,則多次調用x.equals(y)始終返回true或者始終返回false。
- 對於任何非空引用值x,x.equal(null)應返回false。
equals方法用於判斷兩個對象在實際意義上是否是同一個對象,比如有兩張照片判斷其中的人是否是同一個人,雖然兩張中的穿着、所在環境都不一樣,但是在實際意義上是同一個人。equals方法常常和hashCode()一起使用。
六、hashCode和equals
equals方法上面有介紹,hashCode()定義於Object類中,該方法用於獲取哈希散列碼,它返回一個int類型的值,哈希散列碼的作用是確定該對象在哈希表中的索引位置,目的是爲了支持Map接口。在Object類中hashCode是一個native方法,它是由在虛擬機堆的位置唯一確定,一般在重寫該方法時需要自己定義其中的算法。
重寫equals時必須重寫hashCode方法?
其實是不一定的,網上很多文章都說必須同時重寫,這是建立在設計合理的基礎上。如果一個類不涉及HashSet、Hashtable、HashMap等內部使用哈希表的數據結構的類時,可以不必重寫hashCode方法,因爲如果不涉及哈希表hashCode就毫無意義。但是在實際編碼時又要求同時重寫,因爲你無法預測該類是否會應用到含有哈希表的類,所以通常會有“重寫equals時必須重寫hashCode方法”的說法。
- 兩個對象相等,則hashCode一定也是相等的;
- 兩個對象相等,互相調用equals方法也都返回true;
- 兩個對象有相同的hashCode值,它們互相調用equals也不一定返回true(可能發生Hash碰撞)。
七、String、StringBuilder、StringBuffer區別
可變性
String類不可變,它每次申請固定長度的char字符數組final char value[]
,並且不可修改,平時所使用的+號字符串拼接實際上是開闢了多個內存空間,最後結果字符串的堆內存可用,其餘的空間全部成爲垃圾,讀者可閱讀我曾經寫的一篇文章瞭解:Java中String對象最容易被忽略的知識。
StringBuffer和StringBuilder都是可變型字符串類,它們都繼承自AbstractStringBuilder
類,其中的字符數組定義是可變的char[] value
,在其中每次字符串拼接如果容量充足就在當前堆內存改變,如果不足纔開闢新的空間,其中每次擴容是原來容量的2倍+2,源碼中是這樣實現的:(value.length << 1) + 2
,最大容量是Integer.MAX_VALUE - 8
,爲什麼減8呢?因爲對象頭需要佔用一定空間,實際佔用大小因虛擬機位數而定。
多線程安全
String和StringBuffer是多線程安全的,String的字符數組是final的,所以它不存在修改也就天然線程安全,而StringBuffer則是通過同步鎖實現線程安全的,它的所有方法都是使用的synchronized修飾保證其線程安全性。而StringBuilder則是非線程安全的。
適用條件
當字符串拼接很少時適合String類。當字符串拼接很頻繁時,如果僅在單線程操作變量,適合StringBuilder;如果在多線程情況下,使用StringBuffer能更好保證其安全性。在單線程情況下,StringBuilder相比於StringBuffer有15%左右的性能提升。
八、"abc"與new String(“abc”)
通常創建字符串有兩種方法,一種是直接使用雙引號創建"abc"
,一種是new一個String類。兩種方法都能創建字符串,但其流程卻有所差別,詳細內容可閱讀這篇文章:Java中String對象最容易被忽略的知識。
這兩種方法涉及到String的intern方法實現,在jdk6和jdk6以後具體實現有所差別,這裏只講jdk6以後的實現。
- 雙引號創建會先檢查常量池是否存在該字符串,如果常量池有則直接返回常量池的引用,如果沒有則檢查該字符串是否存在於堆中,如果存在則將堆中對此對象的引用添加到常量池中,並返回該引用,如果堆中不存在,就在池中創建字符串並返回其引用。
- new一個String類是直接在堆內存中創建一個新對象,但是構造函數傳入的字符串又是一個String對象,如果對象池中沒有這個字符串就會在堆內存中多一塊垃圾,所以平常使用時推薦使用第一種雙引號創建。
九、構造函數、構造代碼塊、靜態代碼塊
先看一下這三個在代碼中的樣子
public class Test1 {
Test1() {
System.out.println("構造函數");
}
{
System.out.println("構造代碼塊");
}
static {
System.out.println("靜態代碼塊");
}
public static void main(String[] args) {
System.out.println("main函數執行");
new Test1();
}
}
上面代碼的運行結果是:
靜態代碼塊
main函數執行
構造代碼塊
構造函數
-
構造函數
- 對象一建立,就會調用與之相應的構造函數,不實例化對象,構造函數不會運行。
- 構造函數的作用是用於給對象進行初始化。
- 一個對象建立,構造函數只運行一次,而一般方法可以被該對象調用多次。
-
構造代碼塊
- 構造代碼塊的作用是給對象進行初始化。
- 對象一建立就運行構造代碼塊,而且優先於構造函數執行。有對象實例化,纔會運行構造代碼塊,類不能調用構造代碼塊。
- 構造代碼塊與構造函數的區別是:構造代碼塊是給所有對象進行統一初始化,而構造函數是給對應的對象初始化,因爲構造函數是可以多個的,運行哪個構造函數就會建立什麼樣的對象,但無論建立哪個對象,都會先執行相同的構造代碼塊。也就是說,構造代碼塊中定義的是不同對象共性的初始化內容。
-
靜態代碼塊
- 它是隨着類的加載而執行,只執行一次,並優先於主函數。該過程發生在類加載生命週期的初始化階段讀者可以閱讀我之前寫的這篇文章淺談一個Java類的生命週期。
- 靜態代碼塊是給類初始化,構造代碼塊是給對象初始化。
- 靜態代碼塊中的變量是局部變量,與普通函數中的局部變量性質沒有區別。
- 一個類中可以有多個靜態代碼塊
十、final、finally、finalize區別
- final:Java關鍵字,可以用來修飾類、方法、變量,分別有不同的意義,final修飾的class代表不可以繼承擴展,final的變量是不可以修改的,而final的方法也是不可以重寫的(override)。
- finally: Java保證重點代碼一定要被執行的一種機制。我們可以使用
try-finally
或者try-catch-finally
來進行類似關閉 JDBC連接、保證unlock鎖等動作。 - finalize:基礎類Object的一個方法,它的設計目的是保證對象在被垃圾收集前完成特定資源的回收。finalize機制現在已經不推薦使用,並且在JDK9開始被標記爲deprecated。
十一、Java中的值傳遞與引用傳遞
因爲這部分知識容易饒,所以我將結合代碼描述。
值傳遞
方法傳遞對象是基本數據類型,方法得到的是參數值的拷貝,無論該方法對其傳遞變量做什麼樣的修改,其原本的值均不會改變,因爲方法體操作的是拷貝的數據。
public static void main(String[] args) {
int a = 1, b = 2;
change(a, b);
System.out.println("a = " + a + ",b = " + b);
// 運行結果是: a = 1,b = 2
}
static void change(int a, int b) {
a = 3;
b = 4;
}
引用傳遞
引用傳遞的對象是引用數據類型或數組類型,方法得到的是對象的堆內存地址,方法可以改變堆內存中對象的內容,但是它和值傳遞有一點很容易弄混淆,我相信看下面的代碼就不會混淆了。
static class User{
String name;
User(String name){
this.name = name;
}
}
public static void main(String[] args) {
User user1 = new User("北風");
User user2 = new User("tz");
swap(user1,user2);
// 該方法是交換user1和user2的堆內存,結果明顯是會失敗的,
// 因爲它們兩個的棧內存並沒有改變,仍然指向的是原來的堆內存
System.out.println("user1:"+user1.name+"; user2:"+user2.name);
// 運行結果是: user1:北風; user2:tz
change(user1,user2);
// 該方法是改變user1和user2的name屬性,會成功,
// 因爲引用傳遞能修改堆內存的內容
System.out.println("user1:"+user1.name+"; user2:"+user2.name);
// 運行結果是: user1:AAAAAAA; user2:BBBBBBB
}
// 交換user1和user2地址
static void swap(User user1,User user2) {
User temp = user1;
user1 = user2;
user2 = temp;
}
// 修改user1和user2的內容
static void change(User user1,User user2) {
user1.name = "AAAAAAA";
user2.name = "BBBBBBB";
}
十二、獲得一個類的實例有哪些方法
- new對象,使用關鍵字new直接在堆內存創建一個對象實例。
- clone(),clone()方法定義於Object類,用於從堆內存中克隆一個一模一樣的對象到新的堆內存中,被克隆的對象必須實現Cloneable接口,該接口無任何實際定義,僅用於標識。
- 反射,在運行狀態中對任意一個類進行實例化,並且可以調用其所有屬性和方法,甚至可以打破訪問控制權限的規則,即private定義也可以被訪問。憂點是動態加載類,可以提高代碼靈活度。缺點是容易造成性能瓶頸,類解釋過程交由JVM去做,增加JVM負擔。
- 反序列化ObjectInputStream,,將二進制流轉換爲類對象,二進制流必須是由Java序列化而來。具體實現可閱讀我之前寫的文章序列化與反序列化