虽然 Java 编译器允许在单个源文件中定义多个顶级类,但这样做没有任何好处,并且存在重大风险。 风险源于在源文件中定义多个顶级类使得为类提供多个定义成为可能。 使用哪个定义会受到源文件传递给编译器的顺序的影响。
为了具体说明,请考虑下面源文件,其中只包含一个引用其他两个顶级类(Utensil
和 Dessert
类)的成员的 Main 类:
public class Main {
public static void main(String[] args) {
System.out.println(Utensil.NAME + [Dessert.NAME](http://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";
}
当然,main
方法会打印 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
。 程序的行为因此受到源文件传递给编译器的顺序的影响,这显然是不可接受的。
解决这个问题很简单,将顶层类(如我们的例子中的 Utensil
和 Dessert
)分割成单独的源文件。 如果试图将多个顶级类放入单个源文件中,请考虑使用静态成员类(详见第 24 条)作为将类拆分为单独的源文件的替代方法。 如果这些类从属于另一个类,那么将它们变成静态成员类通常是更好的选择,因为它提高了可读性,并且可以通过声明它们为私有(详见第 15 条)来减少类的可访问性。下面是我们的例子看起来如何使用静态成员类:
// 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](http://Dessert.NAME));
}
private static class Utensil {
static final String NAME = "pan";
}
private static class Dessert {
static final String NAME = "cake";
}
}
这个教训很清楚:永远不要将多个顶级类或接口放在一个源文件中。 遵循这个规则保证在编译时不能有多个定义。 这又保证了编译生成的类文件以及生成的程序的行为与源文件传递给编译器的顺序无关。
文章列表
- 高效Java编程-01. 考虑使用静态工厂方法替代构造方法
- 高效Java编程-02. 当构造方法参数过多时使用builder模式
- 高效Java编程-03. 使用私有构造方法或枚类实现Singleton属性
- 高效Java编程-04. 使用私有构造方法执行非实例化
- 高效Java编程-05. 依赖注入优于硬连接资源(hardwiring resources)
- 高效Java编程-06. 避免创建不必要的对象
- 高效Java编程-07. 消除过期的对象引用
- 高效Java编程-08. 避免使用Finalizer和Cleaner机制
- 高效Java编程-09. 使用try-with-resources语句替代try-finally语句
- 高效Java编程-10. 重写equals方法时遵守通用约定
- 高效Java编程-11. 重写equals方法时同时也要重写hashcode方法
- 高效Java编程-12. 始终重写 toString 方法
- 高效Java编程-13. 谨慎地重写 clone 方法
- 高效Java编程-14. 考虑实现Comparable接口
- 高效Java编程-15. 使类和成员的可访问性最小化
- 高效Java编程-16. 在公共类中使用访问方法而不是公共属性
- 高效Java编程-17. 最小化可变性
- 高效Java编程-18. 组合优于继承
- 高效Java编程-19. 要么设计继承并提供文档说明,要么禁用继承
- 高效Java编程-20. 接口优于抽象类
- 高效Java编程-21. 为后代设计接口
- 高效Java编程-22. 接口仅用来定义类型
- 高效Java编程-23. 类层次结构优于标签类
- 高效Java编程-24. 支持使用静态成员类而不是非静态类
- 高效Java编程-25. 将源文件限制为单个顶级类
- 高效Java编程-26. 不要使用原始类型
- 高效Java编程-27. 消除非检查警告
- 高效Java编程-28. 列表优于数组
- 高效Java编程-29. 优先考虑泛型
- 高效Java编程-30. 优先使用泛型方法
- 高效Java编程-31. 使用限定通配符来增加API的灵活性
- 高效Java编程-32. 合理地结合泛型和可变参数
- 高效Java编程-33. 优先考虑类型安全的异构容器
- 高效Java编程-34. 使用枚举类型替代整型常量
- 高效Java编程-35. 使用实例属性替代序数
- 高效Java编程-汇总