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

