Java面向對象基礎之封裝、繼承與多態(tài)詳細攻略
1. 前言
這篇博客主要整理 Java 面向對象基礎內(nèi)容,包括 類、方法、構造方法、方法重載、繼承和多態(tài)。
相比前兩篇的基礎語法和流程控制,這一篇開始進入 Java 比較核心的部分。因為之前學過一些 C++,所以“類”“對象”“構造方法”“繼承”這些概念理解起來會有一定幫助。不過 Java 也有自己的規(guī)則,比如 Java 只支持單繼承、所有類最終都繼承自 Object,以及子類構造方法必須先調(diào)用父類構造方法等。
2. 類文件結構
一個 Java 源文件中可以定義多個類,但最多只能有一個 public 類。
如果有 public 類,那么:
public類名必須和文件名一致。
例如文件名是 Main.java,那么其中的 public 類應該寫成:
public class Main {
}
如果要定義多個 public 類,就必須拆分到多個 .java 文件中。
3. 方法與封裝
3.1 為什么字段一般要 private?
在面向對象中,類的字段一般不直接暴露給外部,而是用 private 修飾。
例如:
class Person {
private String name;
private int age;
}
這樣外部代碼不能直接修改 name 和 age,可以避免隨便賦值導致對象狀態(tài)不合理。
字段變成 private 后,外部通過方法間接操作字段讀寫它們。
例如:
public void setAge(int age) {
if (age < 0 || age > 100) {
throw new IllegalArgumentException("invalid age value");
}
this.age = age;
}
這樣外部代碼不能直接修改 age,只能調(diào)用 setAge()。在方法內(nèi)部,我們就可以檢查參數(shù)是否合法。
再比如:
public void setName(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("invalid name");
}
this.name = name.strip();
}
這里可以避免傳入 null 或空字符串,并且用 strip() 去掉首尾空格。
簡單理解:private 字段負責隱藏數(shù)據(jù),public 方法負責提供安全的操作入口。
3.2 方法是對外接口
一個類可以通過方法給外部代碼暴露一些操作,同時自己保證內(nèi)部邏輯正確。
例如:
Person ming = new Person();
ming.setName("Xiao Ming");
ming.setAge(18);
調(diào)用方法的格式是:
實例變量.方法名(參數(shù));
4. this 關鍵字
在方法內(nèi)部,this 表示當前實例。
例如:
class Person {
private String name;
public String getName() {
return this.name;
}
}
如果沒有命名沖突,也可以省略 this:
public String getName() {
return name;
}
但是如果局部變量和字段同名,就必須使用 this。
例如:
class Person {
private String name;
public void setName(String name) {
this.name = name;
}
}
這里:
- 前面的
this.name表示當前對象的字段; - 后面的
name表示方法參數(shù)。
如果不寫 this,就分不清了。
5. private 方法
方法也可以用 private 修飾:只能在類內(nèi)部使用,外部無法調(diào)用。
例如:
class Person {
private int birth;
public int getAge() {
return calcAge(2026);
}
private int calcAge(int currentYear) {
return currentYear - this.birth;
}
}
這里 calcAge() 是一個私有方法,外部不能直接調(diào)用,但 getAge() 可以在類內(nèi)部調(diào)用它。
這個例子也說明:類對外暴露的是 getAge(),至于內(nèi)部到底有沒有 age 字段,調(diào)用者并不需要知道。
6. 可變參數(shù)
可變參數(shù)用 類型...變量名 表示。
例如:
class Group {
private String[] names;
public void setNames(String... names) {
this.names = names;
}
}
調(diào)用時可以傳入任意個參數(shù):
Group g = new Group();
g.setNames("Xiao Ming", "Xiao Hong", "Xiao Jun");
g.setNames("Xiao Ming");
g.setNames();
可變參數(shù)本質(zhì)上類似數(shù)組。
也可以寫成數(shù)組參數(shù):
public void setNames(String[] names) {
this.names = names;
}
但這樣調(diào)用時需要手動創(chuàng)建數(shù)組:
g.setNames(new String[] {"Xiao Ming", "Xiao Hong"});
所以可變參數(shù)寫起來更方便。
可變參數(shù)可以傳入 0 個參數(shù),此時接收到的是空數(shù)組,而不是
null。
7. 參數(shù)傳遞
7.1 基本類型參數(shù)
如果參數(shù)是基本類型,傳進去的是值的副本。
例如:
public static void change(int n) {
n = 100;
}
外部變量不會被修改。
7.2 引用類型參數(shù)
如果參數(shù)是引用類型,傳進去的是引用的副本。這個副本和外部變量指向同一個對象。
所以,如果通過這個引用修改對象內(nèi)部狀態(tài),外部也能看到變化。
但是,如果只是讓參數(shù)變量重新指向新對象,不會改變外部變量本身。
簡單記:Java 只有值傳遞。引用類型傳遞的是“引用的值”,所以可以修改對象內(nèi)部狀態(tài),但不能讓外部變量改指向。
8. 構造方法
構造方法用于創(chuàng)建對象時初始化實例。
- 方法名必須和類名相同;
- 沒有返回值,也不能寫
void; - 調(diào)用構造方法必須使用
new。
例如:
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
創(chuàng)建對象:
Person p = new Person("Xiao Ming", 18);
如果一個類沒有定義構造方法,編譯器會自動生成一個無參構造方法。
但是,如果我們自己定義了構造方法,編譯器就不會再自動生成默認構造方法。
例如:
class Person {
public Person(String name) {
}
}
這時再寫:
Person p = new Person();
就會報錯,因為無參構造方法不存在。
字段也可以直接初始化:
class Person {
private String name = "Unnamed";
private int age = 10;
}
構造方法也可以重載,也就是定義多個參數(shù)不同的構造方法。
9. 方法重載
方法重載指的是:方法名相同,但參數(shù)不同。
例如:
class Hello {
public void sayHello() {
System.out.println("Hello");
}
public void sayHello(String name) {
System.out.println("Hello, " + name);
}
}
參數(shù)不同可以是:個數(shù)、類型、順序
但一般不推薦只靠參數(shù)順序區(qū)分,容易讓人看混。
需要注意:
返回值不同不能作為方法重載的判斷依據(jù)。
例如下面這樣不算合法重載:
public int test() {
return 1;
}
public String test() {
return "hello";
}
因為調(diào)用 test() 時,編譯器無法只根據(jù)返回值判斷要調(diào)用哪個方法。
10. 繼承
10.1 extends 關鍵字
Java 使用 extends 實現(xiàn)繼承。
例如:
class Person {
protected String name;
protected int age;
}
class Student extends Person {
private int score;
}
這里 Student 繼承自 Person。
可以理解為:
Student is a Person
通過繼承,子類可以復用父類已有的字段和方法,只需要編寫自己新增的功能。
在術語上:
Person是父類、超類、基類;Student是子類、擴展類。
注意:子類不要定義和父類重名的字段,否則容易造成混亂。
10.2 Object 類
在 Java 中,如果一個類沒有顯式寫 extends,編譯器會自動讓它繼承 Object。
也就是說:
class Person {
}
實際上可以理解為:
class Person extends Object {
}
所以,所有 Java 類最終都繼承自 Object。
繼承關系可以簡單理解為:
Object ↑ Person ↑ Student
Java 只允許一個類繼承一個父類,也就是單繼承。
11. protected 關鍵字
protected 可以讓字段或方法在繼承樹內(nèi)部訪問。
例如:
class Person {
protected String name;
}
子類可以訪問:
class Student extends Person {
public String hello() {
return "Hello, " + name;
}
}
訪問權限大致可以簡單記成:
private 只能本類訪問 protected 本類和子類可以訪問 public 外部都可以訪問
12. super 關鍵字
super 表示父類。
在子類中,可以用 super 調(diào)用父類構造方法或父類方法。
12.1 子類構造方法必須先調(diào)用父類構造方法
例如:
class Person {
protected String name;
protected int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
如果子類這樣寫:
class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
this.score = score;
}
}
會報錯。
原因是:Java 要求子類構造方法的第一行必須先調(diào)用父類構造方法。
如果沒有寫,編譯器會自動加上:
super();
但父類 Person 沒有無參構造方法,所以會報錯。
正確寫法是:
class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
super(name, age);
this.score = score;
}
}
如果父類沒有無參構造方法,子類必須顯式調(diào)用
super(...)。
12.2 子類不會繼承父類構造方法
這一點也容易忘:
子類不會繼承父類的構造方法。
子類要創(chuàng)建對象,必須有自己的構造方法。如果沒寫,編譯器可以生成默認構造方法,但它不是從父類繼承來的。
13. 向上轉型和向下轉型
13.1 向上轉型
把子類類型賦值給父類類型,叫向上轉型。
例如:
Student s = new Student(); Person p = s; Object o = s;
這是安全的,因為 Student is a Person,同時也是 Object。
13.2 向下轉型
把父類類型強制轉成子類類型,叫向下轉型。
例如:
Person p1 = new Student(); Student s1 = (Student) p1;
這是可以的,因為 p1 實際指向的是 Student 對象。
但是:
Person p2 = new Person(); Student s2 = (Student) p2;
會在運行時報錯,因為 p2 實際上不是 Student。
向下轉型可能失敗,失敗時會拋出
ClassCastException。
13.3 instanceof
為了避免向下轉型出錯,可以先用 instanceof 判斷。
Person p = new Student();
if (p instanceof Student) {
Student s = (Student) p;
}
instanceof 用來判斷一個引用變量指向的對象是不是某個類型,或者這個類型的子類。
如果變量是 null,那么 instanceof 的結果一定是 false。
較新的 Java 版本中,還可以寫成:
Object obj = "hello";
if (obj instanceof String s) {
System.out.println(s.toUpperCase());
}
這樣判斷成功后,可以直接使用變量 s。
14. 繼承和組合
繼承表示 is-a 關系。
例如:
Student is a Person
組合表示 has-a 關系。
例如:
Car has an Engine
如果只是“擁有某個對象”,不應該強行使用繼承。
簡單記:is-a 用繼承,has-a 用組合。
實際開發(fā)中,很多時候會優(yōu)先考慮組合,因為繼承會讓子類和父類綁定得更緊,而且 Java 只支持單繼承。
15. 多態(tài)
15.1 Override 和 Overload 的區(qū)別
Override 是覆寫,發(fā)生在父類和子類之間。
如果子類定義了一個和父類方法簽名相同的方法,就是覆寫。
例如:
class Person {
public void run() {
System.out.println("Person run");
}
}
class Student extends Person {
@Override
public void run() {
System.out.println("Student run");
}
}
Overload 是重載,發(fā)生在同一個類中,方法名相同,但參數(shù)不同。
例如:
public void hello() {
}
public void hello(String name) {
}
兩者區(qū)別:
| 概念 | 含義 |
|---|---|
Overload | 方法名相同,參數(shù)不同,是新方法 |
Override | 方法簽名相同,子類覆蓋父類方法 |
建議覆寫方法時加上 @Override。
這樣如果方法簽名寫錯,編譯器會提醒。
例如:
class Person {
public void run() {
}
}
class Student extends Person {
@Override
public void run(String s) {
}
}
這里會編譯報錯,因為 run(String s) 并沒有覆寫父類的 run()。
15.2 動態(tài)方法調(diào)用
多態(tài)最重要的一點是:
Java 的實例方法調(diào)用,取決于對象的實際類型,而不是變量的聲明類型。
例如:
Person p = new Student(); p.run();
雖然變量 p 的聲明類型是 Person,但它實際指向的是 Student 對象。
如果 Student 覆寫了 run(),那么最終調(diào)用的是 Student 的 run()。
這就是多態(tài)。
多態(tài)的好處是:代碼可以面向父類編寫,運行時根據(jù)實際子類對象調(diào)用不同實現(xiàn)。
15.3 覆寫 Object 的方法
所有類最終都繼承自 Object,所以可以根據(jù)需要覆寫 Object 中的一些方法。
常見方法有:
| 方法 | 作用 |
|---|---|
toString() | 把對象轉換成字符串 |
equals() | 判斷對象是否邏輯相等 |
hashCode() | 返回對象的哈希值 |
例如:
class Person {
private String name;
@Override
public String toString() {
return "Person: name=" + name;
}
@Override
public boolean equals(Object o) {
if (o instanceof Person p) {
return this.name.equals(p.name);
}
return false;
}
@Override
public int hashCode() {
return this.name.hashCode();
}
}
初學階段先知道:如果希望對象打印得更清楚,可以覆寫 toString();如果希望按內(nèi)容比較對象,可以覆寫 equals()。
16. final 關鍵字
final 可以修飾類、方法和字段。
16.1 final 類
如果一個類用 final 修飾,那么它不能被繼承。
例如:
public final class String {
}
String 類就是 final 的,所以不能被繼承。
16.2 final 方法
如果一個方法用 final 修飾,那么它不能被子類覆寫。
class Person {
public final void run() {
}
}
16.3 final 字段
如果字段用 final 修飾,那么初始化后不能再重新賦值。
class Person {
public final String name;
public Person(String name) {
this.name = name;
}
}
這有點像 C++ 里的 const,可以保證字段初始化后不再改變。
需要注意:如果 final 修飾的是引用類型,那么這個引用不能再指向別的對象,但對象內(nèi)部狀態(tài)是否能變,要看這個對象本身是否可變。
17. 今日易錯點總結
17.1private字段不是不能用,而是要通過方法間接使用
字段設成 private 后,外部不能直接訪問,但可以通過 get、set 方法間接訪問。
這樣做的好處是可以在方法里檢查參數(shù),保證對象狀態(tài)合理。
17.2this主要用來區(qū)分字段和局部變量
如果字段和參數(shù)同名,必須寫:
this.name = name;
否則容易把局部變量和字段混在一起。
17.3 返回值不同不能構成重載
重載看的是參數(shù)列表,不看返回值。
17.4 子類構造方法必須先調(diào)用父類構造方法
如果父類沒有無參構造方法,子類必須顯式寫:
super(...);
并且它必須是構造方法中的第一條語句。
17.5 向下轉型前最好先用 instanceof 判斷
向上轉型一定安全,向下轉型可能失敗。
為了避免 ClassCastException,可以先判斷:
if (p instanceof Student) {
Student s = (Student) p;
}
17.6 Override 和 Overload 不要混
Overload:方法名相同,參數(shù)不同;Override:子類覆寫父類方法,方法簽名相同。
覆寫時建議加上 @Override,讓編譯器幫忙檢查。
18. 學習小結
今天感覺最不好理解的是 多態(tài)。尤其是 Person p = new Student(); 這種寫法,變量的聲明類型和對象的實際類型不一樣,最后調(diào)用方法時又是根據(jù)實際類型來決定,這個地方剛開始看會有點繞。現(xiàn)在可以先簡單理解為:父類變量可以指向子類對象,而真正執(zhí)行哪個方法,要看運行時對象到底是誰。
不過 getName()、setName() 這一類方法就比較好理解,也能明顯感覺到它們的作用。字段設成 private 后,外部不能隨便修改數(shù)據(jù),只能通過方法間接訪問。這樣就可以在 setAge()、setName() 里做參數(shù)檢查,避免把年齡設置成負數(shù),或者把名字設置成空字符串。相比直接暴露字段,這種寫法對數(shù)據(jù)有更好的保護性,是很值得學習的寫法。
目前感覺,面向對象的重點不是只記住某個語法形式,而是理解“類如何對外提供接口、如何隱藏內(nèi)部細節(jié)、子類和父類之間到底是什么關系”。這些關系理清楚之后,后面再學接口、抽象類和更復雜的多態(tài)應該會更容易一些。
第三篇 Java 學習博客就先記錄到這里~
19. 參考資料
到此這篇關于Java面向對象基礎之封裝、繼承與多態(tài)的文章就介紹到這了,更多相關Java封裝、繼承與多態(tài)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Nacos+Spring Cloud Gateway動態(tài)路由配置實現(xiàn)步驟
Nacos最近項目一直在使用,本文通過gateway、nacos-consumer、nacos-provider三個簡單模塊來展示:Nacos下動態(tài)路由配置,,感興趣的小伙伴們可以參考一下2021-08-08
SpringCloud之Config配置中心與Redis分布式鎖詳解
這篇文章主要給大家介紹了SpringCloud Alibaba中Config配置中心,Redis分布式鎖,文中有詳細的代碼示例供大家參考,需要的朋友可以參考閱讀2023-05-05
在本地用idea連接虛擬機上的hbase集群的實現(xiàn)代碼
這篇文章主要介紹了在本地用idea連接虛擬機上的hbase集群的實現(xiàn)代碼,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-10-10

