一、Java基礎
1. final
關鍵字的作用:
- 修飾類時,被修飾的類無法被繼承。
- 修飾方法時,被修飾的方法無法被重寫。
- 修飾變量時,變量為常量,初始化后無法重新賦值。
2. static
關鍵字的作用:
- 修飾變量和方法時,被修飾的變量和方法是靜態的,可以直接通過類來引用,而不需要創建實例。
- 修飾代碼塊時,是靜態代碼塊,在類加載時自動加載,只執行一次。
- 修飾內部類時,是靜態內部類,只能訪問外部類的靜態成員變量和方法。靜態內部類可以單獨創建。
- 修飾導入包中的靜態方法或變量時,可以直接使用被修飾的方法和變量,而不需要加上所屬的類。
3. 基本類型和引用類型的區別:
在Java編程語言中,數據類型可以分為兩大類:基本數據類型(又稱原始數據類型)和引用數據類型。這兩者的區別主要體現在以下幾個方面:
存儲內容:
- 基本數據類型:直接存儲實際值在棧(Stack)中,例如數值、字符或布爾值。
- 引用數據類型:存儲堆(Heap)內存地址在棧中,該地址指向實際數據所在的位置。
內存分配:
- 基本數據類型:在變量聲明時,系統會在棧上為其分配空間并直接存儲值。
- 引用數據類型:聲明引用變量時,棧上分配的空間存放的是對象的內存地址,對象本身的數據存儲在堆上。
數據類型種類:
- 基本數據類型:包括整數類型(byte、short、int、long)、浮點類型(float、double)、字符類型(char)和布爾類型(boolean)。
- 引用數據類型:包括類(Class)、接口(Interface)、數組(Array)、枚舉(Enum)、注解(Annotation)和字符串(String)等。
使用方式:
- 基本數據類型:可以直接使用算術運算符進行操作,比如加減乘除。
- 引用數據類型:不能直接使用算術運算符(除了==和!=比較地址),但可以調用其方法和屬性。
傳遞方式:
- 基本數據類型:作為方法參數傳遞時,傳遞的是數據的值(值傳遞)。
- 引用數據類型:作為方法參數傳遞時,傳遞的是內存地址(引用傳遞),因此方法內部對對象的修改會影響原始對象。
默認值:
- 基本數據類型:具有默認值,例如整型的默認值是0,布爾型的默認值是false。
- 引用數據類型:默認值是null。
性能:
- 基本數據類型:由于存儲在棧上,訪問速度相對較快。
- 引用數據類型:存儲在堆上,需要通過棧上的引用訪問,速度相對較慢-
注意
:在Java中,當談到引用數據類型的參數傳遞時,通常指的是“引用傳遞”(pass by reference),但這可能會引起一些誤解。
在Java中,所有參數傳遞都是按值傳遞(pass by value),包括引用數據類型。
這意味著當我們將一個引用數據類型的變量傳遞給方法時,實際上傳遞的是該變量存儲的值的副本,即對象的引用。以下是這個過程的具體解釋:
按值傳遞的本質: 傳遞的是值的副本。對于基本數據類型,這個值就是數據本身;對于引用數據類型,這個值是對象的引用。
誤解引用傳遞: 有人可能會認為,因為引用數據類型的參數可以改變原始對象的狀態,所以Java使用的是引用傳遞。
舉例說明:
假設有一個對象 Person p 存儲在堆上,其引用存儲在棧上的變量 p 中。
當我們將 p 作為參數傳遞給方法時,棧上會創建一個引用的副本。
方法內部使用這個副本引用來訪問和修改堆上的對象。
示例代碼:
public class Person {String name;public Person(String name) {this.name = name;}
}public void changeName(Person p) {p.name = "New Name";
}public static void main(String[] args) {Person person = new Person("Original Name");changeName(person);// person.name 現在是 "New Name"
}
在這個例子中,changeName 方法接收了一個 Person 對象的引用。雖然看起來像是引用傳遞,但實際上是按值傳遞了這個引用的副本。這個副本指向與原始引用相同的 Person 對象,所以修改是通過引用副本進行的,但影響到了原始對象。
總結: 在Java中,引用數據類型的參數傳遞是按值傳遞的,傳遞的是對象引用的副本,但由于這個副本和原始引用指向同一個對象,所以看起來像是引用傳遞。
真正的引用傳遞(pass by reference)是指將變量的引用傳遞給方法或函數,且按引用傳遞傳遞的不是值的副本,而是實際的引用本身。
4. String是值傳遞還是引用傳遞?
在Java中,String是一種特殊的引用數據類型。對于String類型的參數傳遞,可以認為是按值傳遞的,但情況稍微有些復雜。
String的特殊性: String是不可變(immutable)的,這意味著一旦創建了一個String對象,其內容就不能改變。因此,任何試圖修改String對象內容的操作都會返回一個新的String對象。
參數傳遞行為: 當我們將一個String對象作為參數傳遞給方法時,傳遞的是該String對象的引用的副本。然而,由于String是不可變的,所以方法內部無法直接修改原始的String對象。任何修改操作都會創建一個新的String對象,并將引用副本指向這個新對象。
示例:
public class Main {public static void main(String[] args) {String original = "Hello";changeString(original);System.out.println(original); // 輸出仍然是 "Hello"}public static void changeString(String str) {str = "World"; // 這不會改變原始的String對象System.out.println(str); // 輸出 "World"}
}
在這個例子中,changeString方法試圖修改傳入的String參數。然而,由于String是不可變的,str = "World";
實際上是在棧上創建了一個新的String引用,并將其指向一個新的String對象。原始的String對象仍然保持不變,所以main方法中的original變量打印出來的仍然是"Hello"。
總結: 雖然String的參數傳遞看起來像是引用傳遞,但由于String是不可變的,所以方法內部無法修改原始的String對象。因此,在某種意義上,String的參數傳遞可以被認為是按值傳遞。
5. 接口和抽象類的區別:
- 相同點:都是上層的抽象層,不能被實例化,都能包含抽象方法。
- 不同點:
- 抽象類可以包含非抽象方法,而接口中只能包含抽象方法。
- 抽象類可以包含普通和靜態的成員變量,接口只能包含
public static final
修飾的常量。 - 一個類只能繼承一個抽象類,但可以實現多個接口。
- 抽象類可以包含構造方法,接口不能包含構造方法。
interface MyInterface {void method1(); // Abstract method
}abstract class MyAbstractClass {abstract void method2(); // Abstract methodvoid method3() {// Concrete method}
}
6. 反射是在運行時獲取類的相關信息。可以通過 Class
類來獲取字段、方法等信息,從而對類進行操作。
在Java中,反射允許我們在運行時獲取類的相關信息,并且可以動態地操作類的字段、方法、構造函數等。通過使用Class
類,我們可以獲取類的各種信息并對其進行操作。
下面是一個簡單的示例來說明如何使用反射來獲取類的信息:
import java.lang.reflect.Field;
import java.lang.reflect.Method;public class ReflectionExample {public static void main(String[] args) throws Exception {Class<?> myClass = Class.forName("com.example.MyClass"); // 獲取類的 Class 對象// 獲取類的字段信息Field[] fields = myClass.getDeclaredFields();for (Field field : fields) {System.out.println(field.getName());}// 獲取類的方法信息Method[] methods = myClass.getDeclaredMethods();for (Method method : methods) {System.out.println(method.getName());}// 創建類的實例并調用方法Object obj = myClass.getDeclaredConstructor().newInstance();Method myMethod = myClass.getDeclaredMethod("myMethod");myMethod.invoke(obj);}
}
在這個示例中,我們使用Class.forName
方法來獲取指定類的Class
對象,然后通過該對象可以獲取類的字段和方法信息。我們還演示了如何通過反射來創建類的實例,并調用其方法。
需要注意的是,反射是一種功能強大但也復雜的機制,應該謹慎使用。過度依賴反射會導致代碼可讀性和性能上的問題。因此,在使用反射時需要權衡利弊,并盡量避免濫用。
7. Java 中創建實例對象的初始化順序:
在Java中,創建實例對象的初始化順序通常按照以下步驟進行:
- 父類的靜態變量和靜態代碼塊初始化
- 子類的靜態變量和靜態代碼塊初始化
- 父類的實例變量和代碼塊按照在類中的聲明順序初始化
- 父類的構造函數
- 子類的實例變量和代碼塊按照在類中的聲明順序初始化
- 子類的構造函數
這個順序確保了在創建對象時,各個部分都能夠按照正確的順序得到初始化。這個過程對于理解Java對象創建和初始化非常重要,特別是在涉及到繼承關系和多態性的情況下。
8. 獲取反射的幾種方式:
在Java中,獲取反射的幾種方式包括使用Class.forName()、對象.getClass()和直接通過類名.class來獲取Class對象。這些方式都可以用于獲取反射對象并進行動態操作。
-
Class.forName():
使用Class.forName()方法可以根據類的全限定名(包括包名)來獲取對應的Class對象。Class clazz = Class.forName("com.example.YourClass");
-
對象.getClass():
在已經存在對象的情況下,可以通過調用對象的getClass()方法來獲取對應的Class對象。YourClass obj = new YourClass(); Class clazz = obj.getClass();
-
直接通過類名.class:
可以直接通過類名后加上.class來獲取對應的Class對象。Class clazz = YourClass.class;
這些方式都可以用于獲取Class對象,然后通過Class對象進行反射操作,例如創建對象、調用方法、訪問字段等。根據具體的情況選擇合適的方式來獲取反射對象,以便實現靈活的編程和動態的操作。
9. 類加載雙親委派模型:
當然可以,以下是完整的內容,包括了9.1和9.2的部分:
9什么是雙親委派模型
在雙親委派模型中,類加載器收到類加載請求時,會按照以下步驟操作:
-
檢查該類是否已被加載:首先檢查這個類是否已經被當前類加載器加載過,如果已經加載過,則直接返回已加載的類。
-
委派給父類加載器:如果該類尚未被加載,則當前類加載器會將請求委派給其父類加載器去處理。這一過程會一直上溯到啟動類加載器(Bootstrap Class Loader)。
-
嘗試加載:只有當父類加載器無法完成這個類的加載請求時(例如,該類不在父類加載器的搜索路徑中),當前類加載器才會嘗試自己加載這個類。
為什么叫雙親委派模型
這個模型被稱為“雙親委派”是因為每個類加載器都有一個“父”類加載器,它首先將加載請求委托給這個“父”加載器。這里的“雙親”并不是指生物學上的雙親,而是指在類加載器層次結構中的“父輩”。
雙親委派模型的好處
雙親委派模型有以下幾個好處:
- 避免類的重復加載:通過委派給父類加載器,可以避免同一個類被不同的類加載器多次加載,確保每個類在JVM中只存在一個副本。
- 保證Java核心API不被篡改:由于Bootstrap Class Loader是位于委派鏈的最頂端,負責加載Java的核心類庫,因此可以確保這些核心類庫不會被自定義的類加載器加載,從而保護了Java核心API的安全性和穩定性。
如何打破雙親委派模型
在某些特殊情況下,可能需要打破雙親委派模型。可以通過以下方式實現:
- 自定義類加載器:通過定義自己的類加載器并重寫其loadClass方法,可以改變委派邏輯,實現自定義的類加載行為。
- 場景舉例:例如,在Java EE容器或者某些Web服務器(如Tomcat)中,由于需要實現容器的隔離性或者熱替換等特性,會實現自己的類加載器,并不完全遵循雙親委派模型。
綜上所述,雙親委派模型是Java類加載機制中的一個重要概念,它通過委派的方式提高了類加載的效率和安全性,但在特定場景下,也可以根據需要進行適當的打破和調整。
public class ClassLoadingExample {public static void main(String[] args) {ClassLoadingExample example = new ClassLoadingExample();ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();System.out.println("Application ClassLoader: " + appClassLoader);System.out.println("Extension ClassLoader: " + appClassLoader.getParent());System.out.println("Bootstrap ClassLoader: " + appClassLoader.getParent().getParent());}
}
10. 在重寫 equals
方法時通常需要重寫 hashCode
方法,以確保相等的對象具有相同的 hashCode
值,避免在集合類中可能出現的問題。
在Java中,如果一個類重寫了equals方法以改變兩個對象相等的定義,通常也需要重寫hashCode方法。以下是需要同時重寫這兩個方法的原因:
10.1 equals和hashCode方法的關系
在Java中,equals和hashCode方法之間有一個重要的契約,這個契約在java.lang.Object類的文檔中有詳細的描述:
- 如果兩個對象根據equals(Object)方法返回的結果是相等的,那么調用這兩個對象各自的hashCode()方法必須返回相同的整數結果。
- 如果兩個對象根據equals(Object)方法返回的結果是不相等的,那么調用這兩個對象各自的hashCode()方法通常(不是必須)應該返回不同的整數結果。
10.2 為什么需要重寫hashCode
- 一致性:當對象的狀態沒有改變時,多次調用同一個對象的hashCode()方法應該返回相同的值。
- 相等對象必須有相同的hashCode:如果兩個對象相等(即equals方法返回true),它們必須有相同的hashCode值,這是為了保證在使用哈希表(如HashSet、HashMap等)時,這兩個對象能夠被存儲在同一個桶(bucket)中。
- 不相等對象應該有不同的hashCode:雖然不是必須的,但如果兩個對象不相等,它們有不同的hashCode值可以提高哈希表的性能,因為這樣可以減少哈希沖突的可能性。
10.3 不遵守契約的問題
如果不遵守這個契約,在集合類(如HashSet、HashMap等)中可能會出現以下問題:
- 如果兩個相等的對象具有不同的hashCode值,那么在哈希表中它們可能會被存儲在不同的桶中,這將導致equals方法不會被調用,從而無法正確處理這兩個對象(例如,無法刪除其中一個對象)。
- 如果多個不相等的對象具有相同的hashCode值,那么它們都會被映射到同一個桶中,這將增加哈希表的沖突率,導致性能下降。
10.4 如何正確重寫hashCode
為了遵守上述契約,重寫hashCode方法時,以下是一些通用的指導原則:
- 確保相同的對象總是返回相同的hashCode值。
- 確保如果兩個對象根據equals方法相等,它們也必須有相同的hashCode值。
- 盡量使不相等的對象的hashCode值不同,以減少哈希沖突。
- 通常,hashCode方法會基于對象中用于確定相等性的所有字段來計算哈希值,并且計算方式應該盡可能簡單和高效。很多IDE都提供了自動生成hashCode和equals方法的功能,這通常是一個安全且高效的方式。
public class Person {private String name;private int age;@Overridepublic boolean equals(Object obj) {if (this == obj) return true;if (obj == null || getClass() != obj.getClass()) return false;Person person = (Person) obj;return age == person.age && Objects.equals(name, person.name);}@Overridepublic int hashCode() {return Objects.hash(name, age);}
}
11. 面向對象的特征:
- 封裝:將對象的屬性和行為封裝在一起,隱藏內部細節,提供對外的接口。
- 繼承:允許子類繼承父類的屬性和方法,實現代碼復用。
- 多態:同一方法在不同對象上表現出不同的行為。
當然可以,下面是帶有英文縮寫的設計原則:
1. 抽象
- 過程抽象:將復雜的操作或行為抽象為一個簡單的函數或方法調用。
- 數據抽象:定義數據類型和可以對這些類型執行的操作,而無需關心數據的具體表示。
2. 封裝
- 封裝(Encapsulation):隱藏對象的內部細節,僅對外暴露需要公開的接口。這有助于保護對象的狀態不被外部干擾和不恰當的使用。
3. 繼承
- 繼承(Inheritance):允許某個類(子類)繼承另一個類(父類)的屬性和方法,實現代碼重用并添加新功能。
4. 多態
- 多態(Polymorphism):允許不同類的對象對同一消息做出響應,實現同一操作通過不同對象執行不同行為。
擴展的特征和設計原則
- 單一職責原則(SRP - Single Responsibility Principle):一個類應該只有一個改變的理由。
- 開放封閉原則(OCP - Open/Closed Principle):軟件實體應該對擴展開放,對修改封閉。
- 里氏替換原則(LSP - Liskov Substitution Principle):子類必須能夠替換其父類在程序中的任何位置。
- 合成/聚合原則(C/A - Composition/Aggregation Principle):優先使用對象組合,而不是類繼承。
- 迪米特法則(LoD - Law of Demeter):一個對象應當對其他對象有盡可能少的了解。
12. StringBuffer
和 StringBuilder
的區別:
StringBuffer
是線程安全的,使用synchronized
關鍵字來保證線程安全,效率較低。StringBuilder
是非線程安全的,效率較高。
public class StringBuilderExample {public static void main(String[] args) {StringBuilder sb = new StringBuilder("Hello");sb.append(" World");System.out.println(sb.toString());}
}
13. 淺拷貝和深拷貝:
- 淺拷貝:復制對象時只復制對象本身和其內部基本類型屬性的值,而不復制引用類型屬性指向的對象。
- 深拷貝:復制對象時會遞歸地復制所有引用類型屬性指向的對象,使得新對象和原對象完全獨立。
public class DeepCopyExample {public static void main(String[] args) {List<String> list1 = new ArrayList<>();list1.add("item1");list1.add("item2");// Shallow copyList<String> list2 = new ArrayList<>(list1);list2.add("item3");// Deep copyList<String> list3 = new ArrayList<>(list1.size());for (String item : list1) {list3.add(new String(item));}list3.add("item4");System.out.println("List1: " + list1);System.out.println("List2 (Shallow Copy): " + list2);System.out.println("List3 (Deep Copy): " + list3);}
}