一. 面向對象三大特性之繼承
1. 繼承的概念
繼承(inheritance)機制:是面向對象程式設計使代碼可以複用的最重要的手段,它允許程式員在保持原有類特性的基礎上進行擴充,增加新功能,這樣産生新的類,稱派生類(子類)。
繼承呈現了面向對象程式設計的層次結構, 展現了由簡單到複雜的認知過程。繼承主要解決的問題是:共性的抽取,實作代碼複用。
例如:狗和貓都是動物,那麼我們就可以将共性的内容進行抽取,然後采用繼承的思想來達到共用
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAnYldHL0FWby9mZvwFN4ETMfdHLkVGepZ2XtxSZ6l2clJ3LcV2Zh1Wa9M3clN2byBXLzN3btgHL9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsQTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5SN1ATN3EzN5gDNwIzMkF2NzYzX0QzMykDMxIzLcFTMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
上述圖示中,Dog和Cat都繼承了Animal類,其中:Animal類稱為父類/基類/超類,Dog和Cat可以稱為Animal的子類/派生類,繼承之後,子類可以複用父類中成員,子類在實作時隻需關心自己新增加的成員即可。
2. 繼承的文法
在Java中如果要表示類之間的繼承關系,需要借助extends關鍵字,具體如下:
修飾符 class 子類 extends 父類 {
// ...
}
此時将 1 中的設計思想使用代碼實作:
// Animal.java
public class Animal{
String name;
int age;
public void eat(){
System.out.println(name + "正在吃飯");
}
public void sleep(){
System.out.println(name + "正在睡覺");
}
}
// Dog.java
public class Dog extends Animal{
void bark(){
System.out.println(name + "汪汪汪~~~");
}
}
// Cat.Java
public class Cat extends Animal{
void mew(){
System.out.println(name + "喵喵喵~~~");
}
}
// TestExtend.java
public class TestExtend {
public static void main(String[] args) {
Dog dog = new Dog();
// dog類中并沒有定義任何成員變量,
//name和age屬性是從父類Animal中繼承下來的
System.out.println(dog.name);
System.out.println(dog.age);
//dog通路的eat()和sleep()方法也是從Animal中繼承下來的
dog.eat();
dog.sleep();
dog.bark();
}
}
注意:
1.子類會将父類中的成員變量或者成員方法繼承到子類中了
2.子類繼承父類之後,必須要新添加自己特有的成員,展現出與基類的不同,否則就沒有必要繼承了
3. 父類成員的通路
3.1 子類中通路父類的成員變量
1.子類和父類不存在同名成員變量
public class Base {
int a;
int b;
}
public class Derived extends Base{
int c;
public void method(){
a = 10; // 通路從父類中繼承下來的a
b = 20; // 通路從父類中繼承下來的b
c = 30; // 通路子類自己的c
}
}
2.子類和父類成員變量同名
public class Base {
int a;
int b;
int c;
}
public class Derived extends Base{
int a; // 與父類中成員a同名,且類型相同
char b; // 與父類中成員b同名,但類型不同
public void method(){
a = 100; // 通路子類自己新增的a
b = 101; // 通路子類自己新增的b
c = 102; // 子類沒有c,通路從父類繼承下來的c
// d = 103; // 編譯失敗,因為父類和子類都沒有定義成員變量d
}
}
在子類方法中 或者 通過子類對象通路成員時:
• 如果通路的成員變量子類中有,優先通路自己的成員變量。
• 如果通路的成員變量子類中無,則通路父類繼承下來的,如果父類也沒有定義,則編譯報錯。
• 如果通路的成員變量與父類中成員變量同名,則優先通路自己的。
成員變量通路遵循就近原則,自己有優先自己的,如果沒有則向父類中找
3.2 子類中通路父類的成員方法
1.父類和子類的成員方法名字不同
public class Base {
public void methodA(){
System.out.println("Base中的methodA()");
}
}
public class Derived extends Base{
public void methodB(){
System.out.println("Derived中的methodB()方法");
}
public void methodC(){
methodB(); // 通路子類自己的methodB()
methodA(); // 通路父類繼承的methodA()
// methodD(); // 編譯失敗,在整個繼承體系中沒有發現方法methodD()
}
}
2.父類和子類的成員方法名字相同
public class Base {
int a;
int b;
public void methodA(){
System.out.println("Base中的methodA()");
}
public void methodB(){
System.out.println("Base中的methodB()");
}
}
public class Derived extends Base{
public void methodA(int a) {
System.out.println("Derived中的method(int)方法");
}
public void methodB(){
System.out.println("Derived中的methodB()方法");
}
public void methodC(){
methodA(); // 沒有傳參,通路父類中的methodA()
methodA(20); // 傳遞int參數,通路子類中的methodA(int)
methodB(); // 直接通路,則永遠通路到的都是子類中的methodB(),基類的無法通路到
}
}
【總結】:
• 通過子類對象通路父類與子類中不同名方法時,優先在子類中找,找到則通路;否則在父類中找,找到則通路,如果父類中也沒有則編譯報錯。
• 通過派生類對象通路父類與子類同名方法時,如果父類和子類同名方法的參數清單不同(重載),根據調用方法适傳遞的參數選擇合适的方法通路;如果沒有則報錯。
• 通過派生類對象通路父類與子類同名方法時,如果父類和子類同名方法的參數清單、傳回值都相同,則遵循就近原則,直接通路子類中的方法,不會通路父類中的方法。
4. protected 的使用場景
結合下面代碼了解父類中不同通路權限的成員在子類中的可見性:
注意:如果和子類在不同包中,父類中的成員被protected修飾,要想通路父類中的成員必須通過super關鍵字來通路;父類中private成員變量雖然在子類中不能直接通路,但是也繼承到子類中了
// extend01包中
public class B {
private int a;
protected int b;
public int c;
int d;
}
// extend01包中
// 同一個包中的子類
public class D extends B{
public void method(){
// super.a = 10; // 編譯報錯,父類private成員在相同包子類中不可見
super.b = 20; // 父類中protected成員在相同包子類中可以直接通路
super.c = 30; // 父類中public成員在相同包子類中可以直接通路
super.d = 40; // 父類中預設通路權限修飾的成員在相同包子類中可以直接通路
}
}
// extend02包中
// 不同包中的子類
public class C extends B {
public void method(){
//super.a = 10; // 編譯報錯,父類中private成員在不同包子類中不可見
super.b = 20; // 父類中protected修飾的成員在不同包子類中可以直接通路
super.c = 30; // 父類中public修飾的成員在不同包子類中可以直接通路
//super.d = 40; // 父類中預設通路權限修飾的成員在不同包子類中不能直接通路
}
}
// extend02包中
// 不同包中的類
public class TestC {
public static void main(String[] args) {
C c = new C();
c.method();
// System.out.println(c.a); // 編譯報錯,父類中private成員在不同包其他類中不可見
// System.out.println(c.b); // 父類中protected成員在不同包其他類中不能直接通路
System.out.println(c.c); // 父類中public成員在不同包其他類中可以直接通路
// System.out.println(c.d); // 父類中預設通路權限修飾的成員在不同包其他類中不能直接通路
}
}
5. 繼承方式
但在Java中隻支援以下幾種繼承方式:
注意:Java中不支援多繼承,且一般我們不希望出現超過三層的繼承關系。
6. final 關鍵字
final關鍵可以用來修飾變量、成員方法以及類。
1.修飾變量或字段,表示常量(即不能修改)
final int a = 10;
a = 20; // 編譯出錯,這裡a是常量,不可以被修改
2.修飾類:表示此類不能被繼承
final public class Animal {
...
}
public class Bird extends Animal {
...
}
// 編譯出錯
Error:(3, 27) java: 無法從最終com.bit.Animal進行繼
觀察 String 字元串類的源碼, 預設就是用 final 修飾的, 不能被繼承.
3.修飾方法:final修飾的方法叫做密封方法,不能被重寫。
7. 繼承與組合
和繼承類似, 組合也是一種表達類之間關系的方式, 也是能夠達到代碼重用的效果。組合并沒有涉及到特殊的文法(諸如 extends 這樣的關鍵字), 僅僅是将一個類的執行個體作為另外一個類的字段。
繼承表示對象之間是is-a的關系,比如:狗是動物,貓是動物
組合表示對象之間是has-a的關系,比如:汽車和其輪胎、發動機、方向盤、車載系統等的關系就應該是組合,因為汽車是有這些部件組成的。
// 輪胎類
class Tire{
// ...
} /
/ 發動機類
class Engine{
// ...
} /
/ 車載系統類
class VehicleSystem{
// ...
}
class Car{
private Tire tire; // 可以複用輪胎中的屬性和方法
private Engine engine; // 可以複用發動機中的屬性和方法
private VehicleSystem vs; // 可以複用車載系統中的屬性和方法
// ...
} /
/ 奔馳是汽車
class Benz extend Car{
// 将汽車中包含的:輪胎、發送機、車載系統全部繼承下來
}
組合和繼承都可以實作代碼複用,應該使用繼承還是組合,需要根據應用場景來選擇,一般建議:能用組合盡量用組合。
二. 面向對象三大特性之多态
1. 多态的概念
俗來說,就是多種形态,具體點就是去完成某個行為,當不同的對象去完成時會産生出不同 的狀态;同一件事情,發生在不同對象身上,就會産生不同的結果。
2. 重寫
重寫(override):也稱為覆寫。重寫是子類對父類非靜态、非private修飾,非final修飾,非構造方法等的實作過程進行重新編寫, 傳回值和形參都不能改變。即外殼不變,核心重寫!重寫的好處在于子類可以根據需要,定義特定于自己的行為。 也就是說子類能夠根據需要實作父類的方法。
【方法重寫的規則】
• 子類在重寫父類的方法時,一般必須與父類方法原型一緻: 傳回值類型 方法名 (參數清單) 要完全一緻
• 被重寫的方法傳回值類型可以不同,但是必須是具有父子關系的
• 通路權限不能比父類中被重寫的方法的通路權限更低。例如:如果父類方法被public修飾,則子類中重寫該方法就不能聲明為 protected
• 父類被static、private、final修飾的方法,構造方法都不能被重寫。
• 重寫的方法, 可以使用 @Override 注解來顯式指定. 有了這個注解能幫我們進行一些合法性校驗. 例如不小心将方法名字拼寫錯了 (比如寫成 aet), 那麼此時編譯器就會發現父類中沒有 aet 方法, 就會編譯報錯, 提示無法構成重寫
【重寫和重載的差別】
方法重載是一個類的多态性表現,而方法重寫是子類與父類的一種多态性表現
【重寫的設計原則】
對于已經投入使用的類,盡量不要進行修改。最好的方式是:重新定義一個新的類,來重複利用其中共性的内容,并且添加或者改動新的内容。
靜态綁定:也稱為前期綁定(早綁定),即在編譯時,根據使用者所傳遞實參類型就确定了具體調用那個方法。典型代表函數重載。
動态綁定:也稱為後期綁定(晚綁定),即在編譯時,不能确定方法的行為,需要等到程式運作時,才能夠确定具體調用那個類的方法。 這也是多态的特征。
3. 向上轉型和向下轉型
3.1 向上轉型
向上轉型:實際就是建立一個子類對象,将其當成父類對象來使用
文法格式:父類類型 對象名 = new 子類類型( )
//animal是父類類型,但可以引用一個子類對象,因為是從小範圍向大範圍的轉換。
Animal animal = new Cat("元寶",2);
【使用場景】
1.直接指派
2.方法傳參
3.方法傳回
public class TestAnimal {
// 2. 方法傳參:形參為父類型引用,可以接收任意子類的對象
public static void eatFood(Animal a){
a.eat();
}
// 3. 作傳回值:傳回任意子類對象
public static Animal buyAnimal(String var){
if("狗" == var){
return new Dog("狗狗",1);
}else if("貓" == var){
return new Cat("貓貓", 1);
}else{
return null;
}
}
public static void main(String[] args) {
Animal cat = new Cat("元寶",2);
// 1. 直接指派:子類對象指派給父類對象
Dog dog = new Dog("小七", 1);
eatFood(cat);
eatFood(dog);
Animal animal = buyAnimal("狗");
animal.eat();
animal = buyAnimal("貓");
animal.eat();
}
}
public class Animal{
String name;
int age;
public void eat(){
System.out.println(name + "正在吃飯");
}
public void sleep(){
System.out.println(name + "正在睡覺");
}
}
// Dog.java
public class Dog extends Animal{
void bark(){
System.out.println(name + "汪汪汪~~~");
}
}
// Cat.Java
public class Cat extends Animal{
void mew(){
System.out.println(name + "喵喵喵~~~");
}
}
向上轉型的優點:讓代碼實作更簡單靈活。
向上轉型的缺陷:不能調用到子類特有的方法。
3.2 向下轉型
将一個子類對象經過向上轉型之後當成父類方法使用,再無法調用子類的方法,但有時候可能需要調用子類特有的方法,此時:将父類引用再還原為子類對象即可,即向下轉型
public class TestAnimal {
public static void main(String[] args) {
Cat cat = new Cat("元寶",2);
Dog dog = new Dog("小七", 1);
// 向上轉型
Animal animal = cat;
animal.eat();
animal = dog;
animal.eat();
// 編譯失敗,編譯時編譯器将animal當成Animal對象處理
// 而Animal類中沒有bark方法,是以編譯失敗
// animal.bark();
// 向上轉型
// 程式可以通過程式設計,但運作時抛出異常---因為:animal實際指向的是狗
// 現在要強制還原為貓,無法正常還原,運作時抛出:ClstException
cat = (Cat)animal;
cat.mew();
// animal本來指向的就是狗,是以将animal還原為狗也是安全的
dog = (Dog)animal;
dog.bark();
}
}
向下轉型用的比較少,而且不安全,萬一轉換失敗,運作時就會抛異常。Java中為了提高向下轉型的安全性,引入了 instanceof ,如果該表達式為true,則可以安全轉換。
instanceof 是 Java 的保留關鍵字。它的作用是測試它左邊的對象是否是它右邊的類的執行個體,傳回 boolean 的資料類型。
public class TestAnimal {
public static void main(String[] args) {
Cat cat = new Cat("元寶",2);
Dog dog = new Dog("小七", 1);
// 向上轉型
Animal animal = cat;
animal.eat();
animal = dog;
animal.eat();
if(animal instanceof Cat){
cat = (Cat)animal;
cat.mew();
}
if(animal instanceof Dog){
dog = (Dog)animal;
dog.bark();
}
}
}
4. 多态實作條件
在java中要實作多态,必須要滿足如下幾個條件,缺一不可:
1.必須在繼承體系下
2.子類必須要對父類中方法進行重寫
3.通過父類的引用調用重寫的方法
多态展現:在代碼運作時,當傳遞不同類對象時,會調用對應類中的方法。
實作多态的執行個體:
public class Animal {
String name;
int age;
public Animal(String name, int age){
this.name = name;
this.age = age;
}
public void eat(){
System.out.println(name + "吃飯");
}
}
public class Cat extends Animal{
public Cat(String name, int age){
super(name, age);
}
@Override
public void eat(){
System.out.println(name+"吃魚~~~");
}
}
public class Dog extends Animal {
public Dog(String name, int age){
super(name, age);
}
@Override
public void eat(){
System.out.println(name+"吃骨頭~~~");
}
}
///分割線//
public class TestAnimal {
// 編譯器在編譯代碼時,并不知道要調用Dog 還是 Cat 中eat的方法
// 等程式運作起來後,形參a引用的具體對象确定後,才知道調用那個方法
// 注意:此處的形參類型必須時父類類型才可以
public static void eat(Animal a){
a.eat();
}
public static void main(String[] args) {
Cat cat = new Cat("元寶",2);
Dog dog = new Dog("小七", 1);
eat(cat);
eat(dog);
}
}
執行結果:
在上述代碼中, 分割線上方的代碼是 類的實作者 編寫的, 分割線下方的代碼是 類的調用者(調用類中的方法) 編寫的。
當類的調用者在編寫 eat 這個方法的時候, 參數類型為 Animal (父類), 此時在該方法内部并不知道, 也不關注目前的 a 引用指向的是哪個類型(哪個子類)的執行個體. 此時 a這個引用調用 eat方法可能會有多種不同的表現(和 a 引用的執行個體相關), 這種行為就稱為 多态.
5. 多态的優缺點
5.1 使用多态好處
1.能夠降低代碼的 “圈複雜度”, 避免使用大量的 if - else
圈複雜度是一種描述一段代碼複雜程度的方式,計算一段代碼中條件語句和循環語句出現的個數, 這個個數就稱為 “圈複雜度”;如果一個方法的圈複雜度太高, 就需要考慮重構,不同公司對于代碼的圈複雜度的規範不一樣. 一般不會超過 10 。
例如要列印多個形狀, 如果不基于多态, 實作代碼如下:
public class Test{
public static void drawShapes() {
Rect rect = new Rect();
Cycle cycle = new Cycle();
Flower flower = new Flower();
String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};
for (String shape : shapes) {
if (shape.equals("cycle")) {
cycle.draw();
} else if (shape.equals("rect")) {
rect.draw();
} else if (shape.equals("flower")) {
flower.draw();
}
}
}
public static void main(String[] args) {
drawShapes();
}
}
class Shape {
//屬性....
public void draw() {
System.out.println("畫圖形!");
}
}
class Rect extends Shape{
@Override
public void draw() {
System.out.println("♦");
}
}
class Cycle extends Shape{
@Override
public void draw() {
System.out.println("●");
}
}
class Flower extends Shape{
@Override
public void draw() {
System.out.println("❀");
}
}
如果使用使用多态, 則不必寫這麼多的 if - else 分支語句, 代碼更簡單
public static void drawShapes() {
// 我們建立了一個 Shape 對象的數組.
Shape[] shapes = {
new Cycle(),
new Rect(),
new Cycle(),
new Rect(),
new Flower()
};
for (Shape shape : shapes) {
shape.draw();
}
}
class Triangle extends Shape {
@Override
public void draw() {
System.out.println("△");
}
}