java中一个接口A,以及一个实现它的类B,一个A类型的引用对象作为一个方法的参数,这个参数的类型可以是B的类型吗?

简介: 本文探讨了面向对象编程中接口与实现类的关系,以及里氏替换原则(LSP)的应用。通过示例代码展示了如何利用多态性将实现类的对象传递给接口类型的参数,满足LSP的要求。LSP确保子类能无缝替换父类或接口,不改变程序行为。接口定义了行为规范,实现类遵循此规范,从而保证了多态性和代码的可维护性。总结来说,接口与实现类的关系天然符合LSP,体现了多态性的核心思想。

theme: github

思考

在面向对象编程中,如果有一个接口 A 和一个实现它的类 B,并且一个方法的参数是 A 类型的引用对象,那么这个参数的类型可以B 类型。原因是 B 实现了接口 A,这使得 BA 的子类型(满足 Liskov 替换原则,即 Liskov Substitution Principle),因此在方法调用时,可以将 B 的对象传递给 A 类型的参数。

4beebd6c979790b4964e1a745f8a2fdb.jpg

解释:

这是多态性(Polymorphism)的一个典型应用。通过接口编程,方法只需要关心这个参数实现了 A 的接口,而不必关心它的具体类型(B 或其他实现类)。

代码示例

interface A {
   
    void doSomething();
}

class B implements A {
   
    @Override
    public void doSomething() {
   
        System.out.println("B is doing something.");
    }
}

public class Main {
   
    public static void main(String[] args) {
   
        B b = new B();
        // 可以将 B 类型的对象传递给 A 类型的参数
        execute(b);
    }

    // 方法接收一个 A 类型的参数
    public static void execute(A a) {
   
        a.doSomething();
    }
}

在这个例子中:

  • B 实现了接口 A
  • 方法 execute(A a) 需要一个 A 类型的参数。
  • main 方法中,我们传入了一个 B 类型的对象 b,这是完全合法的,因为 BA 的子类型。

解释

  • A 类型的参数:方法 execute 的参数是接口 A 类型的。根据多态性,任何实现 A 的类的实例都可以作为参数传入。
  • 传递 B 类型的对象:因为 B 实现了 A,所以可以将 B 类型的对象传递给 A 类型的参数。

结论

因此,在接口编程中,参数类型可以是实现该接口的类的类型。这不仅合规而且是多态性的重要体现之一。

关于里氏替换原则

里氏替换原则(Liskov Substitution Principle, LSP) 是面向对象编程中的一个核心原则,它的存在确保了继承关系在使用中的一致性。接下来,我们来深入探讨一下这个原则。

里氏替换原则的定义

里氏替换原则(LSP)由著名的计算机科学家 Barbara Liskov 提出。其基本定义是:

“如果一个对象 B 是对象 A 的子类型,那么所有引用 A 的程序或函数,都能够透明地使用 B 的对象,而不需要知道 B 的存在。”

里氏替换原则的核心思想

LSP 的核心思想是:子类必须能够替换父类而不影响程序的正确性。在面向对象设计中,如果一个类 B 是类 A 的子类,那么在所有需要 A 的地方,都可以使用 B 而不会影响功能的正确性。这也是实现多态的一个基础。

里氏替换原则的重要性

  • 多态的基础:LSP 是面向对象编程中多态性的核心原则,它确保子类和父类的可替换性。多态允许使用父类或接口的引用指向具体的子类实例,这使得代码更加灵活和可扩展。
  • 增强代码的可维护性:如果违反了 LSP,代码可能会变得脆弱和不可预测。遵循 LSP 可以确保子类与父类之间的契约不被破坏,从而增强代码的可维护性和可扩展性。

LSP 的实现条件

要确保遵循 LSP,需要满足以下几个条件:

  1. 子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说,子类的行为应该和父类保持一致或扩展,而不是修改。
  2. 子类不能违背父类的约定。例如,如果父类规定某个方法不抛出异常,那么子类也不能在这个方法中抛出新的异常。
  3. 父类可以被替换为子类,而不会改变程序的行为或引发错误。

代码示例

// 定义一个基类 Bird
class Bird {
   
    public void fly() {
   
        System.out.println("Bird is flying.");
    }
}

// 定义一个子类 Sparrow
class Sparrow extends Bird {
   
    // 燕子可以飞,因此它继承了 fly 方法
}

// 定义一个子类 Ostrich (鸵鸟)
class Ostrich extends Bird {
   
    // 鸵鸟不会飞,所以我们重写 fly 方法
    @Override
    public void fly() {
   
        throw new UnsupportedOperationException("Ostriches can't fly.");
    }
}

public class Main {
   
    public static void main(String[] args) {
   
        Bird sparrow = new Sparrow();
        Bird ostrich = new Ostrich();

        sparrow.fly();  // 正常工作,输出 "Bird is flying."
        ostrich.fly();  // 会抛出 UnsupportedOperationException
    }
}

在上面的例子中,Ostrich 类违反了 LSP,因为 Ostrich 无法替换 Bird。在调用 fly 方法时,父类期望所有的 Bird 子类都可以飞行,但 Ostrich 破坏了这一假设。这种情况会导致程序行为不可预测,破坏了程序的稳定性。

设计原则

要遵循 LSP,我们在设计子类时要注意以下几点:

  • 子类应该实现父类期望的行为。不要在子类中引入父类所没有的例外或新的限制。
  • 不要随意重写或忽略父类的行为。子类重写方法时,必须确保新方法的功能扩展或细化了父类的功能,而不是修改或限制。
  • 确保替换后程序依然能按预期运行。这就需要在设计和编码时通过测试来验证。

为什么接口与实现类符合 LSP?

事实上,接口和实现类的关系,天然地符合 LSP,因为接口本身定义了一个契约,而实现类需要完全遵循这个契约。所以从设计上来看,实现类必须满足接口的所有要求,这正是 LSP 所强调的。

  1. 契约的定义:接口定义了一组方法,这些方法就是契约(Contract)。任何实现该接口的类,必须提供这些方法的实现。这就保证了在接口类型的引用下,所有的实现类都具有统一的行为(方法名、参数、返回值等)。

  2. 实现类的替代性:在代码中使用接口类型来引用对象(而不是使用具体实现类)是一种最佳实践。这种方式使得不同的实现类能够透明地替换,而不影响代码的逻辑。

  3. 多态性:在多态性中,父类可以是一个接口,而子类可以是实现该接口的任何一个类。通过这种方式,可以利用接口引用调用所有实现类的实例,从而实现子类型的替换。

代码示例

// 定义一个接口 A
interface A {
   
    void doSomething();
}

// 类 B 实现接口 A
class B implements A {
   
    @Override
    public void doSomething() {
   
        System.out.println("B is doing something.");
    }
}

// 类 C 也实现接口 A
class C implements A {
   
    @Override
    public void doSomething() {
   
        System.out.println("C is doing something.");
    }
}

public class Main {
   
    public static void main(String[] args) {
   
        // 使用接口类型的引用来指向实现类的实例
        A obj1 = new B();
        A obj2 = new C();

        // 由于 B 和 C 都实现了 A,符合 LSP,可以透明地进行替换
        execute(obj1);  // 输出 "B is doing something."
        execute(obj2);  // 输出 "C is doing something."
    }

    // 方法使用接口 A 作为参数
    public static void execute(A a) {
   
        a.doSomething();
    }
}

在这个例子中:

  • A 是一个接口,它定义了 doSomething 方法。
  • BC 都实现了接口 A,并提供了自己的 doSomething 方法的实现。
  • 方法 execute 接收一个 A 类型的参数,因此它可以接受任何实现了 A 的类的对象(例如 BC)。

由于接口 A 定义了一种统一的行为契约,并且所有实现类都必须遵循这个契约,因此,在任何使用 A 类型的地方,使用 BC 类型的对象都是符合 LSP 的。

接口与实现类如何符合 LSP?

  1. 接口是行为的抽象:接口定义了行为的规范,所有实现类必须实现接口中的所有方法。因此,不管实现类如何变化,它们的行为都符合接口的要求。
  2. 可替代性:所有实现类都可以透明地替代接口的引用,因此在使用接口类型的地方,可以无缝地替换为任何实现类的实例。
  3. 多态的基础:接口和实现类是多态性的基础,而 LSP 通过定义子类或实现类的行为一致性来确保多态的正确性。

总结

接口和实现类的关系是 LSP 的一个完美体现。因为接口定义了方法签名,所有实现该接口的类都必须提供实现。这种设计确保了多态的正确使用和行为的一致性。

hrvoje-grubisic-wWtOyQJYtG4-unsplash.jpg

所以,总结来说:

  • 接口与实现类的关系天然符合 LSP,因为接口定义了行为的规范,而实现类则是具体的实现。
  • 在使用接口引用来调用实现类实例时,我们实际上就是在遵循 LSP。
相关文章
|
6月前
|
Java
Java语言实现字母大小写转换的方法
Java提供了多种灵活的方法来处理字符串中的字母大小写转换。根据具体需求,可以选择适合的方法来实现。在大多数情况下,使用 String类或 Character类的方法已经足够。但是,在需要更复杂的逻辑或处理非常规字符集时,可以通过字符流或手动遍历字符串来实现更精细的控制。
420 18
|
6月前
|
Java Go 开发工具
【Java】(9)抽象类、接口、内部的运用与作用分析,枚举类型的使用
抽象类必须使用abstract修饰符来修饰,抽象方法也必须使用abstract修饰符来修饰,抽象方法不能有方法体。抽象类不能被实例化,无法使用new关键字来调用抽象类的构造器创建抽象类的实例。抽象类可以包含成员变量、方法(普通方法和抽象方法都可以)、构造器、初始化块、内部类(接 口、枚举)5种成分。抽象类的构造器不能用于创建实例,主要是用于被其子类调用。抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类abstract static不能同时修饰一个方法。
289 0
|
6月前
|
Java 编译器 Go
【Java】(5)方法的概念、方法的调用、方法重载、构造方法的创建
Java方法是语句的集合,它们在一起执行一个功能。方法是解决一类问题的步骤的有序组合方法包含于类或对象中方法在程序中被创建,在其他地方被引用方法的优点使程序变得更简短而清晰。有利于程序维护。可以提高程序开发的效率。提高了代码的重用性。方法的名字的第一个单词应以小写字母作为开头,后面的单词则用大写字母开头写,不使用连接符。例如:addPerson。这种就属于驼峰写法下划线可能出现在 JUnit 测试方法名称中用以分隔名称的逻辑组件。
288 4
|
6月前
|
存储 算法 安全
Java集合框架:理解类型多样性与限制
总之,在 Java 题材中正确地应对多样化与约束条件要求开发人员深入理解面向对象原则、范式编程思想以及JVM工作机理等核心知识点。通过精心设计与周密规划能够有效地利用 Java 高级特征打造出既健壮又灵活易维护系统软件产品。
170 7
|
6月前
|
编解码 Java 开发者
Java String类的关键方法总结
以上总结了Java `String` 类最常见和重要功能性方法。每种操作都对应着日常编程任务,并且理解每种操作如何影响及处理 `Strings` 对于任何使用 Java 的开发者来说都至关重要。
388 5
|
Oracle Java 关系型数据库
我的Java开发学习之旅------>解惑Java进行三目运算时的自动类型转换
今天看到两个面试题,居然都做错了。通过这两个面试题,也加深对三目运算是的自动类型转换的理解。 题目1.以下代码输出结果是()。 public class Test { public static void main(String[] args) { int a=5; System.
1157 0
|
6月前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
297 1
|
6月前
|
JSON 网络协议 安全
【Java基础】(1)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
317 1
|
7月前
|
数据采集 存储 弹性计算
高并发Java爬虫的瓶颈分析与动态线程优化方案
高并发Java爬虫的瓶颈分析与动态线程优化方案