1 面向對象編程有哪些特性?
面向對象編程(Object-Oriented Programming,簡稱 OOP)是一種以對象為核心的編程范式,它通過模擬現實世界中的事物及其關系來組織代碼。OOP 具有三大核心特性:封裝、繼承、多態。接下來我會逐一詳細說明這些特性。
第一,封裝
封裝是指將數據(屬性)和行為(方法)捆綁在一起,并對外隱藏對象的內部實現細節。通過訪問修飾符(如 private、protected 和 public),我們可以控制哪些部分是對外可見的,哪些是內部私有的。這種機制提高了代碼的安全性和可維護性。例如,在 Java 中,我們通常會將類的屬性設置為 private,并通過 getter 和 setter 方法提供受控的訪問方式。
第二,繼承
繼承允許一個類(子類)基于另一個類(父類)來構建,從而復用父類的屬性和方法。通過繼承,子類不僅可以擁有父類的功能,還可以擴展或重寫父類的行為。Java 中使用 extends 關鍵字實現繼承。例如,我們可以通過定義一個通用的 Animal 類,然后讓 Dog 和 Cat 類繼承它,這樣就避免重復編寫相同的代碼。
第三,多態
多態是指同一個方法調用可以根據對象的實際類型表現出不同的行為。多態分為兩種形式:編譯時多態(方法重載)和運行時多態(方法重寫)。運行時多態是通過動態綁定實現的,即程序在運行時決定調用哪個方法。例如,如果父類 Animal 有一個 eat() 方法,子類 Dog 和 Cat 可以分別重寫這個方法,當調用 animal.eat() 時,具體執行的是 Dog 或 Cat 的實現。多態使得代碼更加靈活和可擴展。
延伸
面向對象和面向過程的區別
(1)面向對象編程(OOP)
定義:面向對象編程是一種以對象為核心的編程范式,它將現實世界中的事物抽象為對象,并通過對象之間的交互來完成任務。OOP 的核心思想是“萬物皆對象”。
特點:
以對象為中心,強調數據和行為的封裝。
使用類(Class)作為模板來創建對象(Object)。
支持三大特性:封裝、繼承和多態。
優點:
模塊化:代碼結構清晰,易于維護和擴展。
復用性高:通過繼承和組合可以復用已有代碼。
靈活性強:支持多態,能夠適應復雜需求的變化。
可讀性強:貼近現實世界的思維方式,便于理解和協作。
缺點:
性能開銷較大:由于引入了類、對象等概念,運行效率可能不如面向過程高效。
學習曲線較陡:需要理解類、對象、繼承等概念,初學者可能覺得復雜。
(2)面向過程編程(POP)
定義:
面向過程編程是一種以過程(函數)為核心的編程范式,它將程序分解為一系列函數或步驟,按照順序執行任務。
特點:
以函數為中心,強調邏輯流程。
數據和函數分離,通常通過參數傳遞數據。
程序由一個個獨立的函數組成,函數之間通過調用關系協作。
優點:
簡單直接:邏輯清晰,適合小型項目或簡單任務。
性能較高:沒有額外的對象開銷,運行效率更高。
易于實現:不需要復雜的類設計,開發速度快。
缺點:
擴展性差:隨著項目規模增大,代碼容易變得混亂,難以維護。
復用性低:函數之間的耦合度高,難以復用代碼。
不適合復雜系統:面對復雜業務邏輯時,代碼會顯得冗長且難以管理。
(3)舉例
- 面向過程:實現一個簡單計算器,用函數依次處理加減乘除。
-
public class Calculator {public static int add(int a, int b) {return a + b;}public static int subtract(int a, int b) {return a - b;}public static void main(String[] args) {System.out.println(add(3, 5)); // 輸出 8} }
- 面向對象:為計算器定義一個類,通過實例化對象調用方法。
-
public class Calculator {private int result;public void add(int a) {result += a;}public int getResult() {return result;}public static void main(String[] args) {Calculator calc = new Calculator();calc.add(5);System.out.println(calc.getResult()); // 輸出 5} }
2 接口、普通類和抽象類的區別和共同點
在 Java 中,接口、普通類和抽象類是構建面向對象程序的三種重要結構。我會從定義、方法實現、繼承關系以及成員變量這4個方面詳細講解它們的區別,然后再總結它們的共同點。
第一個是定義上的區別
普通類是一個完整的、具體的類,可以直接實例化為對象。它包含屬性和方法,并且可以有構造方法。
抽象類是一個不能直接實例化的類,通常用來作為其他類的基類。它可以包含抽象方法(沒有實現的方法)和具體方法(有實現的方法)。
接口是一種完全抽象的結構,用于定義行為規范。它只包含抽象方法(Java 8 之后可以包含默認方法和靜態方法)。
第二個是方法實現上的區別
普通類的所有方法都可以有具體實現(即方法體)。
抽象類可以包含具體方法和抽象方法。
接口默認只包含抽象方法(Java 8 后可以包含默認方法和靜態方法)。
第三是繼承關系上的區別
普通類支持單繼承(一個類只能繼承一個父類)。
抽象類也支持單繼承(一個類只能繼承一個抽象類)。
接口支持多實現(一個類可以實現多個接口)。
第四是成員變量上的區別
普通類和抽象類都可以有各種類型的成員變量(實例變量、靜態變量等)。
接口只能有常量(public static final)。
最后是共同點,一共有3點
首先,它們都是面向對象編程的基礎結構,都可以用來組織代碼,實現封裝、繼承和多態等特性。
其次,它們都可以包含方法,盡管接口中的方法默認是抽象的。
最后,它們都可以被繼承或實現,普通類可以通過繼承擴展功能,抽象類和接口則需要子類繼承或實現后才能使用。
3 深拷貝和淺拷貝區別
深拷貝和淺拷貝的核心區別在于是否遞歸地復制對象內部的引用類型數據,接下來,我會從定義、實現方式以及使用場景三個方面詳細講解它們的區別。
首先是定義上的區別,
淺拷貝是指創建一個新對象,但新對象中的引用類型字段仍然指向原對象中引用類型的內存地址。換句話說,淺拷貝只復制了對象本身,而沒有復制對象內部的引用類型數據。修改新對象中的引用類型數據會影響原對象。
深拷貝是指創建一個新對象,并且遞歸地復制對象內部的所有引用類型數據。換句話說,深拷貝不僅復制了對象本身,還復制了對象內部的所有引用類型數據。修改新對象中的引用類型數據不會影響原對象。
其次是實現方式上的區別,
淺拷貝可以使用 Object 類的 clone() 方法,也可以使用實現 Cloneable 接口并重寫 clone() 的方法。
深拷貝可以手動對引用類型字段進行遞歸拷貝,也可以使用序列化(Serialization)的方式將對象序列化為字節流,再反序列化為新對象。
最后是使用場景上的區別,
淺拷貝適用于當對象內部的引用類型數據不需要獨立復制的情況。
深拷貝適用于當對象內部的引用類型數據需要完全獨立的情況。
延伸
1,淺拷貝使用方法?
public class Resume implements Cloneable {public Object clone() {try {return (Resume) super.clone();} catch (Exception e) {e.printStackTrace();return null;}}
}
2,深拷貝使用方法
class Student implements Cloneable {String name;int age;Professor p;Student(String name, int age, Professor p) {this.name = name;this.age = age;this.p = p;}public Object clone() {Student o = null;try {o = (Student) super.clone();} catch (CloneNotSupportedException e) {System.out.println(e.toString());}o.p = (Professor) p.clone();return o;}
}
4?int和Integer的區別
Integer 和 int 的區別如下:
類型
int 是基本數據類型,存儲的是數值。
Integer 是 int 的包裝類,是一個對象,存儲的是 int 值的引用。
內存分配
int 存儲在棧中,效率高,直接存儲數值。
Integer 是對象,存儲在堆中,效率相對較低。
用途
int 用于基本的數值運算。
Integer 提供了更多功能,比如可以與集合類(List、Map 等)一起使用,因為集合類只能存儲對象。
延伸
1 自動裝箱和拆箱
從 Java 5 開始,Java 提供了自動裝箱和拆箱功能,Java 自動支持基本類型與其包裝類之間的轉換(自動裝箱和拆箱)。
裝箱:int 轉換為 Integer。
拆箱:Integer 轉換為 int。
自動裝箱是指將基本數據類型(如 int、double、boolean 等)自動轉換為對應的包裝類對象(如 Integer、Double、Boolean 等)。這個過程由編譯器自動完成,無需手動調用包裝類的構造方法或靜態方法。
當存儲一個基本數據類型到需要用到對象的場景中(例如集合),Java 編譯器會檢測到基本數據類型需要被轉換為包裝類對象,編譯器會自動調用包裝類的 valueOf() 方法來創建對應的包裝類對象,生成的對象會被存儲到目標位置。
自動拆箱是指將包裝類對象(如 Integer、Double、Boolean 等)自動轉換為對應的基本數據類型(如 int、double、boolean 等)。同樣,這個過程也是由編譯器自動完成的。
當你從一個需要對象的場景中取出值并賦給基本數據類型時,Java 編譯器會檢測到目標變量是一個基本數據類型。編譯器會自動調用包裝類的 xxxValue() 方法,比如 intValue()、doubleValue() 等,來獲取基本數據類型的值。返回的基本數據類型值會被賦給目標變量。
2 內存分配
int:
存儲在棧內存中,直接保存值,效率高。
Integer:
是一個對象,存儲在堆內存中,包含元數據(如值和方法),占用更多內存。
3 Integer 緩存機制
Java 對于 -128 到 127 之間的 Integer 對象進行了緩存。
當值在該范圍內時,會直接返回緩存中的對象。
如果值超出范圍,會創建新的對象。
public class Main {
public static void main(String[] args) {
Integer a = 100;
Integer b = 100;
Integer c = 200;
Integer d = 200;
?
System.out.println(a b); // true,使用緩存
System.out.println(c d); // false,超出緩存范圍
}
}
輸出結果:true
false
4?與集合的結合使用
int 不能直接存儲在集合中,必須使用 Integer。
5?重載和重寫的區別
重載(Overloading)和重寫(Overriding)是 Java 中兩個非常重要的概念,用于實現多態。它們的區別如下:
- 定義:
- 重載:同一個類中方法名稱相同,但參數列表(參數個數或類型)不同。
- 重寫:子類對父類方法進行重新定義,方法名、參數列表都相同。
- 作用域:
- 重載:只發生在同一個類中。
- 重寫:發生在子類和父類之間。
- 方法簽名:
- 重載:方法名相同,參數列表不同。
- 重寫:方法名、參數列表相同。
- 返回值:
- 重載:返回值可以相同或不同。
- 重寫:返回值必須與父類方法兼容(從 Java 5 開始可以是協變返回類型)。
- 訪問修飾符:
- 重載:不受訪問修飾符限制。
- 重寫:子類方法的訪問修飾符不能比父類方法的訪問修飾符更嚴格。
- 關鍵字:
- 重寫:需要用到?
@Override
?注解。 - 重載:無需特殊注解。
- 重寫:需要用到?
延伸
1.什么是重載?
重載是指在同一個類中,允許存在多個同名方法,但這些方法的參數列表必須不同。重載的核心在于方法簽名的不同,而返回值類型不影響重載。
當定義一個重載方法時,
首先,方法名必須相同。
其次,參數列表必須不同,包括參數的數量、類型或順序。
然后,返回值類型可以相同也可以不同,但它不影響重載的判斷。
最后,訪問修飾符(如?public
、private
?等)和異常聲明也不影響重載。
2.什么是重寫?
重寫是指子類對父類中已有的方法進行重新定義,以提供特定的實現。重寫的核心在于**繼承關系**,并且要求方法簽名完全一致。
當定義一個重寫方法時,
首先,方法名必須與父類中的方法名相同。
其次,參數列表必須與父類中的方法完全一致。
然后,返回值類型必須相同或是父類返回值類型的子類型(協變返回類型)。
最后,訪問修飾符不能比父類更嚴格(例如,父類方法是 protected,子類方法可以是 protected 或 public,但不能是 private)。
3.重載的細節
重載是方法的編譯時多態,即方法的調用在編譯期由參數列表決定。以下是一個示例:
public class OverloadingExample {public int add(int a, int b) {return a + b;}public double add(double a, double b) {return a + b;}public String add(String a, String b) {return a + b;}
}這里?add?方法重載了三次,分別接收不同類型的參數。
在編譯時,編譯器會根據傳入的參數類型選擇對應的方法。
4.重寫的細節
重寫是方法的運行時多態,由子類在運行期決定具體調用哪個方法。以下是一個示例:
class Parent {public void display() {System.out.println("Parent display method");}
}class Child extends Parent {@Overridepublic void display() {System.out.println("Child display method");}
}
public class OverridingExample {public static void main(String[] args) {Parent obj = new Child();obj.display(); // 輸出:Child display method}
}
這里 Child 類重寫了 Parent 類的 display 方法。
在運行時,即使引用類型是 Parent,實際調用的是子類的 display 方法。
6?==和 equals 的區別
==
?比較的是?兩個變量的內存地址,即是否引用了同一個對象。如果是基本數據類型,==
?比較的是它們的值。equals()
是?對象的比較方法,默認實現(在?Object
?類中)是比較內存地址,和?==
?類似。但是,大多數類(例如?String
、Integer
?等)會重寫?equals()
?方法,用于比較內容(值)是否相同。
示例代碼:
public class Main {public static void main(String[] args) {String str1 = new String("hello");String str2 = new String("hello");String str3 = "hello";String str4 = "hello";// == 比較System.out.println(str1 == str2); // false,兩個對象的內存地址不同System.out.println(str3 == str4); // true,字符串常量池優化// equals 比較System.out.println(str1.equals(str2)); // true,內容相同System.out.println(str3.equals(str4)); // true,內容相同}
}
延伸
1.為什么需要重寫?equals()
?方法?
- 默認的equals()方法比較的是對象地址,而在許多場景下,我們需要比較對象的內容是否相等。例如:比較兩個?
Person
?對象的?name
?和?age
?是否相等。 - 當你重寫?
equals()
?方法時,一般還需要重寫?hashCode()
?方法,保證邏輯一致。
2.hashCode()
?和?equals()
?的關系
- 如果兩個對象通過?
equals()
?方法相等,它們的?hashCode()
?必須相等。 - 如果兩個對象的?
hashCode()
?相等,它們未必通過?equals()
?方法相等。 - 這條規則對于集合(如?
HashMap
?和?HashSet
)非常重要。
示例:hashCode()
?與?equals()
?的一致性
class Person {private String name;public Person(String name) {this.name = name;}@Overridepublic boolean equals(Object obj) {if (this == obj) return true;if (obj == null || getClass() != obj.getClass()) return false;Person person = (Person) obj;return name.equals(person.name);}@Overridepublic int hashCode() {return name.hashCode();}
}
3.hashCode() 的作用
hashCode() 是 Java 中 Object 類的一個方法,用于返回對象的哈希碼(Hash Code),它是一個整數值。這個哈希碼的主要作用是確定對象在基于哈希表的數據結構中的存儲位置。
(1) 哈希碼的核心作用:定位對象
快速定位:
在基于哈希表的數據結構(如 HashMap、HashSet 和 Hashtable)中,hashCode() 被用來計算對象的存儲位置。
哈希表通過將對象的哈希值映射到特定的“桶”(Bucket)中,從而實現高效的插入、查找和刪除操作。
這種機制使得哈希表的時間復雜度通常為 O(1),即常數時間復雜度,極大提升了性能。
7 什么是泛型?有什么作用?
泛型(Generics)?是 Java 中的一種語言特性,允許我們在類、接口和方法中使用參數化類型。它提供了一種在編譯時檢測類型安全的機制,從而減少運行時的類型轉換錯誤。泛型的主要目的是使代碼更加類型安全、可讀性強、通用性高。
泛型的作用主要有4點
第一點是提高代碼的復用性,它允許我們編寫與類型無關的通用代碼。
第二點是增強類型安全性,在沒有泛型的情況下,集合類(如 ArrayList)默認存儲的是 Object 類型,取出元素時需要手動進行類型轉換,容易引發 ClassCastException。而泛型在編譯時就會進行類型檢查,避免了運行時的類型錯誤。
第三點是簡化代碼,使用泛型后,我們無需顯式地進行類型轉換,減少了冗余代碼,提高了代碼的可讀性和維護性。
第四點是支持復雜的類型約束,泛型可以通過通配符(如 ? extends T 和 ? super T)實現更復雜的類型限制,滿足特定場景下的需求。
延伸
1.泛型的語法與使用
泛型類?泛型可以用于定義類,使類可以處理多種數據類型。
例子:
public class Box<T> {private T value;public T getValue() {return value;}public void setValue(T value) {this.value = value;}
}public class Example {public static void main(String[] args) {Box<String> stringBox = new Box<>();stringBox.setValue("Hello");System.out.println(stringBox.getValue()); // 輸出:HelloBox<Integer> intBox = new Box<>();intBox.setValue(123);System.out.println(intBox.getValue()); // 輸出:123}
}
T 是一個類型參數,表示該類可以接受任意類型。
使用時,通過指定具體類型(如String、Integer)實現類型安全。
泛型方法?泛型也可以用于方法,使方法可以接受不同類型的參數。
例子:
public class Example {// 泛型方法public static <T> void printArray(T[] array) {for (T element : array) {System.out.println(element);}}public static void main(String[] args) {Integer[] intArray = {1, 2, 3};String[] strArray = {"A", "B", "C"};printArray(intArray); // 輸出:1 2 3printArray(strArray); // 輸出:A B C}
}方法前面的<T>表示這是一個泛型方法,T是類型參數。
方法可以接受任意類型的數組作為參數。
泛型接口?泛型可以用于接口,使接口適配不同的實現類型。
例子:
public interface Pair<K, V> {K getKey();V getValue();
}public class KeyValue<K, V> implements Pair<K, V> {private K key;private V value;public KeyValue(K key, V value) {this.key = key;this.value = value;}public K getKey() {return key;}public V getValue() {return value;}
}public class Example {public static void main(String[] args) {Pair<String, Integer> pair = new KeyValue<>("Age", 25);System.out.println("Key: " + pair.getKey()); // 輸出:Key: AgeSystem.out.println("Value: " + pair.getValue()); // 輸出:Value: 25}
}
泛型接口允許在實現時指定不同的類型參數。
?通配符:泛型可以使用通配符(?
)來表示未知類型
-
通配符的使用:
? extends T
:表示類型是T
或T
的子類(用于讀取)。? super T
:表示類型是T
或T
的父類(用于寫入)。-
?
:表示任意類型。例子:
import java.util.ArrayList; import java.util.List;public class Example {public static void printNumbers(List<? extends Number> list) {for (Number number : list) {System.out.println(number);}}public static void main(String[] args) {List<Integer> intList = new ArrayList<>();intList.add(1);intList.add(2);List<Double> doubleList = new ArrayList<>();doubleList.add(1.1);doubleList.add(2.2);printNumbers(intList); // 輸出:1 2printNumbers(doubleList); // 輸出:1.1 2.2} }? extends Number 表示列表中的元素必須是Number或其子類,保證類型安全。
2.泛型的限制
- Java 的泛型是通過類型擦除(Type Erasure)實現的,泛型信息只存在于編譯階段,運行時被擦除為
Object
。 - 例如,
List<Integer>
和List<String>
在運行時實際上是相同的類型。例子:
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass()); // 輸出:true
3.泛型不能使用基本數據類型
- 泛型不支持基本數據類型(如
int
、double
等),只能使用包裝類(如Integer
、Double
等)。 - 解決方案:自動裝箱和拆箱。
4.泛型數組的創建
不能直接創建泛型數組,如T[]
,因為類型信息在運行時被擦除。
8 什么是反射?應用?
反射是一種在運行時動態獲取類信息的能力。通過反射,我們可以在程序運行時加載類、獲取類的結構(如字段、方法、構造器等),甚至可以調用類的方法或修改字段的值。
反射主要應用在這5個場景
第一個是框架開發,很多 Java 框架都有使用反射,比如如 Spring、Hibernate 等。
第二個是動態代理,動態代理是反射的一個重要應用,常用于 AOP(面向切面編程)。通過反射,我們可以在運行時動態生成代理類,攔截方法調用并添加額外邏輯。
第三個是注解處理,注解本身不會對程序產生任何影響,但通過反射,我們可以在運行時讀取注解信息并執行相應的邏輯。
第四個是插件化開發,在某些場景下,我們需要動態加載外部的類或模塊。反射可以幫助我們在運行時加載這些類并調用其方法,從而實現插件化開發。
第五個是測試工具,單元測試框架(如 JUnit)利用反射來發現和運行測試方法,而無需手動指定每個測試用例。
延伸
1.為什么引入反射
在傳統的 Java 編程中,所有的類和方法調用必須在編譯時確定。 這種模式雖然安全且高效,但在某些場景下顯得不夠靈活。而Java 引入反射是為了在運行時動態獲取類的信息并操作類或對象,包括獲取類的結構、創建對象、調用方法、訪問 / 修改字段等,從而增強程序的靈活性和可擴展性。
2.反射的優缺點
優點:
????????靈活性強:允許程序動態操作未知類,非常適合框架開發或插件化設計。
????????動態性:反射支持運行時動態加載類和調用方法,避免了硬編碼。
????????代碼復用性高:通過反射,可以寫出通用代碼,減少重復。
缺點:
????????性能開銷大:反射的調用比直接調用慢得多,因為需要進行許多運行時檢查和處理。
????????安全風險:反射能繞過 Java 的訪問控制機制,如訪問私有字段或方法,可能導致安全問題。
????????代碼可讀性差:反射的代碼往往復雜且難以維護,尤其對于大型項目而言。
????????編譯時安全性降低:使用反射時,編譯器無法驗證被調用的方法或字段是否存在。
9 StringBuffer 的特點
StringBuffer 是一個可變的字符序列,與 String 不同,StringBuffer 的內容是可以被修改的。它的核心特點是線程安全和高效的字符串操作。
StringBuffer有四個特點:
第一個是它具有可變性,我們可以在原有對象上直接修改字符串內容,而無需創建新的對象。
第二個它是線程安全的,StringBuffer 的所有方法都通過 synchronized 關鍵字修飾,因此它是線程安全的。 在多線程環境下,多個線程可以同時操作同一個 StringBuffer 對象,而不會引發數據競爭或不一致問題。
第三個是性能相對較好,StringBuffer 內部使用一個可擴容的字符數組來存儲數據,當容量不足時會自動擴展。相比于 String 的不可變性(每次修改都會生成新對象),StringBuffer 在頻繁修改字符串時性能更高。而相比于非線程安全的 StringBuilder ,性能略低。
第四個是包含豐富的 API,比如:append():追加內容到字符串末尾。 insert():在指定位置插入內容。delete():刪除指定范圍的內容。 reverse():反轉字符串內容。 toString():將 StringBuffer 轉換為 String。
延伸
1.String、StringBuffer、StringBuilder 的區別?
String
- 不可變:
String
?是不可變類,每次對字符串的修改都會生成新的字符串對象。 - 線程安全:因為不可變,所以天然線程安全。
- 性能:由于每次修改都會創建新對象,頻繁操作時效率較低。
- 不可變:
StringBuilder
- 可變:
StringBuilder
?是可變類,對字符串的操作會直接在原對象上修改。 - 線程不安全:不適用于多線程場景。
- 性能:在單線程環境中,操作效率比?
String
?和?StringBuffer
?高。
- 可變:
StringBuffer
- 可變:和?
StringBuilder
?類似,也是可變類。 - 線程安全:通過同步方法實現線程安全,適用于多線程場景。
- 性能:由于線程安全機制的開銷,操作效率比?
StringBuilder
?略低。
- 可變:和?
適用場景:
- 如果字符串內容?不會改變,用?
String
。 - 如果字符串內容會頻繁修改,且在?單線程?環境下使用,選?
StringBuilder
。 - 如果字符串內容會頻繁修改,且在?多線程?環境下使用,選?
StringBuffer
。