15使类和成员的可访问性最小化
面向对象语言设计时的两大“法宝”就是封装和解耦。其中,封装可以隐藏组件所有的实现细节,把API与实现清晰隔离开,因此也就解除组成系统的各个组件之间的耦合关系。
解耦还同时有另外的优点:
一旦完成了一个系统,并通过分析定位到哪些组件影响了系统的性能,这些组件就可以单独被优化
提高了代码的重用性,因为组件之间并不会紧密相连,在其他环境里这些模块可能也有很大用途
降低了构建大型系统的风险,因为系统即使不可用了,这些独立的组件也可能是可用的
这也是当今主流微服务的特色
说了这么多,实现封装和解耦其实很简单,尽可能使每个类或成员不被外部访问即可。如果一个类只是在某一个类里被用到,则应该让这个类成为使用它的那个类的私有嵌套类。
但是里氏替换原则限制了我们降低方法可访问性的目标,即如果方法是覆盖父类中的方法,子类方法的访问级别就不能低于父类中的访问级别。这样可以确保任何可使用父类实例的地方也都可以使用子类的实例。
这条规则有条特殊的:如果是类实现接口,则所有被实现的方法必须是public的
public类的实例域绝不能是public的,因为public属性通常并不是线程安全的。如果有常量是需要暴露的,可以使用public static final来修饰这些属性。
对于数组这种数据结构,让类具有public static final数组属性是错误的,因为数组里面的内容还是可能会被修改的,下面就是一种错误的写法:
// wrong 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(); }
16 在public类中使用访问方法而不是public属性
有时候需要编写一些退化类,用来聚合一些属性,比如这样:
class Point { public double x; public double y; }
上面的写法违反了面向对象设计中封装的规定,应该被杜绝,而是使用下面这种setter的风格代替:
public 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; } }
Java平台类库里java.awt.Point和java.awt.Dimension类违反了这一条原则,这是个错误!
如果属性是final的,可以将其可见范围设为public并强加约束条件,就像下面一样加限制条件:
public class Time { private static final int HOURS_PER_DAY = 24; private static final int MINUTES_PER_HOUR = 60; public final int hour; public final int minute; public Time(int hour, int minute){ if (hour < 0 || hour >= HOURS_PER_DAY) throw new IllegalArgumentException("Hour: " + hour); if (minute < 0 || minute >= MINUTES_PER_HOUR) throw new IllegalArgumentException("Minute: " + minute); this.hour = hour; this.minute = minute; } }
17 可变性最小化原则
不可变类是指shilling不能被修改的类,比如:Sting、基本类型包装类、BigInteger、BigDecimal
之所以要使用不可变类,大致原因有:不可变的类更容易实现,且安全性更高,之后会有更详细的讨论。
不可变类的实现遵循下面四条原则:
1. 不提供任何setter方法
2. 保证类不会被继承
一般做法是将类声明为final的。还有一种做法是让类的构造函数是private或default的,然后使用静态工厂来代替公有的构造函数:
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); } }
3. 所有的属性都是private final的
这条规定比实际使用里严格了一点,实际上为了提高性能,只要保证方法对对象的修改不会被外部可见即可。比如有一些不可变类也有不是final的属性,在第一次请求计算时会将一些昂贵计算开销的结果缓存在这些属性里。
4. 确保对任何可变组件的互斥访问
这句话读起来比较拗口,实际上意思就是:不可变类里面如果有指向可变对象的字段,必须确保客户端不能获得这些字段。
下面是一个不可变类的例子,该类定义了数学上面的复数:
public 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); } }
注意这里算术运算返回的是新创建的Complex实例,它可以保证参与计算的实例是不可变的,这种方式被称为函数式。
上面的方法名都是用的介词(plus),而不是动词(add),强调该方法不会改变对象的值。BigInteger和BigDecimal类是没有遵循这个习惯。
之所以提倡使用不可变类,还是因为它有太多的优点:
1. 不可变对象比较简单
因为它只有一种状态且不会变
2. 不可变对象是线程安全的,可以被随意共享
我们也应该充分利用这种优势,对于经常要用的值,提供public static final常量来复用。比如:
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);
3. 可以共享类的内部信息
例如BigInteger内使用了符号数值表示法。符号用int存储,绝对值用数组存储。negate方法会产生一个符号相反的BigInteger,并不需要拷贝数组,而是指向原实例内的同一个数组
4. 提供了原子性
这个很好理解,因为它是不变的
不可变类具有唯一的缺点就是,对于不同的值要有不同的对象。
如果执行一个多步骤的操作,每个步骤都产生一个新的临时对象的话,性能瓶颈就会显露出来。
对此,也有两种解决方案:
猜测一下会用到哪些多步骤操作,使用基本数据类型
提供一个public的可变配套类
例如String的可变配套类StringBuilder
StringBuffer被废弃掉了
当BigInteger和BigDecimal被编写时,还没有很好地贯彻不可变的类必须为final的理念,所以他们的所有方法都可能被覆盖,所以如果我们代码里面出现了他们的子类,就必须对其进行保护性拷贝:
public static BigInteger safeInstance(BigInteger val){ return val.getClass() == BigInteger.class ? val : new BigInteger(val.toByteArray()); }
此外,还有一些其他需要注意的细节:
如果类不能被做成不可变的,应尽可能限制它的可变性,比如降低其可以存在的状态数
除非属性必须是非final的,否则都应该是private final的
18 复合优先于继承
普通的类如果要继承一个来自其他包的类,是非常危险的。原因是继承打破了封装性。
由于子类依赖于父类的某些实现细节,所以子类必须跟着其父类的更新而更新,下面用两个实际的例子来具体解释一下这一条规则:
例一:
假设我们要给HashSet添加一个方法,返回自从创建依赖添加了多少元素:
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; } }
上面的代码,如果调用addAll方法添加元素就会有bug
InstrumentedHashSet<String> s = new InstrumentedHashSet<>(); s.addAll(List.of("Snap", "Crackle", "Pop"));
我们期望getAddCount方法返回3,但实际上返回的是6。我们分析一下程序的执行过程就能知道原因,InstrumentedHashSet的addAll方法先给addCount加3,然后执行到super.addAll(c)调用HashSet的addAll
这里又会调用被InstrumentedHashSet覆盖了的add方法里,所以addCount又被加了一遍,最终结果等于6。
解决方法也很简单,去掉子类中的addAll即可,但是这样修改的正确性在于:HashSet的addAll方法是在它的add方法上实现的,我们没有办法保证HashSet的addAll方法是一成不变的,所以InstrumentedHashSet类就很脆弱。
例二:
假设我们有一个集合,添加进集合的所有元素都必须满足一定的条件。如果要用一个子类继承它的话,我们就需要覆盖所有能添加元素的方法。
这样一来,一旦父类新增了能插入元素的方法,由于子类并未覆盖,所以就少了判断是否满足条件这一步骤,就可能将非法的元素给加入到集合里了。
将HashTable和Vector加入到Collections框架里面时,就修正了几个这样的漏洞
复合可以避免前面所有的问题,即不继承现有的类,而是在新的类中增加一个私有域,引用现有类的一个实例。
public class InstrumentedSet<E> extends HashSet<E> { private int addCount = 0; public InstrumentedSet(Set<E> s){ super(s); } @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; } }
public class ForwardingSet<E> implements Set<E> { private final Set<E> s; public ForwardingSet(Set<E> s){ this.s = s; } @Override public void clear() { s.clear(); } @Override public boolean contains(Object o) { return s.contains(o); } @Override public boolean isEmpty() { return s.isEmpty(); } @Override public int size() { return s.size(); } @Override public Iterator<E> iterator() { return s.iterator(); } @Override public boolean add(E e) { return s.add(e); } @Override public boolean remove(Object o) { return s.remove(o); } @Override public boolean containsAll(Collection<?> c) { return s.containsAll(c); } @Override public boolean addAll(Collection<? extends E> c) { return s.addAll(c); } @Override public boolean removeAll(Collection<?> c) { return s.removeAll(c); } @Override public boolean retainAll(Collection<?> c) { return s.retainAll(c); } @Override public Object[] toArray() { return s.toArray(); } @Override public <T> T[] toArray(T[] a) { return s.toArray(a); } @Override public boolean equals(Object o) { return s.equals(o); } @Override public int hashCode() { return s.hashCode(); } @Override public String toString() { return s.toString(); } }
构造函数的入参类型是Set,这个类把入参的Set变成了另一个Set并添加了计数器的功能。这里的包装类可以被用来包装任何Set的实现:
Set<Instant> times = new InstrumentedSet<>(new TreeSet<>(cmp)); Set<E> s = new InstrumentedSet<>(new HashSet<>(INIT_CAPACITY));
因为每一个InstrumentedSet实例都将Set实例包装起来了,所以他也是包装类,对应装饰器模式。
包装类不适用于回调框架,在回调框架里,对象将自身的引用传递给其他的对象用于回调,被包装起来的对象不知道外面包着它的对象,所以会传一个this,回调时就会和外面包装对象无关。—— SELF问题
那什么时候用继承呢?
对于两个类A,B,只有他们之间确实存在"is a"关系时才应该使用继承。如果打算让B继承A,就要确保每个B都是A,否则应该使用复合。
Java的类库里也有很多违反这一原则的地方,例如栈并不是向量,所以Stack不应该继承Vector。同理,Properties也不应该继承Hashtable
如果在适合使用复合的地方使用了继承,则会不必要地暴露实现细节,可能导致语义混淆。例如,如果p是Properties实例,p.getProperty(key)就可能产生与p.get(key)不同的结果,前一个方法 考虑了属性表,后一个方法继承自Hashtable。
决定使用继承之前还要再问自己一个问题,父类是否有缺陷?,继承会把父类API中的所有缺陷传播到子类里,复合允许设计新的API来隐藏缺陷。
19 要么设计继承并提供文档,要么禁止继承
这一条讨论专门为继承而设计并具有文档说明的类。
这种类必须有文档说明它可被覆盖的方法的自用性。即,对于每个public或protected的方法,文档必须指明在哪些情况下会调用可覆盖的方法。
如果方法调用了可覆盖的方法,在其文档注释的末尾应该包含关于这些调用的描述信息,以Implementation Requirements开头,以java.util.AbstractCollection规范为例:
// 如果集合中包含一个或多个元素e,就从中删除一个。如果集合中包含e则返回true public boolean remove(Object o)
Implementation Requirements:该实现遍历整合集合来查找指定的元素。如果找到钙元素,将会用迭代器的remove方法将其从集合中删除。
注意:如果该集合的iTerator方法返回的迭代器没有实现remove方法,就会抛UnsupportedOperationException异常
实际上,关于程序文档有一个通识:好的文档应该描述给定的方法做了什么工作,而不是如何做到的。上面这种做法显然违背了,这也是继承破坏了封装性所带来的后果。
对于为继承而设计的类,有以下几点要求:
1. 类必须提供精心挑选的protected方法作为钩子(hook),以便进入其内部工作中
例如:java.util.AbstractList的removeRange方法
// 删除列表中所有索引位于[fromIndex, toIndex)的元素 protected void removeRange(int fromIndex, int toIndex)
这个方法是通过clear操作在这个列表及其子列表中调用的。覆盖这个方法以利用列表实现的内部信息,可以充分改善这个列表及其子列表中clear的性能
Implementation Requirements:这项实现获得了一个处在fromIndex之前的的列表迭代器,并依次重复调用ListIterator.next和ListIterator.remove,直到整个范围都被移除位置
这个方法对于Lst实现的最终用户并无意义,唯一目的在于使子类更易于提供针对子列表的快速clear方法。
2. 为继承而设计的类,唯一的测试方法是编写子类
这也是我们决定要暴露哪些protected方法或属性的依据,如果遗漏了关键的protected成员,尝试编写子类时就会遭受遗漏的“痛苦”。所以必须要在发布类之前先编写好子类对类进行测试。
3. 构造器不能调用可被覆盖的方法
如果违反了这条规则,可能导致调用失败。原因在于父类的构造器先于子类的执行,所以子类中覆盖版本的方法会在子类构造器之前先被调用(我们期望是子类方法在构造器之后运行)。
例如:
public class Super { public Super() { overrideMe(); } public void overrideMe(){ } }
public class Sub extends Super{ private final Instant instant; Sub(){ instant = Instant.now(); } @Override public void overrideMe() { System.out.println(instant); } public static void main(String[] args) { Sub sub = new Sub(); sub.overrideMe(); } }
我们期望它打印两次日期,但实际上它第一次打印的是null,因为overrideMe方法被Super构造器调用时,构造器Sub还没有机会初始化instant域。
4. 当为继承设计的类要实现Cloneable或Serializable接口时,clone和readObject都不能调用可覆盖的方法
因为clone和readObject的行为非常类似于构造器,所以类似的规则限制也是适用的
5. 当为继承设计的类实现了Serializable接口,并且该类有一个readResolve或writeReplace方法,必须声明他们是protected的
因为如果这些方法是private的,子类就会直接忽略掉这两个方法
6. 对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化
有两种方法可以禁止子类化:
① 把这个类声明为final的;
② 把所有的构造器都变成private,并增加一些公有的静态工厂来代替构造器。
7. 如果具体的类没有实现标准的接口,并且必须子类化,需要确保这个类的方法永远不会调用它的其他任何可覆盖的方法
这样被覆盖的方法就不会影响调用它的类的方法
20 接口优于抽象类
接口优于抽象类的主要原因就是:Java只允许单继承,所以用抽象类作为类型定义受到了一定限制。接口就不一样了,一个类可以继承多个接口,灵活度更高。
具体的思考点有以下几个:
1. 类可以很容易实现新的接口来更新,但无法通过多继承一个抽象类的方式来更新
当Comparabl、Iterable、Autocloseable接口被引入Java时,很多已有的类就被更新了,而且只用实现新的接口即可。但如果已经继承了一个类的话,就不能再多继承一个来实现更新。
2. 接口是定义mixin的理想选择
mixin类型:类实现了这个mixin类型,以表明它提供了某些可供选择的行为。之所以叫mixin是因为它允许函数可被混合到类的主要功能中
例如Comparable是一个mixin接口,它允许实现它的实例可以与其他实例比较。抽象类不能用与定义mixin,原因是:不可能有一个以上的父类,类层次结构里没有合适的地方来插入mixin
3. 接口允许构造非层次的类框架
例如,我们定义代表singer和songwriter的接口
public interface Singer { AudioClip sing(Song s); }
public interface Songwriter { Song compose(int chartPosition); }
在实际生活里,有些歌唱家本身也是作曲家,所以类同时实现Singer和Songwriter也是可以的:
public interface SingerSongwriter extends Singer, Songwriter{ AudioClip strum(); void actSensitive(); }
4. 包装类(wrapper)模式使得接口可以安全地增强类的功能
如果使用抽象类来定义类型,只能使用继承的手段来增加功能,这样得到的类与包装类相比,功能更差也更脆弱。
5. 通过对接口提供一个抽象的骨架实现类,可以把接口和抽象类的优点结合起来
接口负责定义类型,骨架实现类则负责实现非基本类型接口方法 —— 模板方法模式。
例如Coleections Framework为每个重要的集合接口(AbstractCollection、AbstractSet、AbstractList、AbstractMap)提供了一个骨架实现,例如下面这种实现:
static List<Integer> intArrayList(int[] a){ Objects.requireNonNull(a); return new AbstractList<Integer>() { @Override public Integer get(int index) { return a[index]; } @Override public Integer set(int index, Integer element) { int oldVal = a[index]; a[index] = element; return oldVal; } @Override public int size() { return a.length; } }; }
此外,还有一个模拟多重继承的概念:实现了接口的类可以把对于接口方法的调用转发到一个内部private类的实例上,这个内部private类继承了骨架实现类。
编写骨架类比较简单,这里用一个例子说明,以Map.Entry接口为例,明显的方法是getKey,getValue,setValue,接口里面定义了equals和hashCode方法,但不允许Object方法提供默认实现,所以所有实现都放在骨架实现类里:
public abstract class AbstractMapEntry<K, V> implements Map.Entry<K, V> { // 必须重写这个方法 @Override public V setValue(V value) { throw new UnsupportedOperationException(); } @Override public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof Map.Entry)) return false; Map.Entry<?,?> e= (Map.Entry) o; return Objects.equals(e.getKey(), getKey()) && Objects.equals(e.getValue(), getValue()); } @Override public int hashCode() { return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); } @Override public String toString() { return getKey() + "=" + getValue(); } }
6. 骨架实现类是为了继承的目的而设计的,对于骨架实现类而言,绝对需要一个好的文档
骨架实现上有个类似的简单实现,例如AbstractMap.SimpleEntry。简单实现就像骨架实现一样,因为它实现了接口,并且是为了继承而设计的,区别是它并不是抽象的
21 为后代设计接口
这一条主要讨论Java8新增的缺省方法构造。
它的目的是允许给现有的接口添加方法,但实际上给现有的接口添加方法有很大的风险。
缺省方法的声明中包括一个缺省实现,这是给实现了该接口但没有实现默认方法的类用的。
有了缺省方法,接口的现有实现就可能会出现编译时没有报错,运行时失败的情况
这里书本里有一个翻译错误,我专门去看了英文原版,注意是编译时没有报错
Java8在核心集合接口里添加了很多新的缺省方法,主要是为了便于使用lambda表达式,以removeIf方法为例,这个方法被添加到了Collection接口里,用来移除 所有元素并返回一个boolean类型的值:
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; }
当filter.test(each.next())结果为true时就移除。
但是在某些Collection实例里会出错,例如org.apache.commons.collections4.Collection.SynchronizedCollection,它是Apache提供的一个包装类,所有方法在委托给包装类之前,都在客户端传入的对象上进行同步(上锁)。如果这个类与Java8结合使用就会继承removeIf的缺省实现,由于缺省根本不知道同步这回事,所以就不能保证同步,如果SynchronizedCollection实例调用了removeIf方法,并且有另一个线程对集合进行修改,就会报异常。
所以综上所述可以得到结论:尽管缺省方法已经是Java的一部分,但谨慎设计接口仍然至关重要,建议避免利用缺省方法在现有接口上添加新方法。缺省方法可以在现有的接口上添加方法,但还是存在很大的风险,因此在程序发布之前,测试每一个接口是非常重要的。
22 接口只用于定义类型
为了其他目的而定义接口是不恰当的。
当类实现接口时,接口就作为一种类型,引用这个类的实例。
比如HashSet就是Set
所以类实现了接口,就表明客户端可以对这个类的实例实施某些动作。
常量接口不满足上面的条件,这种接口只包含静态的final域,例如:
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; }
注意常量接口是对接口的不正确使用!,实现常量接口会导致把这样的实现细节泄漏到类的导出API里,如果类之后被修改了,变得不再需要这些常量了,它依然需要实现这个接口,以确保二进制兼容性。
Java里面有一些常量接口是不值得效仿的,如java.io.ObjectStreamConstants
如果要导出常量,有几种合理的选择方案:
1. 如果这些常量与某个现有的类或接口紧密相关,就应该把这些常量添加到这个类或接口中
例如Java平台类库里所有的包装类Integer和Double都导出了MIN_VALUE和MAX_VALUE常量
2. 如果这些常量最好被看作是枚举类型的成员,应该用枚举类型
3. 否则应该是用不可实例化的工具类
例如:
public class PhysicalConstants { private PhysicalConstants(){} public static final double AVOGADROS_NUMBER = 6.022_140_857e23; public static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23; public static final double ELECTRON_MASS = 9.109_383_56e-31; }
工具类通常要求客户端要用类名来修饰这些常量名,例如PhysicalConstants.AVOGADROS_NUMBER,如果要大量利用工具类导出的常量,需要通过静态导入,避开使用类名修饰常量名:
import static com.wjw.batch1.PhysicalConstants.AVOGADROS_NUMBER; public class Test { double atoms(double mols){ return AVOGADROS_NUMBER * mols; } }
23 类层次优于标签类
有时可能会碰到一种类,它的实例有多种⻛格,并且包含一个标签字段(tag field)来表示具体是哪一种风格的,例如:
public class Figure { enum Shape {RECTANGLE, CIRCLE}; final Shape shape; double length; double width; 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); } } }
上面这个类能表示矩形或者圆形,但它有很多缺点:
可读性差,多个实现在一个类中混杂在一起;
内存使用增加,比如表示"圆形"的代码还存储了一些表示"矩形"的代码;
构造方法必须设置标签字段并初始化正确的数据字段:如果初始化错误的字段,程序将在运行时失败;
代码扩展性差,如果想要添加一个图形,必须给每个switch 语句添加一个case。
总结成一句话就是:标签类冗⻓,容易出错,且效率低下,不建议用!
我们应该用子类型化(subtyping)来定义一个能够表示多种不同⻛格对象的单个类
24 静态成员类优于非静态成员类
这一条说的是什么时候应该用哪种嵌套类
嵌套类(nested class)是指定义在另一个类中的类,它存在的目的是为它的外围类提供服务。Java里面有四种嵌套类:静态成员类,非静态成员类,匿名类和局部类。除了第一种以外,剩下的三种都称为内部类。
(非)静态成员类
静态成员类可以被看做是普通的类,有以下特点:
可以访问所有外部类的成员(包括private成员);
是外部类的静态成员,遵循与其他静态成员相同的可访问性规则;
如果被声明为private,则只能在外部类中访问。
静态成员类的一个常⻅用途是作为公有的辅助类,仅在与其外部类一起使用时才有用。例如,考虑一个
计算器,它支持很多操作,Operation 枚举应该是Calculator 类的公有静态成员类,之后就可以用Calculator.Operation.PLUS这类语法来引用这些操作。
静态成员类和非静态成员类之间的区别很大:
非静态成员类的每个实例都隐含地与其外部实例相关联;
在非静态成员类的方法中,可以调用外部实例上的方法,或者使用修饰过的this获得对外部实例的引用;
不可能在没有外部实例的情况下创建非静态成员类的实例。
非静态成员类实例创建时(语法enclosingInstance.new MemberClass(args)),它和外部实例之间的关联关系也建立起来,需要占用非静态成员类实例的空间,造成额外开销。
非静态成员类的一个常⻅用法是定义一个Adapter, 它允许将外部类的实例视为某个不相关的类的实例。例如,Map 接口的实现通常使用非静态成员类来实现它们的集合视图(keySet,entrySet,values)迭代器等:
public class MySet<E> extends AbstractSet<E> { // Bulk of the class omitted @Override public Iterator<E> iterator() { return new MyIterator(); } private class MyIterator implements Iterator<E> { ... } }
如果声明成员类不需要访问外部实例,要把它声明成为一个静态成员类,如果省略了static,每个实例都会包含一个额外的指向外部对象的引用。会导致即使外部类在满足垃圾回收的条件时仍然留在内存中——内存泄漏
私有静态成员类的常⻅用法是代表外部类对象的组件。例如Map的Entry对象,虽然每个Entry都与Map关联,Entry上的方法(getKey,getValue 和 setValue)不需要访问Map,所以将Entry声明为非静态时就会很浪费,所以private的静态成员类是最佳的选择。
匿名类、局部类
匿名类在使用时同时被声明和实例化。匿名类可以出现在代码中任何允许存在表达式的地方。即使它们出现在静态环境中,它们也不能有除常量变量(final类型)或者被初始化成常量表达式的字符串,之外的任何静态成员。
局部类使用的最少,一个局部类可以在任何可以声明局部变量的地方声明,并遵守相同的作用域规则。
上面四种类的应用场景:
如果一个嵌套的类需要在一个方法之外可⻅,或者代码太⻓,使用成员类;
如果一个成员类的每个实例都需要一个对其宿主实例的引用,使其成为非静态的,否则,使其静态类;
假设这个类属于一个方法内部,并且创建的实例只需要用一次,类也提前定义好了,使用匿名类;
其他情况使用局部类。
25 一个文件只定义一个顶级类
在一个文件里定义多个顶级类时,可能导致给一个类提供多个定义,具体哪个定义会被用到,取决于源文件被传给编译器的顺序,例如:
public class Main { public static void main(String[] args) { System.out.println(Utensil.NAME + Dessert.NAME); } }
假设在Utensil.java文件里同时定义了Utensil 和 Dessert
class Utensil { static final String NAME = "pan"; } class Dessert { static final String NAME = "cake"; }
现在假设又在Dessert.java里定义了同样的两个类:
class Utensil { static final String NAME = "pan"; } class Dessert { static final String NAME = "cake"; }
如果用命令javac Main.java Dessert.java来编译程序,编译就会失败,此时编译器会提醒定义了多个Utensil 和 Dessert类,原因是编译器会先编译Main.java,当它看到Utensil 的引用,就会在Utensil.java中查看这个类,结果找到了两个类。当编译器执行到Dessert.java时,也会去查找该文件,发现又会遇到这两个定义
如果用命令javac Main.java或javac Main.java Utensil.java编译程序,结果会输出pancake
如果用命令javac Dessert.java Main.java编译程序,就会输出potpie
上面的结果显然是我们不接受的
如果一定要把多个顶级类放进同一个源文件里,就要考虑使用静态成员类,例如:
public class Main { 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"; } }