【Java编程】面向对象程序设计--继承与多态

​​​​​​​目录

面向对象程序设计——Java语言_浙江大学_中国大学MOOC(慕课)​​​​​​​

一、继承

1.1 案例

1.2 继承语法

1.3 子类与父类的关系

二、多态

2.1 多态变量

2.2 子类的对象可以被看作是父类的对象来使用

2.3 造型

三、类型系统

3.1 Object 类

3.2 equals() 比较两个对象内容是否相同

3.3 DoME的新媒体类型

四、继承类型

五、其他关键字

5.1 implements

5.2 final


一、继承

面向对象程序设计语言(Object Oriented Programming)OOP /uːp/ 有三大特性:封装、继承和多态性。继承是 Java 中实现软件重用的重要手段,是 java 面向对象编程技术的一块基石。因为它允许创建分等级层次的类。继承是非常简单而强大的设计思想,它提供了我们代码重用程序组织的有力工具。没有继承的语言只能被称作“使用对象的语言”

类是规则,用来制造对象的规则。我们不断地定义类,用定义的类制造一些对象。类定义了对象的属性和行为,就像图纸决定了房子要盖成什么样子。一张图纸可以盖很多房子,它们都是相同的房子,但是坐落在不同的地方,会有不同的人住在里面。

假如现在我们想盖一座新房子,和以前盖的房子很相似,但是稍微有点不同。一般建筑师都会拿以前盖的房子的图纸来,稍加修改,成为一张新图纸,然后盖这座新房子。所以一旦我们有了一张设计良好的图纸(有共同的特征和动作放在一个类的通用类,让其它类共享),我们就可以基于这张图纸设计出很多相似但不完全相同的房子的图纸来(扩展为其他多个继承了通用类中的特征和动作的特定类)。父类更通用,子类更具体。

基于已有的设计创造新的设计,就是面向对象程序设计中的继承。在继承中,新的类不是凭空产生的,而是基于一个已经存在的类而定义出来的。通过继承,新的类自动获得了基础类中所有的成员,包括成员变量和方法,包括各种访问属性的成员,无论是 public还是 private(但不能访问)。当然,在这之后,程序员还可以加入自己的新的成员,包括变量和方法。显然,通过继承来定义新的类,远比从头开始写一个新的类要简单快捷和方便。

1.1 案例

实现一个媒体资料库database,用来存所有张 CD 以及 DVD 的数据。

1.1.1 组织程序

每一个CD/DVD对象的内容信息的存储--> 由 class CD \ class DVD 执行;

对每一个CD/DVD对象进行的操作(包括增加、查询等)-- > 由 class database 控制;

1.1.2. 选择容器

class database 需要实现无限增加对象的功能,选择容器 ArrayList<>。每一个 ArrayList 的数据类类型分别用自定义类类型:class CD、DVD。未来可能还有 MP3 等类型加入。

1.1.3. 实现细节

class CD/DVD 成员变量 + 构造函数 + 一般方法 print();

class database 创建容器 + 一般方法 add() + 一般方法 print()

上述表现形式,存在较多重复代码。联想到前期时钟例,将 hour 和 minute 的展示部分,放在一个 display类里。database 里也有较多 CD和DVD类的功能重复,需要整理。

1.2 继承语法

我们把用来做基础派生其它类的那个类叫做父类、超类或者基类,而派生出来的新类叫做子类。Java用关键字extends表示这种继承/派生关系:

class 父类 { 父类属性;父类方法;
}
 
class 子类 extends 父类 { 
子类扩展属性;
子类扩展方法;(可重写父类方法)
}

// 调用
子类 对象名称 = new 子类();

继承表达了一种 is-a 关系,就是说,子类的对象可以被看作是父类的对象。比如鸡是从鸟派生出来的,因此任何一只都可以被称作是一只鸟。但是反过来不行,有些鸟是鸡,但并不是所有的鸟都是鸡。

如果你设计的继承关系,导致当你试图把一个子类的对象看作是父类的对象时显然很不合逻辑。比如你让鸡类从水果类得到继承,然后你试图说:这只鸡是一种水果,所以这碗鸡煲就像水果色拉。这显然不合逻辑,如果出现这样的问题,那就说明你的类的关系的设计是不正确的。

Java的继承只允许单继承,即一个类只能有一个父类

对于案例媒体库的修改:

关系图

Database 类

import java.util.ArrayList;
public class database {
    ArrayList<Item> listItem = new ArrayList<Item>();

    public static void main(String[] args) {
        database db = new database();
        // 继承表达了一种 is-a 关系,就是说,子类的对象可以被看作是父类的对象
        db.add(new CD("33", 10, false, "还行", "z", 10));
        db.add(new CD("44", 111, false, "一般", "q", 16));
        db.add(new DVD("44", 88, false, "凑合", "12"));
        db.list();
    }

    public void add(Item item) {
        listItem.add(item); // 实现一个增加 类的对象
    }

    public void list() {
        for (Item i : listItem) {
            i.print();
            // 为什么直接先进入CD调用其print(),而不是先进同样有print()的Item?
            // 因为listItem里面当下它(i)实际所管理(动态类型)的是一个CD的对象。它先去了子类的 print()。多态类型
        }
    }
}

Item 类(父类)

public class Item {
    private String title;
    private float playingTime;
    private boolean gotIt;
    private String comment;

    public Item(String title, float playingTime, boolean gotIt, String comment) {
        this.title = title;
        this.playingTime = playingTime;
        this.gotIt = gotIt;
        this.comment = comment;
    }

    public void print() {
        System.out.println("Item:title-" + title);
    }
}

需要继承父类Item通用成员变量,通用一般方法的两个子类 -- CD类

public class CD extends Item {
    private String artist;
    private int numofTracks;

    // 子类需要的参数一个都不能少写,由“继承部分+自持部分”构成。
    // 同时super(继承成员变量名:() , 多个由逗号隔开);继承方法:super.
    public CD(String title, float playingTime, boolean got_it, String comment, String artist, int numofTracks) {
        super(title, playingTime, got_it, comment);// 能在一个构造函数里调用两次super()吗?super()必须在构造函数的第一行吗?
        this.artist = artist;
        this.numofTracks = numofTracks;
    }

    public void print() {
        super.print();
        System.out.println("CD: artist-" + artist);
    }
}

需要继承父类Item通用成员变量,通用一般方法的两个子类 -- DVD 类

public class DVD extends Item {
    private String director;

    public DVD(String title, float playingTime, boolean gotIt, String comment, String director) {
        super(title, playingTime, gotIt, comment);
        this.director = director;
    }

    public void print() {
        super.print();
        System.out.println("DVD:director-" + director);
    }
}

1. 将所有 DVD 和 CD 里面共有的成员变量、构造函数、一般方法都放入 Item 类里,如 print() 方法。不过print() 具体打印拆分了,可以适当注意下。DVD类和CD类保留各自特有的部分,如 DVD 的 成员变量 director,CD的成员变量 artist, numofTracks。

2. 关于构造方法

当我们去构造一个子类的对象的时候,首先要确保,它父类所拥有的那些成员变量,得到恰当的初始化。在程序运行过程中,子类对象的一部分空间存放的是父类对象。因为子类从父类得到继承,在子类对象初始化过程中可能会使用到父类的成员。所以父类的空间正是要先被初始化的,然后子类的空间才得到初始化

这里的“恰当的初始化”,包含两件事情:

第一,定义初始化。

第二,构造器。

总是父类的成员变量得到恰当的初始化,再进行自己的。

在这个过程中,如果父类的构造方法需要参数,如何传递参数就很重要了

如果父类的构造器带有参数,则必须在子类的构造器中 显式地通过 super 关键字调用父类的构造器并配以适当的参数列表。
如果父类构造器没有参数,则在子类的构造器中不需要使用 super 关键字调用父类构造器,系统会自动调用super父类的无参构造器。

3. 关于一般方法 print()

如果子类的方法覆盖了父类的方法,我们也说父类的那个方法在子类有了新的版本或者新的实现。覆盖的新版本具有与老版本相同的方法签名:相同的方法名称参数表。因此,对于外界来说,子类并没有增加新的方法,仍然是在父类中定义过的那个方法。不同的是,这是一个新版本,所以通过子类的对象调用这个方法,执行的是子类自己的方法。

覆盖关系并不说明父类中的方法已经不存在了,而是当通过一个子类的对象调用这个方法时,子类中的方法取代了父类的方法,父类的这个方法被“覆盖”起来而看不见了。而当通过父类的对象调用这个方法时,实际上执行的仍然是父类中的这个方法。注意我们这里说的是对象而不是变量,因为一个类型为父类的变量有可能实际指向的是一个子类的对象。

当调用一个方法时,究竟应该调用哪个方法,这件事情叫做绑定。绑定表明了调用一个方法的时候,我们使用的是哪个方法。绑定有两种:一种是早绑定,又称静态绑定,这种绑定在编译的时候就确定了;另一种是晚绑定,即动态绑定。动态绑定在运行的时候根据变量当时实际所指的对象的类型动态决定调用的方法。

Java缺省使用动态绑定。所有成员函数的调用都应该被看作是动态绑定。

1.3 子类与父类的关系

对理解继承来说,子类从父类那里得到了什么?答案是:所有的东西,所有的父类的成员,包括变量和方法,都成为了子类的成员,除了构造方法,构造方法是父类所独有的。但是得到不等于可以随便使用

每个成员有不同的访问属性,子类继承得到了父类所有的成员,但是不同的访问属性使得子类在使用这些成员时有所不同:有些父类的成员直接成为子类的对外的界面,有些则被深深地隐藏起来,即使子类自己也不能直接访问。下表列出了不同访问属性的父类成员在子类中的访问属性:

父类成员访问属性在父类中的含义在子类中的含义
public对所有人开放对所有人开放
protected只有包内其它类、自己和子类可以访问只有包内其它类、自己和子类可以访问
缺省只有包内其它类可以访问如果子类与父类在同一个包内:子类可以访问父类。否则:相当于private,不能访问
private只有自己可以访问不能访问

对于父类的缺省的未定义访问属性的成员来说,他们是在父类所在的包内可见,如果子类不属于父类的包,那么在子类里面,这些缺省属性的成员和private的成员是一样的:不可见(属于一个包,可见)。

父类的private的成员在子类里仍然是存在的,只是子类中不能直接访问。

我们不可以在子类中重新定义继承得到的成员的访问属性。如果我们试图重新定义一个在父类中已经存在的成员变量那么我们是在定义一个与父类的成员变量完全无关的变量。在子类中我们可以访问这个定义在子类中的变量,在父类的方法中访问父类的那个。尽管它们同名但是互不影响,两者独立使用。

老师课堂交流区:能在一个构造函数里调用两次super()吗?

如果super()两次想调用的都是父类构造函数,就不行;

如果super()两次调用的不都是父类的构造函数,两次可以是:父构造函数+父一般函数;父一般函数+ 父一般函数。

老师课堂交流区: super()必须在构造函数的第一行吗?

​如果子类构造函数,想调用父类的构造函数,super()就必须在第一行;

如果子类构造函数,想调用父类的非构造函数,super()不用在第一行;


二、多态

2.1 多态变量

当把一个对象赋值给一个变量时,对象的类型必须与变量的类型相匹配,如: Car myCar = new Car();是一个有效的赋值,因为 Car 类型的对象被赋值给声明为保存 Car 类型对象的变量。但是由于引入了继承,这里的类型规则就得叙述得更完整些:一个变量可以保存其所声明的类型或该类型的任何子类型,这个变量叫对象变量

Java中保存对象类型的变量多态变量“多态”这个术语(字面意思是许多形态)是指一个变量可以保存不同类型(即其声明的类型或任何子类型)的对象。

2.2 子类的对象可以被看作是父类的对象来使用

2.2.1 传递给需要父类对象的函数

        database db = new database();
        db.add(new CD("33", 10, false, "还行", "z", 10));
        db.add(new CD("44", 111, false, "一般", "q", 16));
        db.add(new DVD("44", 88, false, "凑合", "12"));

add() 方法要求的是在db对象的 ArrayList 里面 new 进去一个 item,但是上面代码,我们 new 出的是两个 item 的子类对象 CD和DVD。

2.2.2 赋值给父类变量(父类指针指向子类对象)

Item item = new Item("a",0,true,"...");
CD cd = new CD("a",0,,0,true,"...");
item = cd; 

子类对象 cd 可以将自己管理的内容,交给(赋)给父类的对象 item 管理。

反之,子类的变量无法管理父类的对象。(错:cd = item | cc =(CD)item 强制父类向下转型也不行)

如何使 cc =(CD)item 强制父类向下转型成功?当父类变量 item 当时实际管理的类型是 CD类的对象,如下:

Item item = new Item("a",0,true,"...");
CD cd = new CD("a",0,,0,true,"...");

item = cd;  // 当下(item=cd)item 类型里面实际指了个子类的内容
CD cc = item; // 错,从编译的角度,想要把这部分内容交给子类同类型的变量 cc 管理,不行。
CD cc = (CD)item; // 这个向下造型,可以绕过编译器,效果类似 CD cc= cd

2.2.3 放进存放父类对象的容器里

在创建 ArrayList 的时候,这条里面装的是父类的类型的容器,它理应放父类对象管理者 Item1 item2 item3...但是它也放了子类对象管理者 CD1...DVD1...DVD2...

2.3 造型

将一个类型的对象赋给另外一个类型的变量,这个过程叫造型 CAST。

子类对象可赋值给父类变量(Java中不存在对象对象的赋值!对象是具体实例,有明确定义的状态和行为)。

  • 用括号围起来放在值的前面;
  • 运行时有机制来检查这样的转化是否合理 ClassCastException;
  • 对象本身并没有发生任何变化(所以不是类型转换(int)float 这是将那个 float 真实转换了);

造型是“把被造型对象当成那样的类型来看”。对于基本类型叫“类型转换”,对于对象类型叫“造型”。

向上造型

向上造型是默认的,不需要运算符;向上造型总是安全的;

老师课堂交流区:以下哪些赋值是合规则的?
假设现有4个类:Person、Teacher、Student和PhDStudent。Teacher 和Student都是Person的子类,PhDStudent是Student的子类。以下的赋值语句哪些是合法的,为什么?:
Person p1 = new Student();
Person p2 = new PhDStudent();
PhDStudent phd1 = new Student();
Teacher t1 = new Person();
Student s1 = new PhDStudent();
s1 = p1;
s1 = p2;
p1 = s1;
t1 = s1;
s1 = phd1;
phd1 = s1;
// 自己的答案不一定对
Person p1 = new Student();  
Person p2 = new PhDStudent();
Student s1 = new PhDStudent();
s1=p1;
s1=p2;
p1=s1;
老师课堂交流区:谁是谁的父类合适?
有时候事物本身比其第一印象更复杂。考虑一下,矩形和正方形的继承关系是怎样的?
因为从数学上,正方形是一种特殊的矩形。但是,从程序实现上考虑,正方形只要一个边长,而矩形可以从正方形继承后再增加一个边长成员变量。另一方面来说,如果正方形从矩形继承,那么就会出现冗余的第二个边长变量。你怎么看这个事情?
(引用)数学上的特殊情况是简化了对象的参数,而程序上反而是父类是提取了对象间相同的参数,而子类拓展了参数。

三、类型系统

3.1 Object 类

考虑在CD类里new CD,如果在CD子类里面,需要将我们送进去的信息,打印出来的话,显然直接打印,直接用toString()都不可以。返回一个带地址的东西 database.CD@30f39991。

        CD cd = new CD("33", 10, false, "还行", "z", 10);
        System.out.println(cd); // database.CD@30f39991
        System.out.println(cd.toString()); // database.CD@30f39991

需要怎么操作?自己写 toString() 方法或者利用 application 提供的方法,自动 @Override 一个属于子类自己的 toString() / equals() 方法。

Sourse --> Generate--> toString() 选择(String concat (+) and super.toString())会将自己的成员变量先用上
    @Override
    public String toString() { 
        return "database.CD{" +
                "artist='" + artist + '\'' +
                ", numofTracks=" + numofTracks +
                "} " + super.toString(); 
    }

这一步只会将自己独有的成员变量显示了。还需要到父类,再用一次上述方法,写出一个 String toString()的方法。

    @Override
    public String toString() {
        return "database.Item{" +
                "title='" + title + '\'' +
                ", playingTime=" + playingTime +
                ", gotIt=" + gotIt +
                ", comment='" + comment + '\'' +
                '}';
    }

3.2 equals() 比较两个对象内容是否相同

    public static void main(String[] args) {
        CD cd = new CD("33", 10, true, "还行", "z", 10);
        CD cd1 = new CD("33", 10, true, "还行", "z", 10);
        System.out.println(cd.equals(cd1));
    }
// false 

== 只能比较两个变量是否管理相同的对象,但是在这里即使用了 cd.equals(cd1) ,且它们都是相同内容的输入,我们的结果也依然是 false,为什么?

因为我们没有做自己的 equals,我们使用的是 Object 里面的 equals。 对于它来说 equals 就是判断里面两个管理者是不是管理的同一个对象。Sourse --> Override/Implement Methods

public boolean equals(Object obj) {
return (this == obj);
}

Override不能省略,否则编辑器不会检查,结果仍然是绕过写的CD equals,使用了object 的 equals。

    @Override
    public boolean equals(Object obj) {  // 如果这个对象和obj参数相同,返回True;
        CD cc =(CD)obj;// 因为上面参数Object 类型无法改变,所以需要向下造型
        return artist.equals(cc.artist); //以artist作比
    }

    public static void main(String[] args) {
        CD cd = new CD("33", 10, true, "还行", "z", 10);
        CD cd1 = new CD("33", 10, true, "还行", "z", 10);
        System.out.println(cd.equals(cd1));
    }

3.3 DoME的新媒体类型

可扩展性:代码不需要经过修改,就可以扩展适应新的内容和数据。

package database;

public class VideoGames extends Item{
    private int numberOfPlayers;
    private String platform;

    public VideoGames(String title, float playingTime, boolean gotIt, String comment,int numberOfPlayers,String platform) {
        super(title, playingTime, gotIt, comment);
        this.numberOfPlayers=numberOfPlayers;
        this.platform=platform;
    }

    @Override
    public void print() {
        super.print();
        System.out.println("Item:numberOfPlayers" + numberOfPlayers +"\nItem:platform"+platform);
    }
}

以上代码加上善用 IJ提供的工具,我们就可以轻松增加一个新类型 VideoGames。

四、继承类型

五、其他关键字

5.1 implements

/ˈɪmplɪm(ə)nts/ˈɪmplɪments; ˈɪmplɪmənts/ 实现。变相的使 java 具有多继承的特性,使用范围为类继承接口的情况,可以同时继承多个接口(接口跟接口之间采用逗号分隔)。

5.2 final

修饰变量(包括类属性、对象属性、局部变量和形参)、方法(包括类方法和对象方法)和类。

把类定义为最终类,不能被继承,或者用于修饰方法,该方法不能被子类重写。

 我的知乎地址【Java编程】面向对象程序设计--继承与多态 - 知乎

面向对象程序设计——Java语言_浙江大学_中国大学MOOC(慕课)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值