本部分内容用来指导程序员怎样才能设计出更加有用、健壮、灵活的类和接口。
内容导图如下:
1.使类和成员的可访问性最小化
封装是面向对象思想的特性之一,它的基本思想就是就是仅暴露必要的接口来和其他对象交互,隐藏其内部数据和具体实现细节。设计良好的模块会隐藏所有的实现细节,把它的API与它的实现清晰地隔离开来,模块间只通过它们的API进行通信,一个模块不需要知道其它模块内部是如何工作的。它可以有效地解除组成系统的各模块之间的耦合关系,使得这些模块可以独立地开发、测试、优化、使用、理解和修改。
Java语言提供了private、package-private、protected、public四种访问级别,用来对类、接口和成员进行访问控制。原则是尽可能地使每个类或成员不被外界访问,换言之,尽可能在最小的访问级别完成软件功能的实现。
信息隐藏的另一个重要原因就是程序员必须为每个公开的API和成员负责,它们必须得到永远的支持。它们也代表了类对于某个实现细节的公开承诺。因为无法预估和控制其他程序员如何使用公开的API和成员,所有这些公开的信息在整个软件的生命周期内都必须始终如一地保持其原有的功能,暴露的越多,需要维护的就越多,会对软件后续的维护、迭代带来高昂的成本,甚至会压垮整个软件系统。
最后,包含公有可变域的类也不是线程安全的,在多线程模式下,可能会造成类的状态不一致,从而导致灾难性的后果。
//公开域会造成无法控制的访问,也会造成对象状态的不一致
public int value = 1;
public static int VALUE = 1;
//OBJECTS是不可变的,但它所指的对象却是可变的,这点特别容易被忽视,造成安全漏洞
public static final Thing[] VALUES = {...};
//修改办法一
private static final Thing[] PRIVATE_VALUES = {...};
public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
//修改办法二
private static final Thing[] PRIVATE_VALUES = {...};
public static final Thing[] values() {
return PRIVATE_VALUES.clone();
}
应始终尽可能地降低可访问性。在仔细设计了一个最小的API之后,应该防止把任何散乱的类、接口和成员变成API的一部分。除了公有静态final域的特殊情形之外,公有类都不应该包含公有域,并且要确保公有静态final域所引用的对象都是不可变的。
我们生活的世界实际就是一个物质的、面向对象的世界,生活中的衣食住行都要与一个个的具体对象实例打交道。买衣服不必知晓衣服是如何缝制出来的,仅关心衣服所呈现的外在特性是否满足个人需求及审美;吃饭不必知道如何烹饪,只需要知道菜品是否合乎自己的口味需求;住宿也不需要我们知道房屋是如何建造的,只需要看它提供的条件是否满足我们的需求;开车也不用先弄清楚车是怎样生产制造的,只要能根据车辆提供的控制接口正确操作就可以了。封装的思想隐藏于我们生活的方方面面,也许正是因为它无处不在,反而最容易被人们忽视。
封装于软件模块而言,就是仅提供它要提供给外界模块使用的API接口,实现这些API接口所需要的数据和其他实现细节是隐藏于模块内部的,外界模块不关心、不必要知晓这些API是如何实现的,模块间仅仅通过固定的API接口进行交互就可以了。满足这样设计的模块就会容易替换、修改,更加灵活。举例来讲,我们常会找人办事,实际是要找具备能办成此事(对应于模块API)这种能力的人,他可能直接就能解决事情,也可能还需委托多个中间人,具体如何操作,一般我们是不关心的。
2.在公有类中使用方法而非公有域
有时候可能会编写一些退化类用来集中实例域:
class Point {
public double x;
public double y;
}
这种类的数据域是可以被直接访问的,没有提供封装的功能。如果不改变API,就无法改变它的数据表示法(更改命名),也无法强加任何约束条件。在域被访问的时候,无法采取任何辅助的行为。面向对象的设计会使用公有访问方法来代替:
class Point {
private double x;
private double 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;
}
}
如果类可以在它所在的包外进行访问,就提供访问方法,以保留将来改变该类的内部表示法的灵活性。如果公有类暴露了它的数据域,要想在将来改变其内部表示法是不可能的,因为公有域的客户端代码已经遍布各处了。
如果类是包级私有的,或者是私有的嵌套类,直接暴露它的数据域是可以接受的。对它的修改是不会对客户端代码造成影响的。
公有类永远都不应该暴露可变的域。
3.使可变性最小化
不可变类指的是实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内固定不变。Java平台类库中包含许多不可变的类,有String、BigInteger、BigDecimal、基本类型包装类。
存在不可变类有很多理由:不可变的类比可变类更加易于设计、实现和使用,它们不易出错,且更加安全。
为了使类不可变,要遵循下面5条原则:
- 不要提供任何会修改对象状态的方法
- 保证类不会被扩展
- 使所有的域都是final的
- 使所有的域都成为私有的
- 确保对于任何可变组件的互斥访问
如果类具有指向可变对象的域,必须确保该类的客户端无法获得指向这些对象的引用。永远不要用客户端提供的对象来初始化这样的域,也不要从任何访问方法中返回该对象的引用。在构造器、访问方法和readObject方法中使用保护性拷贝技术。
不可变类的优点:
- 不可变对象比较简单
- 不可变对象本质上是线程安全的,它们不要求同步
- 不可变对象可以被自由地共享,不需要进行保护性拷贝
- 不可变对象为其他对象提供了大量的构件
不可变类的缺点:
- 对于每个不同的值都需要一个单独的对象
创建这种对象的代价可能很高,特别是对于大型对象的情形。对于大型的对象,最好的办法是提供一个公有的可变配套类,如String的公有配套类StringBuilder。
为了确保不可变性,类绝对不允许自身被子类化。除了“使类成为final的”这种方法外,还可以让类的所有构造器都变成私有的或者包级私有的,并添加公有的静态工厂来代替公有的构造器。
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);
}
...
}
坚决不要为每个get方法编写一个相应的set方法。除非有很好的理由要让类成为可变的类,否则就应该是不可变的。
不可变类唯一的缺点是在特定情况下存在潜在的性能问题。当把较大的值对象做成不可变类时,若其会对性能造成影响,可以为它提供相应的可变配套类。
如果类不能被做成不可变的,仍然要尽可能地限制它的可变性。降低对象可以存在的状态数,可以更容易地分析该对象的行为,同时降低出错的可能性。
构造器应该创建完全初始化的对象,并建立起所有的约束关系。不要在构造器或静态工厂之外再提供公有的初始化方法,除非有令人信服的理由必须要那样做。不应该提供“重新初始化”方法。
4.复合优先于继承
继承是实现代码复用的有力手段,但使用不当会导致软件变得很脆弱。在包的内部使用继承是非常安全的,在包的内部,子类和超类的实现都处在同一个程序员的控制之下。对于专门为了继承而设计,且有很好的文档说明的类来说,使用继承也是非常安全的。但是,对普通的具体类进行跨越包边界的继承,是非常危险的。
继承打破了封装性。子类依赖于超类中特定功能的实现细节。超类的实现有可能会随着发行版本的不同而有所变化,如果真的发生了变化,子类可能会遭到破坏。因此,子类必须要跟着超类的更新而演变,除非超类是专门为了扩展而设计的,且具有很好的文档说明。
下面的例子说明了继承的危险性:
//本来是为了统计HashSet自创建以来曾经添加了多少个元素,但结果却是不正确的
//原因就是HashSet中addAll()的实现依赖于add()
public class InstrumentedHashSet<E> extends HashSet<E> {
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;
}
}
另一个危险的原因来源于overriding动作。如果在扩展一个类的时候,仅仅是添加新的方法,而不覆盖现有的方法,你可能会认为这是安全的。但是,如果超类在后续的发行版本中获得了一个新的方法,并且不幸的是,你给子类提供了一个签名相同但返回类型不同的方法,这样的子类将无法通过编译。
采用复合则可以避免上述问题。复合不依赖于现有类的实现细节,即使现有的类添加了新的方法,也不影响新的类。
只有当子类真正是超类的子类型时,才适合继承。换言之,对于两个类A和B,只有当两者之间确实存在“is-a”关系的时候,类B才应该扩展类A。
如果在适合于使用复用的地方使用了继承,则会不必要地暴露实现细节。这样得到的API会把你限制在原始的实现上,永远限定了类的性能。更严重的是,由于暴露了内部的细节,客户端就有可能直接访问这些内部细节,导致语义上的混淆或直接导致错误发生。
在复用代码的时候,优先考虑复合而非继承,只有当子类和超类确实存在子类型关系时,使用继承才是恰当的。即便如此,如果子类和超类处于不同的包中,并且超类并不是为了继承而设计的,那么继承将会导致脆弱性,为了避免这种脆弱性,可以用复用来代替继承。
5.要么为继承而设计,并提供文档,要么就禁止继承
对不是为了继承而设计、并且没有文档说明的“外来”类进行子类化是很危险的。
对于为了继承而设计的类,该类的文档必须要精确地描述覆盖每个方法所带来的影响。即该类必须有文档说明它可覆盖的方法的自用性。对于每个公有的或受保护的方法或构造器,它的文档必须指明该方法或构造器调用了哪些可覆盖的方法,是以什么顺序调用的,每个调用的结果又是如何影响后续的处理过程的。
另外,为了使程序员能够编写出更加有效的子类,而无需承受不必要的痛苦,类必须通过某种形式提供适当的钩子(hook),以便能够进入到它的内部工作流程中。
对于为了继承而设计的类,唯一的测试方法就是编写子类。经验表明,3个子类通常就足以测试一个可扩展的类。
在为了继承而设计有可能被广泛使用的类时,必须要意识到,对于文档中所说明的自用模式,以及对于其protected方法和域中所隐含的实现策略,你实际上已经做出了永久性的承诺。这些承诺使得你在后续的版本中提高这个类的性能或者增加新功能都变得非常困难,甚至不可能。因此,必须在发布类之前首先编写子类对类进行测试。
为了允许继承,类还必须遵守其他一些约束:构造器决不能调用可被覆盖的方法,无论是直接调用还是间接调用。如果违反了这条规则,很有可能导致程序失败。
对于普通的具体类,它们既不是final的,也不是为了子类化而设计和编写文档的,继承它们是非常危险的,因为每次对这种类进行修改,从这个类扩展得到的客户类都有可能遭到破坏。对这个问题的最佳解决方案是:对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化。有两种方法可以禁止子类化:
- 把这个类声明为final的
- 把所有的构造器都变成私有的,或者包级私有的,并增加一些公有的静态工厂来替代构造器。
如果禁止继承可能会带来不便,或者认为必须允许从这样的类继承,一种合理的办法是确保这个类永远不会调用它的任何可覆盖的方法,并在文档中说明这一点。换言之,完全消除这个类中可覆盖方法的自用特性。这样做之后,就可以创建“能够安全地进行子类化”的类。覆盖方法将永远也不会影响其他任何方法的行为。
你可以消除类中可覆盖方法的自用特性,而不改变它的行为。将每个可覆盖方法的代码体移到一个私有的“辅助方法”中,并且让每个可覆盖的方法调用它的私有辅助方法,然后直接调用私有的辅助方法来代替可覆盖方法,去除可覆盖方法的自用调用。
为继承而设计的类,要提供文档说明,并在发布前编写子类进行测试;
对于普通的类,要尽可能地禁止其子类化,可以将其声明为final或使用静态方法来代替构造器;
对于无法禁止子类化的类,要消除这种类中可被子类覆盖的方法的自用性,即在父类中使用的方法,要确保其在子类中不可被覆盖。
6.接口优先于抽象类
Java语言提供了接口和抽象类两种机制来定义多个实现的类型。由于Java只允许单继承,抽象类作为类型定义受到了极大的限制。
使用接口定义类型的好处如下:
- 现有的类可以很容易被更新,以实现新的接口类型
- 接口是定义mixin(混合类型)的理想选择
- 接口允许我们构造非层次结构的类型框架
public interface Singer {
AudioClip sing(Song s);
}
public interface Songwriter {
Song compose(boolean hit);
}
public interface SingerSongwriter extends Singer, Songwriter {
AudioClip strum();
void actSensitive();
}
使用接口定义类型并不妨碍为程序员提供实现上的帮助。通过对你导出的每个重要接口都提供一个抽象的骨架实现类,可以把接口和抽象类的优点结合起来。接口的作用仍然是定义类型,但骨架类实现接管了所有与接口实现相关的工作。
骨架类实现的美妙之处在于:它们为抽象类提供了实现上的帮助,但又不强加“抽象类被用作类型定义时”所特有的严格限制。对于接口的大多数实现来讲,扩展骨架类实现是一个好的选择,但并不是必需的。如果预置的类无法扩展骨架实现类,这个类始终可以手工实现这个接口。
骨架类可以被用来实现模拟多重继承。利用接口定义类型,实现接口的类可以把对接口方法的调用,转发到一个内部私有类的实例上,这个内部私有类扩展了骨架实现类。
下图是Java容器框架图,利用接口和骨架类,Joshua为我们实现了强大而又灵活的各个容器类。
编写骨架实现类的步骤:
- 认真研究接口,确定哪些方法是最为基本的,其他方法则可以根据它们来实现
- 将上述基本方法定义成骨架实现类中的抽象方法
- 在骨架实现类中实现接口中其他的方法
public abstract class AbstractMapEntry<K, V> implements Map.Entry<K, V> {
public abstract K getKay();
public abstract V getValue();
public V setValue(V value) {
throw new UnsupportedOperationException();
}
@Override
public int hashCode() {
return hashCode(getKay()) ^ hashCode(getValue());
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return true;
}
if (!(obj instanceof Map.Entry)) {
return false;
}
Map.Entry<?, ?> arg = (Map.Entry)obj;
return equals(getKey(), arg.getKey()) && equals(getValue(), arg.getValue());
}
private static boolean equals(Object o1, Object o2) {
return o1 == null ? o2 == null : o1.equals(o2);
}
private static int hashCode(Object obj) {
return obj == null ? 0 : obj.hashCode();
}
}
骨架实现类是为了继承的目的而设计的,应该提供相应的文档说明。
抽象类定义多个实现类型的优势在于:抽象类的演变比接口的演变要容易很多。如果在后续的发行版本中,希望在抽象类中增加新的方法,始终可以增加具体方法,它包含合理的默认实现,则该抽象类的所有实现类都将获得这个新的方法。而在公共接口中增加方法,就会破坏实现这个接口的所有现有的类。
设计公有接口要非常谨慎,因为接口一旦被公开发行,并且已被广泛实现,再想改变这个接口几乎是不可能的了。在发行接口的时候,最好的做法是:在接口被“冻结”之前,尽可能让更多的程序员用尽可能多的方式来实现这个接口。这样有助于在可以改正缺陷的时候就发现它们。
接口通常是定义允许多个实现的最佳途径。如果你导出了一个重要的接口,就应该坚决考虑同时提供骨架实现类。应该尽可能谨慎地设计所有的公有接口,并通过编写多个实现来对它们进行全面的测试。
7.接口只用于定义类型
当类实现接口时,接口就充当了可以引用这个类的实例的类型。类实现了接口,就表明客户端可以让这个类的实例执行接口定义的动作。为了任何其他的目的而定义接口是不恰当的。
其中,常量接口模式是对接口的不良使用:
public interface PhysicalConstants {
static final double AVOGANDROS_NUMBER = 6.02214199e23;
static final double BOLTZMANN_CONSTANT = 1.3806503e-23;
static final double ELECTRON_MASS = 9.10938188e-31;
}
类在内部使用某些常量,纯粹是实现细节。实现常量接口,会导致把这样的实现细节泄露到该类的导出API中。更糟糕的是它代表了一种承诺,如果将来的版本中这个类被修改了,不再需要使用这些常量了,它仍必须实现这个接口,以确保二进制兼容性。
导出常量,通常有三种处理方法:
- 如果常量与某个现有类或者接口紧密相关,就应该把常量添加到这个类或接口中
- 如果常量被看作枚举类型的成员,就应该使用枚举类型
- 将常量定义在不可实例化的工具类中
public class PhysicalConstants {
private PhysicalConstants() {}
public static final double AVOGANDROS_NUMBER = 6.02214199e23;
public static final double BOLTZMANN_CONSTANT = 1.3806503e-23;
public static final double ELECTRON_MASS = 9.10938188e-31;
}
JDK1.5及其以后版本在使用这些静态常量时,可使用静态导入机制来简化调用。
接口应该只被用来定义类型,它们不应该被用来导出常量。
8.类层次优于标签类
有时候可能会遇到带有两种甚至更多种风格的实例的类,并包含表示实例风格的标签域。
class Figure {
enum Shape {
RECTANGLE,
CIRCLE
}
final Shape shape;
double length;
double width;
double radius;
Figure(double radius) {
shape = Shape.CIRCLE;
this.radius = radius;
}
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();
}
}
}
这种标签类有许多缺点。它充斥着样板代码,将多个实现挤在单个类中,可读性很差;内存占用增加;如果要添加风格,必须得给每个条件语句添加一个条件;实例的数据类型没有提供任何关于其风格的线索。简而言之,标签类过于冗长、容易出错、效率低下。
Java可以采用子类型化来构建类型层次,以此替换标签类。为将标签类转变成类层次,具体做法如下:
- 定义一个抽象类,将标签类中依赖标签值的方法定义在抽象类中
- 为每种原始标签类都定义根类的具体子类
abstract class Figure {
abstract double area();
}
class Circle extends Figure {
final double radius;
Circle(double radius) {
this.radius = radius;
}
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;
}
double area() {
return length * width;
}
}
类层次结构避免了标签类所有的缺点,更重要的是它反映了类型之间本质上的层次关系,有助于增强灵活性,并可提供编译时检查,且非常容易扩展。
标签类很少有适用的时候。当你要编写一个包含显式标签域的类时,应该考虑一下,这个标签类是否可以被取消,是否可以使用类层次来代替。当你遇到一个包含标签域的现有类时,应考虑将它重构到一个层次结构中去。
9.用函数对象表示策略
有些语言支持函数指针、代理、lambda表达式,或者支持类似的机制,允许程序把“调用特殊函数的能力”存储起来并传递这种能力。这种机制通常用于允许函数的调用者通过传入第二个函数,来指定自己的行为。
Java没有提供函数指针,但可以用对象引用实现同样的功能。可以定义这样一种对象:它的方法执行其他对象(这些对象被显式传递给这些方法)上的操作。如果一个类仅仅导出一个这样的方法,它的实例实际上就等同于一个指向该方法的指针。这样的实例被称为函数对象。
/*
* StringLengthComparator导出一个带两个字符串参数的方法
*指向它对象的引用可以被当作是一个指向该比较器的函数指针,它可以在任意一对字符串上调用
*/
class StringLengthComparator {
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}
作为典型的具体策略类,StringLengthComparator是无状态的,它没有域,所以这个类的所有实例在功能上都是相互等价的。因此,它作为一个Singleton是非常合适的,可以节省不必要的对象创建开销:
class StringLengthComparator {
private StringLengthComparator() {}
public static final StringLengthComparator INSTANCE = new StringLengthComparator();
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}
为了把StringLengthComparator实例传递给方法,需要适当的参数类型。使用StringLengthComparator并不好,因为客户端将无法传递任何其他的比较策略。因此,在设计具体的策略类时,需要定义一个策略接口:
public interface Comparator<T> {
public int compare(T t1, T t2);
}
具体的策略类往往使用匿名类声明:
Arrays.sort(stringArray, new Comparator<String>() {
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
});
使用匿名类时,将会在每次执行调用时创建一个新的实例。如果它被重复执行,可以考虑将一个函数对象存储到一个私有的静态final域里,并重用它。这样做另外一个好处是:可以为这个函数对象取一个有意义的域名称。
//通过公有静态final域导出具体实现策略
class Host {
private static class StrLenCmp implements Comparator<String> {
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}
public static final Comparator<String> STRING_LENGTH_COMPARATOR = new StrLenCmp();
}
函数指针的主要用途就是实现策略模式。为了在Java中实现这种模式,要声明一个接口来表示该策略,并且为每个具体策略声明一个实现了该接口的类。当一个具体策略只被用来使用一次时,通常使用匿名类来声明和实例化这个具体策略类。当一个具体策略是设计用来重复使用的时候,它的类通常被实现为私有的静态成员类,并通过公有的静态final域被导出,其类型为该策略接口。
10.优先考虑静态成员类
嵌套类是指被定义在另一个类的内部的类。嵌套类存在的目的应该只是为它的外围类提供服务。如果嵌套类将来可能会用于其它的某个环境中,它就应该被设计为顶层类。
嵌套类有四种:静态成员类、非静态成员类、匿名类、局部类。
可以把静态成员类看作是普通的类,只是被声明在另一个类的内部而已,它可以访问外部类的所有成员,包括那些声明为私有的成员。静态成员类是外围类的一个静态成员,与其他的静态成员一样,也遵守同样的可访问性规则。如果它被声明为私有的,它就只能在外围类的内部才可以被访问。静态成员类的一种常见用法是作为公有的辅助类,仅当与它的外部类一起使用时才有意义。
非静态成员类的每个实例都隐含着与外围类的一个外围实例相关联。在非静态成员类的实例方法内部,可以调用外围实例上的方法,或者利用修饰过的this构造获得外围实例的引用。
当非静态成员类的实例被创建的时候,它和外围实例之间的关联关系也随之被建立起来,这种关联关系以后不能被修改。
非静态成员类的一个常见用法是定义一个Adapter,它允许外部类的实例被看作是另一个不相关的类的实例。
//非静态成员类实现了Set的集合视图
public class MySet<E> extends AbstractSet<E> {
...
public Iterator<E> iterator() {
return new MyIterator();
}
private class MyIterator implements Iterator<E> {
...
}
}
如果声明成员类不要求访问外围类实例,就要始终使用静态成员类。因为非静态成员类每个实例都包含一个额外的指向外围对象的引用,保存这份引用要消耗时间和空间,并且会导致外围实例在符合垃圾回收时仍得以保留。
匿名类没有名字,它在使用的同时被声明和实例化,由于匿名类出现在表达式中,它们必须保持简短--大约10行或者更少些--否则就会影响程序的可读性。
匿名类的常见用法:
- 动态地创建函数对象,如匿名的Comparator实例
- 创建过程对象,如Runnable、Thread、TimeTask实例
- 用在静态工厂方法的内部
static List<Integer> intArrayAsList(final int[] a) {
if(a == null) {
throw new NullPointerException();
}
return new AbstractList<Integer>() {
public Integer get(int i) {
return a[i];
}
public Integer set(int i, Integer val) {
int oldVal = a[i];
a[i] = val;
return oldVal;
}
public int size() {
return a.length;
}
};
}
局部类是四种嵌套类中用的最少的类,在任何可以声明局部变量的地方都可以声明局部类,并且局部类也遵守同样的作用域规则。它有名字,可被重复地使用,它不能包含静态成员,必须非常简短,以免影响可读性。
四种嵌套类都有自己的用途。如果一个嵌套类需要在单个方法之外仍然可见,或者它太长了,不适合放在方法内部,就应该使用成员类。如果成员类的每个实例都需要一个指向其外围实例的引用,就要把成员类声明为非static的;否则就声明为static的。如果这个嵌套类属于一个方法的内部,而你只需要在一个地方创建实例,并且已经有了一个预置的类型可以说明这个类的特征,就要把它做成匿名类;否则,就做成局部类。