vlambda博客
学习文章列表

读书笔记《the-complete-coding-interview-guide-in-java》第六章面向对象程序设计

Chapter 6: Object-Oriented Programming

本章涵盖了在 Java 面试中遇到的与 面向对象编程 (OOP) 相关的最流行的问题和问题。

请记住,我的目标不是教你 OOP,或者更笼统地说,本书的目的不是教你 Java。我的目标是教你如何在面试中回答问题和解决问题。在这种情况下,面试官想要一个清晰简洁的答案;您将没有时间进行论文和教程。你必须能够清晰而有说服力地表达你的想法。你的回答应该是有意义的,你必须让面试官相信你真的理解你在说什么,而且你不仅仅是在背诵一些毫无意义的定义。大多数时候,你应该能够用一个或几个关键段落来表达几页的文章或一本书的一章。

在本章结束时,您将知道如何回答 40 多个涵盖 OOP 基本方面的问题。作为基本方面,您必须详细了解它们。如果您不知道这些问题的正确和简洁的答案,没有任何借口。缺乏这方面的知识会严重影响你面试成功的机会。

所以,让我们总结一下我们的议程如下:

  • 面向对象的概念
  • 坚实的原则
  • GOF 设计模式
  • 编码挑战

让我们从与 OOP 概念相关的问题开始。

Technical requirements

你可以在 GitHub 上找到本章中的所有代码。请访问以下链接:https:// /github.com/PacktPublishing/The-Complete-Coding-Interview-Guide-in-Java/tree/master/Chapter06

Understanding OOP concepts

OOP 模型基于 几个概念。任何计划设计和编写依赖于对象的应用程序的开发人员都必须熟悉这些概念。因此,让我们从枚举它们开始,如下所示:

  • 目的
  • 班级
  • 抽象
  • 封装
  • 遗产
  • 多态性
  • 协会
  • 聚合
  • 作品

通常,当这些概念被包裹在问题中时,它们会以 What is ...? 为前缀,例如 What is an object? em>,或 什么是多态性?

重要的提示

这些问题的正确答案是技术知识和现实世界的类比或例子的结合。避免使用超级技术细节和没有示例的冷酷答案(例如,不要谈论对象的内部表示)。注意你在说什么,因为面试官可能会直接从你的回答中提取问题。如果您的回答顺便提到了一个概念,那么下一个问题可能会涉及该概念。换句话说,不要在你的答案中添加你不熟悉的任何方面。

那么,让我们在面试环境中回答与 OOP 概念相关的问题。请注意,我们应用了我们在 第 5 章中学到的知识,如何应对编码挑战。更准确地说,我们按照理解问题|提名关键词/关键点|包装一个答案技巧。首先,为了熟悉这项技术,我将提取关键点作为项目符号列表,并在答案中将它们用斜体表示。

What is an object?

您应该在您的答案中封装的关键点如下:

  • 对象是 OOP 的 核心概念之一。
  • 对象是现实世界的实体。
  • 对象具有状态(字段)和行为(方法)。
  • 一个对象代表一个类的一个实例。
  • 一个对象占用了一些内存空间。
  • 一个对象可以与其他对象通信。

现在,我们可以给出如下答案:

对象是OOP的核心概念之一。对象是现实世界的实体,例如汽车、 桌子或猫。在其生命周期中,对象具有状态和行为。例如,一只猫的状态可以是颜色、名字和品种,而它的行为可以是玩耍、吃东西、睡觉和喵喵叫。在 Java 中,对象是通常通过 new 关键字构建的类的实例,它的状态存储在字段中并通过方法。每个实例占用一些内存空间并可以与其他对象通信。例如,作为另一个对象的男孩可以抚摸一只猫并且它睡觉。

如果需要更多详细信息,那么您可能想谈谈对象可以具有不同的访问修饰符和可见性范围,可以是可变的、不可修改的或不可变的,并通过垃圾收集器收集的事实。

What is a class?

你应该在你的答案中包含的关键点是以下:

  • 类是 OOP 的核心概念之一。
  • 类是创建对象的模板或蓝图。
  • 一个类不消耗内存。
  • 一个类可以被实例化多次。
  • 一个班级做一件事,而且只有一件事。

现在,我们可以给出如下答案:

类是 OOP 的核心概念之一。 类是构建 特定类型所需的一组指令的对象。我们可以将类视为模板、蓝图或告诉我们如何创建该类的对象的配方。 创建该类的对象是一个称为实例化的过程,通常通过 new 关键字完成。 我们可以实例化任意数量的对象。类定义不消耗内存 保存为硬盘驱动器上的文件。一个类应该遵循的最佳实践之一是单一职责原则 (SRP)。在符合这一原则的同时,一个类应该被设计和编写为做一件事,而且只做一件事。

如果需要更多详细信息,那么您可能想谈谈类可以有不同的访问修饰符和可见性范围,支持不同类型的变量(本地、类和实例变量),并且可以声明为 abstractfinalprivate ,嵌套在另一个类(内部类)中,依此类推。

What is abstraction?

应包含在您的答案中的关键点如下:

  • 抽象是OOP的核心概念之一。
  • 抽象是仅向用户展示与他们相关的事物并隐藏其余细节的概念。
  • 抽象允许用户专注于应用程序做什么,而不是它是如何做的。
  • 抽象是通过抽象类和接口在 Java 中实现的。

现在,我们可以给出如下答案:

爱因斯坦声称一切都应该尽可能简单,而不是简单抽象是 OOP 的主要概念之一,它努力使用户的事情尽可能简单。换句话说,抽象只向用户展示与他们相关的事物并隐藏其余的细节。在 OOP 术语中,我们说一个对象应该只向其用户公开一组高级操作,而这些操作的内部实现是隐藏的。因此,抽象允许用户专注于应用程序的功能,而不是应用程序的功能。这样,抽象降低了暴露事物的复杂性,增加了代码的可重用性,避免了代码重复,并保持了低耦合和高内聚。此外,它通过仅公开重要细节来维护应用程序的安全性和自由裁量权。

让我们考虑一个现实生活中的 示例:一个驾驶汽车的人。这个人知道每个踏板的作用和方向盘的作用,但他不知道这些事情是如何在汽车内部完成的。他不知道赋予这些东西力量的内在机制。这就是抽象。 在Java中,抽象可以通过抽象类和接口来实现

如果需要更多详细信息,那么您可以共享屏幕或使用纸和笔并为您的示例编写代码。

所以,我们说一个人在开车。男子可以通过相应的踏板加速或减速汽车。他还可以借助方向盘左右转向。所有这些动作都集中在一个名为 Car 的界面中:

public interface Car {
    public void speedUp();
    public void slowDown();
    public void turnRight();
    public void turnLeft();
    public String getCarType();
}

接下来,每种类型的汽车都应实现 Car 接口并重写这些方法以提供这些操作的实现。这个实现对用户(驾驶汽车的人)是隐藏的。例如,ElectricCar 类如下所示(实际上,代替 System.out.println,我们有 复杂的业务逻辑):

public class ElectricCar implements Car {
    private final String carType;
    public ElectricCar(String carType) {
        this.carType = carType;
    }        
    @Override
    public void speedUp() {
        System.out.println("Speed up the electric car");
    }
    @Override
    public void slowDown() {
        System.out.println("Slow down the electric car");
    }
    @Override
    public void turnRight() {
        System.out.println("Turn right the electric car");
    }
    @Override
    public void turnLeft() {
        System.out.println("Turn left the electric car");
    }
    @Override
    public String getCarType() {
        return this.carType;
    }        
}

这个 类的用户可以在不知道 的情况下访问这些public 方法实施情况:

public class Main {
    public static void main(String[] args) {
        Car electricCar = new ElectricCar("BMW");
        System.out.println("Driving the electric car: " 
		  + electricCar.getCarType() + "\n");
        electricCar.speedUp();
        electricCar.turnLeft();
        electricCar.slowDown();
    }
}

输出如下:

Driving the electric car: BMW
Speed up the electric car
Turn left the electric car
Slow down the electric car

所以,这是一个通过接口进行抽象的例子。完整的应用程序被命名为Abstraction/AbstractionViaInterface。在本书捆绑的代码中,您可以找到通过抽象类实现的相同场景。 完整的应用程序被命名为Abstraction/AbstractionViaAbstractClass.

继续,让我们谈谈封装。

What is encapsulation?

您应该在答案中封装的关键点是以下

  • 封装是OOP的核心概念之一。
  • 封装是一种技术,其中对象状态对外部世界隐藏,并且公开了一组用于访问该状态的公共方法。
  • 当每个对象在类中保持其状态为私有时,就实现了封装。
  • 封装tion 被称为数据隐藏机制。
  • 封装具有许多与之相关的重要优势,例如松散耦合、可重用、安全和易于测试的代码。
  • 在 Java 中,封装是通过访问修饰符实现的——publicprivateprotected。

现在,我们可以给出如下答案:

封装是OOP的核心概念之一。主要是,封装将代码和数据绑定在一个工作单元(一个类)中,并充当不允许外部代码直接访问这些数据的防御屏障。主要是 是一种将对象状态向外界隐藏并暴露一组 public 访问此状态的方法。当每个对象在一个类中保持其状态private,我们可以说实现了封装。这就是为什么封装也被称为数据隐藏机制利用封装的代码是松耦合的(例如,我们可以在不破坏客户端代码)、可重用、安全(客户端不知道如何在类中操作数据)和易于测试(测试方法比测试字段更容易)。在 Java 中,可以通过访问修饰符 publicprivateprotected 来实现封装 。通常,当一个对象管理自己的状态时,它的状态通过 private 变量声明,并通过 public 访问和/或修改方法。让我们考虑一个例子:Cat 类的状态可以由 mood饿了能量。虽然 Cat 类的外部代码不能直接修改这些字段,但它可以调用 public 方法,例如 play()feed()sleep() code class="literal">Cat 内部状态。 Cat 类也可能有 private 方法,这些方法在类外无法访问,例如 meow()。这是封装。

如果需要更多详细信息,那么您可以共享屏幕或使用纸和笔并为您的示例编写代码。

因此,我们示例中的 Cat 类可以按照下面的代码块进行编码。请注意,此类的状态是通过 private 字段封装的,因此不能从类外部直接访问:

public class Cat {
    private int mood = 50;
    private int hungry = 50;
    private int energy = 50;
    public void sleep() {
        System.out.println("Sleep ...");
        energy++;
        hungry++;
    }
    public void play() {
        System.out.println("Play ...");
        mood++;
        energy--;
        meow();
    }
    public void feed() {
        System.out.println("Feed ...");
        hungry--;
        mood++;
        meow();
    }
    private void meow() {
        System.out.println("Meow!");
    }
    public int getMood() {
        return mood;
    }
    public int getHungry() {
        return hungry;
    }
    public int getEnergy() {
        return energy;
    }
}

修改状态的唯一方法是通过公共方法,play() feed()sleep(),如下例所示:

public static void main(String[] args) {
    Cat cat = new Cat();
    cat.feed();
    cat.play();
    cat.feed();
    cat.sleep();
    System.out.println("Energy: " + cat.getEnergy());
    System.out.println("Mood: " + cat.getMood());
    System.out.println("Hungry: " + cat.getHungry());
}

输出如下:

Feed ...Meow!Play ...Meow!Feed ...Meow!Sleep ...
Energy: 50
Mood: 53
Hungry: 49

完整的应用程序被命名为Encapsulation。现在,让我们有一个 继承的概要。

What is inheritance?

应包含在您的答案中的关键点是以下

  • 继承是OOP的核心概念之一。
  • 继承允许一个对象基于另一个对象。
  • 继承通过允许一个对象重用另一个对象的代码并添加自己的逻辑来维持代码的可重用性。
  • 继承称为 IS-A 关系,也称为 称为父子关系。
  • 在 Java 中,继承是通过 extends 关键字实现的。
  • 继承的对象被引用为超类,继承了超类的对象被引用为子类。
  • 在 Java 中,不能继承多个类。

现在,我们可以给出如下答案:

继承是OOP的核心概念之一。它允许一个对象基于另一个对象,这在不同对象非常相似并且共享一些共同逻辑但它们不相同时很有用。 继承通过允许一个对象在添加自己的逻辑的同时重用另一个对象的代码来维持代码的可重用性。所以,为了实现继承,我们在另一个类中重用通用逻辑并提取唯一逻辑。 这称为 IS-A 关系,也称为父子关系。这就像说 Foo IS-A Buzz 类型的东西。例如,猫 IS-A 猫科动物,以及训练 IS-A 车辆。 IS-A 关系是用于定义类层次结构的工作单元。 在 Java 中,继承是通过 extends 关键字通过从父级派生子级实现的< /em>。子级可以重用其父级的字段和方法,并添加自己的字段和方法。 被继承的对象被引用为超类,或者父类,继承了超类的对象被引用为子类,或者子类。在Java中,继承不能是多重的;因此,一个子类或子类不能继承多个超类或父类的字段和方法。例如,一个Employee类(父类)可以定义软件公司中任何员工的通用逻辑,而另一个类(子类),名为 Programmer,可以扩展 Employee 以使用此通用逻辑并添加特定于程序员的逻辑。其他类也可以扩展 ProgrammerEmployee 类。

如果需要更多详细信息,那么您可以共享屏幕或使用纸和笔并为您的示例编写代码。

Employee 类非常简单。它包装了员工的姓名:

public class Employee {
    private String name;
    public Employee(String name) {
        this.name = name;
    }
    // getters and setters omitted for brevity
}

然后,Programmer 类扩展了 Employee。与任何员工一样,程序员有一个名字,但他们也被分配到一个团队:

public class Programmer extends Employee {
    private String team;
    public Programmer(String name, String team) {
        super(name);
        this.team = team;
    }
    // getters and setters omitted for brevity
}

现在,让我们 通过创建一个 Programmer 并调用 getName() 来测试继承,继承自 Employee 类和 getTeam(),继承自 Programmer 类:

public static void main(String[] args) {
    Programmer p = new Programmer("Joana Nimar", "Toronto");
    String name = p.getName();
    String team = p.getTeam();
    System.out.println(name + " is assigned to the " 
          + team + " team");
}

输出如下:

Joana Nimar is assigned to the Toronto team

完整的 应用程序被命名为Inheritance。继续,让我们谈谈关于多态性。

What is polymorphism?

您应该在答案中包含以下要点:

  • 多态是 OOP 的核心概念之一。
  • 多态性在希腊语中的意思是多种形式
  • 多态性允许对象在某些情况下表现不同。
  • 多态性可以通过方法重载(称为编译时多态性)或在 IS-A 关系的情况下通过方法覆盖(称为 作为运行时多态性)来形成。

现在,我们可以给出如下答案:

多态是OOP的核心概念之一。多态是由两个希腊词组成的词:poly,意思是许多,和morph ,表示表单。因此,多态意味着多种形式

更准确地说,在 OOP 上下文中,多态性允许对象在某些情况下表现不同,或者换句话说,允许以不同的方式(方法)完成操作。 实现多态的一种方法是通过方法重载。这被称为编译时多态性,因为编译器可以在编译时识别出要调用的重载方法的形式(具有相同名称但参数不同的多个方法)。因此,根据调用哪种形式的重载方法,对象的行为会有所不同。例如,一个名为 Triangle 的类可以使用不同的参数定义多个名为 draw() 的方法。

另一种实现多态的方法是通过方法覆盖,这是我们有 IS-A 关系时常用的方法。它被称为运行时多态性,或动态方法分派。通常,我们从一个包含一堆方法的接口开始。接下来,每个类都实现这个接口并覆盖这些方法以提供 特定行为。这一次,多态性允许我们完全像其父类(接口)一样使用这些类中的任何一个,而不会混淆它们的类型。这是可能的,因为在运行时,Java 可以区分这些类并知道使用哪一个。例如,一个名为 Shape 的接口可以声明一个名为 draw() 的方法,而 TriangleRectangleCircle 类实现 Shape接口并重写 draw() 方法来绘制相应的形状。

如果需要更多详细信息,那么您可以共享屏幕或使用纸和笔并为您的示例编写代码。

Polymorphism via method overloading (compile time)

Triangle 类包含 三个draws() 方法,如下:

public class Triangle {
    public void draw() {
        System.out.println("Draw default triangle ...");
    }
    public void draw(String color) {
        System.out.println("Draw a triangle of color " 
            + color);
    }
    public void draw(int size, String color) {
        System.out.println("Draw a triangle of color " + color
           + " and scale it up with the new size of " + size);
    }
}

接下来,注意对应的draw()方法是如何被调用的:

public static void main(String[] args) {
    Triangle triangle = new Triangle();
    triangle.draw();
    triangle.draw("red");
    triangle.draw(10, "blue");
}

输出如下:

Draw default triangle ...
Draw a triangle of color red
Draw a triangle of color blue and scale it up 
with the new size of 10

完整的应用程序被命名为Polymorphism/CompileTime。继续,让我们看一个实现运行时多态的例子。

Polymorphism via method overriding (runtime)

这一次,draw()方法被声明在一个接口中,如下:

public interface Shape {
    public void draw();
}

TriangleRectangleCircle 类实现 Shape 接口并重写 draw() 方法来绘制 对应的形状:

public class Triangle implements Shape {
    @Override
    public void draw() {
        System.out.println("Draw a triangle ...");
    }
}
public class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("Draw a rectangle ...");
    }
}
public class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("Draw a circle ...");
    }
}

接下来,我们创建一个三角形、一个 矩形和一个圆形。对于这些实例中的每一个,让我们调用 draw() 方法:

public static void main(String[] args) {
    Shape triangle = new Triangle();
    Shape rectangle = new Rectangle();
    Shape circle = new Circle();
    triangle.draw();
    rectangle.draw();
    circle.draw();
}

输出显示,在运行时,Java 调用了正确的 draw() 方法:

Draw a triangle ...
Draw a rectangle ...
Draw a circle ...

完整的应用程序被命名为 Polymorphism/Runtime。继续,让我们谈谈关联。

重要的提示

有些人认为多态是 OOP 中最重要的概念。此外,有些声音认为运行时多态是唯一真正的多态,而编译时多态实际上并不是多态的一种形式。在采访中,不建议发起这样的辩论。最好充当调解人并呈现硬币的两面。我们将很快讨论如何处理这种情况。

What is association?

应包含在您的答案中的关键点如下:

  • 关联是OOP的核心概念之一。
  • 关联定义了两个相互独立的类之间的关系。
  • 协会没有所有者。
  • 关联可以是一对一、一对多、多对一和多对多。

现在,我们可以给出如下答案:

关联是OOP的核心概念之一。关联的目标是定义两个相互独立的类之间的关系,也被称为对象之间的多重性关系。 没有关联的所有者。关联中涉及的对象可以相互使用(双向关联),也可以只有一个使用另一个(单向关联),但它们有自己的生命周期。 关联可以是单向/双向、一对一、一对多、多对一和多对多。例如,在 PersonAddress 对象之间,我们可能有一个双向的多对多关系。换句话说,一个人可以与多个地址相关联,而一个地址可以属于多个人。然而,人们可以没有地址而存在,反之亦然。

如果需要更多详细信息,那么您可以共享屏幕或使用纸和笔并为您的示例编写代码。

PersonAddress 类非常简单:

public class Person {
    private String name;
    public Person(String name) {
        this.name = name;
    }
    // getters and setters omitted for brevity
}
public class Address {
    private String city;
    private String zip;
    public Address(String city, String zip) {
        this.city = city;
        this.zip = zip;
    }
    // getters and setters omitted for brevity
}

PersonAddress 之间的关联main() 方法,如 如下代码块所示:

public static void main(String[] args) {
    Person p1 = new Person("Andrei");
    Person p2 = new Person("Marin");
    Address a1 = new Address("Banesti", "107050");
    Address a2 = new Address("Bucuresti", "229344");
    // Association between classes in the main method 
    System.out.println(p1.getName() + " lives at address "
            + a2.getCity() + ", " + a2.getZip()
            + " but it also has an address at "
            + a1.getCity() + ", " + a1.getZip());
    System.out.println(p2.getName() + " lives at address "
            + a1.getCity() + ", " + a1.getZip()
            + " but it also has an address at "
            + a2.getCity() + ", " + a2.getZip());
}

输出是 列出如下:

Andrei lives at address Bucuresti, 229344 but it also has an address at Banesti, 107050
Marin lives at address Banesti, 107050 but it also has an address at Bucuresti, 229344

完整的 应用程序被命名为Association。继续,让我们谈谈聚合。

What is aggregation?

您应该在您的答案中封装的关键点如下:

  • 聚合是 OOP 的核心概念之一。
  • 聚合是单向关联的一种特殊情况。
  • 聚合表示 HAS-A 关系。
  • 两个聚合对象有自己的生命周期,但其中一个对象是 HAS-A 关系的所有者。

现在,我们可以给出如下答案:

聚合是OOP的核心概念之一。主要是,聚合是单向关联的一种特殊情况。虽然关联定义了两个相互独立的类之间的关系,但聚合表示这两个类之间的 HAS-A 关系。换句话说,两个聚合对象都有自己的生命周期,但其中一个对象是HAS-A关系的所有者。拥有自己的生命周期意味着结束一个对象不会影响另一个对象。对于 示例,TennisPlayer 具有 Racket。这是一个 单向关联,因为Racket 不能有TennisPlayer。即使 TennisPlayer 死亡,Racket 也不受影响。

重要的提示

请注意,当我们定义聚合的概念时,我们也有关于什么是关联的声明。每当两个概念紧密相关并且其中一个是另一个的特例时,请遵循此方法。接下来应用相同的做法将组合定义为聚合的特殊情况。面试官会注意到并欣赏您对事物的概述,并且您可以提供一个没有忽略上下文的有意义的答案。

如果需要更多详细信息,那么您可以共享屏幕或使用纸和笔并为您的示例编写代码。

我们从 Rocket 类开始。这是网球拍的简单表示:

public class Racket {
    private String type;
    private int size;
    private int weight;
    public Racket(String type, int size, int weight) {
        this.type = type;
        this.size = size;
        this.weight = weight;
    }
    // getters and setters omitted for brevity
}

TennisPlayer HAS-A Racket。因此,TennisPlayer 必须能够 接收 Racket 如下:

public class TennisPlayer {
    private String name;
    private Racket racket;
    public TennisPlayer(String name, Racket racket) {
        this.name = name;
        this.racket = racket;
    }
    // getters and setters omitted for brevity
}

接下来,我们创建一个 Racket 和一个使用此 RacketTennisPlayer

public static void main(String[] args) {
    Racket racket = new Racket("Babolat Pure Aero", 100, 300);
    TennisPlayer player = new TennisPlayer("Rafael Nadal", 
        racket);
    System.out.println("Player " + player.getName() 
        + " plays with " + player.getRacket().getType());
}

输出如下:

Player Rafael Nadal plays with Babolat Pure Aero

完整的应用程序被命名为Aggregation。继续,让我们谈谈组成。

What is composition?

应包含在您的答案中的关键点如下:

  • 组合是OOP 的核心概念之一
  • 组合是聚合的限制性更强的情况。
  • 组合表示包含不能单独存在的对象的 HAS-A 关系。
  • 组合支持代码重用和对象的可见性控制。

现在,我们可以给出如下答案:

组合是OOP的核心概念之一首先,组合是一种更具限制性的聚合情况。聚合表示两个具有自己生命周期的对象之间的 HAS-A 关系,composition 表示包含不能单独存在的对象的 HAS-A 关系。为了突出这种耦合,HAS-A 关系也可以命名为 PART-OF。例如,Car 有一个 Engine。换句话说,发动机是汽车的一部分。如果汽车被摧毁,那么引擎也会被摧毁。据说组合优于继承,因为它支持代码重用和对象的可见性控制

如果需要更多详细信息,那么您可以共享屏幕或使用纸和笔并编写示例代码。

Engine非常简单:

public class Engine {
    private String type;
    private int horsepower;
    public Engine(String type, int horsepower) {
        this.type = type;
        this.horsepower = horsepower;
    }
    // getters and setters omitted for brevity
}

接下来,我们有 Car 类。查看这个类的构造函数。由于 EngineCar 的一部分,我们使用 Car 创建它:

public class Car {
    private final String name;
    private final Engine engine;
    public Car(String name) {
        this.name = name;
        Engine engine = new Engine("petrol", 300);
        this.engine=engine;
    }
    public int getHorsepower() {
        return engine.getHorsepower();
    }
    public String getName() {
        return name;
   }    
}

接下来,我们可以从 main() 方法中测试组合,如下所示:

public static void main(String[] args) {
    Car car = new Car("MyCar");
    System.out.println("Horsepower: " + car.getHorsepower());
}

输出如下:

Horsepower: 300

完整的应用程序被命名为Composition .

到目前为止,我们已经讨论了有关 OOP 概念的基本问题。请记住,此类问题可能出现在几乎所有涉及编码或架构应用程序的职位的 Java 技术面试中。尤其是如果你有 2-4 年左右的 经验,你被问到前面的问题的机会很大,你必须知道答案,否则这将是一个黑标记你。

现在,让我们继续 SOLID 原则。这是另一个基本领域,也是 OOP 概念之外的必知主题。当涉及到有关面试的最终决定时,缺乏这方面的知识将被证明是有害的。

Getting to know the SOLID principles

在本节中,我们将制定 对应于编写类的五种著名设计模式——SOLID 原则的问题的答案。顺便说一句,SOLID 是以下内容的首字母缩写词:

  • S:单一职责原则
  • O:开闭原则
  • L:里氏替换原则
  • I:接口隔离原则
  • D:依赖倒置原则

在采访中,与 SOLID 有关的最常见问题是 What is ...? 类型。例如,什么是 S?什么是 D? 通常,与 OOP 相关的问题会故意含糊不清。这样,面试官会测试你的知识水平,并想看看你是否要求进一步澄清。所以,让我们依次解决这些问题,并提供一个很棒的答案,让面试官印象深刻。

What is S?

应包含在您的答案中的关键点如下:

  • S 代表 单一责任原则 (SRP)。
  • S 代表一个类应该有一个,并且只有一个,责任
  • S 告诉我们只为一个目标编写一个类。
  • S 在应用程序模块中维持高可维护性和可见性控制。

现在,我们可以给出 的答案,如下所示:

首先,SOLID 是 Robert C. Martin 阐述的前五个 Object-Oriented Design (OOD) 原则的首字母缩写词,也称为鲍勃叔叔(可选短语)。 S 是 SOLID 的第一条原则,被称为 单一责任原则 (SRP< /强>)。 这个原则转化为一个类应该有一个,并且只有一个,责任。这是一个非常重要的原则,在任何类型的项目中,任何类型的类(模型、服务、控制器、管理器类等)都应该遵循这一原则。 只要我们只为一个目标编写一个类,我们就会在应用程序模块之间保持高度的可维护性和可见性控制。换句话说,通过保持高可维护性,这一原则具有重大的业务影响,并且通过提供跨应用程序模块的可见性控制 ,这个原则支持封装。

如果需要更多详细信息,那么您可以共享屏幕或使用纸和笔来编写此处介绍的示例。

例如,您要计算矩形的面积。矩形的尺寸最初以米为单位,面积也以米为单位计算,但我们希望能够将计算的面积转换为其他单位,例如英寸。让我们看看打破 SRP 的方法。

打破 SRP

在单个类RectangleAreaCalculator中实现前面的问题,可以按如下方式完成。但是这个类不仅仅做一件事:它破坏了 SRP。请记住,通常,当您使用 and 来表达类的功能时,这表明 SRP 已损坏。例如,以下类计算面积并将其转换为英寸:

public class RectangleAreaCalculator {
    private static final double INCH_TERM = 0.0254d;
    private final int width;
    private final int height;
    public RectangleAreaCalculator(int width, int height) {
        this.width = width;
        this.height = height;
    }
    public int area() {
        return width * height;
    }
    // this method breaks SRP
    public double metersToInches(int area) {
        return area / INCH_TERM;
    }    
}

由于此代码 违反了 SRP,我们必须修复它以遵循 SRP。

Following the SRP

可以通过从 RectangleAreaCalculator 中删除 metersToInches() 方法 解决这种情况, 如下:

public class RectangleAreaCalculator {
    private final int width;
    private final int height;
    public RectangleAreaCalculator(int width, int height) {
        this.width = width;
        this.height = height;
    }
    public int area() {
        return width * height;
    }       
}

现在,RectangleAreaCalculator 只做一件事(它计算矩形区域),从而观察 SRP。

接下来,metersToInches() 可以 提取到单独的类中。此外,我们还可以添加一种将米转换为英尺的新方法:

public class AreaConverter {
    private static final double INCH_TERM = 0.0254d;
    private static final double FEET_TERM = 0.3048d;
    public double metersToInches(int area) {
        return area / INCH_TERM;
    }
    public double metersToFeet(int area) {
        return area / FEET_TERM;
    }
}

这个类也遵循 SRP,因此我们的 工作已经完成。完整的应用程序被命名为 SingleResponsabilityPrinciple。继续,让我们谈谈第二个 SOLID 原则,即开闭原则。

什么是O?

您应该 封装在您的答案中的关键点如下:

  • O 代表 开放封闭原则 (OCP)。
  • O 代表软件组件应该对扩展开放,对修改关闭
  • O 坚持这样一个事实,即我们的类不应该包含要求其他开发人员修改我们的类以完成他们的工作的约束——其他开发人员应该只扩展我们的类来完成他们的工作。
  • O 以一种通用的、直观的和无害的方式维持软件的可扩展性。

现在,我们可以给出如下答案:

首先,SOLID 是 Robert C. Martin 阐述的前五个 Object-Oriented Design (OOD) 原则的首字母缩写词,也称为鲍勃叔叔(可选短语)。 O 是来自 SOLID 的第二个原则,被称为 Open Closed Principle (OCP)。这个原则代表软件组件应该对扩展开放,对修改关闭。这意味着我们的类的设计和编写方式应该使其他开发人员可以通过简单地扩展它们来改变这些类的行为。因此,我们的类不应该包含要求其他开发人员修改我们的类以完成他们的工作的约束——其他开发人员应该只扩展我们的类来完成他们的工作

虽然我们必须以通用、直观且无害的方式维持软件可扩展性,但我们不必认为其他开发人员会想要更改整个逻辑或核心我们类的逻辑。首先,如果我们遵循这个原则,那么我们的代码将充当一个很好的 框架,它不允许我们修改它们的核心逻辑,但我们可以修改它们的流程和/或 扩展某些类、传递初始化参数、覆盖方法、传递不同选项等的行为。

如果需要更多详细信息,那么您可以共享屏幕或使用纸和笔来编写一个示例,例如此处提供的示例。

现在,例如,您有不同的形状(例如,矩形、圆形),我们想要对它们的面积求和。首先,让我们看看打破 OCP 的实现。

Breaking the OCP

每个形状 都将实现 Shape 接口。因此,代码非常简单:

public interface Shape {    
}
public class Rectangle implements Shape {
    private final int width;
    private final int height;
    // constructor and getters omitted for brevity
}
public class Circle implements Shape {
    private final int radius;
    // constructor and getter omitted for brevity
}

此时,我们可以轻松使用这些类的构造函数来创建不同大小的矩形和圆形。一旦我们有几个形状,我们想要总结它们的面积。为此,我们可以定义一个 AreaCalculator 类,如下所示:

public class AreaCalculator {
    private final List<Shape> shapes;
    public AreaCalculator(List<Shape> shapes) {
        this.shapes = shapes;
    }
    // adding more shapes requires us to modify this class
    // this code is not OCP compliant
    public double sum() {
        int sum = 0;
        for (Shape shape : shapes) {
            if (shape.getClass().equals(Circle.class)) {
                sum += Math.PI * Math.pow(((Circle) shape)
                    .getRadius(), 2);
            } else 
            if(shape.getClass().equals(Rectangle.class)) {
                sum += ((Rectangle) shape).getHeight() 
                    * ((Rectangle) shape).getWidth();
            }
        }
        return sum;
    }
}

由于每个形状都有自己的面积公式,我们需要一个if-else(或switch)结构来确定 类型的形状。此外,如果我们想添加一个新的形状(例如,一个三角形),我们必须修改 AreaCalculator 类来添加一个新的 if 案例。这意味着前面的代码破坏了 OCP。修复此代码以观察 OCP 会在所有类中进行多项修改。因此,请注意,修复不遵循 OCP 的代码可能非常棘手,即使是在一个简单示例的情况下也是如此。

Following the OCP

主要思路是AreaCalculator中提取出对应Shape中每个形状的面积公式 类。因此,矩形将计算其面积、圆以及等等。为了强制每个形状必须计算其面积这一事实,我们将 area() 方法添加到 Shape 合约中:

public interface Shape { 
    public double area();
}

接下来,RectangleCircle 实现 Shape 如下:

public class Rectangle implements Shape {
    private final int width;
    private final int height;
    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }
    public double area() {
        return width * height;
    }
}
public class Circle implements Shape {
    private final int radius;
    public Circle(int radius) {
        this.radius = radius;
    }
    @Override
    public double area() {
        return Math.PI * Math.pow(radius, 2);
    }
}

现在,AreaCalculator 可以循环 形状列表,并通过调用正确的 对区域求和area() 方法:

public class AreaCalculator {
    private final List<Shape> shapes;
    public AreaCalculator(List<Shape> shapes) {
        this.shapes = shapes;
    }
    public double sum() {
        int sum = 0;
        for (Shape shape : shapes) {
            sum += shape.area();
        }
        return sum;
    }
}

该代码符合 OCP。我们可以添加一个新的形状,不需要修改AreaCalculator。因此,AreaCalculator 对修改关闭,当然,对扩展开放。完整的应用程序被命名为 OpenClosedPrinciple。继续,我们来谈谈第三个SOLID原则,Liskov的替换原则。

What is L?

应包含在中的关键点如下:

  • L 代表 Liskov's Substitution Principle (LSP)。
  • L 代表派生类型必须完全可以替代它们的基类型
  • L 支持这样一个事实,即子类的对象必须以与超类的对象相同的方式表现。
  • L 对于运行时类型识别非常有用,然后是强制转换。

现在,我们可以给出如下答案:

首先,SOLID 是 Robert C. Martin 阐述的前五个 Object-Oriented Design (OOD) 原则的首字母缩写词,也称为鲍勃叔叔(可选短语)。 L 是 SOLID 的第三个原则,被称为 Liskov 的替换原则 (LSP)。这个原则代表派生类型必须完全可以替代它们的基类型。这意味着扩展我们的类的类应该可以在整个应用程序中使用而不会导致失败。更准确地说,这个原则支持子类的对象必须以与超类的对象相同的方式表现这一事实,因此每个子类(或派生类)都应该能够替换它们的超类没有任何问题。大多数情况下,这对于运行时类型识别以及紧随其后的强制转换很有用。例如,考虑 foo(p),其中 p 的类型为 T 。然后,如果 qS 类型,则 foo(q) 应该可以正常工作code> 和 ST 的子类型。

如果需要更多详细信息,那么您可以共享屏幕或使用纸和笔来编写一个示例,例如此处提供的示例。

我们有一个国际象棋俱乐部,接受三种类型的会员:高级会员、VIP 会员和免费会员。我们有一个名为 Member 的抽象类作为基类,以及三个子类 - PremiumMemberVipMemberFreeMember。让我们看看这些成员类型中的每一个是否都可以替代基类。

Breaking the LSP

Member 类是抽象的,它 代表我们国际象棋俱乐部所有成员的基类:

public abstract class Member {
    private final String name;
    public Member(String name) {
        this.name = name;
    }
    public abstract void joinTournament();
    public abstract void organizeTournament();
}

PremiumMember 类也可以加入国际象棋锦标赛或组织此类锦标赛。因此,它的实现非常简单:

public class PremiumMember extends Member {
    public PremiumMember(String name) {
        super(name);
    }
    @Override
    public void joinTournament() {
        System.out.println("Premium member joins tournament");         
    }
    @Override
    public void organizeTournament() {
        System.out.println("Premium member organize 
            tournament");        
     }
}

VipMember 类与 PremiumMember 类大致相同,所以我们可以跳过它,专注于 FreeMember 类。 FreeMember 类可以加入锦标赛,但不能组织锦标赛。这是我们需要在 organizeTournament() 方法中解决的问题。我们可以用有意义的 消息抛出异常,或者我们可以显示如下消息:

public class FreeMember extends Member {
    public FreeMember(String name) {
        super(name);
    }
    @Override
    public void joinTournament() {
        System.out.println("Classic member joins tournament 
            ...");
            
    }
    // this method breaks Liskov's Substitution Principle
    @Override
    public void organizeTournament() {
        System.out.println("A free member cannot organize 
            tournaments");
            
    }
}

但是抛出异常或者显示消息并不代表我们遵循LSP。由于自由会员不能组织比赛,因此不能替代基类,因此它违反了 LSP。查看以下成员列表:

List<Member> members = List.of(
    new PremiumMember("Jack Hores"),
    new VipMember("Tom Johns"),
    new FreeMember("Martin Vilop")
); 

以下循环表明我们的代码不符合 LSP,因为当 FreeMember 类必须替换 Member 类时,它不能完成它的工作,因为 FreeMember 无法组织国际象棋锦标赛:

for (Member member : members) {
    member.organizeTournament();
}

这种情况很引人注目。我们无法继续执行我们的应用程序。我们必须重新设计我们的 解决方案以获得符合 LSP 的代码。所以让我们这样做吧!

Following the LSP

重构过程首先定义两个接口,用于分离两个动作,加入和组织国际象棋锦标赛:

public interface TournamentJoiner {
    public void joinTournament();
}
public interface TournamentOrganizer {
    public void organizeTournament();
}

接下来,抽象基类实现这两个接口如下:

public abstract class Member 
    implements TournamentJoiner, TournamentOrganizer {
    private final String name;
    public Member(String name) {
        this.name = name;
    }  
}

PremiumMemberVipMember 保持不变。它们扩展了 Member 基类。但是,不能组织锦标赛的 FreeMember 类不会扩展 Member 基类。它将 仅实现 TournamentJoiner 接口:

public class FreeMember implements TournamentJoiner {
    private final String name;
    public FreeMember(String name) {
        this.name = name;
    }
    @Override
    public void joinTournament() {
        System.out.println("Free member joins tournament ...");
    }
}

现在,我们可以定义一个可以参加国际象棋锦标赛的成员列表,如下所示:

List<TournamentJoiner> members = List.of(
    new PremiumMember("Jack Hores"),
    new PremiumMember("Tom Johns"),
    new FreeMember("Martin Vilop")
);

循环此列表并用每种类型的成员替换 TournamentJoiner 接口按预期工作并观察 LSP:

// this code respects LSP
for (TournamentJoiner member : members) {
    member.joinTournament();
}   

按照同样的逻辑,可以组织国际象棋锦标赛的成员列表可以写成如下:

List<TournamentOrganizer> members = List.of(
    new PremiumMember("Jack Hores"),
    new VipMember("Tom Johns")
);

FreeMember 没有实现 TournamentOrganizer 接口。因此,不能将其添加到此列表中。循环 此列表并用每种类型的成员替换 TournamentOrganizer 接口按预期工作并遵循 LSP:

// this code respects LSP
for (TournamentOrganizer member : members) {
    member.organizeTournament();
}

完毕!现在我们有了一个符合 LSP 的代码。完整的应用程序被命名为 LiskovSubstitutionPrinciple。继续,让我们谈谈第四个 SOLID 原则,接口隔离原则。

What is I?

你应该封装在你的答案的关键点如下:

  • I 代表 Interface Segregation Principle (ISP)。
  • 我代表不应强迫客户实施他们不会使用的不必要的方法
  • 我将一个接口拆分为两个或多个接口,直到客户端不强制执行他们不会使用的方法。

现在,我们可以给出如下答案:

首先,SOLID 是 Robert C. Martin 阐述的前五个 Object-Oriented Design (OOD) 原则的首字母缩写词,也称为鲍勃叔叔(可选短语)。这是 SOLID 的第四条原则,被称为 接口隔离原则 (ISP )。这个原则代表不应强迫客户实现他们不会使用的不必要的方法。换句话说,我们应该将一个接口拆分为两个或多个接口,直到客户端不会被迫实现他们不会使用的方法。例如,考虑 Connection 接口,它有三个方法:connect()socket ()http()。客户端可能只想为通过 HTTP 的连接实现此接口。因此,它们不需要 socket() 方法。大多数时候,客户端会将此方法留空,这是一个糟糕的设计。为了避免这种情况,只需将Connection接口拆分成两个接口即可; SocketConnection 使用 socket() 方法,HttpConnection 使用 http() 方法。这两个接口都将扩展 Connection 接口,该接口保留了通用方法 connect()

如果需要更多详细信息,那么您可以共享屏幕或使用纸和笔来编写一个示例,例如此处提供的示例。既然我们已经描述了前面的例子,让我们跳到关于破坏 ISP 的部分。

Breaking the ISP

Connection 接口 定义了如下三个方法:

public interface Connection {
    public void socket();
    public void http();
    public void connect();
}

WwwPingConnection 是一个通过 HTTP ping 不同网站的类;因此,它需要 http() 方法,但不需要 socket() 方法。注意虚拟的 socket() 实现——因为 WwwPingConnection 实现了 Connection,它也被迫socket() 方法提供一个实现:

public class WwwPingConnection implements Connection {
    private final String www;
    public WwwPingConnection(String www) {
        this.www = www;
    }
    @Override
    public void http() {
        System.out.println("Setup an HTTP connection to " 
            + www);
    }
    @Override
    public void connect() {
        System.out.println("Connect to " + www);
    }
    // this method breaks Interface Segregation Principle
    @Override
    public void socket() {
    }
}

有一个空的 实现或从不需要的方法中抛出一个有意义的异常,例如 socket(),真的很丑陋解决方案。检查以下代码:

WwwPingConnection www 
    = new WwwPingConnection 'www.yahoo.com');
www.socket(); // we can call this method!
www.connect();

我们期望从这段代码中获得什么?什么都不做的工作代码,或者由于没有 HTTP 端点而由 connect() 方法引起的异常?或者,我们可以从 socket() 类型中抛出异常:Socket is not supported!。那么,为什么会在这里?!因此,现在是重构代码以遵循 ISP 的时候了。

Following the ISP

为了符合ISP,我们需要隔离Connection接口。由于任何客户端都需要 connect() 方法,因此我们将其保留在此接口中:

public interface Connection {
    public void connect();
}

http()socket() 方法分布在扩展 的单独接口中连接界面如下:

public interface HttpConnection extends Connection {
    public void http();
}
public interface SocketConnection extends Connection {
    public void socket();
}

这次 WwwPingConnection 类只能实现 HttpConnection 接口 并使用http() 方法:

public class WwwPingConnection implements HttpConnection {
    private final String www;
    public WwwPingConnection(String www) {
        this.www = www;
    }
    @Override
    public void http() {
        System.out.println("Setup an HTTP connection to "
            + www);
    }
    @Override
    public void connect() {
        System.out.println("Connect to " + www);
    } 
}

完毕!现在,代码遵循 ISP。 完整的应用程序被命名为InterfaceSegregationPrinciple。继续,让我们谈谈最后一个 SOLID 原则,即依赖倒置原则。

What is D?

应包含在中的关键点如下:

  • D 代表依赖倒置原则 (DIP)。
  • D 代表依赖于抽象,而不是具体化
  • D 支持使用抽象层将具体模块绑定在一起,而不是让具体模块依赖于其他具体模块。
  • D 支持具体模块的解耦。

现在,我们可以给出如下答案:

首先,SOLID 是 Robert C. Martin 阐述的前五个 Object-Oriented Design (OOD) 原则的首字母缩写词,也称为鲍勃叔叔(可选短语)。 D 是 SOLID 的最后一个原则,被称为 依赖倒置原则 (DIP)。这个原则代表Depend on abstractions,not on concretions。这意味着我们应该依靠抽象层将具体模块绑定在一起,而不是让具体模块依赖于其他具体模块。为了实现这一点,所有具体模块都应该只公开抽象。这样,具体模块允许在另一个具体模块中扩展功能或插件,同时保持具体模块的解耦。通常,高层混凝土模块和低层混凝土模块之间会发生高度耦合。

如果需要更多详细信息,您可以共享屏幕或使用纸和笔编写示例代码。

一个数据库 JDBC URL,PostgreSQLJdbcUrl,可以是一个低级模块,而一个连接到数据库的类可能代表一个高级模块,例如 ConnectToDatabase#connect()

Breaking the DIP

如果我们将 PostgreSQLJdbcUrl 参数传递给 connect() 方法类型,那么我们已经违反了 DIP。我们看一下PostgreSQLJdbcUrlConnectToDatabase的代码:

public class PostgreSQLJdbcUrl {
    private final String dbName;
    public PostgreSQLJdbcUrl(String dbName) {
        this.dbName = dbName;
    }
    public String get() {
        return "jdbc:// ... " + this.dbName;
    }
}
public class ConnectToDatabase {
    public void connect(PostgreSQLJdbcUrl postgresql) {
        System.out.println("Connecting to "
            + postgresql.get());
    }
}

如果我们创建另一种类型的 JDBC URL(例如,MySQLJdbcUrl),那么我们不能使用前面的 connect(PostgreSQLJdbcUrl postgreSQL)方法。因此,我们必须放弃对 具体的这种依赖,并创建对抽象的依赖。

Following the DIP

抽象可以 由每种类型的JDBC URL 应实现的接口表示:

public interface JdbcUrl {
    public String get();
}

接下来,PostgreSQLJdbcUrl 实现 JdbcUrl 以返回特定于 PostgreSQL 数据库的 JDBC URL:

public class PostgreSQLJdbcUrl implements JdbcUrl {
    private final String dbName;
    public PostgreSQLJdbcUrl(String dbName) {
        this.dbName = dbName;
    }
    @Override
    public String get() {
        return "jdbc:// ... " + this.dbName;
    }
}

以完全相同的方式,我们可以编写 MySQLJdbcUrlOracleJdbcUrl 等。最后,ConnectToDatabase#connect()方法依赖于JdbcUrl抽象,所以可以 连接到任何实现此抽象的 JDBC URL:

public class ConnectToDatabase {
    public void connect(JdbcUrl jdbcUrl) {
        System.out.println("Connecting to " + jdbcUrl.get());
    }
}

完毕!完整的应用程序被命名为DependencyInversionPrinciple

到目前为止,我们已经介绍了 OOP 的基本概念和流行的 SOLID 原则。如果您打算申请一个包含应用程序设计和架构的 Java 职位,那么建议您查看一般职责分配软件原则 (GRASP) 以及 (https://en. wikipedia.org/wiki/GRASP_(object-oriented_design)。这不是面试中的热门话题,但你永远不会知道!

接下来,我们将扫描一堆结合这些概念的流行问题。现在你已经熟悉了理解问题| 提名关键点 |答案技巧,我只会突出答案中的关键点,而不会事先将它们提取为列表。

Popular questions pertaining to OOP, SOLID, and GOF design patterns

在本节中,我们将解决一些需要真正理解 OOP 概念、SOLID 设计原则和 Gang of Four (GOF ) 设计模式。请注意,本书不涉及 GOF 设计模式,但有很多很棒的书籍和视频 专门针对这个主题。我建议您尝试 Learn Design Patterns with Java,作者 Aseem Jain (https://www.packtpub.com/application-development/learn-design-patterns-java-video)。

What is method overriding in OOP (Java)?

方法覆盖是一种面向对象的编程技术,允许开发人员编写两种方法(非静态、非私有和非最终< /strong>) 具有相同的名称和签名,但行为不同。 方法覆盖可以在存在继承或运行时多态的情况下使用。

在存在继承的情况下,我们在超类中有一个 方法(称为重写方法),我们在子类中重写它(称为重写方法)。在运行时多态中,我们在接口中有一个方法,实现该接口的类将覆盖该方法。

Java 在运行时决定应该调用的实际方法,具体取决于对象的类型。方法覆盖支持灵活和可扩展的代码,或者换句话说,它支持以最少的代码更改添加新功能

如果需要更多详细信息,那么您可以列出管理方法覆盖的主要规则:

  • 方法的名称和签名(包括相同的返回类型或子类型)在超类和子类中,或者在接口和实现中是相同的。
  • 我们不能在同一个类中重载方法(但我们可以在同一个类中重载它)。
  • 我们不能覆盖 privatestaticfinal 方法。
  • 覆盖方法不能降低被覆盖方法的可访问性,但反之亦然。
  • 重写方法不能抛出异常层次结构中高于 被重写方法抛出的已检查异常的已检查异常。
  • 始终将 @Override 注释用于覆盖方法。

Java 中重写方法的一个例子是在捆绑到本书的代码中,名称为MethodOverriding

What is method overloading in OOP (Java)?

方法重载是一种面向对象的编程技术,它允许开发人员编写两个名称相同但签名和功能不同的方法(静态或非静态)。通过不同的签名,我们可以理解不同的参数数量、不同类型的参数和/或不同顺序的参数列表. 返回类型不是方法签名的一部分。因此,两个方法具有相同签名但返回类型不同的情况不是方法重载的有效情况。因此,这是一种强大的技术,它允许我们编写具有相同名称但具有不同输入的方法(静态或非静态)。 编译器将重载方法调用绑定到实际方法;因此,在运行时不进行绑定。方法重载的一个著名例子是 System.out.println()println() 方法有几种重载风格。

因此,有四个主要规则来管理方法重载:

  • 重载是通过更改方法签名来完成的。
  • 返回类型不是方法签名的一部分。
  • 我们可以重载 privatestaticfinal 方法。
  • 我们可以在同一个类中重载一个方法(但我们不能在同一个类中重载它)。

如果需要更多详细信息,您可以尝试编写示例代码。 Java 中方法重载的示例可在本书中捆绑的 MethodOverloading 代码中找到。

重要的提示

除了上述两个问题,您可能还需要回答一些其他相关问题,包括哪些规则管理方法重载和覆盖(见上文)?、方法重载和覆盖之间的主要区别是什么(见上文)?,我们可以覆盖静态或私有方法(简短的回答是 ,见上文)?,我们可以重写最终方法(简短的回答是 ,见上)?,我们可以重载静态方法(简短的回答是,见上)?,< em class="italic">我们可以更改覆盖方法的参数列表(简短的回答是,见上文)?因此,建议提取和准备这些问题的答案。所有需要的信息都可以在前面的部分中找到。另外,要注意是否只能通过final修饰符来防止覆盖方法之类的问题?这种措辞是为了混淆候选人,因为答案需要对所涉及的概念进行概述。这里的答案可以表述为 这不是真的,因为我们也可以通过将方法标记为私有或静态来防止覆盖方法。此类方法不能被覆盖

接下来,让我们研究几个与覆盖和重载方法相关的其他问题。

What is covariant method overriding in Java?

协变方法覆盖是 Java 5 中引入的一个鲜为人知的特性。通过这个特性,一个覆盖方法可以返回其实际返回类型的子类型。这意味着覆盖方法不需要 返回类型的显式类型转换。例如,Java clone() 方法返回 Object。这意味着,当我们重写此方法以返回一个克隆时,我们会返回一个 Object,它必须显式转换为 Object 的实际子类 我们需要的。但是,如果我们利用 Java 5 协变方法覆盖特性,那么覆盖 clone() 方法可以直接返回必需的子类,而不是 对象

几乎总是,像这样的问题需要一个示例作为答案的一部分,所以让我们考虑实现 Rectangle可克隆 接口。 clone() 方法可以返回 Rectangle 而不是 Object,如下所示:

public class Rectangle implements Cloneable {
    ...  
    @Override
    protected Rectangle clone() 
            throws CloneNotSupportedException {
        Rectangle clone = (Rectangle) super.clone();
        return clone;
    }
}

调用 clone() 方法不需要显式转换:

Rectangle r = new Rectangle(4, 3);
Rectangle clone = r.clone();

完整的应用程序被命名为CovariantMethodOverriding。注意 关于协变方法覆盖的不太直接的问题。比如可以这样表述:我们可以在覆盖的同时修改方法的返回类型为子类吗? 这个问题的答案和什么是 Java 中的协变方法覆盖?,在此讨论。

重要的提示

了解针对 Java 鲜为人知的特性的问题的答案可能是面试中的一大优势。这向面试官展示了您具有深厚的知识水平,并且您与 Java 发展保持同步。如果您需要通过大量示例和 最小理论对所有 JDK 8 到 JDK 13 功能进行超音速更新,那么您会喜欢我题为 Java 的书编码问题,由 Packt (packtpub.com/au/programming/java-coding-problems) 发布。

What are the main restrictions in terms of working with exceptions in overriding and overloading methods?

首先,让我们讨论 覆盖方法。 如果我们谈论未经检查的异常,那么我们必须说在覆盖方法中使用它们没有任何限制。这样的 方法可以抛出未经检查的异常,因此,任何RuntimeException。另一方面,在检查异常的情况下,覆盖方法只能抛出被覆盖方法的检查异常或该检查异常的子类。换句话说,重写方法不能抛出比被重写方法抛出的已检查异常范围更广的已检查异常。比如重写方法抛出SQLException,那么重写方法可以抛出BatchUpdateException等子类,但不能抛出super Exception 等类。

其次,让我们讨论重载方法。 这样的方法没有任何限制。这意味着我们可以根据需要修改 throw 子句。

重要的提示

注意按照主要...?,你能列举某些...?,你能提名...?,你能突出显示的问题吗? ...?,等等。通常,当问题包含 main、someone、nominate、highlight 等词时,面试官希望得到清晰简洁的答案这听起来应该像一个项目符号列表。回答此类问题的最佳做法是直接跳入响应并将每个项目枚举为压缩且有意义的陈述。在给出预期答案之前,不要犯一个常见的错误,即开始讲述所涉及的概念的故事或论文。面试官希望在检查你的知识水平的同时看到你综合和净化事物并提取本质的能力。

如果需要更多细节,那么您可以编写一个示例,就像本书捆绑的代码中的示例一样。考虑检查 OverridingExceptionOverloadingException 应用程序。现在,让我们继续回答更多 问题。

How can the superclass overridden method be called from the subclass overriding method?

我们可以通过Java从子类覆盖方法调用超类覆盖方法super 关键字< /em>。例如,考虑一个 超类 A,它包含一个方法 foo()< /code>,以及一个名为 BA 子类。如果我们重写子类 B 中的 foo() 方法,我们调用 super.foo () 来自覆盖方法 B#foo(),然后我们调用覆盖方法 A#foo()< /代码>。

Can we override or overload the main() method?

我们必须记住main()方法是静态的。这意味着我们可以重载它。但是,我们不能覆盖它,因为静态方法是在编译时解析的,而我们可以覆盖的方法是在运行时解析的,具体取决于对象的类型。

Can we override a non-static method as static in Java?

不。我们不能将非静态方法重写为静态方法。而且,反过来也是不可能的。两者都会导致 编译错误。

重要的提示

中肯的问题,例如前面提到的最后两个问题,应该得到一个简短而简洁的答案。面试官会触发这样的手电筒问题来衡量你分析情况和做出决定的能力。主要是,答案很简短,但您需要一些时间说 YesNo。这样的问题得分不高,但如果你不知道答案,它们可能会产生重大的负面影响。如果你知道答案,面试官可能会在心里说,好吧,好吧,反正这是一个简单的问题! 但是,如果你不知道答案,那么他可能会说,他错过了一个简单的!她/他的基础知识存在严重缺陷。

接下来,我们再看一些与其他 OOP 概念相关的问题。

我们可以在 Java 接口中使用非抽象方法吗?

在 Java 8 之前,我们不能在 Java 接口中使用非抽象方法。接口 中的所有方法都是隐式公共和抽象的。但是,从 Java 8 开始,我们有了可以添加到接口的新方法类型。 实际上,从 Java 8 开始,我们可以直接在接口中添加具有实现的方法。这可以通过使用 defaultstatic 来完成 关键字。 default 关键字是在 Java 8 中引入的,用于在接口中包含称为 默认,防御者,扩展方法。他们的主要目标是允许我们在确保向后兼容性的同时改进现有接口。 JDK 本身使用默认方法通过添加新功能而不破坏现有代码来发展 Java。 另一方面,static 接口中的方法与默认方法非常相似,即唯一的区别是我们不能覆盖实现这些接口的类中的static方法。由于 static 方法没有绑定到对象,因此可以使用前面带点的接口名称和方法名称来调用它们。此外,static 方法可以在其他 defaultstatic 方法中调用。

如果需要更多详细信息,则可以尝试编写示例代码。考虑到我们有一个用于塑造像蒸汽车一样的车辆的界面(这是一种与旧代码完全相同的旧汽车类型):

public interface Vehicle {
    public void speedUp();
    public void slowDown();    
}

显然,不同种类的蒸汽车已经通过下面的SteamCar类来构建:

public class SteamCar implements Vehicle {
    private String name;
    // constructor and getter omitted for brevity
    @Override
    public void speedUp() {
        System.out.println("Speed up the steam car ...");
    }
    @Override
    public void slowDown() {
        System.out.println("Slow down the steam car ...");
    }
}

由于 SteamCar 类实现了 Vehicle 接口,它覆盖了 speedUp()slowDown() 方法。一段时间后,汽油车被发明出来,人们开始关心马力和油耗。因此,我们的代码必须不断发展,以便为汽油车提供支持。为了计算消耗水平,我们可以通过添加 computeConsumption() 默认方法来改进 Vehicle 接口,如下所示:

public interface Vehicle {
    public void speedUp();
    public void slowDown();
    default double computeConsumption(int fuel, 
            int distance, int horsePower) {        
        // simulate the computation 
        return Math.random() * 10d;
    }        
}

Vehicle 界面的发展不会破坏 SteamCar 的兼容性。此外,还发明了电动汽车。计算电动汽车的消耗量与汽油汽车的消耗量不同,但公式依赖于相同的术语:燃料、距离和马力。这意味着 ElectricCar 将覆盖 computeConsumption(),如下所示:

public class ElectricCar implements Vehicle {
    private String name;
    private int horsePower;
    // constructor and getters omitted for brevity
    @Override
    public void speedUp() {
        System.out.println("Speed up the electric car ...");
    }
    @Override
    public void slowDown() {
        System.out.println("Slow down the electric car ...");
    }
    @Override
    public double computeConsumption(int fuel, 
            int distance, int horsePower) {
        // simulate the computation
        return Math.random()*60d / Math.pow(Math.random(), 3);
    }     
}

因此,我们可以覆盖 default 方法,或者我们可以使用隐式实现。最后,我们必须向我们的 界面添加描述,因为它现在服务于蒸汽、汽油和电动汽车。我们可以通过向 Vehicle 添加一个名为 description()static 方法来做到这一点>,如下:

public interface Vehicle {
    public void speedUp();
    public void slowDown();
    default double computeConsumption(int fuel, 
        int distance, int horsePower) {        
        return Math.random() * 10d;
    }
    static void description() {
        System.out.println("This interface control
            steam, petrol and electric cars");
    }
}

这个static方法不绑定任何类型的汽车,可以直接通过调用Vehicle.description()。完整的代码名为 Java8DefaultStaticMethods

接下来,让我们继续其他问题。到目前为止,您应该对理解问题|提名关键点|非常熟悉回答技巧,所以我不再强调重点。从现在开始,你的工作就是发现它们。

What are the main differences between interfaces with default methods and abstract classes?

Java 8 接口和抽象类之间的区别中,我们可以提到一个事实,抽象类可以有构造函数,而接口不支持构造函数。因此,抽象类可以有状态,而接口不能有状态。此外,接口仍然是完全抽象的第一公民,主要目的是实现,而抽象类则用于部分抽象。接口仍然被设计为针对完全抽象的事物,它们本身不做任何事情,但指定了关于事物在实现时如何工作的合同。默认方法代表一种在不影响客户端代码和不更改状态的情况下向接口添加附加功能的方法。它们不应该用于其他目的。换句话说,另一个区别在于,拥有一个没有抽象方法的抽象类是完全可以的,但只有一个 接口默认方法。这意味着我们已经创建了接口作为实用程序类的替代品。这样,我们就破坏了要实现的接口的主要目的。

重要的提示

当你必须列举两个概念之间的一堆差异或相似之处时,请注意将你的答案限制在问题所解决的坐标上。例如,在上一个问题的情况下,不要说一个区别在于接口支持多重继承而抽象类不支持这一事实。这是接口和类之间的一般变化,而不是 Java 8 接口和抽象类之间的具体变化。

What is the main difference between abstract classes and interfaces?

在 Java 8 之前,抽象类和接口之间的主要区别 在于抽象类可以包含非抽象方法,而接口不能包含此类方法。从 Java 8 开始,主要区别在于抽象类可以具有构造函数和状态,而接口不能具有其中任何一个。

Can we have an abstract class without an abstract method?

我们可以。通过将 abstract 关键字添加到 类,它就变成了抽象的。它不能被实例化,但它可以有构造函数并且只有非抽象方法。

Can we have a class that is both abstract and final at the same time?

final 类不能被子类化或继承。抽象类旨在扩展以便使用。因此,final 和 abstract 是相反的概念。这 意味着它们不能同时应用于同一个类。编译器会抛出错误。

What is the difference between polymorphism, overriding, and overloading?

在这个问题的上下文中,重载 技术被称为 Compiled-Time Polymorphism,而覆盖技术被称为 < strong class="bold">运行时多态。重载涉及使用静态(或早期)绑定,而覆盖 使用动态(或后期)绑定。

接下来的两个问题构成了这个问题的附加问题,但它们也可以被表述为独立的。

什么是绑定操作?

绑定操作确定要调用的 方法(或变量)作为其在代码行中的引用的结果。换句话说,将方法调用与方法体相关联的过程称为绑定操作。一些引用在编译时绑定,而其他引用在运行时绑定。那些在运行时绑定的取决于对象的类型。在编译时解析的引用称为静态绑定操作,而在运行时解析的引用称为动态绑定操作。

What are the main differences between static and dynamic binding?

首先,静态绑定发生在编译时,而动态绑定发生在运行时。要考虑的第二件事涉及私有、静态和最终成员(方法和变量)使用静态绑定,而虚拟方法在运行时根据对象类型绑定。也就是说,静态绑定是通过Type(Java中的类)信息完成的,而动态绑定是通过Object完成的,意思是依赖于静态绑定的方法不与对象关联,而是在 Type(Java 中的类)上调用,而依赖于动态绑定的方法与 <代码类="literal">对象。依赖于静态绑定的方法的执行比那些依赖于动态绑定的方法要快一些。静态和 动态绑定也用于多态性。静态绑定用于编译时多态性(重载方法),而动态绑定用于运行时多态性(覆盖方法)。静态绑定在编译时增加了性能开销,而动态绑定在运行时增加了性能开销,这意味着静态绑定更可取。

What is method hiding in Java?

方法隐藏特定于 静态方法。更准确地说,如果我们在父类和子类中声明两个具有相同签名和名称的 静态方法,那么它们将相互隐藏。从超类调用方法会从超类调用静态方法,从子类调用相同的方法会从子类调用静态方法。隐藏与覆盖不同,因为静态方法不能是多态的。

如果需要更多详细信息,那么您可以编写一个示例。考虑具有 move() 静态方法的 Vehicle 超类:

public class Vehicle {
    public static void move() {
        System.out.println("Moving a vehicle");
    }
}

现在,考虑具有相同静态方法的 Car 子类:

public class Car extends Vehicle {
    // this method hides Vehicle#move()
    public static void move() {
        System.out.println("Moving a car");
    }
}

现在,让我们从 main() 方法中调用这两个静态方法:

public static void main(String[] args) {
    Vehicle.move(); // call Vehicle#move()
    Car.move();     // call Car#move()
}

输出显示这两个静态方法相互隐藏:

Moving a vehicle
Moving a car

请注意,我们通过类名调用静态方法。在实例上调用静态方法是一种非常糟糕的做法,所以在面试时避免这样做!

Can we write virtual methods in Java?

我们可以!实际上,在 Java 中,所有 非静态方法默认都是虚拟方法。我们可以编写一个非虚拟方法,用 private 和/或 final 标记它 关键字。换句话说,多态行为可以被继承的方法是虚方法。或者,如果我们把这条语句的逻辑颠倒过来,不能被继承的方法(标记为private)和不能被覆盖的方法(标记为final) 是非虚拟的。

What is the difference between polymorphism and abstraction?

抽象和多态代表了两个相互依赖的基本 OOP 概念。抽象 允许开发人员设计可重用和可定制的通用解决方案,而多态性允许开发人员推迟选择应在运行时执行的代码。虽然抽象是通过接口和抽象类实现的,但多态性依赖于覆盖和重载技术。

Do you consider overloading an approach for implementing polymorphism?

这是一个有争议的话题。一些 人不认为重载是多态性;因此,他们不接受编译时多态的想法。这样的 声音认为唯一的覆盖方法是真正的多态性。该语句背后的论点说,只有覆盖才允许代码根据运行时条件表现出不同的行为。换句话说,展示多态行为是方法覆盖的特权。我认为,只要我们了解重载和覆盖的前提,我们也了解这两种变体如何维持多态行为。

重要的提示

解决有争议话题的问题很微妙,很难正确处理。因此,建议直接使用这句话这是一个有争议的话题进入答案。当然,面试官也很想听听你的意见,但他会很高兴看到你了解硬币的两面。根据经验,尝试以客观的方式回答,不要以激进主义或论据不足的方式接近硬币的一面。有争议的事情毕竟还是有争议的,现在不是揭开它们神秘面纱的合适时间和地点。

好的,现在让我们继续基于 SOLID 原则和著名且不可或缺的 Gang Of Four (GOF) 设计模式的一些问题.请注意,这本书不涉及 GOF 设计模式,但有很多专门针对此主题的书籍和视频。我建议您尝试 Learn Design Patterns with Java,作者 Aseem Jain (https://www.packtpub.com/application-development/learn-design-patterns-java-video)

Which OOP concept serves the Decorator design pattern?

服务于装饰器设计模式的OOP概念是Composition。通过这个 OOP 概念,装饰器设计模式在不修改原始类的情况下提供了新的功能。

When should the Singleton design pattern be used?

当我们只需要一个类的应用程序级(全局)实例时,单例设计模式 似乎是正确的选择。尽管如此,还是应该谨慎使用 Singleton,因为它会增加类之间的耦合,并可能成为开发、测试和调试过程中的瓶颈。正如著名的 Effective Java 所指出的,使用 Java 枚举是实现这种模式的最佳方式。依赖单例模式进行全局配置(例如,记录器、java.lang.Runtime)、硬件访问、数据库连接等是一种常见的场景。

重要的提示

只要您可以引用或提及著名的参考文献,就这样做。

What is the difference between the Strategy and State design patterns?

状态设计模式是意味着根据状态做某件事(它在不同的states 不改变类)。另一方面,策略设计模式旨在用于在一系列算法之间切换,而无需修改使用它的代码(客户端通过组合和运行时委托交替使用算法)。此外,在 State 中,我们有一个明确的 state 转换顺序(流程是通过将每个 state 链接到另一个 state),而在 Strategy 中,客户端可以按任意顺序选择它想要的算法。例如,状态模式可以定义向客户端发送包的状态

包从有序状态开始,继续已交付状态,以此类推,直到通过每个state 并在客户端 received 包裹时到达最终 state。另一方面,策略模式定义了完成每个状态的不同策略(例如,我们可能有不同的策略来交付包)。

What is the difference between the Proxy and Decorator patterns?

代理设计模式对于提供对某物的访问控制网关很有用。通常,这种模式会创建代理对象来代替真实对象。对真实对象的每个请求都必须通过代理对象,代理对象决定如何以及何时将其转发给真实对象。装饰器设计模式从不创建对象,它只是在运行时用新功能装饰现有对象。虽然链接代理不是一种可取的做法,但以特定顺序链接装饰器以正确的方式利用了这种模式。例如,虽然 Proxy 模式可以代表 Internet 的代理服务器,但 Decorator 模式可用于使用不同的自定义设置来装饰代理服务器。

What is the difference between the Facade and Decorator patterns?

虽然装饰器设计 模式旨在为对象添加新功能(换句话说,装饰对象),但外观设计模式根本不添加新功能。它只是对现有功能进行外观化(隐藏系统的复杂性),并通过向客户端公开的友好面孔在幕后调用它们。 Facade 模式可以公开一个简单的接口,调用各个组件来完成复杂的任务。例如,装饰者模式可用于通过用发动机、变速箱等装饰底盘来制造汽车,而外观模式可以通过为命令工业机器人公开一个简单的接口来隐藏制造汽车的复杂性。了解构建过程的详细信息。

模板方法和策略模式之间的主要区别是什么?

模板方法和策略模式将特定领域的算法集封装到对象中,但它们的做法不同。关键区别在于策略模式旨在根据需求在运行时决定不同策略(算法)之间的关系,而模板方法模式旨在遵循算法的固定框架(预定义的步骤序列)实现.某些步骤是固定的,而其他步骤可以针对 不同用途进行修改。例如,策略模式可以决定不同的支付策略(例如,信用卡或 PayPal),而模板方法可以描述使用特定策略支付的预定义步骤顺序(例如,通过 PayPal 支付需要一个固定的步骤顺序)。

What is the key difference between the Builder and Factory patterns?

工厂模式在单个方法调用中创建一个 对象。我们必须在此调用中传递所有必要的参数,工厂将返回对象(通常通过调用构造函数)。另一方面,Builder 模式旨在通过一系列 setter 方法构建复杂的对象,这些方法允许我们塑造任何参数组合。在链的末尾,Builder 方法公开了一个 build() 方法,该方法表明参数列表已设置,是时候构建对象了。换句话说,Factory 充当构造函数的包装器,而 Builder 则更加精细,充当您可能想要传递给构造器的所有可能参数的包装器。通过 Builder,我们避免了用于暴露所有可能的参数组合的伸缩构造函数。例如,回想一下 Book 对象。一本书的特点是具有固定参数,例如作者、标题、ISBN 和格式。很可能,您在创建书籍时不会处理这些参数的数量,因此工厂模式将非常适合分解书籍。但是 Server 对象呢?好吧,服务器是一个具有大量可选参数的复杂对象,因此 Builder 模式在这里更合适,甚至是这些模式的组合,其中 Factory 内部依赖于 Builder。

What is the key difference between the Adapter and Bridge patterns?

适配器模式力求在我们无法修改的现有代码(例如第三方代码)和新系统或接口之间提供兼容性。另一方面,桥接模式是预先实现的,旨在将抽象与实现分离,以避免大量的类。因此,Adapter 在设计之后努力提供事物之间的兼容性(按照 A 来自 After 的思路思考),而 Bridge 是预先构建的,让抽象和实现有所不同独立(按照 B 来自 Before 的思路思考)。虽然适配器充当了两个系统之间的中间人,这些系统独立工作但无法相互通信(它们没有兼容的输入/输出),桥模式进入了场景当我们的 问题可以通过正交类层次结构解决时,我们会遇到可伸缩性问题和有限的扩展。例如,考虑两个类,ReadJsonRequestReadXmlRequest,它们能够从多个设备读取,例如 D1D2D3D1D2 只产生 JSON 请求,而 D3 只产生 XML 请求.通过 Adapter,我们可以在 JSON 和 XML 之间进行转换,这意味着这两个类可以与所有三种设备进行通信。另一方面,通过桥接模式,我们可以避免以许多类结束,例如 ReadXMLRequestD1ReadXMLRequestD2ReadXMLRequestD3ReadJsonRequestD1ReadJsonRequestD2ReadJsonRequestD3< /代码>。

我们可以继续比较设计模式,直到我们完成所有可能的组合。最后几个问题涵盖了 设计模式 1 与设计模式 2 类型的最流行问题。强烈建议用这些类型的问题挑战自己,并尝试找出两个或多个给定设计模式之间的相似之处和不同之处。大多数时候,这些问题使用来自同一类别的两种设计模式(例如,两种结构模式或两种创建模式),但它们也可以来自不同的类别。在这种情况下,这是面试官希望听到的第一个陈述。因此,在这种情况下,首先要说明所涉及的每个设计模式属于哪个类别。

请注意,我们跳过了类型的所有简单问题,什么是接口?,什么是抽象类?等等。通常,避免此类问题,因为它们并没有说明您的理解水平,更多的是背诵一些定义。面试官可以问抽象类和接口的主要区别是什么?,他可以从你的回答中推断出你是否知道接口和抽象类是什么。始终准备好举例说明。无法塑造一个例子表明严重缺乏对事物本质的理解。

拥有 OOP 知识只是问题的一半。另一半代表具有将这些知识应用于设计应用程序的远见和敏捷性。这就是我们将在接下来的 10 个示例中所做的事情。请记住,我们专注于设计,而不是实施。

Coding challenges

接下来,我们将解决有关面向对象编程的几个编码挑战。对于每个问题,我们将按照 第 5 章中的图 5.2,如何应对编码挑战。主要是从问面试官一个问题开始,例如设计约束是什么? 通常,围绕OOD的编码挑战由面试官以一般的方式表达。这样做是故意让您询问有关设计约束的详细信息。

一旦我们清楚地了解约束,我们就可以尝试一个示例(可以是草图、分步运行时可视化、项目符号列表等) .然后,我们找出算法/解决方案,最后,我们提供设计框架。

Example 1: Jukebox

亚马逊、谷歌

问题:设计点唱机音乐机的主要类

问什么:点唱机播放什么——CD、MP3?我应该设计什么——自动点唱机的构建过程,它是如何工作的,还是其他什么?这是一个免费的点唱机还是需要钱?

采访者:免费的点唱机只能播放CD吗?设计它的主要功能,从而设计它的工作方式。

解决方案:为了了解我们的设计中应该涉及哪些类,我们可以尝试可视化点唱机并确定其主要部分和功能。沿着此处的线条绘制图表也有助于面试官了解您的想法。我建议您始终采用以书面形式将问题可视化的方法——草图是一个完美的开始:

读书笔记《the-complete-coding-interview-guide-in-java》第六章面向对象程序设计

图 6.1 – 点唱机

因此,我们可以识别点唱机的两个主要部分:CD 播放器(或特定点唱机播放机制)和 接口,其中包含用户命令。 CD 播放器能够管理播放列表并播放这些歌曲。我们可以将命令的接口想象成Jukebox实现的Java接口,如下代码所示。除了以下代码,您还可以使用此处的 UML 图:https://github.com/PacktPublishing/The-Complete-Coding-Interview-Guide-in-Java/blob/master/Chapter06/Jukebox/JukeboxUML.png

public interface Selector {
    public void nextSongBtn();
    public void prevSongBtn();
    public void addSongToPlaylistBtn(Song song);
    public void removeSongFromPlaylistBtn(Song song);
    public void shuffleBtn();
}
public class Jukebox implements Selector {
    private final CDPlayer cdPlayer;
    public Jukebox(CDPlayer cdPlayer) {
        this.cdPlayer = cdPlayer;        
    }            
    @Override
    public void nextSongBtn() {...}
    // rest of Selector methods omitted for brevity
}

CDPlayer 点唱机的核心。通过Selector,我们控制CDPlayer的行为。 CDPlayer 必须具有 访问可用 CD 集和播放列表的权限:

public class CDPlayer {
    private CD cd;
    private final Set<CD> cds;
    private final Playlist playlist;
    public CDPlayer(Playlist playlist, Set<CD> cds) {
        this.playlist = playlist;
        this.cds = cds;
    }                
    protected void playNextSong() {...}
    protected void playPrevSong() {...}   
    protected void addCD(CD cd) {...}
    protected void removeCD(CD cd) {...}
    // getters omitted for brevity
}

接下来,Playlist 管理 歌曲:

public class Playlist {
    private Song song;
    private final List<Song> songs; // or Queue
    public Playlist(List<Song> songs) {
        this.songs = songs;
    }   
    public Playlist(Song song, List<Song> songs) {
        this.song = song;
        this.songs = songs;
    }        
    protected void addSong(Song song) {...}
    protected void removeSong(Song song) {...}
    protected void shuffle() {...}    
    protected Song getNextSong() {...};
    protected Song getPrevSong() {...};
    // setters and getters omitted for brevity
}

UserCDSong暂时被跳过,但您可以在 名为 Jukebox 的完整应用程序中找到它们。这种问题可以通过多种方式实现,所以也可以随意尝试自己的设计。

Example 2: Vending machine

亚马逊、谷歌、Adobe

问题:设计支持实现 功能的主要类典型的自动售货机。

问什么:这是一个售卖不同类型硬币和物品的自动售货机吗?它是否公开功能,例如检查商品价格、购买商品、退款和重置?

采访者:是的,没错!对于硬币,您可以考虑一分钱、五分钱、一角硬币和四分之一。

解决方案:为了了解我们的设计应该涉及哪些类,我们可以尝试画一个自动售货机。自动售货机种类繁多。简单地画一个你知道的(如下图中的那个):

读书笔记《the-complete-coding-interview-guide-in-java》第六章面向对象程序设计

图 6.2 – 自动售货机

首先,我们立即 注意到物品和硬币是很好的Java 枚举候选对象。我们有四种类型的硬币和几种类型的物品,所以我们可以编写两个 Java 枚举如下。除了以下代码,您还可以使用此处的 UML 图:https://github.com/PacktPublishing/The-Complete-Coding-Interview-Guide-in-Java/blob/master/Chapter06/VendingMachine/VendingMachineUML.png

public enum Coin {
    PENNY(1), NICKEL(5), DIME(10), QUARTER(25);
    ...
}
public enum Item {
    SKITTLES("Skittles", 15), TWIX("Twix", 35) ...
    ...
}

自动售货机需要内部库存来跟踪硬币的物品和状态。我们可以将其概括如下:

public final class Inventory<T> {
    private Map<T, Integer> inventory = new HashMap<>();
    protected int getQuantity(T item) {...}
    protected boolean hasItem(T item) {...}
    protected void clear() {...}    
    protected void add(T item) {...}
    protected void put(T item, int quantity) {...}
    protected void deduct(T item) {...}
}

接下来,我们可以关注客户端用来与自动售货机交互的按钮。正如您在前面的示例中所看到的,通常将这些按钮提取到界面中,如下所示:

public interface Selector {
    public int checkPriceBtn(Item item);
    public void insertCoinBtn(Coin coin);
    public Map<Item, List<Coin>> buyBtn();
    public List<Coin> refundBtn();
    public void resetBtn();    
}

最后,自动售货机可以被塑造成实现 Selector 接口,并提供一堆用于完成内部任务的私有方法:

public class VendingMachine implements Selector {
    private final Inventory<Coin> coinInventory
        = new Inventory<>();
    private final Inventory<Item> itemInventory
        = new Inventory<>();
    private int totalSales;
    private int currentBalance;
    private Item currentItem;
    public VendingMachine() {
        initMachine();
    }   
    private void initMachine() {
        System.out.println("Initializing the
            vending machine with coins and items ...");
    }
    // override Selector methods omitted for brevity
}

完整的 应用程序被命名为VendingMachine。通过前面提到的两个例子,你可以尝试设计一台ATM、一台洗衣机和类似的东西。

Example 3: Deck of cards

亚马逊、谷歌、Adobe、微软

问题:设计一副 通用纸牌的主要类别。

要问什么:既然卡片几乎可以是 任何东西,你能定义通用时间>?

Interviewer:一张牌的特点是一个符号(花色)和一个数值。例如,考虑一个标准的 52 张牌组。

解决方案:为了了解我们的设计中应该涉及哪些类,我们可以快速为标准的 52 张卡片组画出一张卡片和一副卡片,如图6.3:

读书笔记《the-complete-coding-interview-guide-in-java》第六章面向对象程序设计

图 6.3 – 一副纸牌

由于每张牌都有花色和价值,我们需要一个封装这些字段的类。我们称这个类为 StandardCardStandardCard 的套装包含 Spade、Heart、DiamondClub ,所以这套衣服很适合 Java 枚举。 StandardCard 值可以介于 1 和 13 之间。

一张卡片可以独立存在,也可以是一组卡片的一部分。多张牌构成一副牌(例如,标准的 52 张牌组构成一副牌)。一包中的牌张数通常以可能的花色和值之间的笛卡尔积形式获得(例如,4 花色 x 13 值 = 52 张牌)。因此,52 个 StandardCard 对象形成 StandardPack

最后,一副纸牌应该是一个能够使用这个 StandardPack 执行某些操作的类。例如,一副牌可以洗牌,可以发一手牌或一张牌,等等。这意味着还需要一个 Deck 类。

到目前为止,我们已经决定使用 一个 Java enumStandardCardStandardPackDeck 类。如果我们添加所需的抽象层来避免这些具体层之间的高耦合,那么我们将获得以下实现。除了以下代码,您还可以使用此处的 UML 图:https://github.com/PacktPublishing/The-Complete-Coding-Interview-Guide-in-Java/blob/master/Chapter06/DeckOfCards/DeckOfCardsUML.png

  • 对于标准卡实施:
public enum StandardSuit {   
    SPADES, HEARTS, DIAMONDS, CLUBS;
}
public abstract class Card {
    private final Enum suit;
    private final int value;
    private boolean available = Boolean.TRUE;
    public Card(Enum suit, int value) {
        this.suit = suit;
        this.value = value;
    }
    // code omitted for brevity
}
public class StandardCard extends Card {
    private static final int MIN_VALUE = 1;
    private static final int MAX_VALUE = 13;
    public StandardCard(StandardSuit suit, int value) {
        super(suit, value);
    }
    // code omitted for brevity
}
  • 卡的标准包 实现提供以下代码:
public abstract class Pack<T extends Card> {
    private List<T> cards;
    protected abstract List<T> build();
    public int packSize() {
        return cards.size();
    }
    public List<T> getCards() {
        return new ArrayList<>(cards);
    }
    protected void setCards(List<T> cards) {
        this.cards = cards;
    }
}
public final class StandardPack extends Pack {
    public StandardPack() {
        super.setCards(build());
    }
    @Override
    protected List<StandardCard> build() {
        List<StandardCard> cards = new ArrayList<>();
        // code omitted for brevity        
        return cards;
    }
}
  • Deck 卡片实现 提供以下内容:
public class Deck<T extends Card> implements Iterable<T> {
    private final List<T> cards;
    public Deck(Pack pack) {
        this.cards = pack.getCards();
    }
    public void shuffle() {...}
    public List<T> dealHand(int numberOfCards) {...}
    public T dealCard() {...}
    public int remainingCards() {...}
    public void removeCards(List<T> cards) {...}
    @Override
    public Iterator<T> iterator() {...}
}

代码的demo可以快速写成如下:

// create a single classical card
Card sevenHeart = new StandardCard(StandardSuit.HEARTS, 7);       
// create a complete deck of standards cards      
Pack cp = new StandardPack();                   
Deck deck = new Deck(cp);
System.out.println("Remaining cards: " 
    + deck.remainingCards());

此外,您 可以通过扩展 CardPack 轻松添加更多类型的卡片代码>类。完整的代码是,名为DeckOfCards

Example 4: Parking lot

亚马逊、谷歌、Adobe、微软

问题:设计停车场的主要类。

问什么:是单层还是多层停车场? 所有停车位都一样吗?我们应该停放什么类型的车辆?是免费停车吗?我们使用停车票吗?

采访者:同步自动多层免费停车场。所有停车位的大小相同,但我们预计汽车(需要 1 个停车位)、货车(需要 2 个停车位)和卡车(需要 5 个停车位)。其他类型的车辆应在不修改代码的情况下添加。系统会发布一张停车票,以后可以使用该票来取消停车。但如果司机只介绍车辆信息(假设丢失了车票),系统仍应工作并在停车场定位车辆并将其停放。

解决方案:为了了解我们的设计中应该涉及哪些类,我们可以快速绘制一个停车场以识别主要参与者和行为,如图 6.4 所示:

读书笔记《the-complete-coding-interview-guide-in-java》第六章面向对象程序设计

图 6.4 – 一个停车场

该图揭示了两个主要参与者:停车场和自动停车系统。

首先,让我们关注停车场。 停车场的主要用途是停放车辆;因此,我们需要塑造可接受的车辆(汽车、货车和卡车)。这看起来像是一个抽象类(Vehicle)和三个子类(Car货车卡车)。但是这是错误的!司机提供有关他们车辆的信息。它们不能有效地将车辆(对象)推入停车场系统,因此我们的系统不需要汽车、货车、卡车等专用对象。从停车场的角度思考。它需要车牌和停车所需的免费停车位。它不关心货车或卡车的特性。所以,我们可以如下塑造一个Vehicle。除了以下代码,您还可以使用此处的 UML 图:https://github.com/PacktPublishing/The-Complete-Coding-Interview-Guide-in-Java/blob/master/Chapter06/ParkingLot/ParkingLotUML.png

public enum VehicleType {
    CAR(1), VAN(2), TRUCK(5);
}
public class Vehicle {
    private final String licensePlate;
    private final int spotsNeeded;
    private final VehicleType type;
    public Vehicle(String licensePlate, 
            int spotsNeeded, VehicleType type) {
        this.licensePlate = licensePlate;
        this.spotsNeeded = spotsNeeded;
        this.type = type;
    }
    // getters omitted for brevity    
    // equals() and hashCode() omitted for brevity
}

接下来,我们要设计停车场。主要是一个停车场有几个层(或层),每层都有停车位。其中,停车场应公开停放车辆的方法。这些方法会将停车/取消停车任务委托给每个楼层(或某个楼层),直到它成功或没有要扫描的楼层:

public class ParkingLot {
    private String name;
    private Map<String, ParkingFloor> floors;
    public ParkingLot(String name) {
        this.name = name;
    }
    public ParkingLot(String name, 
            Map<String, ParkingFloor> floors) {
        this.name = name;
        this.floors = floors;
    }    
    // delegate to the proper ParkingFloor
    public ParkingTicket parkVehicle(Vehicle vehicle) {...}
    // we have to find the vehicle by looping floors  
    public boolean unparkVehicle(Vehicle vehicle) {...} 
    // we have the ticket, so we have the needed information
    public boolean unparkVehicle
        ParkingTicket parkingTicket) {...} 
    public boolean isFull() {...}
    protected boolean isFull(VehicleType type) {...}
    // getters and setters omitted for brevity
}

停车楼层控制某一楼层的停车/出车过程。它拥有自己的停车罚单登记处,并能够管理其停车位。主要是每个停车楼层作为一个独立的停车场。这样,我们可以关闭一个完整的楼层,而其余楼层不受影响:

public class ParkingFloor {
    private final String name;
    private final int totalSpots;
    private final Map<String, ParkingSpot>
        parkingSpots = new LinkedHashMap<>();
    // here, I use a Set, but you may want to hold the parking 
    // tickets in a certain order to optimize search
    private final Set<ParkingTicket>
        parkingTickets = new HashSet<>();
    private int totalFreeSpots;
    public ParkingFloor(String name, int totalSpots) {
        this.name = name;
        this.totalSpots = totalSpots;
        initialize(); // create the parking spots
    }
    protected ParkingTicket parkVehicle(Vehicle vehicle) {...}     
    //we have to find the vehicle by looping the parking spots  
    protected boolean unparkVehicle(Vehicle vehicle) {...} 
    // we have the ticket, so we have the needed information
    protected boolean unparkVehicle(
        ParkingTicket parkingTicket) {...} 
    protected boolean isFull(VehicleType type) {...}
    protected int countFreeSpots(
        VehicleType vehicleType) {...}
    // getters omitted for brevity
    private List<ParkingSpot> findSpotsToFitVehicle(
        Vehicle vehicle) {...}    
    private void assignVehicleToParkingSpots(
        List<ParkingSpot> spots, Vehicle vehicle) {...}    
    private ParkingTicket releaseParkingTicket(
        Vehicle vehicle) {...}    
    private ParkingTicket findParkingTicket(
        Vehicle vehicle) {...}    
    private void registerParkingTicket(
        ParkingTicket parkingTicket) {...}           
    private boolean unregisterParkingTicket(
        ParkingTicket parkingTicket) {...}                    
    private void initialize() {...}
}

最后,一个停车位是一个 对象,它包含有关其名称(标签或编号)、可用性(是否免费)和车辆(车辆是否是 停在那个地方)。它还具有将车辆分配到该地点/从该地点移除/移除车辆的方法:

public class ParkingSpot {
    private boolean free = true;
    private Vehicle vehicle;
    private final String label;
    private final ParkingFloor parkingFloor;
    protected ParkingSpot(ParkingFloor parkingFloor, 
            String label) {
        this.parkingFloor = parkingFloor;
        this.label = label;
    }
    protected boolean assignVehicle(Vehicle vehicle) {...}
    protected boolean removeVehicle() {...}
    // getters omitted for brevity
}

此刻,我们拥有了停车场的所有大班。接下来,我们将重点介绍自动停车系统。这可以被塑造成一个单独的 类,充当停车场的调度员:

public class ParkingSystem implements Parking {
   
    private final String id;
    private final ParkingLot parkingLot;
    public ParkingSystem(String id, ParkingLot parkingLot) {
        this.id = id;
        this.parkingLot = parkingLot;
    }
    @Override
    public ParkingTicket parkVehicleBtn(
        String licensePlate, VehicleType type) {...}
    @Override
    public boolean unparkVehicleBtn(
        String licensePlate, VehicleType type) {...}
    @Override
    public boolean unparkVehicleBtn(
        ParkingTicket parkingTicket) {...}     
    // getters omitted for brevity
}

包含部分实现的完整应用程序命名为ParkingLot

Example 5: Online reader system

问题:设计在线阅读器系统的主要 类。

要问什么:需要哪些 功能?可以同时阅读多少本书?

采访者:系统应该能够管理读者和书籍。您的代码应该能够添加/删除阅读器/书籍并显示阅读器/书籍。该系统一次可以为单个读者和一本书提供服务。

解决方案:为了理解我们的设计中应该包含哪些类,我们可以考虑绘制一些如图 6.5 所示的东西:

读书笔记《the-complete-coding-interview-guide-in-java》第六章面向对象程序设计

图 6.5 – 在线阅读器系统

为了管理读者和书籍,我们需要有这样的对象。这是一个小而容易的部分,在采访中从这些部分开始对于打破僵局和解决手头的问题非常有帮助。当我们在采访中设计对象时,没有必要拿出一个完整版本的对象。例如,具有姓名和电子邮件的读者,以及具有作者、标题和 ISBN 的书籍就绰绰有余了。让我们在下面的代码中看到它们。除了以下代码,您还可以使用此处的 UML 图:https://github.com/PacktPublishing/The-Complete-Coding-Interview-Guide-in-Java/blob/master/Chapter06/OnlineReaderSystem/OnlineReaderSystemUML.png

public class Reader {
    private String name;
    private String email;
    // constructor omitted for brevity
    // getters, equals() and hashCode() omitted for brevity
}
public class Book {
    private final String author;
    private final String title;
    private final String isbn;
    // constructor omitted for brevity
    public String fetchPage(int pageNr) {...}
    // getters, equals() and hashCode() omitted for brevity
}

接下来,如果我们考虑书籍通常由图书馆管理,那么我们可以在一个类中包装几个功能,例如添加、查找和删除书籍,如下所示:

public class Library {
    private final Map<String, Book> books = new HashMap<>();
    protected void addBook(Book book) {
       books.putIfAbsent(book.getIsbn(), book);
    }
    protected boolean remove(Book book) {
       return books.remove(book.getIsbn(), book);
    }
    protected Book find(String isbn) {
       return books.get(isbn);
    }
}

阅读器可以由名为ReaderManager的类似类管理。您可以在完整的应用程序中找到此类。要阅读一本书,我们需要一个 显示器。 Displayer 应该显示读者和书籍的详细信息,并且应该能够浏览书籍页面:

public class Displayer {
    private Book book;
    private Reader reader;
    private String page;
    private int pageNumber;
    protected void displayReader(Reader reader) {
        this.reader = reader;
        refreshReader();
    }
    protected void displayBook(Book book) {
        this.book = book;
        refreshBook();
    }
    protected void nextPage() {
        page = book.fetchPage(++pageNumber);
        refreshPage();
    }
    protected void previousPage() {
        page = book.fetchPage(--pageNumber);        
        refreshPage();
    }        
    private void refreshReader() {...}    
    private void refreshBook() {...}    
    private void refreshPage() {...}
}

最后,我们要做的就是包装LibraryReaderManager,和 OnlineReaderSystem 类中的 Displayer。此 类在此处列出:

public class OnlineReaderSystem {
    private final Displayer displayer;
    private final Library library;
    private final ReaderManager readerManager;
    private Reader reader;
    private Book book;
    public OnlineReaderSystem() {
        displayer = new Displayer();
        library = new Library();
        readerManager = new ReaderManager();
    }
    public void displayReader(Reader reader) {
        this.reader = reader;
        displayer.displayReader(reader);
    }
    public void displayReader(String email) {
        this.reader = readerManager.find(email);
        if (this.reader != null) {
            displayer.displayReader(reader);
        }
    }
    public void displayBook(Book book) {
        this.book = book;
        displayer.displayBook(book);
    }
    public void displayBook(String isbn) {
        this.book = library.find(isbn);
        if (this.book != null) {
            displayer.displayBook(book);
        }
    }
    public void nextPage() {
        displayer.nextPage();
    }
    public void previousPage() {
        displayer.previousPage();
    }
    public void addBook(Book book) {
        library.addBook(book);
    }
    public boolean deleteBook(Book book) {
        if (!book.equals(this.book)) {
            return library.remove(book);
        }
        return false;
    }
    public void addReader(Reader reader) {
        readerManager.addReader(reader);
    }
    public boolean deleteReader(Reader reader) {
        if (!reader.equals(this.reader)) {
            return readerManager.remove(reader);
        }
        return false;
    }
    public Reader getReader() {
        return reader;
    }
    public Book getBook() {
        return book;
    }
}

完整的应用程序被命名为OnlineReaderSystem

Example 6: Hash table

亚马逊、谷歌、Adobe、微软

问题:设计一个哈希表(这是面试中非常流行的问题)。

问什么:需要哪些功能? 应该应用什么技术来解决索引冲突?键值对的数据类型是什么?

采访者:说到功能,我不想要什么特别的东西。我只想要典型的 add()get() 操作。为了解决索引冲突,我建议你使用 chaining 技术。键值对应该是通用的。

哈希表的简要概述: 哈希表是一种存储键值对的数据结构。通常,一个数组包含表中的所有键值条目,并且该数组的大小设置为适应预期的数据量。每个 key-value 的 key 通过一个散列函数(或多个散列函数)传递,输出散列值或散列。主要是hash值表示key-value对在hash表中的索引(比如我们用一个数组来存储所有key-value对,那么hash函数返回这个数组的索引应该保存当前的键值对)。通过散列函数传递相同的键应该每次都产生相同的索引——这对于通过其键查找值很有用。

当哈希函数为不同的键生成两个相同的索引时,我们会遇到索引冲突。解决索引冲突问题最常用的技术是线性探测(这种技术线性搜索表中的下一个空闲槽——试图在数组中找到一个槽(一个index) 不包含键值对) 和 chaining (这种技术表示实现为链表数组的哈希表 - 冲突存储在 与链表节点相同的数组索引)。下图 是一个用于存储姓名-电话对的哈希表。它具有 chaining 功能(检查 Marius-0838234 条目,该条目链接到 Karina- 0727928,因为它们的键,MariusKarina,导致相同的数组索引,126):

读书笔记《the-complete-coding-interview-guide-in-java》第六章面向对象程序设计

图 6.6 – 哈希表

解决方案:首先,我们需要塑造一个哈希表条目(HashEntry)。如上图所示,键值对包含三个主要部分:键、值和指向下一个键值对的链接(这样,我们实现了 chaining )。由于哈希表条目只能通过专用方法访问,例如 get()put(),因此我们将其封装如下:

public class HashTable<K, V> {
    private static final int SIZE = 10;
    private static class HashEntry<K, V> {
        K key;
        V value;
        HashEntry <K, V> next;
        HashEntry(K k, V v) {
            this.key = k;
            this.value = v;
            this.next = null;
        }        
    }
    ...

接下来,我们定义保存HashEntry的数组。为了测试 目的,10 元素的大小就足够了,它允许我们测试 chaining 容易(体积小容易发生碰撞)。实际上,这样的数组要大得多:

    private final HashEntry[] entries 
        = new HashEntry[SIZE];
    ...

接下来,我们添加 get()put() 方法。他们的代码非常直观:

    public void put(K key, V value) {
        int hash = getHash(key);
        final HashEntry hashEntry = new HashEntry(key, value);
        if (entries[hash] == null) {
            entries[hash] = hashEntry;
        } else { // collision => chaining
            HashEntry currentEntry = entries[hash];
            while (currentEntry.next != null) {
                currentEntry = currentEntry.next;
            }
            currentEntry.next = hashEntry;
        }
    }
    public V get(K key) {
        int hash = getHash(key);
        if (entries[hash] != null) {
            HashEntry currentEntry = entries[hash];
            // Loop the entry linked list for matching 
            // the given 'key'
            while (currentEntry != null) {                
                if (currentEntry.key.equals(key)) {
                    return (V) currentEntry.value;
                }
                currentEntry = currentEntry.next;
            }
        }
        return null;
    }

最后,我们添加一个虚拟哈希 函数(实际上,我们使用诸如 Murmur 3 之类的哈希函数 - https://en.wikipedia.org/wiki/MurmurHash):

    private int getHash(K key) {        
        return Math.abs(key.hashCode() % SIZE);
    }    
}

完毕!完整的 应用程序被命名为HashTable

对于以下四个示例,我们跳过了书中的源代码。花点时间剖析每个例子。能够理解现有设计只是您可以用来塑造设计技能的另一种工具。当然,您可以在查看本书的代码并比较最后的结果之前尝试自己的方法。

Example 7: File system

问题:设计文件系统的主要类。

问什么:需要哪些功能?文件系统的组成部分是什么?

Interviewer:您的设计应该支持目录和文件的添加、删除和重命名。我们 正在讨论目录和文件的层次结构,就像大多数操作系统一样。

解决方案:完整的应用程序被命名为FileSystem。请访问以下链接以检查 UML:https://github.com/PacktPublishing/The-Complete-Coding-Interview-Guide-in-Java/blob/master/Chapter06/FileSystem/FileSystemUML.png

Example 8: Tuple

亚马逊、谷歌

问题:设计一个元组数据结构。

要问什么:一个元组可以 有 1 到 n 个元素。那么,你期望什么样的元组呢?元组中应该存储哪些数据类型?

面试官:我期待一个包含两个通用元素的元组。元组也称为 pair

解决方案:完整的应用程序被命名为 Tuple

请访问以下链接以检查 UML:https://github.com/PacktPublishing/The-Complete-Coding-Interview-Guide-in-Java/tree/master/Chapter06/Tuple

Example 9: Cinema with a movie ticket booking system

亚马逊、谷歌、Adobe、微软

问题:设计一个带有电影票预订系统的电影院。

问什么:电影院的主要结构是什么?它有多个电影院吗?我们有哪些类型的门票?我们如何播放电影(仅在房间内,每天一次)?

采访者:我期待一个有多个相同房间的电影院。一部电影可以同时在多个房间播放,并且一天可以在同一个房间播放多次。根据座位类型,有三种类型的门票,简单、银色和金色。可以通过非常通用的方式添加/删除电影(例如,我们可以在特定开始时间从某些房间删除电影,或者我们可以将电影添加到所有房间)。

解决方案:完整的应用程序名为MovieTicketBooking。请访问以下链接以检查 UML:https://github.com/PacktPublishing/The-Complete-Coding-Interview-Guide-in-Java/blob/master/Chapter06/MovieTicketBooking/MovieTicketBookingUML.png

示例 10:循环字节缓冲区

亚马逊、谷歌、Adobe

问题:设计一个循环字节缓冲区。

问什么:它应该可以调整大小?

采访者:是的,它应该可以调整大小。主要是,我希望您设计所有您认为必要的方法的签名。

解决方案:完整的应用程序名为CircularByteBuffer

请访问以下链接以检查 UML:https://github.com/PacktPublishing/The-Complete-Coding-Interview-Guide-in-Java/tree/master/Chapter06/CircularByteBuffer

到目前为止,一切都很好!我建议您也可以尝试自己的设计来解决前面的 10 个问题。不要认为提出的解决方案是唯一正确的解决方案。通过改变问题的背景,尽可能多地练习,并用其他问题挑战自己。

本章的源代码包以 Chapter06 的名称提供。

Summary

本章涵盖了有关 OOP 基础的最热门问题和面试中非常流行的 10 个设计编码挑战。在第一部分,我们从 OOP 概念(对象、类、抽象、封装、继承、多态性、关联、聚合和组合)开始,继续 SOLID 原则,并以结合 OOP 概念、SOLID 原则的问题结束,以及设计模式知识。在第二部分中,我们解决了 10 个精心设计的编码挑战,包括设计点唱机、自动售货机和著名的哈希表。

练习这些问题和问题将使您能够解决面试中遇到的任何 OOP 问题。

在下一章中,我们将讨论大 O 符号和时间。