JavaRush /Java 博客 /Random-ZH /设计类和接口(文章的翻译)
fatesha
第 22 级

设计类和接口(文章的翻译)

已在 Random-ZH 群组中发布
设计类和接口(文章翻译)- 1

内容

  1. 介绍
  2. 接口
  3. 界面标记
  4. 函数式接口、静态方法和默认方法
  5. 抽象类
  6. 不可变(永久)类
  7. 匿名类
  8. 能见度
  9. 遗产
  10. 多重继承
  11. 继承与组合
  12. 封装
  13. 最终类和方法
  14. 下一步是什么
  15. 下载源代码

1. 简介

无论您使用哪种编程语言(Java 也不例外),遵循良好的设计原则是编写干净、可理解和可验证代码的关键;并使其具有长久的生命力并轻松支持问题的解决。在本教程的这一部分中,我们将讨论 Java 语言提供的基本构建块,并介绍一些设计原则,以帮助您做出更好的设计决策。更具体地说,我们将讨论接口和使用默认方法的接口(Java 8 中的一个新功能)、抽象类和最终类、不可变类、继承、组合,并重新审视我们在第 1 部分课程“如何创建和销毁对象”

2. 接口

在面向对象编程中,接口的概念构成了契约开发的基础。简而言之,接口定义了一组方法(契约),每个需要支持该特定接口的类必须提供这些方法的实现:一个相当简单但功能强大的想法。许多编程语言都具有一种或另一种形式的接口,但 Java 特别为此提供了语言支持。让我们看一下Java中的一个简单的接口定义。
package com.javacodegeeks.advanced.design;

public interface SimpleInterface {
void performAction();
}
在上面的代码片段中,我们调用的接口SimpleInterface仅声明了一个名为 的方法performAction。接口和类之间的主要区别在于接口概述了联系应该是什么(它们声明一个方法),但不提供它们的实现。然而,Java 中的接口可能更复杂:它们可以包括嵌套接口、类、计数、注释和常量。例如:
package com.javacodegeeks.advanced.design;

public interface InterfaceWithDefinitions {
    String CONSTANT = "CONSTANT";

    enum InnerEnum {
        E1, E2;
    }

    class InnerClass {
    }

    interface InnerInterface {
        void performInnerAction();
    }

    void performAction();
}
在这个更复杂的示例中,接口无条件地对嵌套构造和方法声明施加了一些限制,并且这些限制是由 Java 编译器强制执行的。首先,即使没有显式声明,接口中的每个方法声明都是公共的(并且只能是公共的)。因此以下方法声明是等效的:
public void performAction();
void performAction();
值得一提的是,接口中的每个方法都隐式声明为抽象,甚至这些方法声明也是等效的:
public abstract void performAction();
public void performAction();
void performAction();
至于声明的常量字段,除了public之外,它们也是隐式静态的并标记为Final。因此以下声明也是等效的:
String CONSTANT = "CONSTANT";
public static final String CONSTANT = "CONSTANT";
最后,嵌套类、接口或计数除了是public之外,还隐式声明为static。例如,这些声明也相当于:
class InnerClass {
}

static class InnerClass {
}
您选择的样式是个人喜好,但了解界面的这些简单属性可以帮助您避免不必要的打字。

3. 接口标记

标记接口是一种特殊的接口,没有方法或其他嵌套结构。Java 库如何定义它:
public interface Cloneable {
}
界面标记本身不是契约,而是一种将某些特定特征与类“附加”或“关联”的有用技术。例如,对于Cloneable,该类被标记为可克隆,但它可以或应该实现的方式不是接口的一部分。界面标记的另一个非常著名且广泛使用的示例是Serializable
public interface Serializable {
}
该接口将类标记为适合序列化和反序列化,并且它没有指定如何或应该如何实现。接口标记在面向对象编程中占有一席之地,尽管它们不满足接口作为契约的主要目的。 

4. 函数接口、默认方法和静态方法

自 Java 8 发布以来,接口获得了一些非常有趣的新功能:静态方法、默认方法以及 lambda(函数式接口)的自动转换。在接口部分,我们强调了Java中的接口只能声明方法,但不提供其实现。对于默认方法,情况有所不同:接口可以使用default关键字标记方法并为其提供实现。例如:
package com.javacodegeeks.advanced.design;

public interface InterfaceWithDefaultMethods {
    void performAction();

    default void performDefaulAction() {
        // Implementation here
    }
}
在实例级别,每个接口实现都可以覆盖默认方法,但接口现在还可以包含静态方法,例如:package com.javacodegeeks.advanced.design;
public interface InterfaceWithDefaultMethods {
    static void createAction() {
        // Implementation here
    }
}
可以说,在接口中提供实现违背了合约编程的全部目的。但这些特性被引入 Java 语言的原因有很多,无论它们多么有用或令人困惑,它们都供您和您使用。函数式接口是一个完全不同的故事,并且已被证明是该语言的非常有用的补充。基本上,函数式接口是一种仅声明一个抽象方法的接口。Runnable标准库接口是这个概念的一个很好的例子。
@FunctionalInterface
public interface Runnable {
    void run();
}
Java 编译器以不同的方式对待函数式接口,并且可以将 lambda 函数转换为有意义的函数式接口实现。让我们考虑以下函数描述: 
public void runMe( final Runnable r ) {
    r.run();
}
要在 Java 7 及更低版本中调用此函数,必须提供接口的实现Runnable(例如,使用匿名类),但在 Java 8 中,使用 lambda 语法提供 run() 方法的实现就足够了:
runMe( () -> System.out.println( "Run!" ) );
此外,@FunctionalInterface注解(注解将在教程的第 5 部分中详细介绍)暗示编译器可以检查接口是否只包含一个抽象方法,因此将来对该接口所做的任何更改都不会违反这一假设。

5. 抽象类

Java 语言支持的另一个有趣的概念是抽象类的概念。抽象类有点类似于Java 7中的接口,并且非常接近Java 8中的默认方法接口。与常规类不同,抽象类不能实例化,但可以子类化(更多详细信息请参阅继承部分)。更重要的是,抽象类可以包含抽象方法:一种没有实现的特殊方法,就像接口一样。例如:
package com.javacodegeeks.advanced.design;

public abstract class SimpleAbstractClass {
    public void performAction() {
        // Implementation here
    }

    public abstract void performAnotherAction();
}
在此示例中,该类SimpleAbstractClass被声明为抽象类并包含一个声明的抽象方法。抽象类非常有用;大部分甚至某些部分的实现细节可以在许多子类之间共享。尽管如此,它们仍然敞开大门,允许您使用抽象方法自定义每个子类固有的行为。值得一提的是,与只能包含公共声明的接口不同,抽象类可以使用可访问性规则的全部功能来控制抽象方法的可见性。

6. 立即上课

如今,不变性在软件开发中变得越来越重要。多核系统的兴起引发了许多与数据共享和并行性相关的问题。但肯定出现了一个问题:几乎没有(甚至没有)可变状态会带来更好的可扩展性(可伸缩性),并且更容易对系统进行推理。不幸的是,Java 语言没有为类不变性提供适当的支持。然而,通过结合使用多种技术,可以设计不可变的类。首先,类的所有字段都必须是final的(标记为final)。这是一个好的开始,但并不能保证。 
package com.javacodegeeks.advanced.design;

import java.util.Collection;

public class ImmutableClass {
    private final long id;
    private final String[] arrayOfStrings;
    private final Collection<String> collectionOfString;
}
其次,确保正确的初始化:如果字段是对集合或数组的引用,请勿直接从构造函数参数分配这些字段,而应进行复制。这将确保集合或数组的状态不会在其外部被修改。
public ImmutableClass( final long id, final String[] arrayOfStrings,
        final Collection<String> collectionOfString) {
    this.id = id;
    this.arrayOfStrings = Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
    this.collectionOfString = new ArrayList<>( collectionOfString );
}
最后,确保正确的访问(getter)。对于集合,必须以包装器的形式提供不变性 Collections.unmodifiableXxx:对于数组,提供真正不变性的唯一方法是提供副本而不是返回对数组的引用。从实际角度来看,这可能是不可接受的,因为它非常依赖于数组的大小,并且会给垃圾收集器带来巨大的压力。
public String[] getArrayOfStrings() {
    return Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
}
即使这个小例子也很好地说明了不变性还不是 Java 中的一等公民。如果不可变类有一个引用另一个类的对象的字段,事情就会变得复杂。这些类也应该是不可变的,但没有办法确保这一点。有几种不错的 Java 源代码分析器,例如 FindBugs 和 PMD,它们可以通过检查代码并指出常见的 Java 编程缺陷来提供很大帮助。这些工具是任何 Java 开发人员的好朋友。

7. 匿名类

在 Java 8 之前的时代,匿名类是确保类动态定义并立即实例化的唯一方法。匿名类的目的是减少样板文件并提供一种简短而简单的方法来将类表示为记录。让我们看一下在 Java 中生成新线程的典型老式方法:
package com.javacodegeeks.advanced.design;

public class AnonymousClass {
    public static void main( String[] args ) {
        new Thread(
            // Example of creating anonymous class which implements
            // Runnable interface
            new Runnable() {
                @Override
                public void run() {
                    // Implementation here
                }
            }
        ).start();
    }
}
在此示例中,接口的实现Runnable立即作为匿名类提供。尽管匿名类存在一些限制,但使用它们的主要缺点是 Java 作为一种语言必须采用的非常冗长的构造语法。即使只是一个不做任何事情的匿名类,每次编写也至少需要 5 行代码。
new Runnable() {
   @Override
   public void run() {
   }
}
幸运的是,有了 Java 8、lambda 和函数式接口,所有这些刻板印象很快就会消失,最终编写 Java 代码将看起来真正简洁。
package com.javacodegeeks.advanced.design;

public class AnonymousClass {
    public static void main( String[] args ) {
        new Thread( () -> { /* Implementation here */ } ).start();
    }
}

8. 能见度

我们已经在本教程的第 1 部分中讨论了 Java 中的可见性和可访问性规则。在这一部分中,我们将再次回顾这个主题,但是是在子类化的背景下。 设计类和接口(文章翻译)- 2不同级别的可见性允许或阻止类查看其他类或接口(例如,如果它们位于不同的包中或相互嵌套)或子类查看和访问其父类的方法、构造函数和字段。在下一节“继承”中,我们将看到它的实际应用。

9. 继承

继承是面向对象编程的关键概念之一,是构造一类关系的基础。结合可见性和可访问性规则,继承允许将类设计成可扩展和维护的层次结构。在概念层面上,Java 中的继承是使用子类化和extends关键字以及父类来实现的。子类继承父类的所有公共和受保护元素。此外,如果子类和类都位于同一个包中,则子类将继承其父类的包私有元素。话虽这么说,无论您尝试设计什么,坚持类公开公开的最小方法集或其子类都是非常重要的。例如,让我们看一下该类Parent及其子类Child,以演示可见性级别的差异及其效果。
package com.javacodegeeks.advanced.design;

public class Parent {
    // Everyone can see it
    public static final String CONSTANT = "Constant";

    // No one can access it
    private String privateField;
    // Only subclasses can access it
    protected String protectedField;

    // No one can see it
    private class PrivateClass {
    }

    // Only visible to subclasses
    protected interface ProtectedInterface {
    }

    // Everyone can call it
    public void publicAction() {
    }

    // Only subclass can call it
    protected void protectedAction() {
    }

    // No one can call it
    private void privateAction() {
    }

    // Only subclasses in the same package can call it
    void packageAction() {
    }
}
package com.javacodegeeks.advanced.design;

// Resides in the same package as parent class
public class Child extends Parent implements Parent.ProtectedInterface {
    @Override
    protected void protectedAction() {
        // Calls parent's method implementation
        super.protectedAction();
    }

    @Override
    void packageAction() {
        // Do nothing, no call to parent's method implementation
    }

    public void childAction() {
        this.protectedField = "value";
    }
}
继承本身就是一个非常大的主题,有很多 Java 特有的细节。然而,有一些规则很容易遵循,并且可以在很大程度上保持类层次结构的简洁性。在Java中,每个子类都可以重写其父类的任何继承方法,除非它被声明为final。但是,没有特殊的语法或关键字将方法标记为已重写,这可能会导致混乱。这就是引入@Override注解的原因:每当您的目标是重写继承的方法时,请使用@Override注解来简洁地指示它。Java 开发人员在设计中经常面临的另一个困境是类层次结构(具有具体或抽象类)的构造与接口的实现。我们强烈建议尽可能使用接口而不是类或抽象类。接口更轻,更容易测试和维护,并且还可以最大限度地减少实现更改的副作用。许多高级编程技术,例如在 Java 标准库中创建代理类,都严重依赖于接口。

10. 多重继承

与 C++ 和其他一些语言不同,Java 不支持多重继承:在 Java 中,每个类只能有一个直接父类(该类Object位于层次结构的顶部)。然而,一个类可以实现多个接口,因此接口堆栈是Java中实现(或模拟)多重继承的唯一方法。
package com.javacodegeeks.advanced.design;

public class MultipleInterfaces implements Runnable, AutoCloseable {
    @Override
    public void run() {
        // Some implementation here
    }

    @Override
    public void close() throws Exception {
       // Some implementation here
    }
}
实现多个接口实际上非常强大,但通常需要一遍又一遍地使用实现会导致深层的类层次结构,以此来克服 Java 缺乏对多重继承的支持。 
public class A implements Runnable {
    @Override
    public void run() {
        // Some implementation here
    }
}
// Class B wants to inherit the implementation of run() method from class A.
public class B extends A implements AutoCloseable {
    @Override
    public void close() throws Exception {
       // Some implementation here
    }
}
// Class C wants to inherit the implementation of run() method from class A
// and the implementation of close() method from class B.
public class C extends B implements Readable {
    @Override
    public int read(java.nio.CharBuffer cb) throws IOException {
       // Some implementation here
    }
}
等等...最近发布的 Java 8 在某种程度上解决了默认方法注入的问题。由于默认方法,接口实际上不仅提供了契约,还提供了实现。因此,实现这些接口的类也会自动继承这些实现的方法。例如:
package com.javacodegeeks.advanced.design;

public interface DefaultMethods extends Runnable, AutoCloseable {
    @Override
    default void run() {
        // Some implementation here
    }

    @Override
    default void close() throws Exception {
       // Some implementation here
    }
}

// Class C inherits the implementation of run() and close() methods from the
// DefaultMethods interface.
public class C implements DefaultMethods, Readable {
    @Override
    public int read(java.nio.CharBuffer cb) throws IOException {
       // Some implementation here
    }
}
请记住,多重继承是一个非常强大的工具,但同时也是危险的工具。众所周知的死亡钻石问题经常被认为是多重继承实现中的一个主要缺陷,迫使开发人员非常仔细地设计类层次结构。不幸的是,具有默认方法的 Java 8 接口也成为这些缺陷的受害者。
interface A {
    default void performAction() {
    }
}

interface B extends A {
    @Override
    default void performAction() {
    }
}

interface C extends A {
    @Override
    default void performAction() {
    }
}
例如,以下代码片段将无法编译:
// E is not compilable unless it overrides performAction() as well
interface E extends B, C {
}
在这一点上,可以公平地说,Java 作为一种语言一直试图避免面向对象编程的极端情况,但随着语言的发展,其中一些情况突然开始出现。 

11. 继承和组合

幸运的是,继承并不是设计类的唯一方法。许多开发人员认为比继承更好的另一种选择是组合。这个想法非常简单:不需要创建类的层次结构,而是需要由其他类组成。让我们看一下这个例子:
// E is not compilable unless it overrides performAction() as well
interface E extends B, C {
}
该类Vehicle由发动机和车轮组成(以及为简单起见而保留的许多其他部件)。然而,可以说类Vehicle也是一个引擎,因此可以使用继承来设计。 
public class Vehicle extends Engine {
    private Wheels[] wheels;
    // ...
}
哪种设计方案是正确的?一般核心准则称为 IS-A(是)和 HAS-A(包含)原则。IS-A是一种继承关系:子类也满足父类的类规范和父类的变体(子类)扩展其父类。如果你想知道一个实体是否扩展了另一个实体,可以做一个匹配测试 - IS -A(是)。”)因此,HAS-A 是一种组合关系:一个类拥有(或包含)一个对象,该对象在大多数情况下,HAS-A 原则比 IS-A 更有效,原因有很多: 
  • 设计更加灵活;
  • 该模型更加稳定,因为更改不会通过类层次结构传播;
  • 与将父类与其子类紧密耦合的组合相比,类及其组合是松散耦合的。
  • 类中的逻辑思路更简单,因为它的所有依赖项都包含在其中的一个位置。 
不管怎样,继承有它的地位,并以多种方式解决许多现有的设计问题,所以它不应该被忽视。在设计面向对象模型时,请记住这两种选择。

12. 封装。

面向对象编程中的封装概念是向外界隐藏所有实现细节(如操作模式、内部方法等)。封装的好处是可维护性和易于更改。类的内部实现是隐藏的,对类数据的处理仅通过类的公共方法进行(如果您正在开发一个由许多人使用的库或框架,那么这是一个真正的问题)。Java 中的封装是通过可见性和可访问性规则来实现的。在 Java 中,最佳实践是永远不要直接公开字段,而只能通过 getter 和 setter 公开字段(除非字段被标记为 Final)。例如:
package com.javacodegeeks.advanced.design;

public class Encapsulation {
    private final String email;
    private String address;

    public Encapsulation( final String email ) {
        this.email = email;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public String getEmail() {
        return email;
    }
}
这个例子让人想起 Java 语言中所谓的JavaBean:标准 Java 类是根据一组约定编写的,其中一个约定允许仅使用 getter 和 setter 方法来访问字段。正如我们在继承部分中已经强调的那样,请始终遵守类中的最小公开契约,并使用封装原则。所有不应该公开的内容都应该变为私有(或受保护/包私有,具体取决于您要解决的问题)。从长远来看,这将带来回报,让您可以自由地进行设计,而无需(或至少最大限度地减少)重大更改。 

13. 最终课程和方法

在Java中,有一种方法可以防止一个类成为另一个类的子类:另一个类必须声明为final。 
package com.javacodegeeks.advanced.design;

public final class FinalClass {
}
方法声明中相同的final 关键字可以防止子类重写该方法。 
package com.javacodegeeks.advanced.design;

public class FinalMethod {
    public final void performAction() {
    }
}
没有通用规则来决定一个类或方法是否应该是最终的。Final类和方法限制了可扩展性,并且很难提前考虑一个类是否应该被继承,或者一个方法是否应该在将来被重写。这对于库开发人员来说尤其重要,因为这样的设计决策可能会严重限制库的适用性。Java 标准库有几个 Final 类的示例,最著名的是 String 类。在早期阶段,做出这一决定是为了防止开发人员尝试提出自己的“更好”的字符串实现解决方案。 

14. 下一步是什么

在课程的这一部分中,我们介绍了 Java 中面向对象编程的概念。我们还快速浏览了合约编程,触及了一些函数概念,并了解了该语言如何随着时间的推移而演变。在本课程的下一部分中,我们将了解泛型以及它们如何改变我们在编程中处理类型安全的方式。 

15.下载源代码

您可以在此处下载源代码 - Advanced-java-part-3 源代码:如何设计类
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION