Java基础——五、继承

13 篇文章 0 订阅
6 篇文章 0 订阅

五、继承

简要

1、说明

继承(Inheritance)面向对象编程(OOP)的一个核心概念,它允许一个类(子类)继承另一个类(父类)的属性和方法,从而实现代码重用和结构化组织。通过继承,子类可以扩展父类的功能或者对父类的方法进行重写

  1. 父类(超类、基类)
    • 父类是被继承的类,它包含子类可以使用的属性和方法
    • 在Java中,使用extends关键字来实现继承。
  2. 子类(派生类)
    • 子类是继承父类的类,它可以访问父类的公共和受保护的成员(属性和方法)
    • 子类可以添加新的属性和方法,也可以重写父类的方法
2、继承的优点
  1. 代码重用:
    • 子类可以直接使用父类中定义的属性和方法,减少代码的重复。
  2. 提高可维护性:
    • 由于子类和父类的结构化关系,系统更加模块化,修改父类的方法时,子类也会自动更新,从而提高了系统的可维护性。
  3. 实现多态:
    • 继承是实现多态(Polymorphism)的基础,通过继承和方法重写,程序可以在运行时决定调用哪个类的方法。
3、继承的实现

提供一个简单的Java继承示例:

//父类
class Animal{
    String name;
    
    public void eat(){
        System.out.println("This animal eat food.");
    }
}

//子类
class Dog extend Animal{
    
    public void bark(){
        Sytem.out.println("The dog barks.");
    }
    
    public void eat(){
        System.out.println("The dog eats dog food.");
    }
}

public class Main{
    public static void main(String[] args){
        Dog dog = new Dog();
        dog.name = "Buddy";
        //调用重写的方法。
        dog.eat();
        //调用子类特有的方法。
        dog,bark();
    }
}
4、继承的类型
  1. 单继承:
    • 一个子类只能继承一个父类。Java中不支持多继承(即一个子类继承多个父类),但是可以通过接口来实现,类似多继承的效果。
  2. 多层继承:
    • 一个类继承另一个类,该类又继承另一个类,形成继承链。例如:类C继承类B,类B继承类A。
组合
1、组合 VS 继承

继承:

  • 是一种是一个(is-a)关系。例如,Dog继承自Animal,表示Dog是一个Animal
  • 子类继承父类的所有属性和方法,但这也导致子类与父类之间的耦合度较高。
  • 如果子类发生变化,子类可能需要进行相应的修改。
  • 继承层次过深可能导致代码复杂性增加。

组合:

  • 是一种有一个(has - a)关系。例如,Car有一个Engine,表示car包含一个Engine对象。
  • 通过将一个类的实例作为成员变量引入到另一个类中,来实现类之间的协作。
  • 组合的类之间的耦合度较低,一个类的改变不会直接影响另一个类。
  • 组合更灵活,可以在运行时动态改变组合对象的行为。
2、组合的示例

假设我们有一个场景,需要定义一个交通工具(Vehicle),每种交通工具有不同的移动方式(MoveStrategy)。我们可以使用组合来实现不同交通工具的行为,而不是通过继承。

定义接口和实现类:

package base.inheritance.assembly;

/**
 * @author: LiHao
 * @program: interview
 * @description: 叙述组合的示例(用于对比继承)
 * @Date: 2024-06-10-17:34:10
 * thinking:
 */
interface MoveStrategy {
    /**
     * 移动对象。
     * <p>
     * 该方法定义了对象的移动行为,但没有指定移动的方式或方向。
     * 具体的移动逻辑应在方法体内实现,这里没有提供实现是因为示例的限制。
     *
     * @see #move(int, int) 如果需要更精确的控制移动距离和方向,可以使用带参数的移动方法。
     */
    void move();
}

/**
 * 具体的移动策略实现:开车
 */
class DriveStrategy implements MoveStrategy {
    /**
     * 实现移动方法。
     * 该方法具体实现了车辆在道路上的行驶行为。通过打印信息来模拟车辆的移动过程。
     * 由于这是一个抽象类的抽象方法的具体实现,所以这里使用了@Override注解来标明此方法是对父类抽象方法的实现。
     */
    @Override
    public void move() {
        System.out.println("Driving on the road.");
    }
}

/**
 * 具体的移动策略实现:飞行
 */
class FiyStrategy implements MoveStrategy {

    @Override
    public void move() {
        System.out.println("Flying in the sky.");
    }
}

/**
 * 具体的移动策略实现:行走
 */
class WalkStrategy implements MoveStrategy {

    @Override
    public void move() {
        System.out.println("Walking on the ground.");
    }
}

/**
 * 交通工具类
 */
class Vehicle {
    private MoveStrategy moveStrategy;

    /**
     * 构造函数,用于初始化车辆对象。
     *
     * @param moveStrategy 移动策略对象,车辆将使用该策略来进行移动。
     *                     通过传入不同的移动策略,车辆可以实现不同的移动方式,
     *                     提供了策略模式中的策略对象。
     */
    public Vehicle(MoveStrategy moveStrategy) {
        this.moveStrategy = moveStrategy;
    }

    /**
     * 设置移动策略。
     *
     * 本方法用于更换对象的移动策略,允许对象在运行时根据需要动态调整其移动方式。
     * 通过传入不同的移动策略实例,对象可以实现不同的移动行为,从而提高代码的灵活性和可扩展性。
     *
     * @param moveStrategy 移动策略对象,用于定义对象的移动行为。
     */
    public void setMoveStrategy(MoveStrategy moveStrategy){
        this.moveStrategy = moveStrategy;
    }


    /**
     * 实现移动操作。
     * 通过策略模式,调用指定的移动策略来执行移动操作。
     * 此方法的目的是为了封装移动行为,具体的移动方式由包含的移动策略对象决定。
     */
    public void move() {
        moveStrategy.move();
    }

    /**
     * 使用组合来实现不同的行为
     * @param args
     */
    public static void main(String[] args) {
        //创建不同的移动策略
        MoveStrategy driveStrategy = new DriveStrategy();
        MoveStrategy fiyStrategy = new FiyStrategy();
        MoveStrategy walkStrategy = new WalkStrategy();

        //创建交通工具并设置移动策略
        Vehicle car = new Vehicle(driveStrategy);
        car.move();

        //动态改变交通工具的移动策略
        car.setMoveStrategy(fiyStrategy);
        car.move();

        //创建另一个交通工具并设置不同的移动策略
        Vehicle person = new Vehicle(walkStrategy);
        person.move();
    }
}

输出:

Driving on the road.
Flying in the sky.
Walking on the ground.
3、组合的优点
  1. 灵活性:
    • 可以在运行时动态改变对象的行为,而无需修改类的层次结构。
    • 可以通过组合不同的策略对象来实现多种行为。
  2. 低耦合:
    • 组合的类之间的耦合度较低,一个类的改变不会直接影响另一个类。
    • 更容易维护和扩展系统,添加新的策略不需要修改现有的类。
  3. 遵循单一职责原则:
    • 每一个类只需要关注一个特定的功能,职责更加准确。

通过理解和应用组合的模式,可以创建更灵活、易维护的系统,特别是在需求频繁变化的场景下,组合模式的优势更加明显。

5、注意事项
  1. 访问控制

    • 父类的private成员不能被子类直接访问,但protectedpublic成员可以被子类访问。
  2. 构造方法

    • 构造方法不能被继承,但子类可以调用父类的构造方法(使用super关键字)。
  3. 组合优于继承

    • 在某些情况下,使用组合(即在一个类中包含另一个类的实例)可能比继承更合适,因为它可以提供更好的灵活性和减少耦合度。

通过理解和应用继承,可以创建更简洁、可维护性强且扩展性好的代码结构,这是面向对象编程不可或缺的一部分。

访问权限

Java 中有三个访问权限修饰符:private、protected 以及 public

  • 如果不加访问修饰符,表示包级可见。可以对类或类中的成员(字段和方法)加上访问修饰符。

  • 类可见表示其它类可以用这个类创建实例对象

  • 成员可见表示其它类可以用这个类的实例对象访问到该成员

protected 用于修饰成员,表示在继承体系中成员对于子类可见,但是这个访问修饰符对于类没有意义。

设计良好的模块会隐藏所有的实现细节,把它的 API 与它的实现清晰地隔离开来。模块之间只通过它们的 API 进行通信,一个模块不需要知道其他模块的内部工作情况,这个概念被称为信息隐藏或封装。因此访问权限应当尽可能地使每个类或者成员不被外界访问

如果子类的方法重写了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例去代替,也就是确保满足里氏替换原则。

**字段决不能是公有的,因为这么做的话就失去了对这个字段修改行为的控制,客户端可以对其随意修改。**例如下面的例子中,AccessExample 拥有 id 公有字段,如果在某个时刻,我们想要使用 int 存储 id 字段,那么就需要修改所有的客户端代码

public class AccessExample {
    public String id;
}

可以使用公有的 getter 和 setter 方法来替换公有字段,这样的话就可以控制对字段的修改行为。

public class AccessExample {

    private int id;

    public String getId() {
        return id + "";
    }

    public void setId(String id) {
        this.id = Integer.valueOf(id);
    }
}

但是也有例外,如果是包级私有的类或者私有的嵌套类,那么直接暴露成员不会有特别大的影响。

public class AccessWithInnerClassExample {

    private class InnerClass {
        int x;
    }

    private InnerClass innerClass;

    public AccessWithInnerClassExample() {
        innerClass = new InnerClass();
    }

    public int getValue() {
        return innerClass.x;  // 直接访问
    }
}

抽象类与接口

一、抽象类
1.定义

抽象类是不能被实例化的类,它用来作为其它类的基类。抽象类可以包含抽象方法(没有具体的方法)和具体方法(有方法体的方法)。

抽象类为什么不能被实例化?

设计目的:

  1. 不完整的实现:抽象类是用了作为其它类型的基类的,它包含抽象方法,这些方法没有实现。因为抽象类本身并没有提供所有方法的实现,它不完整,所以不能被实例化。实例化一个不完整的对象是没有意义的。

特征与语言规范:

  1. 强制子类实现:抽象类中的抽象方法定义了子类必须实现的行为,这是一种设计模式,确保所有子类都提供具体实现。如果允许实例化对抽象类,那么这些抽象方法在没有实现的情况下就会被调用,导致错误。
  2. 语法和编译器要求:在Java语言规范中,抽象类被定义为不能被实例化。如果尝试实例化一个抽象类,编译器会报错。这是为了确保编译时就能发现设计上的错误。

示例代码:

abstract class Animal {
    abstract void makeSound(); // 抽象方法,没有方法体

    void eat() {
        System.out.println("This animal is eating.");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Bark");
    }
}

public class Main {
    public static void main(String[] args) {
        // Animal animal = new Animal(); // 编译错误:Animal是抽象的;不能实例化
        Dog dog = new Dog();
        dog.makeSound(); // 输出:Bark
        dog.eat(); // 输出:This animal is eating.
    }
}

在上述示例中:

  • Animal类是一个抽象类,包含一个抽象方法makeSound(),没有方法体。
  • Dog类继承自Animal并实现了makeSound()方法。
  • 试图实例化Animal类会导致编译错误,因为Animal是抽象的,不能直接创建其实例。

总结:

抽象类不能实例化的原因主要是:

  1. 不完整实现:抽象类本身不完整,包含未实现的方法。
  2. 设计模式:确保子类实现必要的方法,强制制定的设计模式。
  3. 语言规范:Java语言规范和编译器的要求,防止设计错误。

通过这些机制,抽象类可以正确地作为其它类的基类,确保代码的健壮性和设计的一致性。

2.关键字

使用abstract关键字类定义一个抽象类的抽象方法。

3.特点
  1. 部分实现:抽象类可以包含具体的方法实现,也可以包含抽象方法。子类可以继承抽象类并实现未实现的方法。
  2. 构造方法:可以有构造方法,但不能实例化对象。构造方法通常被用于子类的实例化过程中调用。
  3. 字段和方法:可以包含字段和方法(即可以是抽象的也可以是具体的)。
  4. 继承:一个类可以继承一个抽象类(单继承)。
  5. 访问修饰符:可以使用各种访问修饰符(public,protected,private)。

抽象类和抽象方法都使用 abstract 关键字进行声明。如果一个类中包含抽象方法,那么这个类必须声明为抽象类

抽象类和普通类最大的区别是:抽象类不能被实例化,只能被继承

public abstract class AbstractClassExample {

    protected int x;
    private int y;

    public abstract void func1();

    public void func2() {
        System.out.println("func2");
    }
}
public class AbstractExtendClassExample extends AbstractClassExample {
    @Override
    public void func1() {
        System.out.println("func1");
    }
}
// AbstractClassExample ac1 = new AbstractClassExample(); // 'AbstractClassExample' is abstract; cannot be instantiated
AbstractClassExample ac2 = new AbstractExtendClassExample();
ac2.func1();

2. 接口

接口是抽象类的延伸,在 Java 8 之前,它可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。

从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类,让它们都实现新增的方法

接口的成员(字段 + 方法)默认都是 public 的,并且不允许定义为 private 或者 protected。从 Java 9 开始,允许将方法定义为 private,这样就能定义某些复用的代码又不会把方法暴露出去。

接口的字段默认都是 static 和 final 的

public interface InterfaceExample {

    void func1();

    default void func2(){
        System.out.println("func2");
    }

    int x = 123;
    // int y;               // Variable 'y' might not have been initialized
    public int z = 0;       // Modifier 'public' is redundant for interface fields
    // private int k = 0;   // Modifier 'private' not allowed here
    // protected int l = 0; // Modifier 'protected' not allowed here
    // private void fun3(); // Modifier 'private' not allowed here
}
public class InterfaceImplementExample implements InterfaceExample {
    @Override
    public void func1() {
        System.out.println("func1");
    }
}
// InterfaceExample ie1 = new InterfaceExample(); // 'InterfaceExample' is abstract; cannot be instantiated
InterfaceExample ie2 = new InterfaceImplementExample();
ie2.func1();
System.out.println(InterfaceExample.x);

3. 比较

  • 从设计层面上看,抽象类提供了一种 IS-A 关系,需要满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系

  • 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类

  • 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制

  • 接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限

4. 使用选择

使用接口:

  • 需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Comparable 接口中的 compareTo() 方法;
  • 需要使用多重继承

使用抽象类

  • 需要在几个相关的类中共享代码
  • 需要能控制继承来的成员的访问权限,而不是都为 public。
  • 需要继承非静态和非常量字段

在很多情况下,接口优先于抽象类。因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。

super

  • 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而委托父类完成一些初始化的工作。应该注意到,子类一定会调用父类的构造函数来完成初始化工作,一般是调用父类的默认构造函数,如果子类需要调用父类其它构造函数,那么就可以使用 super() 函数。
  • 访问父类的成员:如果子类重写了父类的某个方法,可以通过使用 super 关键字来引用父类的方法实现。
public class SuperExample {

    protected int x;
    protected int y;

    public SuperExample(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public void func() {
        System.out.println("SuperExample.func()");
    }
}
public class SuperExtendExample extends SuperExample {

    private int z;

    public SuperExtendExample(int x, int y, int z) {
        super(x, y);
        this.z = z;
    }

    @Override
    public void func() {
        super.func();
        System.out.println("SuperExtendExample.func()");
    }
}
SuperExample e = new SuperExtendExample(1, 2, 3);
e.func();
SuperExample.func()
SuperExtendExample.func()

Using the Keyword super

重写与重载

1. 重写(Override)

存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。

为了满足里式替换原则,重写有以下三个限制:

  • 子类方法的访问权限必须大于等于父类方法;
  • 子类方法的返回类型必须是父类方法返回类型或为其子类型。
  • 子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型。

使用 @Override 注解,可以让编译器帮忙检查是否满足上面的三个限制条件。

下面的示例中,SubClass 为 SuperClass 的子类,SubClass 重写了 SuperClass 的 func() 方法。其中:

  • 子类方法访问权限为 public,大于父类的 protected。
  • 子类的返回类型为 ArrayList,是父类返回类型 List 的子类。
  • 子类抛出的异常类型为 Exception,是父类抛出异常 Throwable 的子类。
  • 子类重写方法使用 @Override 注解,从而让编译器自动检查是否满足限制条件。
class SuperClass {
    protected List<Integer> func() throws Throwable {
        return new ArrayList<>();
    }
}

class SubClass extends SuperClass {
    @Override
    public ArrayList<Integer> func() throws Exception {
        return new ArrayList<>();
    }
}

在调用一个方法时:

  1. 先从本类中查找看是否有对应的方法
  2. 如果没有再到父类中查看,看是否从父类继承来。
  3. 否则就要对参数进行转型,转成父类之后看是否有对应的方法。总的来说,方法调用的优先级为:
  • this.func(this)
  • super.func(this)
  • this.func(super)
  • super.func(super)
/*
    A
    |
    B
    |
    C
    |
    D
 */


class A {

    public void show(A obj) {
        System.out.println("A.show(A)");
    }

    public void show(C obj) {
        System.out.println("A.show(C)");
    }
}

class B extends A {

    @Override
    public void show(A obj) {
        System.out.println("B.show(A)");
    }
}

class C extends B {
}

class D extends C {
}
public static void main(String[] args) {

    A a = new A();
    B b = new B();
    C c = new C();
    D d = new D();

    // 在 A 中存在 show(A obj),直接调用
    a.show(a); // A.show(A)
    // 在 A 中不存在 show(B obj),将 B 转型成其父类 A
    a.show(b); // A.show(A)
    // 在 B 中存在从 A 继承来的 show(C obj),直接调用
    b.show(c); // A.show(C)
    // 在 B 中不存在 show(D obj),但是存在从 A 继承来的 show(C obj),将 D 转型成其父类 C
    b.show(d); // A.show(C)

    // 引用的还是 B 对象,所以 ba 和 b 的调用结果一样
    A ba = new B();
    ba.show(c); // A.show(C)
    ba.show(d); // A.show(C)
}

2. 重载(Overload)

存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同

应该注意的是,返回值不同,其它都相同不算是重载

class OverloadingExample {
    public void show(int x) {
        System.out.println(x);
    }

    public void show(int x, String y) {
        System.out.println(x + " " + y);
    }
}
public static void main(String[] args) {
    OverloadingExample example = new OverloadingExample();
    example.show(1);
    example.show(1, "2");
}
  • 28
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值