Effective Java:03 类和接口

Demon.Lee 2024年06月30日 597次浏览

本文代码实践基于 JDK-17:java 17.0.11 2024-04-16 LTS

继续上回《Effective Java:02 对所有对象都通用的方法》的分享,这次学习第 3 章:类和接口。

15 使类和成员的可访问性最小化

区分一个组件设计得好不好,唯一重要的因素在于,它对于外部的其他组件而言,是否隐藏了其内部数据和其他实现细节。

上面这句话说的是代码封装,为什么封装会是组件设计是否优秀的衡量标准?主要是因为解耦(decouple):这些组件可以独立地开发、测试、优化、使用、理解和修改。

  • 一个大的项目工程,拆解之后,可以分给不同的团队不同的人员分工协作,也就是并行开发,从而提高整体的开发速度;
  • 与此同时,由于各自负责的内容变少了,理解和维护组件的成本也就变少了;
  • 封装并不会提升当前系统的性能,但却可以提升系统未来的性能:当监测到哪些组件存在性能瓶颈时,便可以对这些组件进行独立优化,而不会影响整个系统的正确性;
  • 独立组件可以提高代码复用率,一个组件不仅可以用于 A 系统,也可以用于 B 系统,从而提升软件的可重用性;
  • ……

说到 Java 的封装隐藏,就不得不提访问修饰符:

访问修饰符 当前类 当前包 其他包的子类 其他包
private 可见
default 可见 可见
protected 可见 可见 可见
public 可见 可见 可见 可见

从上面的表格来看,Java 语言的设计者们好像已经将所有场景都考虑到了,是一个完备的定义。其实并没有,这里还有一种情况被遗漏了,那就是 Java 包之间的关系。目前的设计是要么全可见,要么全封闭,这是不合理的。A 包可以只给 B 包暴露一部分接口(比如 A1,A3),也可以只给 C 包暴露一部分接口(比如 A2,A3),而不是现在这样将所有接口(A1,A2,A3)全部暴露出去。

于是从 Java 9 开始,引入了 Java 平台模块系统(Java Platform Module System),重新定义了 Java 包之间的依赖关系

了解了访问控制符之后,接下来就是如何运用了。一条简单而实用的原则是:尽可能地使每个类或者成员不被外界访问。 也就是说,能用 private 就不要使用默认修饰符,能用 protectd 就不要使用 public。以下是一些优秀的实践:

  • 只有当一个包的另一个类需要访问当前某个方法时,才需要将 private 移除,即调整该方法的访问权限为包级私有;
  • 有时候为了便于测试,我们可能会调高类,接口,或方法的访问权限,但上限是包级私有。也就是说,一般不会仅仅为了测试,将访问权限提升到 protectedpublic,这是因为我们可以将测试代码作为被测试包的一部分;
  • 除了 public static final XxxType xxx = xxxValue; (其中的 xxxValue 是基本类型或是不可变对象的引用)这种情况外,其他情况下都不允许将公有类的实例域暴露出去(即使用 public 修饰符)。像 public static final Thing[] VALUE = …… 就是一个反面案例,一旦将这个 VALUE 暴露出去,我们就无法保证它不被修改,从而导致不可预知的后果;
  • ……

16 要在公有类而非公有域中使用访问方法

这一条说起来比较简单:公有类不应该直接暴露数据域

package java.awt;

import java.awt.geom.Point2D;
import java.beans.Transient;
import java.io.Serial;

/**
 * A point representing a location in {@code (x,y)} coordinate space,
 * specified in integer precision.
 *
 * @author      Sami Shaio
 * @since       1.0
 */
public class Point extends Point2D implements java.io.Serializable {
    /**
     * The X coordinate of this {@code Point}.
     * If no X coordinate is set it will default to 0.
     *
     * @serial
     * @see #getLocation()
     * @see #move(int, int)
     * @since 1.0
     */
    public int x;

    /**
     * The Y coordinate of this {@code Point}.
     * If no Y coordinate is set it will default to 0.
     *
     * @serial
     * @see #getLocation()
     * @see #move(int, int)
     * @since 1.0
     */
    public int y;
 
    // ...
    // ...
}

java.awt.Point 中直接暴露 x, y 两个域,是 JDK 中的一个反面案例。当然,我们也不用墨守成规,如果确实需要使用包级私有类私有的嵌套类来暴露域,那也是可行的:这些域确实描述了该类所提供的抽象。毕竟,与调用方法相比,直接访问这些成员变量的代码有更好的可读性。但需要强调的是,这不应该是一个常规操作。

17 使可变性最小化

当一个类被实例化后,该实例就不能再被修改,我们称其为不可变类。很容易想到的是,当我们将类中需要使用到的数据都通过构造函数进行初始化,并且后续不提供修改这些变量的方法,那整个类基本上就是不可变的。在 JDK 中有很多不可变类的实践,比如基本类型的包装类 Double, Long 等,再比如最常使用的 String 等。

public final class Double extends Number
        implements Comparable<Double>, Constable, ConstantDesc {
  
    // ...
    // ...

    /**
     * The value of the Double.
     *
     * @serial
     */
    private final double value;
  
    // ...
    // ...
}

不过为了严谨,我们来看看成为不可变类必须满足的条件:

1)不提供任何会修改对象状态的方法,最常见的就是各种 setter 方法;

2)类不能被扩展,也就是不能被子类化;

3)类中所有的域都是 final 的,也就是一旦被赋值就不能修改;

4)声明所有的域都为私有的。这样可以防止客户端获得访问被域引用的可变对象的权限,并防止客户端直接修改这些对象;

5)确保对于任何可变组件的互斥访问。如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。

对于最后一条,如果类中某个域是可变对象,暴露出去后就极有可能被修改,所以需要对其进行处理,比如下面的 getBirthDate 方法就是一种处理方式:

import java.util.Date;

public final class ImmutablePerson {
    private final String name;
    private final Date birthDate;

    public ImmutablePerson(String name, Date birthDate) {
        this.name = name;
        this.birthDate = new Date(birthDate.getTime());
    }

    public String getName() {
        return name;
    }

    public Date getBirthDate() {
        // 返回一个拷贝,防止外部修改内部的Date对象
        return new Date(birthDate.getTime());
    }
}

再比如下面的 Complex 类,当进行加减乘除时,不会改变当前对象的值,而是返回一个新的对象实例。

public final class Complex {
  private final double re;
  private final double im;

  public Complex(double re, double im) {
    this.re = re;
    this.im = im;
  }

  // 两个复数相加,返回一个新的对象实例
  public Complex plus(Complex c) {
    return new Complex(re + c.re, im + c.im);
  }

  // ...
  // ...
}

这里之所以使用介词(plus)作为方法名,而不使用动词(比如 add),就是为了强调这个方法不会改变对象的值。这类方法又被称为函数(Functional)的方法,用于区别过程的(procedural)或者命令式的(imperative)方法。命令式编程就是将某个运算过程作用在它们的操作数上,从而改变它的状态。

由于不可变对象一旦初始化后就不会改变,所以它是线程安全的,因而这些对象可以被自由地共享。于是我们可以缓存一些经常使用的对象实例,比如:

public static final Complex ZERO = new Complex(0,0);
public static final Complex ONE  = new Complex(1,0);

因为可以自由地共享这些不可变对象,因而这些类完全不需要提供 clone 方法或拷贝构造器,像 String 类还有拷贝构造器完全是因为早期不可变对象没有被大家所理解,才一直保留了下来(为了向后兼容)。

除了上面的方式,其实还有一种方法可以让 Complex 成为不可变类:

public class Complex {
  private final double re;
  private final double im;

  // 私有构造函数
  private Complex(double re, double im) {
    this.re = re;
    this.im = im;
  }

  // 静态工厂方法代替公有构造函数
  public static Complex value0f(double re, double im) {
    return new Complex(re,im);
  }

  // 两个复数相加,返回一个新的对象实例
  public Complex plus(Complex c) {
    return new Complex(re + c.re, im + c.im);
  }

  // ...
  // ...
}

因为私有构造函数的缘故,类即使不用 final 修饰,也是无法进行子类扩展的。

那不可变类有缺点吗?一个经常被人提及的话题就是性能。但这一点,其实无需过多关注,当系统真有性能问题时,再考虑优化也不晚。当然,如果不可变类对象是大型对象,或者构造这个对象需要耗费较高的成本,那确实需要关注。毕竟,在不可变对象中,每一个不同的值都是一个独立的对象,一旦这种对象很多,就会影响系统性能。所以,如果可以,我们应该总是使一些小的值对象,比如 PhoneNumberComplex,成为不可变的。

最后,对于某些类而言,其不可变性是不切实际的。如果类不能被做成不可变的,仍然应该尽可能地限制它的可变性。

18 复合优先于继承

对软件设计稍微了解一点的话,基本上都听过这一条:组合(复合)优于继承。这里主要的问题在于,继承破坏了封装性。当我们继承一个超类时,大概率会用到超类中特定的实现细节,一旦超类在后续版本中调整,相关子类就不得不跟着调整。此时,子类是脆弱的。

有时候,在超类中增加新方法也有可能导致子类无法编译。比如子类中已经有了方法 void a(); ,但超类开发人员可能不知道,便在超类中增加了方法 String a(); 。即便超类中增加的方法与子类方法完全相同,即 void a(); ,也可能会出 bug,甚至比编译不通过(编译时)酿成更大的生产事故(运行时)。因为子类方法的覆盖完全是无意的,不易被发现,而其实现可能与超类天差地别。

为了解决上面的问题,一种叫做组合的设计便出现了:

private static class UnmodifiableMap<K,V> implements Map<K,V>, Serializable {

  @SuppressWarnings("serial") // Conditionally serializable
  final Map<? extends K, ? extends V> m;

  UnmodifiableMap(Map<? extends K, ? extends V> m) {
    if (m==null)
      throw new NullPointerException();
    this.m = m;
  }

  public int size()                        {return m.size();}
  public boolean isEmpty()                 {return m.isEmpty();}
  public boolean containsKey(Object key)   {return m.containsKey(key);}
  public boolean containsValue(Object val) {return m.containsValue(val);}
  public V get(Object key)                 {return m.get(key);}

  public V put(K key, V value) {
    throw new UnsupportedOperationException();
  }

  public V remove(Object key) {
    throw new UnsupportedOperationException();
  }

  // ...
}

如上代码示例所示,UnmodifiableMap 并不继承任何 Map 实现类,而是将某个 Map 实例作为当前类的一个私有域。可以看到,UnmodifiableMap 可以调用 Map 实例中的任何方法,但又不会依赖对方的任何具体实现,从而避免了前面继承中的各种弊端。由于这种类只是将原有类实例进行了简单包装,故又被称为包装类,包装类中调用对应实例的某个方法则被称为转发。

那什么时候需要使用继承呢?只有当 B 和 A 确实存在 is-a 的关系时,类 B 才会扩展类 A。也是说,当你想使用继承时,你就问问自己,每个 B 确实是 A 吗?如果不是或答案不确定,那就不要使用。当我们将 A 作为 B 的一个私有域时,这背后隐藏地逻辑其实是:A本质上不是 B 的一部分,只是它的实现细节而已。

JDK 中一些反面案例包括:

  • Stack extends Vector :但栈不是向量;
  • Properties extends Hashtable :但属性列表并不是散列表;
  • ……

19 要么设计继承并提供文档说明,要么禁止继承

对于包内使用继承,一般来说是安全的,因为这些类不会被外部访问到,即使后续变更,其影响范围也仅波及当前包内的类。而对于普通的具体类,当它们跨越包边界进行继承时,则比较危险。这在上一条中已经讨论过了。如果确实需要跨包进行继承呢?那就提供详细的说明文档。

public abstract class AbstractCollection<E> implements Collection<E> {

   // ...
   // ...

   /**
    * {@inheritDoc}
    *
    * @implSpec
    * This implementation iterates over the collection looking for the
    * specified element.  If it finds the element, it removes the element
    * from the collection using the iterator's remove method.
    *
    * <p>Note that this implementation throws an
    * {@code UnsupportedOperationException} if the iterator returned by this
    * collection's iterator method does not implement the {@code remove}
    * method and this collection contains the specified object.
    *
    * @throws UnsupportedOperationException {@inheritDoc}
    * @throws ClassCastException            {@inheritDoc}
    * @throws NullPointerException          {@inheritDoc}
    */
    public boolean remove(Object o) {
      Iterator<E> it = iterator();
      if (o==null) {
        while (it.hasNext()) {
          if (it.next()==null) {
            it.remove();
            return true;
          }
        }
      } else {
        while (it.hasNext()) {
          if (o.equals(it.next())) {
            it.remove();
            return true;
          }
        }
      }
      return false;
    }

    // ...
    // ...
}

如上面代码示例中的 remove 方法,其 Java Doc 就提供了专门说明,即 @implSpec 注解部分。

@implSpec 标签是在 Java 8 中增加的,在 Java 9 中得到了广泛应用。这个标签应该是默认可用的,但是到 Java 9,Java Doc 工具仍然把它忽略,除非传入命令行参数: -tag "apiNote:a:API Note:"

如果总结一下这里文档的具体要求,那就是:针对超类中的相关受保护(或公共)方法(包括构造器,实例方法,我们用 A 表示),需要说明这些方法调用了哪些可覆盖的方法(我们用 B 表示),调用的顺序是什么,如果在子类中覆盖这些被调用的可覆盖方法(即 B),会产生什么后果。

我们经常说,方法注释需要描述做什么,而不是怎么做,但这里却将实现细节都描述出来,显然是与软件中的封装相悖的。没错,这就是使用继承带来的副作用。所以,在设计可重写的方法时,设计人员需要认真思考,以提供出合适的钩子(hook),让子类精准覆盖。这其实是有一定难度的,那有可以借鉴的技巧吗?很遗憾,没有。我们唯一能做的,就是思考并发挥想象力,然后多写一些子类进行反复测试。而前面总结的可访问性最小化的原则在这里依然适用,即若无必要,方法都要设计成私有的。

另外,当确认需要使用继承时,在超类的构造函数、 clone 方法(超类实现了 Cloneable 接口)或是 readObject 方法(超类实现了 Serializable 接口)中都不能调用可覆盖方法,因为这些方法在子类执行时,都有可能出现错乱(比如执行两次等),从而导致不可预知的问题。

最后,如果某个类并不是为了继承而设计的,但可能有人也将其当作超类使用。为了避免继承中出现的各类问题,我们可以将类设计成不可子类化,其具体实现在第 17 条中已经阐述过了。

20 接口优于抽象类

关于这一条,笔者之前专门总结过,即这篇文章:接口 vs 抽象类,故就不再进行更多的讨论。

书中花了不少篇幅介绍骨架类。什么是骨架类?其实就是对接口的实现提供一个默认实现,在设计模式中又被称为模板模式。像 JDK 中的 AbstractList,AbstractCollection,AbstractMap 等,都是骨架类:

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
  /**
     * Sole constructor.  (For invocation by subclass constructors, typically
     * implicit.)
     */
  protected AbstractList() {
  }
  // ...
  // ...
}

public class ArrayList<E> extends AbstractList<E>
  implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
  // ...
  // ...
}

骨架实现类的美妙之处在于,它们为抽象类提供了实现上的帮助,但又不强加“抽象类被用作类型定义时”所特有的严格限制。

由于骨架类就是为了继承而设计的,所以上一条的相关规则也适用于它。

21 为后代设计接口

这一条比较简单,主要讨论了 Java 8 之后,可以通过在接口中添加缺省方法(default method)来扩展接口的利弊。简单来说就是,即使使用缺省方法,也会有一定的风险。比如,某些类没有对缺省方法进行同步更新,导致与类本身的承诺相违背:

public interface Collection<E> extends Iterable<E> {
  //...
  //...
  default boolean removeIf(Predicate<? super E> filter) {
    Objects.requireNonNull(filter);
    boolean removed = false;
    final Iterator<E> each = iterator();
    while (each.hasNext()) {
      if (filter.test(each.next())) {
        each.remove();
        removed = true;
      }
    }
    return removed;
  }

  //...
  //...
}

public class SynchronizedCollection<E> implements Collection<E>, Serializable {
  //...
  //...
  @Override
  public boolean remove(final Object object) {
    synchronized (lock) {
      return decorated().remove(object);
    }
  }

  @Override
  public boolean removeAll(final Collection<?> coll) {
    synchronized (lock) {
      return decorated().removeAll(coll);
    }
  }
  
  // 未重写 removeIf 方法

  //...
  //...
}

这个例子说的是,Java 8 为 Collection 接口添加了 removeIf 等方法,但第三方的 apache 类库 commons-collections4 中的 SynchronizedCollection 却没有去实现它,这就导致当有客户端使用 SynchronizedCollection 的实例调用 removeIf 方法时,它调用的还是缺省实现,这显然与 SynchronizedCollection 类的初衷是相悖的。

所以,当需要添加缺省方法时,先问自己一个问题:新增该接口是否会破坏现有的接口实现?

22 接口只用于定义类型

接口用于定义类型的意思是:当类实现某个接口时,该接口可以充当这个类实例的类型。而接口又代表着行为,也就是这个类拥有某种能力。但接口用于定义类型,则告诉我们,不要用接口来干其他事,比如常量定义:

package java.io;

/**
 * Constants written into the Object Serialization Stream.
 *
 * @author  unascribed
 * @since JDK 1.1
 */
public interface ObjectStreamConstants {

    /**
     * Magic number that is written to the stream header.
     */
    final static short STREAM_MAGIC = (short)0xaced;

    /**
     * Version number that is written to the stream header.
     */
    final static short STREAM_VERSION = 5;

    //...
    //...

}

这是因为类在内部使用某些常量纯粹是实现细节,不应该轻易暴露到外部,上面的 ObjectStreamConstants 就是一个反例。如果确实需要使用一些常量:

  • 最优:将其放在对应类或接口中,供内部使用;
  • 次优:将某些有关系的常量封装一个枚举类;
  • 最后:将这些常量封装到一个不可实例化的工具类中,比如下面的 PhysicalConstants
public class PhysicalConstants {

  private PhysicalConstants(){
  }

  public static final double AVOGADROS_NUMBER = 6.022_140_857e23;

  //...
  //...

}

23 类层次优于标签类

什么是标签类?看下面的代码示例:

public class Figure {
  enum Shape { RECTANGLE,CIRCLE };

  //Tag field - the shape of this figure
  final Shape shape;

  //These fields are used only if shape is RECTANGLE
  double length;
  double width;

  //This field is used only if shape is CIRCLE
  double radius;

  //Constructor for circle
  Figure(double redius){
    shape=Shape.CIRCLE;
    this.radius=radius;
  }

  //Constructor for rectangle
  public Figure (double lenght,double width) {
    shape=Shape.RECTANGLE;
    this.length=lenght;
    this.width=width;
  }

  double area(){
    switch (shape){
      case RECTANGLE:return length*width;
      case CIRCLE:return Math.PI*(radius * radius);
      default:throw new AssertionError();
    }
  }
}

很显然,这个类里面塞了太多的东西,如枚举类,标签域以及 switch 语句等。像 RECTANGLE,CIRCLE 就是标签,它们其实是平级概念,将它们放在一个类里面就会显得非常臃肿,职责划分也不清晰,比如 radius 域只有在 CIRCLE 中才有效,那当类在表示 RECTANGLE 时,它就是多余的。

换句话说,标签类正是对类层次的一种简单仿效 ,并且是一种拙劣的效仿,而我们要做的就是将其重构为类层次风格,即子类化:

abstract class Figure {
  abstract double area();
}

class Circle extends Figure {
  final double radius;

  public Circle(double radius) {
    this.radius = radius;
  }

  double area() {
    return Math.PI * (radius * radius);
  }
}

class Rectangle extends Figure {
  final double length;
  final double width;

  public Rectangle(double length, double width) {
    this.length = length;
    this.width = width;
  }

  double area() {
    return length * width;
  }
}

划分后,每个类都各司其职,而抽象类中则只有与标签无关的公共行为。由此,原先的 switch 语句也便消失无踪了。

24 静态成员类优于非静态成员类

嵌套类,顾名思义就是嵌套在一个类文件中的类。既然是嵌套,那目的也很明显,就是为它的外围类服务。嵌套类包括下面几种:

1)静态成员类(static member class)

public class OuterClass {
  private static int outerStaticVar = 10;
  private int outerInstanceVar = 20;

  static class StaticNestedClass {
    void display() {
      System.out.println("Outer static variable: " + outerStaticVar);
      // 不能直接访问外围类的实例成员,需要拥有外围类的实例才行
      // System.out.println("Outer instance variable: " + outerInstanceVar); // Error
    }
  }

  public static void main(String[] args) {
    OuterClass.StaticNestedClass nestedObject = new OuterClass.StaticNestedClass();
    nestedObject.display();
  }
}

静态成员类与普通类没什么区别,只是正好将其放在了某个外围类的文件之中。但它可以访问外围类的所有成员。一般来说,静态成员类的作用是作为外围类的辅助类,协助其完成相关业务逻辑。下面是 HashMap 中的 Node 类:

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    //...
    //...

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;

            return o instanceof Map.Entry<?, ?> e
                    && Objects.equals(key, e.getKey())
                    && Objects.equals(value, e.getValue());
        }
    }

    //...

    transient Node<K,V>[] table;

    //...
}

2)非静态成员类(nonstatic member class)

public class OuterClass {
  private int outerInstanceVar = 30;

  class InnerClass {
    void display() {
      System.out.println("Outer instance variable: " + outerInstanceVar);
    }
  }

  public static void main(String[] args) {
    OuterClass outerObject = new OuterClass();
    OuterClass.InnerClass innerObject = outerObject.new InnerClass();
    innerObject.display();
  }
}

非静态成员类与静态成员类虽然只差一个 static 修饰符,但二者却有相当大的的差异。非静态成员类实例化后,默认会有一个指向外围类的引用,并且这个引用一旦建立就不会更改。显然,这很容易造成循环依赖,使内存垃圾回收难度增加,笔者在Java 之 finalize 终结一文中也曾到过静态内部类可能会导致内存泄露。

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    //...
    //...
    final class KeySet extends AbstractSet<K> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        public final Iterator<K> iterator()     { return new KeyIterator(); }
        public final boolean contains(Object o) { return containsKey(o); }
        //...
        //...
    }

    //...
    public Set<K> keySet() {
        Set<K> ks = keySet;
        if (ks == null) {
            ks = new KeySet();
            keySet = ks;
        }
        return ks;
    }
    //...
}

同样是 HashMap 中的内部类,KeySet 就是一个非静态成员类,它的作用可以理解为一个适配器,利用外围类的原始数据进行数据组装或转换,比如这里的集合视图。

3)匿名类(anonymous class)

interface Greeting {
  void greet();
}

public class OuterClass {
  public Greeting createAnonymousClass() {
    Greeting greeting = new Greeting() {
      @Override
      public void greet() {
        System.out.println("Hello from Anonymous Class");
      }
    };
    return greeting;
  }

  public static void main(String[] args) {
    OuterClass outerObject = new OuterClass();
    Greeting greeting = outerObject.createAnonymousClass();
    greeting.greet();
  }
}

匿名类,就是在使用时才创建的类,即声明时实例化。除此之外,我们无法对其进行实例化,基于此,使用它的时候就有了相关限制:

  • 不能用作 instanceof 测试;
  • 不能实现某个接口,或继承某个类;
  • 除了从超类中继承的成员之外,客户端无法使用任何成员;
  • 匿名类要简短( 10 行或更少),否则在表达式中会影响阅读体验;
  • ……

在 Java 引入 Lambda 之前,匿名类是动态创建小型函数对象和过程对象的最佳方式。

4)局部类(local class)

public class OuterClass {
  public void testLocalClass() {
    class LocalClass {
      void display() {
        System.out.println("Hello from Local Class");
      }
    }

    LocalClass localObject = new LocalClass();
    localObject.display();
  }

  public static void main(String[] args) {
    OuterClass outerObject = new OuterClass();
    outerObject.testLocalClass();
  }
}

局部类是使用最少的一种内部类,在任何可以定义局部变量的地方都可以定义局部类,并且遵守相同的作用域规则。局部类同时拥有前面三种内部类的相关特性,由于使用的不多,这里就不再赘述。

简而言之,每一种内部类都有相应的用途,我们要根据应用场景择优选择。当然,能用静态成员类,就不要用非静态成员类。

25 限制源文件为单个顶级类

这一条应该是每个开发人员的共识,所以就不做过多讨论。简单来说就是,不要将多个顶级类放到同一个 Java 文件中,至于这样做的问题,显而易见:没有可读性,没有可维护性,像一团卷在一起的毛线,盘根错节,根本无从下手。


好,类和接口的相关实践就简单总结到这里, 下期再见。