2.3封装和继承设计思想
目录介绍
- 01.快速了解继承
- 1.1 为何需要继承
- 1.2 什么是继承
- 1.3 继承的格式
- 1.4 继承的意义
- 02.继承使用介绍
- 2.1 看一个案例
- 2.2 继承主要事项
- 2.3 继承好处和弊端
- 04.初始化基类
- 4.1 无参数构造方法
- 4.2 有参数构造方法
- 05.委托
- 5.1 什么是委托
- 5.2 有哪些使用场景
- 06.不支持多继承影响
- 07.分析继承初始化顺序
01.快速了解继承
1.1 为何需要继承
- 多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那个类即可。
- 举个例子,猫,狗,马,鸡都具有吃和睡觉的行为,都有年龄和性别属性。那么可以把共有的提取到一个动物类中,那么XX动物都可以继承动物类。
1.2 什么是继承
- 继承是用来表示类之间的 is-a 关系,比如猫是一种哺乳动物。从继承关系上来讲,继承可以分为两种模式,单继承和多继承。单继承表示一个子类只继承一个父类,多继承表示一个子类可以继承多个父类,比如猫既是哺乳动物,又是爬行动物。
1.3 继承的格式
- 为了实现继承这个特性,编程语言需要提供特殊的语法机制来支持,比如 Java 使用 extends 关键字来实现继承。
- class 子类名 extends 父类名 {}
- 单独的这个类称为父类,基类或者超类;这多个类可以称为子类或者派生类
1.4 继承的意义
- 继承存在的意义是什么?它能解决什么编程问题?
- 继承最大的一个好处就是代码复用。
- 假如两个类有一些相同的属性和方法,我们就可以将这些相同的部分,抽取到父类中,让两个子类继承父类。这样,两个子类就可以重用父类中的代码,避免代码重复写多遍。不过,这一点也并不是继承所独有的,我们也可以通过其他方式来解决这个代码复用的问题,比如利用组合关系而不是继承关系。
- 符合人的认知
- 如果我们再上升一个思维层面,去思考继承这一特性,可以这么理解:我们代码中有一个猫类,有一个哺乳动物类。猫属于哺乳动物,从人类认知的角度上来说,是一种 is-a 关系。我们通过继承来关联两个类,反应真实世界中的这种关系,非常符合人类的认知,而且,从设计的角度来说,也有一种结构美感。
- 过度继承的弊端
- 过度使用继承,继承层次过深过复杂,就会导致代码可读性、可维护性变差。为了了解一个类的功能,我们不仅需要查看这个类的代码,还需要按照继承关系一层一层地往上查看“父类、父类的父类……”的代码。还有,子类和父类高度耦合,修改父类的代码,会直接影响到子类。
- 所以,继承这个特性也是一个非常有争议的特性。很多人觉得继承是一种反模式。我们应该尽量少用,甚至不用。关于这个问题,在后面讲到“复用设计”这种设计思想的时候,在进行讨论。
02.继承使用介绍
2.1 看一个案例
- 继承是所有面向对象语言的一个组成部分。
- 事实证明,在创建类时总是要继承,因为除非显式地继承其他类,否则就隐式地继承 Java 的标准根类对象(Object)。
- 当你继承时,你说,“这个新类与那个旧类类似。你可以在类主体的左大括号前的代码中声明这一点,使用关键字 extends 后跟基类的名称。当你这样做时,你将自动获得基类中的所有字段和方法。这里有一个例子:
class Cleanser { private String s = "Cleanser"; public void append(String a) { s += a; } public void dilute() { append(" dilute()"); } public void apply() { append(" apply()"); } public void scrub() { append(" scrub()"); } @Override public String toString() { return s; } public static void main(String[] args) { Cleanser x = new Cleanser(); x.dilute(); x.apply(); x.scrub(); System.out.println(x); } } public class Detergent extends Cleanser { // Change a method: @Override public void scrub() { append(" Detergent.scrub()"); super.scrub(); // Call base-class version } // Add methods to the interface: public void foam() { append(" foam()"); } // Test the new class: public static void main(String[] args) { Detergent x = new Detergent(); x.dilute(); x.apply(); x.scrub(); x.foam(); System.out.println(x); System.out.println("Testing base class:"); Cleanser.main(args); } } /* Output: Cleanser dilute() apply() Detergent.scrub() scrub() foam() Testing base class: Cleanser dilute() apply() scrub() */
- Cleanser 和 Detergent 都包含一个 main() 方法。
- 你可以为每个类创建一个main();这允许对每个类进行简单的测试。当你完成测试时,不需要删除 main(); 你可以将其留在以后的测试中。即使程序中有很多类都有 main() 方法,惟一运行的只有在命令行上调用的 main()。
- 在这里,Detergent.main() 显式地调用 Cleanser.main(),从命令行传递相同的参数(当然,你可以传递任何字符串数组)。
- Cleanser 的接口中有一组方法:
- append()、dilute()、apply()、scrub() 和 toString()。因为 Detergent 是从 Cleanser 派生的(通过 extends 关键字),所以它会在其接口中自动获取所有这些方法,即使你没有在 Detergent 中看到所有这些方法的显式定义。那么,可以把继承看作是复用类。
- 你可以在新类中调用基类的该方法。但是在 scrub() 内部,不能简单地调用 scrub(),因为这会产生递归调用。为了解决这个问题,Java的 super 关键字引用了当前类继承的“超类”(基类)。因此表达式 super.scrub() 调用方法 scrub() 的基类版本。
2.2 继承主要事项
- 继承的注意事项
- a:子类只能继承父类所有非私有的成员(成员方法和成员变量)
- b:子类不能继承父类的构造方法,但是可以通过super(待会儿讲)关键字去访问父类构造方法。
- c:不要为了部分功能而去继承
- 继承中构造方法的注意事项
- 父类没有无参构造方法,子类怎么办?
- a: 在父类中添加一个无参的构造方法
- b:子类通过super去显示调用父类其他的带参的构造方法
- c:子类通过this去调用本类的其他构造方法
- 本类其他构造也必须首先访问了父类构造
- B:注意事项
- super(…)或者this(….)必须出现在第一条语句上
- 父类没有无参构造方法,子类怎么办?
2.3 继承好处和弊端
- 继承的好处
- a:提高了代码的复用性
- b:提高了代码的维护性
- c:让类与类之间产生了关系,是多态的前提
- 继承的弊端
- 类的耦合性增强了。
- 开发的原则:高内聚,低耦合。
- 耦合:类与类的关系
- 内聚:就是自己完成某件事情的能力
04.初始化基类
4.1 无参数构造方法
- 现在涉及到两个类:基类和派生类。
- 想象派生类生成的结果对象可能会让人感到困惑。从外部看,新类与基类具有相同的接口,可能还有一些额外的方法和字段。但是继承并不只是复制基类的接口。当你创建派生类的对象时,它包含基类的子对象。这个子对象与你自己创建基类的对象是一样的。只是从外部看,基类的子对象被包装在派生类的对象中。
- 必须正确初始化基类子对象,而且只有一种方法可以保证这一点 :
- 通过调用基类构造函数在构造函数中执行初始化,该构造函数具有执行基类初始化所需的所有适当信息和特权。Java自动在派生类构造函数中插入对基类构造函数的调用。
- 下面的例子展示了三个层次的继承:
class Art { Art() { System.out.println("Art constructor"); } } class Drawing extends Art { Drawing() { System.out.println("Drawing constructor"); } } public class Cartoon extends Drawing { public Cartoon() { System.out.println("Cartoon constructor"); } public static void main(String[] args) { Cartoon x = new Cartoon(); } } /* Output: Art constructor Drawing constructor Cartoon constructor */
- 构造从基类“向外”进行,因此基类在派生类构造函数能够访问它之前进行初始化。即使不为 Cartoon 创建构造函数,编译器也会为你合成一个无参数构造函数,调用基类构造函数。
4.2 有参数构造方法
- 上面的所有例子中构造函数都是无参数的;编译器很容易调用这些构造函数,因为不需要参数。如果没有无参数的基类构造函数,或者必须调用具有参数的基类构造函数,则必须使用 super 关键字和适当的参数列表显式地编写对基类构造函数的调用:
class Game { Game(int i) { System.out.println("Game constructor"); } } class BoardGame extends Game { BoardGame(int i) { super(i); System.out.println("BoardGame constructor"); } } public class Chess extends BoardGame { Chess() { super(11); System.out.println("Chess constructor"); } public static void main(String[] args) { Chess x = new Chess(); } } /* Output: Game constructor BoardGame constructor Chess constructor */
- 如果没有在 BoardGame 构造函数中调用基类构造函数,编译器就会报错找不到 Game() 的构造函数。此外,对基类构造函数的调用必须是派生类构造函数中的第一个操作。(如果你写错了,编译器会提醒你。)
05.委托
5.1 什么是委托
- Java不直接支持的第三种重用关系称为委托。
- 这介于继承和组合之间,因为你将一个成员对象放在正在构建的类中(比如组合),但同时又在新类中公开来自成员对象的所有方法(比如继承)。例如,宇宙飞船需要一个控制模块:
public class SpaceShipControls { void up(int velocity) {} void down(int velocity) {} void left(int velocity) {} void right(int velocity) {} void forward(int velocity) {} void back(int velocity) {} void turboBoost() {} }
- 建造宇宙飞船的一种方法是使用继承:
public class DerivedSpaceShip extends SpaceShipControls { private String name; public DerivedSpaceShip(String name) { this.name = name; } @Override public String toString() { return name; } public static void main(String[] args) { DerivedSpaceShip protector = new DerivedSpaceShip("NSEA Protector"); protector.forward(100); } }
- 然而, DerivedSpaceShip 并不是真正的“一种” SpaceShipControls ,即使你“告诉” DerivedSpaceShip 调用 forward()。
- 更准确地说,一艘宇宙飞船包含了 SpaceShipControls、,同时 SpaceShipControls 中的所有方法都暴露在宇宙飞船中。委托解决了这个难题:
public class SpaceShipDelegation { private String name; private SpaceShipControls controls = new SpaceShipControls(); public SpaceShipDelegation(String name) { this.name = name; } // Delegated methods: public void back(int velocity) { controls.back(velocity); } public void down(int velocity) { controls.down(velocity); } public void forward(int velocity) { controls.forward(velocity); } public void left(int velocity) { controls.left(velocity); } public void right(int velocity) { controls.right(velocity); } public void turboBoost() { controls.turboBoost(); } public void up(int velocity) { controls.up(velocity); } public static void main(String[] args) { SpaceShipDelegation protector = new SpaceShipDelegation("NSEA Protector"); protector.forward(100); } }
- 方法被转发到底层 control对象,因此接口与继承的接口是相同的。但是,你对委托有更多的控制,因为你可以选择只在成员对象中提供方法的子集。
5.2 有哪些使用场景
06.不支持多继承影响
- Java 不支持多继承影响
- Java 相比于其他面向对象语言,如 C++,设计上有一些基本区别,比如Java 不支持多继承。这种限制,在规范了代码实现的同时,也产生了一些局限性,影响着程序设计结构。Java 类可以实现多个接口,因为接口是抽象方法的集合,所以这是声明性的,但不能通过扩展多个抽象类来重用逻辑。
- 在一些情况下存在特定场景,需要抽象出与具体实现、实例化无关的通用逻辑,或者纯调用关系的逻辑,但是使用传统的抽象类会陷入到单继承的窘境。
- 为什么是单继承而不能多继承呢?
- 若为多继承,那么当多个父类中有重复的属性或者方法时,子类的调用结果会含糊不清,因此用了单继承。
- 多继承虽然能使子类同时拥有多个父类的特征,但是其缺点也是很显著的,主要有两方面:
- 如果在一个子类继承的多个父类中拥有相同名字的实例变量,子类在引用该变量时将产生歧义,无法判断应该使用哪个父类的变量。
- 如果在一个子类继承的多个父类中拥有相同方法,子类中有没有覆盖该方法,那么调用该方法时将产生歧义,无法判断应该调用哪个父类的方法。
- Java是从C++语言上优化而来,而C++也是面向对象的,为什么它却可以多继承的呢?首先,C++语言是1983年在C语言的基础上推出的,Java语言是1995年推出的。其次,在C++被设计出来后,就会经常掉入多继承这个陷阱,虽然它也提出了相应的解决办法,但Java语言本着简单的原则舍弃了C++中的多继承,这样也会使程序更具安全性。
- 为什么是多实现呢?
- 通过实现接口拓展了类的功能,若实现的多个接口中有重复的方法也没关系,因为实现类中必须重写接口中的方法,所以调用时还是调用的实现类中重写的方法。
07.分析继承初始化顺序
- 大概的思路分析
- 创建三个类动物Animal类、狗狗Dog类、哈士奇Huskie类,每个类中有一个非静态变量和无参构造函数,且Huskie类继承Dog类,Dog类继承Animal类;然后实例化一个Huskie类对象,根据输出的日志确认加载顺序。
- 案例代码如下所示。先执行test()方法,然后再执行test1()方法,看看分别输出的日志。
public void test(){ Animal animal = new Animal(); animal.eat(); } public void test1(){ Huskie huskie= new Huskie(); } public class Animal { private Double weight = getWeight(); private Double getWeight() { System.out.println("i am Animal getWeight method"); return new Double(1.0); } private void eat(){ System.out.println("i am eat method"); } public Animal() { System.out.println("i am animal constructor"); } } public class Dog extends Animal{ private int legNum = getLegNum(); private int getLegNum(){ System.out.println("i am Dog getLegNum method"); return 4; } public Dog() { System.out.println("i am Dog constructor"); } } public class Huskie extends Dog { private Boolean isStupid = judgeIQ(); private Boolean isLovely = judgeLovely(); private Boolean judgeIQ(){ System.out.println("i am Huskie judgeIQ method"); return true; } private Boolean judgeLovely(){ System.out.println("i am Huskie judgeLovely method"); return true; } public Huskie() { System.out.println("i am Huskie constructor"); } }
- 执行test()方法
- 发现创建一个对象,先执行成员变量,然后执行构造方法,在执行对象调用的方法。
2021-08-10 10:23:07.976 8878-8878/com.yc.cn.ycnotification I/System.out: i am Animal getWeight method 2021-08-10 10:23:07.976 8878-8878/com.yc.cn.ycnotification I/System.out: i am animal constructor 2021-08-10 10:23:07.976 8878-8878/com.yc.cn.ycnotification I/System.out: i am eat method
- 执行test1()方法
2021-08-10 10:24:22.917 8878-8878/com.yc.cn.ycnotification I/System.out: i am Animal getWeight method 2021-08-10 10:24:22.917 8878-8878/com.yc.cn.ycnotification I/System.out: i am animal constructor 2021-08-10 10:24:22.917 8878-8878/com.yc.cn.ycnotification I/System.out: i am Dog getLegNum method 2021-08-10 10:24:22.917 8878-8878/com.yc.cn.ycnotification I/System.out: i am Dog constructor 2021-08-10 10:24:22.917 8878-8878/com.yc.cn.ycnotification I/System.out: i am Huskie judgeIQ method 2021-08-10 10:24:22.917 8878-8878/com.yc.cn.ycnotification I/System.out: i am Huskie judgeLovely method 2021-08-10 10:24:22.917 8878-8878/com.yc.cn.ycnotification I/System.out: i am Huskie constructor
- 最后根据打印的日志可以分析
- 分析Animal类,创建该对象,可以确认,实例化一个普通类,会先初始化变量,再调用构造函数,再调用对象的调用方法。
- 分析Huskie类,创建该对象,实例化一个子类,会递归找到最最上层的父类,然后按照继承的顺序初始化,本案例中,会依次初始化Animal类,Dog类,Huskie类;并且在舒适化每一个类的时候,先初始化变量,在调用构造器。
- 得出结论如下
- 实例化一个普通类,会先初始化变量,再调用构造函数;
- 实例化一个有继承关系的子类,会递归找到最上层的父类,然后按照继承的顺序依次初始化每一个类;