如何设计类和接口

本文是我们名为“ 高级Java ”的学院课程的一部分。

本课程旨在帮助您最有效地使用Java。 它讨论了高级主题,包括对象创建,并发,序列化,反射等。 它将指导您完成Java掌握的过程! 在这里查看

1.简介

不管您使用哪种编程语言(这里Java也不例外),遵循良好的设计原则是编写简洁,可理解,可测试的代码并提供长期有效且易于维护的解决方案的关键因素。 在本教程的这一部分中,我们将讨论Java语言提供的基础构建块,并介绍一些设计原则,以帮助您做出更好的设计决策。

更确切地说,我们将讨论与默认的方法 接口接口 (Java 8的新功能), 抽象final 类,不可变类,继承,组成和重温了一下可见性 (或可访问)的规则,我们有部分短暂触及教程1如何创建和销毁对象

2.接口

在面向对象的编程中,接口的概念构成了契约驱动(或基于契约)开发的基础。 简而言之,接口定义方法的集合(契约),并且声称支持该特定接口的每个类都必须提供这些方法的实现:一个非常简单但功能强大的想法。

许多编程语言确实具有一种形式或另一种形式的接口,但是Java特别为此提供了语言支持。 让我们看一下Java中的简单接口定义。

package com.javacodegeeks.advanced.design;

public interface SimpleInterface {
    void performAction();
}

在上面的代码片段中,我们命名为SimpleInterface的接口SimpleInterface声明了一个名为performAction方法。 接口相对于类的主要区别在于接口概述了联系方式(声明方法),但未提供其实现。

但是,Java中的接口可能比这更复杂:它们可以包括嵌套的接口,类,枚举,注释(该枚举和注释将在本教程的第5部分中详细介绍如何以及何时使用Enums和Annotations )和常量。 。 例如:

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 ,有关可见性和可访问性规则的更多详细信息,请参阅Visibility部分)。 因此,以下方法声明是等效的:

public void performAction();
void performAction();

值得一提的是,接口中的每个方法都隐式声明为抽象 ,甚至这些方法声明也是等效的:

public abstract void performAction();
public void performAction();
void performAction();

至于常量字段声明,除了是public ,它们是隐式staticfinal因此以下声明也等效:

String CONSTANT = "CONSTANT";
public static final String CONSTANT = "CONSTANT";

最后,除了public之外,嵌套类,接口或枚举还隐式声明为static 。 例如,这些类声明也等效:

class InnerClass {
}

static class InnerClass {
}

您将选择哪种样式是个人喜好,但是了解这些简单的界面质量可以使您避免不必要的键入。

3.标记接口

标记接口是一种特殊的接口,没有定义任何方法或其他嵌套结构。 在本教程的第2部分中 ,我们已经看到了标记接口的一个示例,该接口使用所有对象共有的方法 ,即Cloneable接口。 这是在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
    }
}

作为实例级别,默认方法可以被每个接口实现者覆盖,但是从现在开始,接口还可以包括static方法,例如:

package com.javacodegeeks.advanced.design;

public interface InterfaceWithDefaultMethods {
    static void createAction() {
        // Implementation here
    }
}

可能有人说在接口中提供实现会破坏基于契约的开发的全部目的,但是将这些功能引入Java语言有很多原因,无论它们多么有用或令人困惑,它们都可以为您提供帮助采用。

功能接口是另外一个故事,并且事实证明它们是该语言的非常有用的附加组件。 基本上,功能接口是仅声明了一个抽象方法的接口。 Java标准库中的Runnable接口就是这个概念的一个很好的例子:

@FunctionalInterface
public interface Runnable {
    void run();
}

Java编译器以不同的方式对待功能接口,并且能够将lambda函数转换为有意义的功能接口实现。 让我们看一下以下函数定义:

public void runMe( final Runnable r ) {
    r.run();
}

要在Java 7及以下版本中调用此函数,应提供Runnable接口的实现(例如,使用Anonymous类 ),但是在Java 8中,使用lambda语法传递run()方法实现就足够了:

runMe( () -> System.out.println( "Run!" ) );

此外, @FunctionalInterface批注(批注将在本教程的第5部分“ 如何以及何时使用Enums和Annotations”中详细介绍 )提示编译器验证接口仅包含一种抽象方法,以便对接口中引入的任何更改未来将不会打破这一假设。

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被声明为abstract并且还具有一个abstract方法声明。 当实现细节的大部分甚至一部分可以被许多子类共享时,抽象类非常有用。 但是,它们仍然敞开了大门,并允许通过抽象方法自定义每个子类的固有行为。

值得一提的是,与只能包含public声明的接口相反,抽象类可以使用可访问性规则的全部功能来控制抽象方法的可见性(有关更多详细信息,请参见“ 可见性继承 ”部分)。

6.不可变的类

不可变性在当今的软件开发中变得越来越重要。 多核系统的兴起引起了很多与数据共享和并发相关的担忧(在第9部分并发最佳实践中 ,我们将详细讨论那些主题)。 但是,肯定会出现一件事:可变状态的减少(甚至不存在)导致更好的可伸缩性和系统的简化推理。

不幸的是,Java语言没有为类不变性提供强大的支持。 但是,结合使用多种技术,可以设计不可变的类。 首先,该类的所有字段都应该是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 );
}

最后,提供适当的访问器(获取器)。 对于集合,不可变视图应使用Collections.unmodifiableXxx包装器公开。

public Collection<String> getCollectionOfString() {
    return Collections.unmodifiableCollection( collectionOfString );
}

对于数组,确保真正的不变性的唯一方法是提供副本,而不是返回对数组的引用。 从实际的角度来看,这可能是不可接受的,因为它在很大程度上取决于数组的大小,并可能给垃圾收集器带来很大压力。

public String[] getArrayOfStrings() {
    return Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
}

即使是这个很小的例子,也提供了一个很好的主意,即不变性还不是Java中的一等公民。 如果一个不可变的类具有引用另一个类实例的字段,那么事情就会变得非常复杂。 这些类也应该是不可变的,但是没有简单的方法可以强制执行。

有很多很棒的Java源代码分析器,例如FindBugsPMD ,它们可以通过检查代码并指出常见的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可见性和可访问性规则。 在这一部分中,我们将再次回到该主题,但要涉及子类化。

修饰符 子类 其他所有人
上市 无障碍 无障碍 无障碍
受保护的 无障碍 无障碍 无法访问
<无修饰符> 无障碍 无法访问 无法访问
私人的 无法访问 无法访问 无法访问

表格1

不同的可见性级别允许或不允许这些类查看其他类或接口(例如,如果它们位于不同的程序包中或彼此嵌套)或子类查看和访问其父级的方法,构造函数和字段。

在下一节Inheritance中 ,我们将看到它的作用。

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 (请参阅Final类和方法部分 )。

但是,没有特殊的语法或关键字来将方法标记为已重写,这可能会引起很多混乱。 这就是引入@Override批注的原因:每当您打算覆盖继承的方法时,请始终使用@Override批注进行指示。

Java开发人员在设计中经常面临的另一个难题是构建类层次结构(使用具体或抽象类)与接口实现。 强烈建议尽可能使用接口而不是类或抽象类。 接口轻巧得多,易于测试(使用模拟)和维护,并且使实现更改的副作用最小化。 许多高级编程技术(例如在标准Java库中创建类代理)都严重依赖于接口。

10.多重继承

与C ++和其他一些语言相比,Java不支持多重继承:在Java中,每个类都只有一个直接父级( Object类位于层次结构的顶部,正如我们在本教程的第2部分中已经知道的那样, 使用通用方法)。到所有对象 )。 但是,该类可以实现多个接口,因此,堆叠接口是在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.继承与组成

幸运的是,继承不是设计类的唯一方法。 许多开发人员认为比继承更好的另一种选择是组合。 这个想法很简单:这些类应该由其他类组成,而不是建立类层次结构。

让我们看一下这个例子:

public class Vehicle {
    private Engine engine;
    private Wheels[] wheels;
    // ...
}

Vehicle类由enginewheels (加上许多其他零件,为简单起见而保留)。 但是,可以说Vehicle类也是一种引擎,因此可以使用继承进行设计。

public class Vehicle extends Engine {
    private Wheels[] wheels;
    // ...
}

哪个设计决定是正确的? 一般准则称为IS-AHAS-A原则。 IS-A是继承关系:子类还满足父类规范以及父类的此类IS-A变体。 因此, HAS-A是组成关系:该类拥有(或HAS-A )属于它的对象。 在大多数情况下, HAS-A原理比IS-A更好,其原因有两个:

  • 设计可以更改的方式更加灵活
  • 该模型更加稳定,因为更改不会通过类层次结构传播
  • 与继承紧密关联父级及其子类的继承相比,该类及其组合之间的关联松散
  • 关于类的推理更加简单,因为所有相关性都包含在其中

但是,继承有其自己的位置,以不同的方式解决了实际的设计问题,因此不应忽略。 在设计面向对象的模型时,请牢记这两种选择。

12.封装

面向对象编程中的封装概念全是向外界隐藏实现细节(如状态,内部方法等)。 封装的好处是可维护性和易于更改。 内在细节类暴露的越少,开发人员对更改其内部实现的控制就越多,而不必担心破坏现有代码(如果您正在开发许多人使用的库或框架,这将是一个真正的问题)。

Java中的封装是使用可见性和可访问性规则实现的。 在Java中,最好不要使用getter和setter方法(如果未将字段声明为final )直接暴露字段,这是Java的最佳实践。 例如:

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语言中所谓的JavaBeans :通过遵循一组约定编写的常规Java类,其中一种仅允许使用getter和setter方法访问字段。

正如我们在“ 继承”部分中已强调的那样,请始终遵循封装原则,尝试使类公共契约保持最小。 无论什么都不应该是public ,而应该是private (或protected / package private ,这取决于您要解决的问题)。 从长远来看,它将带来回报,使您可以自由开发设计而无需引入重大更改(或至少将更改减至最少)。

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.下载源代码

这是关于如何设计类和接口的课程。

翻译自: https://www.javacodegeeks.com/2015/09/how-to-design-classes-and-interfaces.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值