繼承
概述
由來
多個類中存在相同屬性和行為時,將這些內容抽取到單獨一個類中,那么多個類無需再定義這些屬性和行為,只要繼承那一個類即可。如圖所示:
其中,多個類可以稱為子類,單獨那一個類稱為父類、超類(superclass)或者基類。
繼承描述的是事物之間的所屬關系,這種關系是: is-a
的關系。例如,圖中兔子屬於食草動物,食草動物屬於動物。可見,父類更通用,子類更具體。我們通過繼承,可以使多種事物之間形成一種關系體系。
定義
- 繼承:就是子類繼承父類的屬性和行為,使得子類對象具有與父類相同的屬性、相同的行為。子類可以直接訪問父類中的非私有的屬性和行為。
好處
- 提高代碼的復用性。
- 類與類之間產生了關系,是多態的前提。
繼承的格式
通過 extends
關鍵字,可以聲明一個子類繼承另外一個父類,定義格式如下:
class 父類 {
...
}
class 子類 extends 父類{
...
}
繼承演示代碼如下:
//定義員工類Employee作為父類
class Employee {
//定義name屬性
String name;
//定義員工的工作方法
public void work() {
System.out.println("盡心盡力地工作");
}
}
//定義教師類Teacher繼承員工類Employee
class Teacher extends Employee {
//定義一個打印name的方法
public void printName() {
System.out.println("name="+ name);
}
}
//定義測試類
public class ExtendDemo01 {
public static void main(String[] args) {
//創建一個教師類對象
Teacher t = new Teacher();
//為該員工類的name屬性賦值
t.name = "小明";
//調用Teacher類中的printName()方法
t.printName(); // name=小明
//調用Teacher類繼承來的Work()方法
t.work(); // 盡心盡力地工作
}
}
繼承后的特點——成員變量
當類之間產生了關系后,其中各類中的成員變量,又產生了哪些影響呢?
成員變量不重名
如果子類父類中出現不重名的成員變量,這時的訪問是沒有影響的。代碼如下:
class Fu {
//Fu中的成員變量
int num = 5;
}
class Zi extends Fu {
//Zi中的成員變量
int num2 = 6;
//Zi中的成員方法
public void show() {
//訪問父類中的num
System.out.println("Fu num = "+num); //繼承而來,所以直接訪問。
//訪問子類中的num2
System.out.println("Zi num2 = "+num2);
}
}
class ExtendsDemo02 {
public static void main(String[] args) {
//創建子類對象
Zi z = new Zi();
//調用子類中的show()方法
z.show();
}
}
演示結果:
Fu num = 5
Zi num2 = 6
成員變量重名
如果子類父類中出現重名的成員變量,這時的訪問是有影響的。代碼如下:
class Fu {
//Fu中的成員變量
int num = 5;
}
class Zi extends Fu {
//Zi中的成員變量
int num = 6;
//Zi中的成員方法
public void show() {
//訪問父類中的num
System.out.println("Fu num = "+num);
//訪問子類中的num
System.out.println("Zi num = "+num);
}
}
class ExtendsDemo02 {
public static void main(String[] args) {
//創建子類對象
Zi z = new Zi();
//調用子類中的show()方法
z.show();
}
}
演示結果:
Fu num = 6
Zi num = 6
子父類中出現了同名的成員變量時,在子類中需要訪問父類中非私有成員變量時,需要使用 super
關鍵字,修飾父類成員變量,類似於之前學過的 this
。
使用格式:
super.父類成員變量名
子類方法需要修改,代碼如下:
class Zi extends Fu {
//Zi中的成員變量
int num = 6;
//Zi中的成員方法
public void show() {
//訪問父類中的num
System.out.println("Fu num = "+super.num);
//訪問子類中的num
System.out.println("Zi num = "+num);
}
}
演示結果:
Fu num = 5
Zi num = 5
小貼士:Fu 類中的成員變量是非私有的,子類中可以直接訪問。若Fu 類中的成員變量私有了,子類是不能直接訪問的。通常編碼時,我們遵循封裝的原則,使用private修飾成員變量,那么如何訪問父類的私有成員 變量呢?對!可以在父類中提供公共的getXxx方法和setXxx方法。
繼承后的特點——成員方法
當類之間產生了關系,其中各類中的成員方法,又產生了哪些影響呢?
成員方法不重名
如果子類父類中出現不重名的成員方法,這時的調用是沒有影響的。對象調用方法時,會先在子類中查找有沒有對應的方法,若子類中存在就會執行子類中的方法,若子類中不存在就會執行父類中相應的方法。代碼如下:
class Fu {
public void show() {
System.out.println("Fu類中的show方法執行");
}
}
class Zi extends Fu {
public void show2() {
System.out.println("Zi類中的show2方法執行");
}
}
public class ExtendsDemo04 {
public static void main(String[] args) {
Zi z = new Zi();
//子類中沒有show方法,但是可以找到父類方法去執行
z.show();
z.show2();
}
}
演示結果:
Fu類中的show方法執行
Zi類中的show2方法執行
成員方法重名——重寫(Override)
如果子類父類中出現重名的成員方法,這時的訪問是一種特殊情況,叫做方法重寫 (Override)。
- 方法重寫 :子類中出現與父類一模一樣的方法時(返回值類型,方法名和參數列表都相同),會出現覆蓋效果,也稱為重寫或者復寫。聲明不變,重新實現。
代碼如下:
class Fu {
public void show() {
System.out.println("Fu類中的show方法執行");
}
}
class Zi extends Fu {
public void show() {
System.out.println("Zi類中的show方法執行");
}
}
public class ExtendsDemo04 {
public static void main(String[] args) {
Zi z = new Zi();
z.show();
}
}
演示結果:
Zi類中的show方法執行
重寫的應用
子類可以根據需要,定義特定於自己的行為。既沿襲了父類的功能名稱,又根據子類的需要重新實現父類方法,從而進行擴展增強。
super.父類成員方法,表示調用父類的成員方法
注意事項
- 子類方法覆蓋父類方法,必須要保證權限大於等於父類權限。
- 子類方法覆蓋父類方法,返回值類型、函數名和參數列表都要一模一樣。
繼承后的特點——構造方法
當類之間產生了關系,其中各類中的構造方法,又產生了哪些影響呢?
首先我們要回憶兩個事情,構造方法的定義格式和作用。
-
構造方法的名字是與類名一致的。所以子類是無法繼承父類構造方法的。
-
構造方法的作用是初始化成員變量的。所以子類的初始化過程中,必須先執行父類的初始化動作。子類的構造方法中默認有一個 super() ,表示調用父類的構造方法,父類成員變量初始化后,才可以給子類使用。代碼如下:
class Fu {
private int n;
Fu() {
System.out.println("Fu()");
}
}
class Zi extends Fu{
Zi() {
super(); //調用父類構造方法
System.out.println("Zi()");
}
}
public class ExtendsDemo07 {
public static void main(String[] args) {
Zi zi = new Zi();
}
}
輸出結果:
Fu()
Zi()
super和this
父類空間優先於子類對象產生
在每次創建子類對象時,先初始化父類空間,再創建其子類對象本身。目的在於子類對象中包含了其對應的父類空間,便可以包含其父類的成員,如果父類成員非private修飾,則子類可以隨意使用父類成員。代碼體現在子類的構造方法調用時,一定先調用父類的構造方法。理解圖解如下:
super和this的含義
- super :代表父類的存儲空間標識(可以理解為父親的引用)。
- this :代表當前對象的引用(誰調用就代表誰)。
super和this的用法
-
訪問成員
this.成員變量 -- 本類的 super.成員變量 -- 父類的 this.成員方法名() -- 本類的 super.成員方法名() -- 父類的
-
訪問構造方法
this(...) -- 本類的構造方法 super(...) -- 父類的構造方法
子類的每個構造方法中均有默認的super(),調用父類的空參構造。手動調用父類構造會覆蓋默認的super()。 super() 和 this() 都必須是在構造方法的第一行,所以不能同時出現。
繼承的特點
-
Java只支持單繼承,不支持多繼承。
//一個類只能有一個父類,不可以有多個父類。 class C extends A{} //ok class C exteds A,B... //error
-
Java支持多層繼承(繼承體系)。
class A{} class B extends A{} class C extends B{}
頂層父類是Object類。所有的類默認繼承Object作為父類。
- 子類和父類是一種相對的概念。
抽象類
概述
由來
父類中的方法,被它的子類們重寫,子類各自的實現都不盡相同。那么父類的方法聲明和方法主體,只有聲明還有意義,而方法主體則沒有存在的意義了。我們把沒有方法主體的方法稱為抽象方法。Java語法規定,包含抽象方法的類就是抽象類。
定義
- 抽象方法 : 沒有方法體的方法。
- 抽象類:包含抽象方法的類。
abstract使用格式
抽象方法
使用 abstract
關鍵字修飾方法,該方法就成了抽象方法,抽象方法只包含一個方法名,而沒有方法體。
定義格式:
修飾符 abstract 返回值類型 方法名 (參數列表);
代碼舉例:
public abstract void run();
抽象類
如果一個類包含抽象方法,那么該類必須是抽象類。
定義格式:
abstract class 類名字 {
}
代碼舉例:
public abstract class Animal {
public abstract void run();
}
抽象的使用
繼承抽象類的子類必須重寫父類所有的抽象方法。否則,該子類也必須聲明為抽象類。最終,必須有子類實現該父類的抽象方法,否則,從最初的父類到最終的子類都不能創建對象,失去意義。
代碼舉例:
public class Cat extends Animal {
public void run() {
System.out.println("小貓在牆頭走");
}
}
public class CatTest {
public static void main(String[] args) {
Cat c = new Cat();
c.run();
}
}
輸出結果:
小貓在牆頭走
此時的方法重寫,,是子類對父類抽象方法的完成實現,我們將這種方法重寫的操作,也叫做實現方法。
注意事項
關於抽象類的使用,以下為語法上要注意的細節,雖然條目較多,但若理解了抽象的本質,無需死記硬背。
1.抽象類不能創建對象,如果創建,編譯無法通過而報錯。只能創建其非抽象子類的對象。
理解:假設創建了抽象類的對象,調用抽象的方法,而抽象方法沒有具體的方法體,沒有意義。
2.抽象類中,可以有構造方法,是供子類創建對象時,初始化父類成員使用的。
理解:子類的構造方法中,有默認的super(),需要訪問父類構造方法。
3.抽象類中,不一定包含抽象方法,但是有抽象方法的類必定是抽象類。
理解:未包含抽象方法的抽象類,目的就是不想讓調用者創建該類對象,通常用於某些特殊的類結構設計。
4.抽象類的子類,必須重寫抽象父類中所有的抽象方法,否則,編譯無法通過而報錯。除非該g子類也是抽象類。
理解:假設不重寫所有抽象方法,則類中可能包含抽象方法。那么創建對象后,調用抽象的方法,沒有意義。
繼承的綜合案例
群主發普通紅包。某群有多名成員,群主給成員發普通紅包。普通紅包的規則:
- 群主的一筆金額,從群主余額中扣除,平均分成n等份,讓成員領取。
- 成員領取紅包后,保存到成員余額中。
請根據描述,完成案例中所有類的定義以及指定類之間的繼承關系,並完成發紅包的操作。
根據描述分析,得到如下繼承體系
案例實現
定義用戶類:
public class User {
private String username; //用戶名
private double leftMoney; //余額
//構造方法
public User(){}
public User(String username, double leftMoney) {
this.username = username;
this.leftMoney = leftMoney;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public double getLeftMoney() {
return leftMoney;
}
public void setLeftMoney(double leftMoney) {
this.leftMoney = leftMoney;
}
public void show() {
System.out.println("用戶名:"+ username + ", 余額為:" + leftMoney + "元");
}
}
定義群主類:
public class Qunzhu extends User {
public Qunzhu(){}
public Qunzhu(String username, double leftMoney) {
super(username, leftMoney);
}
/*
群主發紅包,就是把一個浮點數的金額分成若干份。
1.獲取群主金額,是否夠發紅包 不夠則返回null並提示 夠則繼續
2.修改群主余額
3.拆分紅包
3.1 如果能被整除就平均分
3.2 如果不能 那么就把余數分給最后一份
*/
public ArrayList<Double> send(double money, int count) {
double leftMoney = getLeftMoney();
if (money > leftMoney) {
return null;
}
//修改群主余額
setLeftMoney(leftMoney - money);
//創建一個集合,保存等份金額
ArrayList<Double> list = new ArrayList<>();
//擴大100倍,相當於折算成‘分’為單位,避免小數運算損失精度
money = money * 100;
//每份的金額
double m = money / count;
//不能整除的余數
double l = money % count;
for(int i = 0;i < count - 1; i++) {
list.add(m / 100.0);
}
if(l == 0) {
//能整除,最后一份金額與之前每份金額一致
list.add(m / 100.0);
} else {
//不能整除,最后一份金額為之前每份的金額+余數金額
list.add((m + 1) / 100.0);
}
return list;
}
}
定義成員類:
public class Member extends User{
public Member() {}
public Member(String username, double leftMoney) {
super(username, leftMoney);
}
// 打開紅包,就是從集合中隨機取出一份保存到自己的余額中
public void openHongbao(ArrayList<Double> list) {
Random r = new Random();
int index = r.nextInt(list.size());
Double money = list.remove(index);
setLeftMoney(money);
}
}
測試類:
public class Test {
public static void main(String[] args) {
Qunzhu qz = new Qunzhu("群主", 200);
Scanner sc = new Scanner(System.in);
System.out.println("請輸入金額:");
double money = sc.nextDouble();
System.out.println("請輸入個數:");
int count = sc.nextInt();
//發送紅包
ArrayList<Double> sendList = qz.send(money,count);
if(sendList == null) {
System.out.println("余額不足...");
return;
}
Member m1 = new Member();
Member m2 = new Member();
Member m3 = new Member();
m1.openHongbao(sendList);
m2.openHongbao(sendList);
m3.openHongbao(sendList);
qz.show();
m1.show();
m2.show();
m3.show();
}
}
接口
概述
接口,是Java語言中一種引用類型,是方法的集合,如果說類的內部封裝了成員變量、構造方法和成員方法,那么接口的內部主要就是封裝了方法,包含抽象方法(JDK 7及以前),默認方法和靜態方法(JDK 8),私有方法(JDK 9)。
接口的定義,它與定義類方式相似,但是使用 interface
關鍵字。它也會被編譯成.class文件,但一定要明確它並不是類,而是另外一種引用數據類型。
引用數據類型:數組、類、接口。
接口的使用,它不能創建對象,但是可以被實現( implements ,類似於被繼承)。一個實現接口的類(可以看作是接口的子類),需要實現接口中所有的抽象方法,創建該類對象,就可以調用方法了,否則它必須是一個抽象類。
定義格式
public interface 接口名稱 {
//抽象方法
//默認方法
//靜態方法
//私有方法
}
含有抽象方法
抽象方法:使用 abstract
關鍵字修飾,可以省略,沒有方法體。該方法供子類實現使用。
代碼如下:
public interface InterFaceName {
public abstract void method();
}
含有默認方法和靜態方法
默認方法:使用 default
修飾,不可省略,供子類調用或者子類重寫。
靜態方法:使用 static
修飾,供接口直接調用。
代碼如下:
public interface InterFaceName {
public default void method() {
//執行語句
}
public static void method2() {
//執行語句
}
}
含有私有方法和私有靜態方法
私有方法:使用 private
修飾,供接口中的默認方法或者靜態方法調用。
代碼如下:
public interface InterFaceName {
private void method() {
//執行語句
}
}
基本的實現
實現的概述
類與接口的關系為實現關系,即類實現接口,該類可以稱為接口的實現類,也可以稱為接口的子類。實現的動作類似繼承,格式相仿,只是關鍵字不同,實現使用 implements
關鍵字。
非抽象子類實現接口:
- 必須重寫接口中所有抽象方法。
- 繼承了接口的默認方法,即可以直接調用,也可以重寫。
實現格式:
class 類名 implements 接口名 {
// 重寫接口中抽象方法【必須】
// 重寫接口中默認方法【可選】
}
抽象方法的使用
必須全部實現,代碼如下:
定義接口:
public interface LivaAble {
//定義抽象方法
public abstract void eat();
public abstract void sleep();
}
定義實現類:
public class Animal implements LiveAble {
@Override
public void eat() {
System.out.println("吃東西");
}
@Override
public void sleep() {
System.out.println("晚上睡");
}
}
測試類:
public class InterfaceDemo {
public static void main(String[] args) {
//創建子類對象
Animal a = new Animal();
//調用實現后的方法
a.eat();
a.sleep();
}
}
輸出結果:
吃東西
晚上睡
默認方法的使用
可以繼承,可以重寫,二選一,但是只能通過實現類的對象來調用。
1.默認繼承方法,代碼如下:
定義接口:
public interface LiveAble {
public default void fly() {
System.out.println("天上飛");
}
}
定義實現類:
public class Animal implements LiveAble {
//繼承,什么都不用謝,直接調用
}
測試類:
public class InterfaceDemo {
public static void main(String[] args) {
//創建子類對象
Animal a = new Animal();
//調用默認方法
a.fly();
}
}
輸出結果:
天上飛
2.重寫默認方法,代碼如下:
定義接口:
public interface LiveAble {
public default void fly() {
System.out.println("天上飛");
}
}
定義實現類:
public class Animal implements LiveAble {
@Override
public void fly() {
System.out.println("自由自在的飛");
}
}
測試類:
public class InterfaceDemo {
public static void main(String[] args) {
//創建子類對象
Animal a = new Animal();
//調用重寫方法
a.fly();
}
}
輸出結果:
自由自在的飛
靜態方法的使用
靜態與.class 文件相關,只能使用接口名調用,不可以通過實現類的類名或者實現類的對象調用,代碼如下:
定義接口:
public interface LiveAble {
public static void run() {
System.out.println("地上跑");
}
}
定義實現類:
public class Animale implements LiveAble {
//無法重寫靜態方法
}
測試類:
public class InterfaceDemo {
public static void main(String[] args) {
// Animal.run(); //【錯誤】無法繼承方法,也無法調用
LiveAble.run();
}
}
輸出結果:
地上跑
私有方法的使用
- 私有方法:只有默認方法可以調用
- 私有靜態方法:默認方法和靜態方法可以調用。
如果一個接口中有多個默認方法,並且方法中有重復的內容,那么可以抽取出來,封裝到私有方法中,供默認方法去調用。從設計的角度講,私有的方法是對默認方法和靜態方法的輔助。
定義接口:
public interface LiveAble {
default void func() {
func1();
func2();
}
private void func1(){
System.out.println("func1~~");
}
private void func2(){
System.out.println("func2~~");
}
}
接口的多實現
之前學過,在繼承體系中,一個類只能繼承一個父類。而對於接口而言,一個類是可以實現多個接口的,這叫做接口的多實現。並且,一個類能繼承一個父類,同時實現多個接口。
實現格式:
class 類名 [extends 父類名] implements 接口名1,接口名2,接口名3... {
//重寫接口中的抽象方法【必須】
//重寫接口中的默認方法【不重名時可選】
}
[]:表示可選操作
抽象方法
接口中,有多個抽象方法時,實現類必須重寫所有抽象方法。如果抽象方法有重名的,只需要重寫一次。代碼如下:
定義多個接口:
interface A {
public abstract void showA();
public abstract void show();
}
interface B {
public abstract void showB();
public abstract void show();
}
定義實現類:
public class C implements A,B{
@Override
public void showA() {
System.out.println("showA");
}
@Override
public void showB() {
System.out.println("showB");
}
@Override
public void show() {
System.out.println("show");
}
}
默認方法
接口中,有多個默認方法時,實現類都可繼承使用。如果默認方法有重名的,必須重寫一次。代碼如下:
定義多個接口:
interface A {
public default void methodA(){}
public default void method(){}
}
interface B {
public default void methodB(){}
public default void method(){}
}
定義實現類:
public class C implements A,B {
@Override
public void method() {
System.out.println("method");
}
}
靜態方法
接口中,存在同名的靜態方法並不會沖突,原因是只能通過各自接口名訪問靜態方法。
優先級的問題
當一個類,既繼承一個父類,又實現若干個接口時,父類中的成員方法與接口中的默認方法重名,子類就近選擇執行父類的成員方法。代碼如下:
定義接口:
interface A {
public default void methodA(){
System.out.println("AAA");
}
}
定義父類:
class B {
public void methodA(){
System.out.println("BBB");
}
}
定義子類:
class C extends D implements A {
//未重寫methodA方法
}
測試類:
public class Test {
public static void main(String[] args) {
C c = new C();
c.methodA();
}
}
輸出結果:
BBB
接口的多繼承【了解】
一個接口能繼承另一個或者多個接口,這和類之間的繼承比較相似。接口的繼承使用 extends 關鍵字,子接口繼承父接口的方法。如果父接口中的默認方法有重名的,那么子接口需要重寫一次。代碼如下:
定義父接口:
interface A {
public default void method(){
System.out.println("AAA");
}
}
interface B {
public default void method(){
System.out.println("BBB");
}
}
定義子接口:
interface D extends A,B {
@Override
public default void method() {
System.out.println("DD");
}
}
小貼士:
子接口重寫默認方法時,default關鍵字可以保留。子類重寫默認方法時,default關鍵字不可以保留。
其他成員特點
- 接口中,無法定義成員變量,但是可以定義常量,其值不可以改變,默認使用public static final修飾。
- 接口中,沒有構造方法,不能創建對象。
- 接口中,沒有靜態代碼塊。
多態
概述
引入
多態是繼封裝、繼承之后,面向對象的第三大特性。
生活中,比如跑的動作,小貓、小狗和大象,跑起來是不一樣的。再比如飛的動作,昆蟲、鳥類和飛機,飛起來也是不一樣的。可見,同一行為通過不同的事物,可以體現出來的不同的形態。多態描述的就是這樣的狀態。
定義
- 多態: 是指同一行為,具有多個不同表現形式。
前提【重點】
- 繼承或者實現【二選一】
- 方法的重寫【意義體現:不重寫,無意義】
- 父類引用指向子類對象【格式體現】
多態的體現
多態體現的格式:
父類類型 變量名 = new 子類對象;
變量名.方法名();
父類類型:指子類對象繼承的父類類型,或者實現的父接口類型。
代碼如下:
Fu f = new Zi();
f.method();
當使用多態方式調用方法時,首先檢查父類中是否有該方法,如果沒有,則編譯錯誤;如果有,執行的是子類重寫后方法。
代碼如下:
定義父類:
public abstract class Animal {
public abstract void eat();
}
定義子類:
public Cat extends Animal {
public void eat() {
System.out.println("吃魚");
}
}
public Dog extends Animal {
public void eat() {
System.out.println("吃骨頭");
}
}
測試類:
public class Test {
public static void main(String[] args) {
//多態形式,創建對象
Animal a1 = new Cat();
//調用的是Cat的eat
a1.eat();
//多態形式,創建對象
Animal a2 = new Dog();
//調用的是Dog的eat
a2.eat();
}
}
輸出結果:
吃魚
吃骨頭
多態的好處
實際開發的過程中,父類類型作為方法形式參數,傳遞子類對象給方法,進行方法的調用,更能體現出多態的擴展性與便利。代碼如下:
定義父類:
public abstract class Animal {
public abstract void eat();
}
定義子類:
public Cat extends Animal {
public void eat() {
System.out.println("吃魚");
}
}
public Dog extends Animal {
public void eat() {
System.out.println("吃骨頭");
}
}
測試類:
public class Test_1 {
public static void main(String[] args) {
Cat c = new Cat();
Dog d = new Dog();
showCatEat(c);
showDogEat(d);
/*
以上兩個方法均可以被showAnimalEat(Animal a)方法所替代,且執行效果一致
*/
showAnimalEat(c);
showAnimalEat(d);
}
public static void showCatEat(Cat c){
c.eat();
}
public static void showDogEat(Dog d){
d.eat();
}
public static void showAnimalEat(Animal a){
a.eat();
}
}
由於多態特性的支持,showAnimalEat方法的Animal類型,是Cat和Dog的父類類型,父類類型接收子類對象,當然可以把Cat對象和Dog對象,傳遞給方法。
當eat方法執行時,多態規定,執行的是子類重寫的方法,那么效果自然與showCatEat、showDogEat方法一致,所以showAnimalEat完全可以替代以上兩方法。
不僅僅是替代,在擴展性方面,無論之后再多的子類出現,我們都不需要編寫showXxxEat方法了,直接使用showAnimalEat都可以完成。
所以,多態的好處,體現在,可以使程序編寫的更簡單,並有良好的擴展。
引用類型轉換
多態的轉型分為向上轉型與向下轉型兩種:
向上轉型
- 向上轉型:多態本身是子類類型向父類類型向上轉換的過程,這個過程是默認的。
當父類引用指向一個子類對象時,便是向上轉型。
使用格式:
父類類型 變量名 = new 子類類型();
如 Animal a = new Cat();
向下轉型
- 向下轉型:父類類型向子類類型向下轉換的過程,這個過程是強制的。
一個已經向上轉型的子類對象,將父類引用轉為子類引用,可以使用強制類型轉換的格式,便是向下轉型。
使用格式:
子類類型 變量名 = (子類類型) 父類變量名;
如 Cat c = (Cat) a;
為什么要轉型
當使用多態方式調用方法時,首先檢查父類中是否有該方法,如果沒有,則編譯錯誤。也就是說,不能調用子類擁有,而父類沒有的方法。編譯都錯誤,更別說運行了。這也是多態給我們帶來的一點"小麻煩"。所以,想要調用子類特有的方法,必須做向下轉型。
轉型演示,代碼如下:
定義類:
abstract class Animal {
abstract void eat();
}
class Cat extends Animal {
public void eat() {
System.out.println("吃魚");
}
public void catchMouse() {
Sysetm.out.println("抓老鼠");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃骨頭");
}
public void watchHouse() {
System.out.println("看家");
}
}
測試類:
public class Test {
public static void main(String[] args) {
//向上轉型
Animal a = new Cat();
a.eat(); //調用的是Cat的eat
//向下轉型
Cat c = (Cat) a;
c.catchMouse();
}
}
轉型的異常
轉型的過程中,一不小心就會遇到這樣的問題,請看如下代碼:
public class Test {
public static void main(String[] args) {
//向上轉型
Animal a = new Cat();
a.eat();
//向下轉型
Dog d = (Dog)a;
d.watchHouse(); //調用的是Dog的watchHouse 【運行報錯】
}
}
這段代碼可以通過編譯,但是運行時,卻報出了 ClassCastException
,類型轉換異常!這是因為,明明創建了Cat類型對象,運行時,當然不能轉換成Dog對象的。這兩個類型並沒有任何繼承關系,不符合類型轉換的定義。
為了避免ClassCastException的發生,Java提供了 instanceof
關鍵字,給引用變量做類型的校驗,格式如下:
變量名 instanceof 數據類型
如果變量屬於該數據類型,返回true。
如果變量不屬於該數據類型,返回false。
所以,轉換前,我們好先做一個判斷,代碼如下:
public class Test {
public static void main(String[] args) {
//向上轉型
Animal a = new Cat();
a.eat();
//向下轉型
if (a instanceof Cat) {
Cat c = (Cat) a;
c.catchMouse();
} else if (a instanceof Dog) {
Dog d = (Dog)a;
d.watchHouse();
}
}
}
接口多態的綜合案例
筆記本電腦(laptop)通常具備使用USB設備的功能。在生產時,筆記本都預留了可以插入USB設備的USB接口,但具體是什么USB設備,筆記本廠商並不關心,只要符合USB規格的設備都可以。
定義USB接口,具備基本的開啟功能和關閉功能。鼠標和鍵盤要想能在電腦上使用,那么鼠標和鍵盤也必須遵守USB規范,實現USB接口,否則鼠標和鍵盤的生產出來也無法使用。
案例分析
進行描述筆記本類,實現筆記本使用USB鼠標、USB鍵盤
- USB接口,包含開啟功能、關閉功能
- 筆記本類,包含運行功能、關機功能、使用USB設備功能
- 鼠標類,要實現USB接口,並具備點擊的方法
- 鍵盤類,要實現USB接口,具備敲擊的方法
案例實現
定義USB接口:
public interface USB {
void open(); //開啟功能
void close(); //關閉功能
}
定義鼠標類:
public class Mouse implements USB {
@Override
public void open() {
System.out.println("鼠標開啟,紅燈亮");
}
@Override
public void close() {
System.out.println("鼠標關閉,紅燈滅");
}
public void click() {
System.out.println("鼠標點擊");
}
}
定義鍵盤類:
public class KeyBoard implements USB {
@Override
public void open() {
System.out.println("鍵盤開啟,綠燈亮");
}
@Override
public void close() {
System.out.println("鍵盤關閉,綠燈滅");
}
public void type() {
System.out.println("鍵盤打字");
}
}
定義筆記本類:
class Laptop {
public void run() {
System.out.println("筆記本運行");
}
//筆記本使用usb設備,當筆記本對象調用這個功能時必須給其傳遞一個符合USB規則的USB設備
public void useUSB(USB usb) {
if (usb != null) {
usb.open();
if (usb instanceof Mouse){
Mouse m = (Mouse) usb;
m.click();
} else if (usb instanceof KeyBoard) {
KeyBoard kb = (KeyBoard)usb;
kb.type();
}
usb.close();
}
}
public void shutDown() {
System.out.println("筆記本關閉");
}
}
測試類:
public class Test {
public static void main(String[] args) {
Laptop lt = new Laptop();
lt.run();
//創建鼠標實體對象
USB u = new Mouse();
//筆記本使用鼠標
lt.useUSB(u);
//創建鍵盤實體對象
USB kb = new KeyBoard();
//筆記本使用鍵盤
lt.useUSB(kb);
lt.shutDown();
}
}
final關鍵字
概述
學習了繼承后,我們知道,子類可以在父類的基礎上改寫父類內容,比如,方法重寫。那么我們能不能隨意的繼承API中提供的類,改寫其內容呢?顯然這是不合適的。為了避免這種隨意改寫的情況,Java提供了 final 關鍵字,用於修飾不可改變內容。
- final: 不可改變。可以用於修飾類、方法和變量。
- 類:被修飾的類,不能被繼承。
- 方法:被修飾的方法,不能被重寫。
- 變量:被修飾的變量,不能被重新賦值。
使用方式
修飾類
格式如下:
final class 類名 {
}
查詢API發現像 public final class String
、 public final class Math
、 public final class Scanner
等,很多我們學習過的類,都是被final修飾的,目的就是供我們使用,而不讓我們所以改變其內容。
修飾方法
格式如下:
修飾符 final 返回值類型 方法名(參數列表) {
//方法體
}
重寫被 final
修飾的方法,編譯時就會報錯。
修飾變量
1.局部變量——基本類型
基本類型的局部變量,被final修飾后,只能賦值一次,不能再更改。
2.局部變量——引用類型
引用類型的局部變量,被final修飾后,只能指向一個對象,地址不能再更改。但是不影響對象內部的成員變量值的修改,代碼如下:
public class FinalDemo2 {
public static void main(String[] args) {
//創建User對象
final User u = new User();
//創建另一個User對象
u = new User(); //報錯,指向了新的對象,地址值改變。
//調用setName方法
u.setName("張三"); //可以修改
}
}
3.成員變量
成員變量涉及到初始化的問題,初始化方式有兩種,只能二選一:
-
顯示初始化:
public class User {
final String USERNAME = "張三";
private int age;
} -
構造方法初始化:
public class User {
final String USERNAME;
private int age;
public User(String username, int age) {
this.USERNAME = username;
this.age = age;
}
}
被final修飾的常量名稱,一般都有書寫規范,所有字母都大寫。
權限修飾符
概述
在Java中提供了四種訪問權限,使用不同的訪問權限修飾符修飾時,被修飾的內容會有不同的訪問權限。
- public:公共的
- protected:受保護的
- default:默認的
- private:私有的
不同權限的訪問能力
public | protected | defalut(空的) | private |
---|---|---|---|
同一類中 | √ | √ | √ |
同一包中(子類與無關類) | √ | √ | √ |
不同包的子類 | √ | √ | |
不同包中的無關類 | √ |
可見,public具有最大權限。private則是最小權限。
編寫代碼時,如果沒有特殊的考慮,建議這樣使用權限:
- 成員變量使用
private
,隱藏細節。 - 構造方法使用
public
,方便創建對象。 - 成員方法使用
public
,方便調用方法。
小貼士:不加權限修飾符,其訪問能力與default修飾符相同
內部類
概述
什么是內部類
將一個類A定義在另一個類B里面,里面的那個類A就稱為內部類,B則稱為外部類。
成員內部類
- 成員內部類 :定義在類中方法外的類。
定義格式:
class 外部類 {
class 內部類 {
}
}
在描述事物時,若一個事物內部還包含其他事物,就可以使用內部類這種結構。比如,汽車類 Car
中包含發動機類 Engine
,這時, Engine
就可以使用內部類來描述,定義在成員位置。
代碼舉例:
class Car { //外部類
class Engine { //內部類
}
}
訪問特點
- 內部類可以直接訪問外部類的成員,包括私有成員。
- 外部類要訪問內部類的成員,必須要建立內部類的對象。
創建內部類對象格式:
外部類名.內部類名 對象名 = new 外部類型().new 內部類型();
訪問演示代碼如下:
定義類:
public class Person {
private boolean live = true;
class Heart {
public void jump() {
//直接訪問外部類成員
if (live) {
System.out.println("心臟在跳動");
} else {
Sysetm.out.println("心臟不跳了");
}
}
}
public boolean isLive() {
return live;
}
public void setLive(boolean live) {
this.live = live;
}
}
測試類:
public class InnerDemo {
public static void main(String[] args) {
//創建外部類對象
Person p = new Person();
//創建內部類對象
Heart heart = p.new Heart();
//調用內部類方法
heart.jump();
//調用外部類方法
p.setLive(false);
//調用內部類方法
heart.jump;
}
}
輸出結果:
心臟在跳動
心臟不跳了
內部類仍然是一個獨立的類,在編譯之后會內部類會被編譯成獨立的.class文件,但是前面冠以外部類的類名和\(符號 。 比如,Person\)Heart.class
匿名內部類【重點】
- 匿名內部類 :是內部類的簡化寫法。它的本質是一個
帶具體實現的
父類或者父接口的
匿名的
子類對象。
開發中,最常用到的內部類就是匿名內部類了。以接口舉例,當你使用一個接口時,似乎得做如下幾步操作。
- 定義子類
- 重寫接口中的方法
- 創建子類對象
- 調用重寫后的方法
我們的目的,最終只是為了調用方法,那么能不能簡化一下,把以上四步合成一步呢?匿名內部類就是做這樣的快捷方式。
前提
匿名內部類必須繼承一個父類或者實現一個父接口。
格式
new 父類名或者接口名() {
//方法重寫
@Override
public void method() {
//執行語句
}
};
使用方式
以接口為例,匿名內部類的使用,代碼如下:
定義接口:
public abstract class FlyAble{
public abstract void fly();
}
創建匿名內部類並調用:
public class InnerDemo {
public static void main(String[] args) {
/*
1.等號右邊:是匿名內部類,定義並創建該接口的子類對象
2.等號左邊:是多態賦值,接口類型引用指向子類對象
*/
FlyAble f = new FlyAble() {
public void fly() {
System.out.println("fly~");
}
};
//調用fly方法,執行重寫后的方法
f.fly();
}
}
通常在方法的形式參數是接口或者抽象類時,也可以將匿名內部類作為參數傳遞。代碼如下:
public class InnerDemo2 {
public static void main(String[] args) {
/*
1.等號右邊:定義並創建該接口的子類對象
2.等號左邊:是多態,接口類型引用指向子類對象
*/
FlyAble f = new FlyAble() {
public void fly() {
System.out.println("fly~~");
}
};
//將f傳遞給showFly方法中
showFly(f);
}
public static void showFly(FlyAble f) {
f.fly();
}
}
以上兩步,也可以簡化為一步,代碼如下:
public class InnerDemo3 {
public static void main(String[] args) {
/*
創建匿名內部類,直接傳遞給showFly(FlyAble f)
*/
showFly(new FlyAble(){
public void fly() {
System.out.println("fly~~");
}
});
}
public static void showFly(FlyAble f) {
f.fly();
}
}
引用類型用法總結
實際的開發中,引用類型的使用非常重要,也是非常普遍的。我們可以在理解基本類型的使用方式基礎上,進一步去掌握引用類型的使用方式。基本類型可以作為成員變量、作為方法的參數、作為方法的返回值,那么當然引用類型也是可以的。
class作為成員變量
在定義一個類Role(游戲角色)時,代碼如下:
class Role {
int id; //角色id
int blood; //生命值
String name; //角色名稱
}
使用 int
類型表示 角色id和生命值,使用 String
類型表示姓名。此時, String
本身就是引用類型,由於使用的方式類似常量,所以往往忽略了它是引用類型的存在。如果我們繼續豐富這個類的定義,給 Role
增加武器,穿戴裝備等屬性,我們將如何編寫呢?
定義武器類,將增加攻擊能力:
class Weapon {
String name; //武器名稱
int hurt; //傷害值
}
定義穿戴盔甲類,將增加防御能力,也就是提升生命值:
class Armour {
String name; //裝備名稱
int protect; //防御值
}
定義角色類:
class Role {
int id;
int blood;
String name;
//添加武器屬性
Weapon wp;
//添加盔甲屬性
Armour ar;
//提供get/set方法
public Weapon getWp() {
return wp;
}
public void setWeapon(Weapon wp) {
this.wp = wp;
}
public Armour getArmour() {
return ar;
}
public void setArmour(Armour ar) {
this.ar = ar;
}
//攻擊方法
public void attack() {
System.out.println("使用"+wp.getName() + ", 造成"+wp.getHurt()+"點傷害");
}
//穿戴盔甲
public void wear() {
//增加防御就是增加blood值
this.blood += ar.getProtect();
System.out.println("穿上"+ar.getName()+", 生命值增加"+ar.getProtect());
}
}
測試類:
public class Test {
public static void main(String[] args) {
//創建Weapon對象
Weapon wp = new Weapon("屠龍寶刀", 99999);
//創建Armour對象
Armour ar = new Armour("麒麟甲",10000);
//創建Role對象
Role r = new Role();
//設置屬性
r.setWeapon(wp);
r.setArmour(ar);
r.attack();
r.wear();
}
}
輸出結果:
使用屠龍寶刀,造成99999點傷害
穿上麒麟甲,生命值增加10000
類作為成員變量時,對它進行賦值的操作,實際上是賦給它該類的一個對象。
interface作為成員變量
接口是對方法的封裝,對應游戲當中,可以看作是擴展游戲角色的技能。所以,如果想擴展更強大技能,我們在Role
中,可以增加接口作為成員變量,來設置不同的技能。
定義接口:
//法術攻擊
public interface FaShuSkill {
public abstract void FaShuAttack();
}
定義角色類:
public class Role {
FaShuSkill fs;
public void setFaShuSkill(FaShuSkill fs) {
this.fs = fs;
}
//法術攻擊
public void FaShuSkillAttack() {
System.out.print("發動法術攻擊:");
fa.FaShuAttack();
System.out.println("攻擊完畢");
}
}
測試類:
public class Test {
public static void main(String[] args) {
//創建游戲角色
Role role = new Role();
//設置角色法術技能
role.setFaShuSkill(new FaShuSkill(){
@Override
public void FaShuAttack() {
System.out.println("縱橫天下");
}
});
//發動法術攻擊
role.FaShuSkillAttack();
//更換技能
role.setFaShuSkill(new FaShuSkill() {
@Override
public void FaShuAttack() {
System.out.println("逆轉乾坤");
}
});
//發動法術攻擊
role.FaShuSkillAttack();
}
}
輸出結果:
發動法術攻擊:縱橫天下
攻擊完畢
發動法術攻擊:逆轉乾坤
攻擊完畢
我們使用一個接口,作為成員變量,以便隨時更換技能,這樣的設計更為靈活,增強了程序的擴展性。
接口作為成員變量時,對它進行賦值的操作,實際上,是賦給它該接口的一個子類對象。
interface作為方法參數和返回值類型
當接口作為方法的參數時,需要傳遞什么呢?當接口作為方法的返回值類型時,需要返回什么呢?對,其實都是它的子類對象。 ArrayList
類我們並不陌生,查看API我們發現,實際上,它是 java.util.List
接口的實現類。所以,當我們看見 List
接口作為參數或者返回值類型時,當然可以將 ArrayList
的對象進行傳遞或返回。
請觀察如下方法:獲取某集合中所有的偶數。
定義方法:
public static List<Integer> getEvenNum(List<Integer> list){
//創建保存偶數的集合
ArrayList<Integer> evenList = new ArrayList<>();
for (int i = 0; i < list.size(); i++){
Integer integer = list.get(i);
if(integer % 2 == 0) {
evenList.add(integer);
}
}
/*
返回偶數集合
因為getEvenNum方法的返回值類型是List,而ArrayList是List的子類,所以evenList可以返回
*/
return evenList;
}
調用方法:
public static void main(String[] args) {
//創建ArrayList集合,並添加數字
ArrayList<Integer> srcList = new ArrayList<>();
for(int i = 0; i < 10; i++) {
srcList.add(i);
}
/*
獲取偶數集合
因為getEvenNum方法的參數是List,而ArrayList是List的子類,
所以srcList可以傳遞
*/
List list = getEvenNum(srcList);
System.out.println(list);
}
接口作為參數時,傳遞它的子類對象。
接口作為返回值類型時,返回它的子類對象。