IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Effective Java 第四章、类和接口 -> 正文阅读

[Java知识库]Effective Java 第四章、类和接口

第四章、类和接口

? 类和接口是 Java 编程语言的核心,它们也是 Java 语言的基本抽象单元。 Java 语言提供了许多强大的基本元素,供程序员用来设计类和接口。本章阐述的一些指导原则,可以帮助你更好地利用这些元素,设计出更加有用、健壮和灵活的接口。

1、使类和成员的可访问性最小化
2、在公共类中使用访问方法而不是公共属性
3、使可变性最小化
4、复合优于继承
5、要么设计继承,并提供文档说明,要么进制继承
6、接口优于抽象类
7、为后代设计接口
8、接口只用于定义类型
9、类层次优先于标签
10、静态成员类优于非静态成员类
11、限制源文件为单个顶级类


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

? 区分一个组件设计得好不好,唯一重要的因素在于,它对于外部的其他组件而言,是否隐藏了其内部数据和其他实现细节。设计良好的组件会隐藏其所有的实现细节,把 API 与实现清晰地隔离开来。然后组件之间只通过 API 进行通信,一个模块不需要知道其他模块内部的工作情况。这个概念被称为信息隐藏或封装,是软件设计的基本原则之一。

? 尽可能地使每个类或者成员不背外界访问

? 对于顶层(非嵌套的)类和接口,只有两个可能的访问级别:包级私有(package-private)和公共的(public)。如果你使用 public 修饰符声明顶级类或接口,那么它是公开的;否则,它是包级私有的。

? 如果一个包级私有顶级类或接口只被一个类使用,那么可以考虑这个类作为使用它的唯一类的私有静态嵌套类(条目 24)。这将它的可访问性从包级的所有类减少到使用它的一个类。

? 对于成员 (属性、方法、嵌套类和嵌套接口),有四种可能的访问级别,在这里,按照可访问性从小到大列出:

private —— 该成员只能在声明它的顶级类内访问。

package-private —— 成员可以从被声明的包中的任何类中访问。从技术上讲,如果没有指定访问修饰符 (接口成员除外,它默认是公共的),这是默认访问级别。

protected —— 成员可以从被声明的类的子类中访问(受一些限制,JLS,6.6.2),以及它声明的包中的任何类。

public —— 该成员可以从任何地方被访问。


第 16 条:在公共类中使用访问方法而不是公共属性

? 有时候,你可能会试图写一些退化的类(degenerate classes),除了集中实例属性之外别无用处:

// Degenerate classes like this should not be public!
class Point {
  public double x;
  public double y;
}

由于这些类的数据属性可以直接被访问,因此这些类不提供封装的好处(条目 15)。 如果不更改 API,则无法更改其表示形式,无法强制执行不变量,并且在访问属性时无法执行辅助操作。 坚持面向对象的程序员觉得这样的类是厌恶的,应该被具有私有属性和公共访问方法的类(getter)所取代,而对于可变类来说,它们应该被替换为 setter设值方法:

// Encapsulation of data by accessor methods and mutators
class Point {
  private double x;
  private double y;
    
  public Point(double x, double y) {
    this.x = x;
    this.y = y;
 }
    
  public double getX() { return x; }
  public double getY() { return y; }
    
  public void setX(double x) { this.x = x; }
  public void setY(double y) { this.y = y; }
}

第 17 条:使可变性最小化

? 不可变类简单来说是它的实例不能被修改的类。 包含在每个实例中的所有信息在对象的生命周期中是固定的,因此不会观察到任何变化。 Java 平台类库包含许多不可变的类,包括 String 类,基本类型包装类以及BigInteger 类和 BigDecimal 类。 有很多很好的理由:不可变类比可变类更容易设计,实现和使用。 他们不太容易出错,更安全。

? 要使一个类不可变,请遵循以下五条规则:

1、不要提供修改对象状态的方法(也称为 mutators)。

2、确保这个类不能被继承。 这可以防止粗心的或恶意的子类,假设对象的状态已经改变,从而破坏类的不可变行为。 防止子类化通常是通过 final 修饰类,但是我们稍后将讨论另一种方法。

3、把所有属性设置为 final。 通过系统强制执行,清楚地表达了你的意图。 另外,如果一个新创建的实例的引用从一个线程传递到另一个线程而没有同步,就必须保证正确的行为,正如内存模型[JLS,17.5; Goetz06,16] 所述。

4、把所有的属性设置为 private。 这可以防止客户端获得对属性引用的可变对象的访问权限并直接修改这些对象。 虽然技术上允许不可变类具有包含基本类型数值的公共 final 属性或对不可变对象的引用,但不建议这样做,因为它不允许在以后的版本中更改内部表示(条目 15 和 16)。

5、确保对任何可变组件的互斥访问。 如果你的类有任何引用可变对象的属性,请确保该类的客户端无法获得对这些对象的引用。 切勿将这样的属性初始化为客户端提供的对象引用,或从访问方法返回属性。 在构造方法,访问方法和 readObject 方法(条目 88)中进行防御性拷贝(条目 50)。

? 以前条目中的许多示例类都是不可变的。 其中这样的类是条目 11 中的 PhoneNumber 类,它具有每个属性的访问方法(accessors),但没有相应的设值方法(mutators)。 这是一个稍微复杂一点的例子:

// Immutable complex number class
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 double realPart() {
    return re;
 }
  public double imaginaryPart() {
    return im;
 }
  public Complex plus(Complex c) {
    return new Complex(re + c.re, im + c.im);
 }
  public Complex minus(Complex c) {
    return new Complex(re - c.re, im - c.im);
 }
  public Complex times(Complex c) {
    return new Complex(re * c.re - im * c.im,
        re * c.im + im * c.re);
 }
  public Complex dividedBy(Complex c) {
    double tmp = c.re * c.re + c.im * c.im;
    return new Complex((re * c.re + im * c.im) / tmp,
       (im * c.re - re * c.im) / tmp);
 }
  @Override
  public boolean equals(Object o) {
    if (o == this) {
      return true;
   }
    if (!(o instanceof Complex)) {
      return false;
   }
    Complex c = (Complex) o;
    // See page 47 to find out why we use compare instead of ==
    return Double.compare(c.re, re) == 0
        && Double.compare(c.im, im) == 0;
 }
  @Override
  public int hashCode() {
    return 31 * Double.hashCode(re) + Double.hashCode(im);
 }
  @Override
  public String toString() {
    return "(" + re + " + " + im + "i)";
 }
}

? 这个类代表了一个复数(包含实部和虚部的数字)。 除了标准的 Object 方法之外,它还为实部和虚部提供访问方法,并提供四个基本的算术运算:加法,减法,乘法和除法。 注意算术运算如何创建并返回一个新的Complex 实例,而不是修改这个实例。 这种模式被称为函数式方法,因为方法返回将操作数应用于函数的结果,而不修改它们。 与其对应的过程(procedural)或命令(imperative)的方法相对比,在这种方法中,将一个过程作用在操作数上,导致其状态改变。 请注意,方法名称是介词(如 plus)而不是动词(如 add)。 这强调了方法不会改变对象的值的事实。

? 不可变对象本质上是线程安全的; 它们不需要同步。 被多个线程同时访问它们时并不会被破坏。 这是实现线程安全的最简单方法。 由于没有线程可以观察到另一个线程对不可变对象的影响,所以不可变对象可以被自由地共享。 因此,不可变类应鼓励客户端尽可能重用现有的实例。 一个简单的方法是为常用的值提供公共的静态 final 常量。 例如, Complex 类可能提供这些常量:

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

? 这种方法可以更进一步。 一个不可变的类可以提供静态的工厂(条目 1)来缓存经常被请求的实例,以避免在现有的实例中创建新的实例。 所有基本类型的包装类和 BigInteger 类都是这样做的。 使用这样的静态工厂会使客户端共享实例而不是创建新实例,从而减少内存占用和垃圾回收成本。 在设计新类时,选择静态工厂代替公共构造方法,可以在以后增加缓存的灵活性,而不需要修改客户端。

? 不仅可以共享不可变的对象,而且可以共享内部信息。 例如, BigInteger 类在内部使用符号数值表示法。符号用 int 值表示,数值用 int 数组表示。 negate 方法生成了一个数值相同但符号相反的新 BigInteger实例。 即使它是可变的,也不需要复制数组;新创建的 BigInteger 指向与原始相同的内部数组。

? 不可变对象为其他对象提供了很好的构件(building blocks) ,无论是可变的还是不可变的。 如果知道一个复杂组件的内部对象不会发生改变,那么维护复杂对象的不变量就容易多了。这一原则的特例是,不可变对象可以构成Map 对象的键和 Set 的元素,一旦不可变对象作为 Map 的键或 Set 里的元素,即使破坏了 Map 和 Set的不可变性,但不用担心它们的值会发生变化。

? 不可变对象提供了免费的原子失败机制(条目 76)。 它们的状态永远不会改变,所以不可能出现临时的不一致。

? 不可变类的主要缺点是对于每个不同的值都需要一个单独的对象。 创建这些对象可能代价很高,特别是如果是大型的对象下。 例如,假设你有一个百万位的 BigInteger ,你想改变它的低位:

BigInteger moby = ...;
moby = moby.flipBit(0);

? 为了保证不变性,一个类不得允许子类化。 这可以通过使类用 final 修饰,但是还有另外一个更灵活的选择。 而不是使不可变类设置为 final ,可以使其所有的构造方法私有或包级私有,并添加公共静态工厂,而不是公共构造方法(条目 1)。 为了具体说明这种方法,下面以 Complex 为例,看看如何使用这种方法:

// Immutable class with static factories instead of constructors
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 valueOf(double re, double im) {
    return new Complex(re, im);
 }
 ... // Remainder unchanged
}

? 除非你有很好的理由要让类成为可变的类,否则它就应该是不可变的。

? 如果类不能被做成不可变的,仍然应该尽可能地限制它的可变性。除非有令人信服的理由要使域变成非 final 的,否则要使每个域都是 private final 的。


第 18 条:复合优先于继承

? 继承是实现代码重用的有力手段,但它并非永远是完成这项工作的最佳手段。

? 与方法调用不同的时,继承打破了封装性

? 为了具体说明,假设有一个使用 HashSet 的程序。 为了调整程序的性能,需要查询 HashSet ,从创建它之后已经添加了多少个元素(不要和当前的元素数量混淆,当元素被删除时数量也会下降)。 为了提供这个功能,编写了一个 HashSet 变体,它保留了尝试元素插入的数量,并导出了这个插入数量的一个访问方法。 HashSet 类包含两个添加元素的方法,分别是 add 和 addAll ,所以我们重写这两个方法:

// Broken - Inappropriate use of inheritance!
public class InstrumentedHashSet<E> extends HashSet<E> {
  // The number of attempted element insertions
  private int addCount = 0;
  public InstrumentedHashSet() {
 }
  public InstrumentedHashSet(int initCap, float loadFactor) {
    super(initCap, loadFactor);
 }
  @Override public boolean add(E e) {
    addCount++;
    return super.add(e);
 }
  @Override public boolean addAll(Collection<? extends E> c) {
    addCount += c.size();
    return super.addAll(c);
 }
  public int getAddCount() {
    return addCount;
 }
}

? 这个类看起来很合理,但是不能正常工作。 假设创建一个实例并使用 addAll 方法添加三个元素。 顺便提一句,请注意,下面代码使用在 Java 9 中添加的静态工厂方法 List.of 来创建一个列表;如果使用的是早期版本,请改为使用 Arrays.asList :

InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(List.of("Snap", "Crackle", "Pop"));

? 我们期望 getAddCount 方法返回的结果是 3,但实际上返回了 6。哪里出来问题?在 HashSet 内部, addAll方法是基于它的 add 方法来实现的,即使 HashSet 文档中没有指名其实现细节,倒也是合理的。InstrumentedHashSet 中的 addAll 方法首先给 addCount 属性设置为 3,然后使用 super.addAll 方法调用了 HashSet 的 addAll 实现。然后反过来又调用在 InstrumentedHashSet 类中重写的 add 方法,每个元素调用一次。这三次调用又分别给 addCount 加 1,所以,一共增加了 6:通过 addAll 方法每个增加的元素都被计算了两次。

? 稍微好一点的做法是,重写 addAll 方法遍历指定集合,为每个元素调用 add 方法一次。 不管 HashSet的 addAll 方法是否在其 add 方法上实现,都会保证正确的结果,因为 HashSet 的 addAll 实现将不再被调用。然而,这种技术并不能解决所有的问题。 这相当于重新实现了父类方法,这样的方法可能不能确定到底是否时自用(self-use)的,实现起来也是困难的,耗时的,容易出错的,并且可能会降低性能。

? 导致子类脆弱的一个相关原因是,它们的父类在后续的发布版本中可以添加新的方法。如果在后续的版本中,父类没有新增添加元素的方法,那么这样做没有问题。但是,一旦父类增加了这样的新方法,则很有可能由于调用了未被重写的新方法,将非法的元素添加到子类的实例中。这不是个纯粹的理论问题。

? 幸运的是,有一种方法可以避免上述所有的问题。不要继承一个现有的类,而应该给你的新类增加一个私有属性,该属性是 现有类的实例引用,这种设计被称为组合(composition),因为现有的类成为新类的组成部分。


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

? 类的文档必须精确地描述覆盖每个方法所带来的影响。换句话说,该类必须有说明文档说明它可覆盖的方法的自用性。

? 对于为了继承而设计的类,唯一的测试方法就是编写子类。而且必须在发布类之前先编写子类对类进行测试。

? 为了允许继承,类还必须遵守其他一些约束。构造方法绝不能直接或间接调用可重写的方法。 如果违反这个规则,将导致程序失败。 父类构造方法在子类构造方法之前运行,所以在子类构造方法运行之前,子类中的重写方法被调用。 如果重写方法依赖于子类构造方法执行的任何初始化,则此方法将不会按预期运行。 为了具体说明,这是一个违反这个规则的类:

public class Super {
  // Broken - constructor invokes an overridable method
  public Super() {
    overrideMe();
 }
  public void overrideMe() {
 }
}

? 以下是一个重写 overrideMe 方法的子类, Super 类的唯一构造方法会错误地调用它:

public final class Sub extends Super {
  // Blank final, set by constructor
  private final Instant instant;
  Sub() {
    instant = Instant.now();
 }
  // Overriding method invoked by superclass constructor
  @Override
  public void overrideMe() {
    System.out.println(instant);
 }
  public static void main(String[] args) {
    Sub sub = new Sub();
    sub.overrideMe();
 }
}

? 你可能期望这个程序打印两次 instant 实例,但是它第一次打印出 null ,因为在 Sub 构造方法有机会初始化 instant 属性之前, overrideMe 被 Super 构造方法调用。 请注意,这个程序观察两个不同状态的final 属性! 还要注意的是,如果 overrideMe 方法调用了 instant 实例中任何方法,那么当父类构造方法调用overrideMe 时,它将抛出一个 NullPointerException 异常。 这个程序不会抛出 NullPointerException的唯一原因是 println 方法容忍 null 参数。

? 对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化。 有两种方法禁止子类化。 两者中较容易的是声明类为 final 。 另一种方法是使所有的构造方法都是私有的或包级私有的,并且添加公共静态工厂来代替构造方法。 这个方案在内部提供了使用子类的灵活性,在条目 17 中讨论过。两种方法都是可以接受的。


第 20 条:接口优先于抽象类

? 接口和抽象类明显的区别在于,抽象类允许包含某些方法的实现,但是接口则不允许。

? (1)现有的类可以很容易被更新,以实现新的接口。当Comparable接口被引入到Java平台中时,会更新虚度现有的类,以实现Comparable接口。因为Java支持单继承多多实现。

? (2)接口是定义mixin(混合类型)的理想选择。

? (3)接口允许我们构造非层次结构的类型框架。


第 21 条:为后代设计接口

? 在 Java 8 发行之前,如果不破坏现有的实现,是不可能给接口加方法的。如果给某个接口添加了一个新的方法,一般来说,现有的实现中是没有这个方法的,因此就会导致编译错误。在 Java 8 中,增加了缺省方法构造,目的就是允许给现有的接口添加方法。

public interface SysBlogService{
    public void test1(); // 实现类必须实现 test1()
    // 缺省方法
    default void test2(){
    	System.out.println("我的实现类可以不用实现我")
	}
}

? Java 8 在核心集合接口中增加了许多新的缺省方法,主要是为了便于使用 lambda 。Java 类库的缺省方法是高品质的通用实现,它们在大多数情况下都能正常使用。但是,并非每一个可能的实现的所有变体,始终都可以编写出一个缺省方法

? 建议尽量避免利用缺省方法在现有接口上添加新的方法,除非有特殊的需要,但就算是在那样的情况下也应该慎重考虑:缺省的方法实现是否会破坏现有的接口实现。然而,在创建接口的时候,用缺省方法提供标准的方法实现是非常方便的,它简化了实现接口的任务。

? 尽管缺省方法现在已经是 Java 平台的组成部分,但谨慎设计接口仍然是至关重要的。


第 22 条:接口只用于定义类型

? 有一种接口被称为常量接口,这种接口不包含任何方法,它只包含静态的 final 域,每个域都导出一个常量。使用这些常量的实现类实现这个接口,以避免用类名来修饰常量名。下面举个例子:

// Constant interface antipattern - do not use!
public interface PhysicalConstants {
    // Avogadro's number (1/mol)
    static final double AVOGADROS_NUMBER = 6.022_140_857e23;

    // Boltzmann constant (J/K)
    static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23;

    // Mass of the electron (kg)
    static final double ELECTRON_MASS = 9.109_383_56e-31;
}

? 常量接口模式是对接口的不良使用。接口应该只被用来定义类型,它们不应该被用来导出常量。


第 23 条:类层次优先于标签类

? 有时可能会遇到带有两种甚至更多中风格的实例的类,并包含表示实例风格的标签域。以下面这个类为例,它能够表示圆形或者矩形:

// Tagged class - vastly inferior to a class hierarchy!
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 radius) {
        shape = Shape.CIRCLE;
        this.radius = radius;
    }

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

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

? 这种代码过于冗长、容易出错,并且效率低下。

? Java 提供了更好的方法来定义能够表示多种风格对象的单个数据类型:子类型化。标签类正式对层次类的一种简单的效仿。

? 接下来为每种原始标签类型都定义根类的具体子类。如下:

// Class hierarchy replacement for a tagged class
abstract class Figure {
    abstract double area();
}

class Circle extends Figure {
    final double radius;

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

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

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

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

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

? 类层次的另一个好处在于,它们可以用来反应类型之间本质上的层次关系,有助于增强灵活性。假设上述例子中的标签类也允许表达正方形。层次类可以反应出正方形是一种特殊的矩形这一事实:

class Square extends Rectangle {
  Square(double side) {
    super(side, side);
  }
}

? 简而言之,标签类很少有适用的时候。当你想要编写一个包含显示标签域的类时,应该考虑一下,这个标签是否可以取消,这个类是否可以用层次类来代替。当你遇到一个包含标签域的现有类时,就要考虑将它重构到一个层次结构中去。


第 24 条:静态成员优先于非静态成员

? 嵌套类有四种:静态成员类、非静态成员类、匿名类、局部类。

? 下面是局部类的声明方式,与本条例无关

// 局部类 在方法内部
class Outer{
    private  int age = 10;
    public  void show(){
        //作用范围只有该方法内
        class  Inner4{
            public void showAge(){
                System.out.println(age);
            }
        }
        //只能在方法内部定义
        Inner4 inner = new Inner4();
        inner.showAge();
    }
}

? 如果声明成员类不要求访问外围实例,就要始终把修饰符 static 放在它的声明中。使它成为静态成员类,而不是非静态成员类。如果省略的 static 修饰符,则每个实例都将包含一个额外的指向外围对象的引用。保存这份引用需要消耗时间和空间,并且会导致外围实例在符合垃圾回收时却仍然得以保留。


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

	虽然 Java 编译器允许在一个源文件中定义多个顶级类,但这么做并没有什么好处,只会带来巨大的风险。因为在一个源文件中定义多个顶级类,可能导致给一个类提供多个定义。哪一个定义会被用到,取决于源文件被传给编译器的顺序。

? 下面的例子,这个源文件只包含一个 Main 类,它将引用另外两个顶级类 ( Utensil 和 Dessert ) 的成员:

public class Main {
    public static void main(String[] args) {
        System.out.println(Utensil.NAME + Dessert.NAME);
    }
}

? 现在假设你在给一个名为 Utensil.java 的源文件中同时定义了 Utensil 和 Dessert :

// Two classes defined in one file. Don't ever do this!
class Utensil {
    static final String NAME = "pan";
}

class Dessert {
    static final String NAME = "cake";
}

? 当然,程序会打印出 " pancake "。

? 现在假设你不小心在另一个名为 Dessert.java 的源文件中也定义了同样的两个类:

// Two classes defined in one file. Don't ever do this!
class Utensil {
    static final String NAME = "pot";
}

class Dessert {
    static final String NAME = "pie";
}

? 如果你侥幸是使用命令 javac Main.java Dessert.java 来编译程序,那么编译就会失败,此时编译器会提醒你定义了多个 Utensil 和 Dessert 类。这是因为编译器会先编译Main.java,当它看到 Utensil 的引用(在 Dessert 引用之前),就会在 Utensil.java 中查看这个类,结果找到了 Utensil 和 Dessert 这两个类。当编译器在命令行遇到 Dessert.java时,也会去查找改文件,结果会遇到 Utensil 和 Dessert 这两个定义。

? 如果用命令 javac Main.java 或者 javac Main.java Utensil.java 编译程序结果如同你还没有编写 Dessert.java 文件一样,输出 pancake 。但是如果是用命令 javac Dessert.java Main.java 编译程序,就会输出 potpie 。程序的行为受源文件被传给编译器的顺序影响,这显然是让人无法接受的。

? 这个问题的修正很简单,只要把顶级类分别放入独立的源文件即可。如果一定要把多个顶级类放进一个源文件中,就要考虑使用静态成员类。如果这些类服务于另一个类,那么将它们设计成静态成员类通常比较好,因为这样增强了代码的可读性,如果将这些类声明为私有的,还可以使它们减少被读取的概率。以下就是做成静态成员类的范例:

/ Static member classes instead of multiple top-level classes
public class Test {

    public static void main(String[] args) {
        System.out.println(Utensil.NAME + Dessert.NAME);
    }

    private static class Utensil {
        static final String NAME = "pan";
    }

    private static class Dessert {
        static final String NAME = "cake";
    }
}

? 结论显而易见:永远不要把多个顶级类或者接口放在一个源文件中

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-03-21 20:35:37  更:2022-03-21 20:36:53 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 9:38:09-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码