Java程式設計(2021春)——第四章介面與多型筆記與思考
本章概覽:
4.1 介面(介面的概念和宣告介面、實現介面的語法)
4.2 型別轉換
4.3 多型的概念
4.4 多型的應用
4.5 構造方法與多型(的關係)
4.1 介面
介面可以看作純的抽象類,只提供設計而不提供實現。
- 介面中可以規定方法的原型:方法名、引數列表以及返回型別,但不規定方法主體(即沒有給出方法的實現)。
- 也可以包含基本資料型別的資料成員,但它們都預設為
static
和final
的。
介面的作用
繼承多個設計(可以實現類的設計的多繼承)。
建立類和類之間的協議。
將類根據其實現的功能分組用介面代表,而不必顧慮它所在的類繼承層次;這樣可以最大限度地利用動態繫結,隱藏實現細節。
介面允許在看起來不相關的物件之間定義共同的行為,如下圖。
介面的語法
宣告格式為
[介面修飾符]interface 介面名稱 [extends 父介面名]{
...////方法的原型或靜態變數
}
interface
表明正在宣告的是一個介面。- 介面可以繼承父介面(後續會介紹)。
- 在介面體中可以宣告方法原型和靜態常量。
- 由於介面體中資料成員是靜態的,因此一定要有初值,且此值將不能改變,可以省略
final
關鍵字(預設是常量)。 - 介面中的方法必須是抽象方法,不能有方法體,可以省略
public
和abstract
關鍵字(預設是public
,因為是對外服務介面;預設是abstract
,因為方法是抽象的)。
例:介面宣告
宣告一個介面Shape2D
,包括π
和計算面積的方法原型
interface Shape2D{//宣告Shape2D介面
final double pi = 3.14;//資料成員一定要初始化
public abstract double area();//抽象方法
}
在介面的宣告中,允許省略一些關鍵字,也可宣告如下:
interface Shape2D{
double pi = 3.14;
double area();
}
如上,final
public
abstract
關鍵字都可以省略。
介面中只有方法體的原型,沒有方法體的實現,所以和抽象類一樣,介面不能產生例項(不能new
一個介面物件)。
實現介面
實現介面就是利用介面設計類的過程,成為介面的實現,使用implements
關鍵字,語法如下:
public class 類名稱 implements 介面名稱{
//在類體中實現介面的方法
//本類宣告的更多變數和方法
}
由上,實現介面形式上類似繼承超類(使用extends
關鍵字)
注意:
- 必須實現介面中的所有方法。
- 來自介面的方法必須宣告為
public
。
例:實現介面Shape2D
class Circle implements Shape2D{
double radius;
public Circle(double r){
radius = r;
}
public double area(){
return (pi * radius * radius);
}
}
class Rectangle implements Shape2D{
int width,height;
public Rectangle (int w,int h){
width = w;
height = h;
}
public double area(){
return (width * height);
}
}
測試類
public class InterfaceTester {
public static void main(String[] args) {
Rectangle rect = new Rectangle(5, 6);
System.out.println("Area of rect = " + rect.area());
Circle cir = new Circle(2.0);
System.out.println("Area of cir = " + cir.area());
}
}
輸出
Area of rect = 30.0
Area of cir = 12.56
由上可知,雖然都呼叫了area()
方法,但是分別計算了正確的面積。
例:介面型別的引用變數
宣告介面型別的變數,並用它來訪問物件:
public class VariableTester {
public static void main(String[] args) {
Shape2D var1, var2;//宣告兩個介面型別的引用變數var1,var1
var1 = new Rectangle(5, 6);//將Rectangle物件的引用賦值給Shape2D介面型別的引用,發生了隱含的型別轉換
System.out.println("Area of var1 = " + var1.area());
var2 = new Circle(2.0);//將Circle物件的引用賦值給Shape2D介面型別的引用,發生了隱含的型別轉換
System.out.println("Area of var2 = " + var2.area());
}
}
輸出
Area of var1 = 30.0
Area of var2 = 12.56
由上可知,依然準確執行了各自的area()
方法。
實現多個介面的語法
一個類可以實現多個介面,通過這種機制可以實現對設計的多重繼承(Java中僅支援單繼承,此種方法是拐個彎)。
實現多個介面的語法如下
[類修飾符] class 類名稱 implements 介面1,介面2,...{
//對每個介面中的抽象方法予以實現
}
例:通過實現介面達到(對設計的)多重繼承
宣告Circle
類實現介面Shape2D
和Color
Shape2D
具有常量pi
與area
方法,用來計算面積。Color
則具有setColor
方法,可用來賦值顏色。通過實現這兩個介面,
Circle
類得以同時擁有這兩個介面的成員,達到了對設計進行多重繼承的目的。
interface Shape2D{//宣告Shape2D介面
final double pi = 3.14;//資料成員一定要初始化
public abstract double area();//抽象方法
}
interface Color{
void setColor(String str);//抽象方法
}
class Circle implements Shape2D, Color {
double radius;
String color;
public Circle(double r) {// 構造方法
radius = r;
}
public double area() {
return (pi * radius * radius);
}
public void setColor(String str) {// 定義setColor()的處理方式
color = str;
System.out.println("color = " + color);
}
}
測試類
public class MultiInterfaceTester {
public void main() {
Circle cir;
cir = new Circle(2.0);
cir.setColor("blue");
System.out.println("Area = " + cir.area());
}
}
輸出結果
color = blue
Area = 12.56
介面的擴充套件
介面與介面之間也可以有繼承關係,即擴充套件(extends
)關係,可以從一個已有的介面擴展出更多的介面。已有的介面成為超介面,擴展出來的介面稱為子介面。
實現一個介面的類必須實現其超介面。
介面擴充套件的語法
interface 子介面的名稱 extends 超介面的名稱1,超介面的名稱2,...{
//
}
如下例:
例:介面的擴充套件
//宣告Shape介面
interface Shape{
double pi = 3.14;
void setColor(String str);
}
//宣告Shape2D介面拓展了Shape介面
interface Shape2D extends Shape{//繼承了Shape介面,自動繼承了常量pi和setColor方法
double area();
}
class Circle implements Shape2D {
double radius;
String color;
public Circle(double r) {// 構造方法
radius = r;
}
public double area() {
return (pi * radius * radius);
}
public void setColor(String str) {// 定義setColor()的處理方式
color = str;
System.out.println("color = " + color);
}
}
public calss ExtendsInterfaceTester{
public static void main(String[] args){
Circle cir;
cir = new Circle(2.0);
cir.setColor("blue");
System.out.println("Area = " + cir.area());
}
}
執行結果
color = blue
Area = 12.56
說明
- 首先聲明瞭父介面
Shape
,然後宣告其子介面Shape2D
。 - 之後宣告類
Circle
實現Shape2D
子介面,因而在類內必須明確定義setColor()
與area()
方法的處理方式。 - 最後在主類中聲明瞭
Circle
型別的變數cir
並建立新的物件,最後通過cir
物件呼叫setColor
與area()
方法。
4.2 型別轉換
型別轉換
- 又稱為塑型(
type-casting
)。 - 轉換方式可以分為隱式的型別轉換和顯式的型別轉換。
- 轉換方向可以分為向上轉型和向下轉型。
型別轉換規則
- 基本型別之間的轉換:將值從一種型別轉換成另一種型別。
- 引用型別的型別轉換:
- 將引用轉換為另一型別的引用,並不改變物件本身的型別。
- 引用型別只能被轉為
- 任何一個(直接或間接)超類的型別(向上轉型)。
- 物件所屬的類(或其超類)實現的一個藉口(向上轉型)。
- 被轉為引用指向的物件的型別(唯一可以向下轉型的情況)。
- 當一個引用被轉為其超類引用後,通過它能夠訪問的只有在超類中宣告過的方法,即受限了,轉為介面引用同理。
以下通過舉例說明型別轉換:
Person
繼承或者擴充套件了Object
類;Emploee
類和Customer
類繼承了Person
類;manager
類繼承了Emploee
類。
Person
實現了Insurable
(可保險)介面。
Manager
物件- 可以被塑型為
Emploee
Person
Object
或Insurable
。 - 不能被型為
Customer
、Company
、Car
類,因為沒有繼承關係,也不是實現介面的關係。
- 可以被塑型為
隱式型別轉換
基本資料型別
- 可以轉換的型別之間,儲存容量低的自動向儲存容量高的型別轉換。
引用變數
被轉成更一般的類(將子型別的引用轉換為超型別的引用),例如:
Emploee emp;
emp = new Manager();
//將Manager型別的物件直接賦給Emploee類的引用變數,系統會自動將Manager物件塑型為Emploee類
被塑型為物件所屬類實現的介面型別,例如:
Car jetta = new Car();
Insurable item = jetta;
顯式型別轉化
基本資料型別
(int)871.34354;//結果為871,是高型別向低型別轉換,須顯式轉換,方法為直接截掉小數部分,是有資料丟失的。
(char)65;//結果為'A'
(long)453;//結果為453L
引用變數
Emploee emp;
Manager man;
emp = new Manager();//如上例,發生了自動向超類的隱含轉換,但是emp實際指向的物件就是子類物件
man = (Manager)emp;//將emp顯式轉換為它所指向的物件的型別。在這種情況下可以將emp引用強制轉換為子型別。這個轉換不會自動發生,須顯示轉換。這種強制轉換不會發生錯誤,可以正常執行,就是因為emp真正指向的物件就是子類物件。如果不能確定這一點,一定不要向下轉型。
型別轉換的主要應用場合
- 賦值轉換:將賦值運算子右邊的表示式或物件型別轉換為左邊的型別。
- 方法呼叫轉換:將實參的型別轉換為形參的型別。
- 算術表示式轉換:算數混合運算時,不同型別的運算元轉換為相同的型別在進行運算。
- 字串轉化(字串拼接):字串連線運算時,如果一個運算元為字串,另一個運算元為其他型別,則會自動將其他型別轉換為字串。
型別轉換的應用舉例
manager
類繼承了emploee
類,emploee
類繼承了person
類,在person
類中聲明瞭getName
類方法;在Emploee
類中聲明瞭getEmploeeNumber
類方法。當我們將Manager
型別的引用轉換為Emploee
型別的引用的時候,這個時候只能訪問emploee
類以及它的超類中的方法,如Person
中的getname
方法和Emploee
中的getEmploeenumber()
方法。而Manager
類中的getSalary()
方法就不能通過Manager
類的超類如emploee
的引用去訪問了
4.2.3 方法的查詢
上一節中學習了可以將子類型別的引用向上轉換為超類型別的引用。當發生了引用型別的轉換時,如果該引用轉換前所屬的型別和轉換後所屬的型別中都聲明瞭同樣原型的方法,那麼當發生了型別轉換後,再通過這個引用去呼叫或者訪問這個方法,將要訪問哪個方法體,就是關於方法查詢的問題。
方法查詢
例項方法的查詢
從物件建立時的類開始,沿類層次向上查詢
Manager man = new Manager();
Emploee emp1 = new Emploee();
Emploee emp2 = (Emploee)man;
以下探索呼叫Computepay
方法
emp1.Computepay();//引用是Emploee型別的,實際指向的物件也是emploee型別的,自然呼叫的是Emploee型別中的Computepay方法
man.Computepay();//man是Manager型別的,實際指向的物件也是Manager型別的物件,呼叫的是Manager類中的Computepay方法
emp2.Computepay();//該引用是Emploee型別的,但是指向的物件是Manager型別的,按照如上規則從物件建立時的類開始,沿類層次向上查詢,也就是說從Manager類開始查詢是否有Computepay()方法,所以該語句呼叫的仍然是Manager類中的Computepay()方法。
類方法的查詢
類方法是static
的、靜態的、屬於整個類的。
Manager man = new Manager();
Emploee emp1 = new Emploee();
Emploee emp2 = (Emploee)man;
以下對其進行測試
man.expenseAllowance();//in Manager
emp1.expenseAllowance();//in Emploee
emp2.expenseAllowance();//in Emploee!!
注意,類方法屬於整個類,不屬於某個物件,因此在呼叫emp2.expenseAllowance()
的時候,就不會根據引用所指向的物件是誰來查詢這個方法了,因為類方法不屬於任何一個物件。因此,唯一的查詢方法就是根據引用變數自己的型別。
4.3 多型的概念
多型指的是不同型別的物件可以響應相同的訊息,而各自對這個訊息的相應行為可以是不同的
多型的概念
- 超類物件和從相同的超類派生出來的多個子類的物件,可以被當作同一種類型的物件對待(因為子類的物件總是可以充當超類物件使用)。
- 實現統一介面不同型別的物件,可以被當作同一種類型的物件對待(被當作介面型別的物件對待)。
- 可向這些不同的型別物件傳送同樣的訊息,由於多型性,這些不同類的物件響應同一訊息時的行為可以有所差別。
例如:
- 所有
Object
類的物件都響應同toString()
方法。 - 所有
BankAccount
類的物件都相應deposit()
方法。 - 但是,上述對方法的響應可以不同,因為每個類有自己對超類繼承來的方法的一個覆蓋,即各自實現了方法體。
多型的目的
- 使程式碼變得簡單且容易理解。
- 使程式具有很好的可擴充套件性。
例:圖形類
在超類
Shape
中宣告一個繪圖方法draw()
、一個擦除方法erase()
。在每個子類中覆蓋(重寫)了
draw()
和erase()
方法。以後繪圖可以如下進行:
Shape s = new Circle();
s.draw();//實際呼叫的Circle物件的draw()
繫結的概念
繫結是將一個方法呼叫表示式與方法體的程式碼結合起來。
根據繫結時期的不同,可分為:
- 早繫結:程式執行之前執行繫結(編譯過程中)。
- 晚繫結:也叫做“動態繫結”或“執行期繫結”,是基於物件的類別,在程式執行時執行繫結。
例:動態繫結
仍以繪圖為例,所有類都放在binding
包中
超類Shape
建立了一個通用介面(因為draw
和erase
都是空方法體)
class Shape {
void draw();
void erase();
}
子類覆蓋了draw()
方法,為每種特殊的幾何形狀都提供獨一無二的行為:
calss Circle extends Shape{
void draw(){
System.out.println("Circle.draw()");
}
void erase(){
System.out.println("Circle.erase()");
}
}
calss Square extends Shape{
void draw(){
System.out.println("Square.draw()");
}
void erase(){
System.out.println("Square.erase()");
}
}
calss Triangle extends Shape{
void draw(){
System.out.println("Triangle.draw()");
}
void erase(){
System.out.println("Triangle.erase()");
}
}
對動態繫結進行如下測試:
public class BindingTester{
public satic void main(String[] args){
Shape[] s = new Shape[9];
int n;
for(int i = 0;i < s.length();i++){
n = (int)(Math.random() * 3);
switch(n){
case 0:s[i] = new Circle();
break;
case 1:s[i] = new Square();
break;
case 2:s[i] = new Triangle();
}
}
for(int i = 0;i < s.length();i++){
s[i].draw;
}
}
}
執行結果(由於random
隨機數的特點,所以以下僅為某一次實驗的結果):
Square.draw()
Triangle.draw();
Cicrcle.drwa();
Triangle.draw();
Triangle.draw();
Cicrcle.drwa();
Square.draw()
Cicrcle.drwa();
Triangle.draw();
說明
- 在主方法的迴圈體中,每次隨機生成一個
Circle
、Square()
或者Triangle()
物件。 - 編譯時無法知道
s
陣列元素指向的實際物件型別,執行時才能確定型別,所以是動態繫結。
小結:多型性的基礎,一個是動態繫結技術,一個是向上轉型技術。
4.4 多型的應用舉例
例:二次分發
- 有不同種類的交通工具(
vehicle
),如公共汽車(bus
)及小汽車(car
),由此可以宣告一個抽象類Vehicle
及兩個子類Bus
及Car
。 - 宣告一個抽象類
Driver
和兩個子類FemaleDriver
及MaleDriver
。 - 在
Driver
類中聲明瞭抽象方法drives
,在兩個子類中對這個方法進行覆蓋。 drives
方法接受一個Vehicle
類的引數,當不同型別的交通工具被傳送到此方法時,可以輸出具體的交通工具。- 所有類放在
drive
包中。
測試程式碼:
package drive;
public class DriverTest {
static void main(String args[]) {
Driver a = new FemaleDriver();//雖然a是Driver型別,但是實際指向的物件是FemaleDriver型別
Driver b = new MaleDriver();
Vehicle x = new car();
Vehicle y = new bus();
a.drives(x);
b.drives(y);
}
}
希望得到的輸出:
A Female driver drives a car
A male driver drives a bus
Vehicle
及其子類宣告如下
package drive;
//抽象類
public abstract class Vehicle {
private String type;
public Vehicle() {
};
//抽象方法
public abstract void drivedByFemaleDriver();
//抽象方法
public abstract void drivedByMaleDriver();
}
package drive;
public class Car extends Vehicle {
public Car() {
};
public void drivedByFemaleDriver() {
System.out.println("A Female driver drives a car");
}
public void drivedByMaleDriver() {
System.out.println("A Male driver drives a car");
}
}
package drive;
public class Bus {
public Bus() {
};
public void drivedByFemaleDriver() {
System.out.println("A female driver drives a bus");
}
public void drivedByMaleDriver() {
System.out.println("A male driver drives a bus");
}
}
Driver
及其子類宣告如下
package drive;
public abstract class Driver {
public Driver() {
};
public abstract void drives(Vehicle v);
}
package drive;
public class FemaleDriver extends Driver {
public FemaleDriver() {
};
public void drives(Vehicle v) {
v.drivedByFemaleDriver();
}
}
package drive;
public class MaleDriver extends Driver {
public MaleDriver() {
};
public void drives(Vehicle v) {
v.drivedByMaleDriver();
}
}
說明:
- 這種技術成為二次分發(“
double dispatching
”),即對輸出訊息的請求被分發兩次。 - 首先根據駕駛員的型別被髮送給一個類。
- 之後根據交通工具的型別被髮送給另一個類。
4.5 構造方法與多型性
構造方法與其他方法有區別,是不具有多型性的特點的。但是仍需瞭解在構造方法中呼叫了多型的方法會怎麼樣。
構造子類物件時構造方法的呼叫順序
- 首先呼叫超類的構造方法(如果有超類的話),這個步驟會不斷重複下去,首先被執行的是最遠超類的構造方法。
- 執行當前子類的構造方法體其他語句。
例:構造方法的呼叫順序
構建一個點類Point
,一個球類Ball
,一個運動的球類MovingBall
繼承自Ball
public class Point {
private double xCoordinate;
private double yCoordinate;
public Point() {
};//沒有引數的構造方法
public Point(double x, double y) {
xCoordinate = x;
yCoordinate = y;
}//有引數的構造方法
public String toString() {
return "(" + Double.toString(xCoordinate) + "," + Double.toString(yCoordinate) + ")";
}
}
public class Ball {
private Point center;//中心點
private double radius;//半徑
private String color;//顏色
public Ball() {
};//無引數的構造方法
public Ball(double xValue, double yValue, double r) {//三個引數的構造方法
center = new Point(xValue, yValue);//呼叫Point中的構造方法
radius = r;
}
public Ball(double xValue, double yValue, double r, String c) {//四個引數的構造方法,可以直接服用呼叫三個引數的構造方法
this(xValue, yValue, r);
color = c;
}
public String toString() {
return "A ball with center " + center.toString() + ",radius " + Double.toString(radius) + ",colour" + color;
}
}
public class MovingBall extends Ball {
private double speed;
public MovingBall() {
};
public MovingBall(double xValue, double yValue, double r, String c, double s) {
super(xValue, yValue, r, c);//注意要先呼叫超類的方法
speed = s;
}
public String toString() {
return super.toString() + ",speed" + Double.toString(speed);
}
}
子類不能直接存取父類中宣告的私有資料成員,super.toString()
呼叫父類Ball
的toString
方法輸出類Ball
中宣告的屬性值。
public class Tester {
public static void main(String args[]) {
MovingBall mb = new MovingBall(10, 20, 40, "green", 25);
System.out.println(mb);
}
}
輸出
A ball with center (10.0,20.0),radius 40.0,colourgreen,speed25.0
構造方法的呼叫順序為MovingBall(double xValue,double yValue,double r,String c,double s)
->Ball(double xValue,double yValue,double r,String c)
->Ball(double xValue,double yValue,double r)
->Point(double x,double y)
例:構造方法中呼叫多型方法
在Glyph
中宣告一個抽象方法,並在構造方法內部呼叫之
abstract class Glyph {
abstract void draw();
Glyph() {
System.out.println("Glyph() before draw()");
draw();// 不必擔心找不到方法體,因為抽象類不能生成物件,所以一定是某一個非抽象子類的draw()方法
System.out.println("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph {
int radius = 1;
//第一句應當是呼叫超類構造方法,但是沒有顯式呼叫超類構造方法,就會預設呼叫超類無引數的構造方法
RoundGlyph(int r) {
radius = r;
System.out.println("RoundGlyph.RoundGlyph(),radius = " + radius);
}
void draw() {
System.out.println("RoundGlyph.draw(),radius = " + radius);
}
}
public class PolyConstructors {//測試
public static void main(String args[]) {
new RoundGlyph(5);
}
}
輸出:
Glyph() before draw()
RoundGlyph.draw(),radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(),radius = 5
分析:第一個輸出的radius
等於\(0\)是因為,此時物件還沒構造好,RoundGlyph
中的radius = 1
還沒初始化執行,因此就是沒有初始化的預設值;而在Java
中沒有數值型資料沒有初始化的預設值是\(0\)。
說明:
- 在
Glyph
中,draw()
方法是抽象方法,在子類RoundGlyph
中對此方法進行了覆蓋,Glyph
的構造方法呼叫了這個方法。 - 從執行的結果可以看到:當
Glyph
的構造方法呼叫draw()
時,radius
的值甚至不是預設的初始值\(1\),而是\(0\)。
實現構造方法的注意事項:
- 用盡可能少的動作把物件的狀態設定好,即,構造方法就是用來初始化的,除了初始化以外最好不要做別的事。
- 如果可以避免,不要呼叫任何方法。
- 在構造方法內唯一能夠安全呼叫的是在超類中具有
final
屬性的哪些方法(也適用於private
方法,它們具有final
屬性)。這些方法不能被覆蓋,所以不會出現前述的潛在問題。