天天看点

《Effective Java 3rd》读书笔记——类和接口

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

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

尽可能地使每个类或成员不被外界访问。

对于顶层的(非嵌套的)类和接口,能做成包级私有的,就应该被做成包级私有(即不加任何修饰符)。这样就可以随心所欲的对其进行修改或删除。

如果一个包级私有的顶层类(或接口)只是在某一个类的内部被用到,那就应该考虑使它成为唯一使用它的那个类的私有嵌套类。

公有的实例域决不能是公有的,这条规则也同样适用于静态域。长度非零的数组总是可变的,所以让类具有公有的静态final数组域,或者返回这种域的访问方法,这是错误的。

如果类具有这样的域或者访问方法,客户端将能修改数组中的内容。

// 具有潜在的安全问题
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();
}      

确保公有静态final域所引用的对象都是不可变的。

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

如果类可以在它所在的包之外进行访问,就提供访问方法,以保留将来改变该类的内部表示法的灵活性。

就像这样,将x,y设为私有的,并提供访问方法:

@Data //用于生成getter/setter
class Point {
    private double x;
    private double y;
    public Point(double x,double y) {
        this.x = x;
        this.y = y;
    }
    // 生成的getter/setter
    //...
}      

如果类是包级私有的,或者是私有的嵌套类,直接暴露它的数据域并没有错误。因为这些类不会被外界访问到。

使可变性最小化

坚决不要为每个getter方法都写一个相应的setter方法,除非有很好的理由让类成为可变的类,否则它就应该是不可变的。

如果类不能被做成不可变的,仍然应该尽可能地限制它的可变性。除非有合理的理由,否则每个属性都要是​

​private final​

​​的。

构造器应该创建完全初始化的对象,并建立起所有的约束关系。

不可变类指的是实例不能被修改的类。Java提供了很多不可变类的实现:​

​String​

​​、基本的包装类型、​

​BigInteger​

​​、​

​BigDecimal​

​。

如何设计不可变类

只要遵循下面五条规则:

  • 不要提供任何会修改对象状态的方法(Setter方法)
  • 保证类不可以被扩展(增加​

    ​final​

    ​关键字)
  • 声明所有的属性都是​

    ​final​

    ​的
  • 声明所有的属性都是私有的
  • 确保对于任何可变组件的互斥访问

如果类具有指向可变对象的属性,必须确保该类的客户端无法获得指向这些对象的引用。并且,不要用客户端提供的对象引用来初始化这样的属性;不要从任何访问方法(getter)中返回该对象的引用。在构造器、访问方法和​

​readObject​

​方法中使用保护性拷贝技术。

下面是一个很好的不可变类的例子:

package com.java.effective.classandinterface;

/**
 * 设计良好的不可变类,但不是一个工业级强度的复数实现。
 * @Author: Yinjingwei
 * @Date: 2019/4/20/020 15:19
 * @Description:
 */
public final class Complex {
    //final保证类不可以被扩展

    /**
     * 对于频繁用到的值,为它们提供公有的静态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);

    /**
     * 声明所有的属性都是final&private,且通过设计成原始类保证属性是不可变的
     */
    //实部
    private final double re;
    //虚部
    private final double im;

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

    public double readPart() {
        return re;
    }

    public double imaginaryPart() {
        return im;
    }

    /**
     * 所有的修改操作都返回新的对象,没有提供修改本身属性的Setter方法
     * @param c
     * @return
     */
    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);
    }

    /**
     * 正确equals示例
     * @param other
     * @return
     */
    @Override
    public boolean equals(Object other) {
        if (other == this) {
            return  true;
        }
        if (!(other instanceof  Complex)) {
            return false;
        }
        Complex c = (Complex)other;
        return Double.compare(c.re,re) == 0
                 && Double.compare(c.im,im) == 0;
    }

    /**
     * 31 质数,这里和String#hashCode用了同样的公式
     * @return
     */
    @Override
    public int hashCode() {
        return 31 * Double.hashCode(re) + Double.hashCode(im);
    }
}      

注意这些算术运算如何创建并返回新的​

​Complex​

​实例,而不是修改这个实例。它被称为函数的(functional)方法:返回了一个函数的结果,对操作数进行运算但并不修改它。

一个约定优于配置的例子: 这些函数的方法名称都是介词(如plus),而不是动词(如add)。用于强调该方法不会改变对象的值。这是一种命名习惯。

鼓励客户端尽可能重用现有的实例, 对于频繁用到的值,为它们提供公有的静态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);      

类似于​

​Integer​

​​等类中的​

​IntegerCache​

该书的作者为Java提供了很多源码实现,看了源码后再回过头来阅读本书会有一种恍然大悟的感觉。比如上面的Integer的@author中就可以看到他的名字

不可变对象的特性:

  • 不可变对象是线程安全的。因为不能发生改变,因此可以被自由地共享。
  • 不仅可以共享不可变对象,甚至也可以共享它们的内部信息。如​

    ​BigInteger​

    ​​的​

    ​negate()​

    ​​方法产生新的​

    ​BigInteger​

    ​共享了数值int数组。
  • 不可变对象为其他对象提供了大量的构建。不可变对象构成了大量的Map key和集合元素,京这样破坏了Map 或集合的不变性,但不需要担心它们的值会发生变化。
  • 不可变对象无偿地提供了失败的原子性。它们的状态永远不变,因此不存在临时不一致的可能性。
  • 不可变类唯一的缺点是,对于每个不同的值都需要一个单独的对象。

类不允许被继承,除了使类变成​

​final​

​的之外,还有一种更加灵活的方法可以做到这一点: 让所有的构造器都变成私有(或包级私有),并添加公有的静态工厂来代替公有的构造器。

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);
}      

这种方法的一个重要好处是,可以通过改善静态工厂的缓存能力,在后续的版本中改进该类的性能。

关于没有方法会修改对象,并且它的所有属性都必须是​

​final​

​​的。这个规则比真正的要求要严格一点,为了提高性能可以有所放松。事实上应该是:没有一个方法能对对象的状态产生外部可见的改变。

许多不可变类拥有一个或多个非​​

​final​

​​的属性,它们在第一次被请求执行这些计算的时候,把一些开销昂贵的计算结果缓存到这些域中。将来再次请求同样的计算,直接返回缓存值。因为对象是不可变的,它的

不可变性保证了这些计算再次执行也能产生同样的结果。如​​

​String​

​​的​

​hashCode​

​属性。

/** Cache the hash code for the string */
private int hash; // Default to 0
public int hashCode() {
    int h = hash;
    //如果hash!=0则直接返回
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}      

如果选择让不可变类实现​

​Serializable​

​​接口,并且包含一个或多个指向可变对象的属性,就必须提供一个显式的​

​readObject​

​​或​

​readResolve​

​​方法,或使用​

​ObjectOutputStream.writeUnshared​

​​和​

​ObjectOutputStream.readUnshared​

​方法。

组合优先于继承

继承打破了封装性。子类依赖于其父类中特定功能的实现细节。如果超类的实现发生变化,子类可能会遭到破坏,即使子类的代码完全没有发生改变。除非父类是专门为了继承而设计的,并且具有很好的文档说明。

比如,我们想实现一个​

​HashSet​

​的变体,它可以记录被插入元素的数量。

package com.java.effective.classandinterface;

import java.util.Collection;
import java.util.HashSet;

/**
 * 记录插入的元素数量
 * @Author: Yinjingwei
 * @Date: 2019/4/20/020 16:17
 * @Description:
 */
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);
    }

    /**
     * Collection<? extends E> 表示可以c既可以是E子类的集合,比如E表示水果,子类为香蕉、苹果等。然后香蕉、苹果集合也能加入到水果集合中。
     * @param c
     * @return
     */
    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }
    
    public int getAddCount() {
        return addCount;
    }
}      
这里​

​addAll()​

​​方法参数中泛型的写法做一个简单的说明:​

​Collection<? extends E>​

​​ 表示可以​

​c​

​​可以是任何​

​E​

​​子类的集合,比如​

​E​

​​表示水果,子类为香蕉、苹果等。然后香蕉、苹果集合也能加入到水果集合中。

更详细的说明见博客——​​​Java通配符泛型详解​​

该类看起来很合理,但是不能正常工作。比如:

InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(Arrays.asList("Snap","Crackle","pop"));
System.out.println(s.getAddCount());//6      

期望返回的是3,实际上返回的是6。原因是​

​HashSet​

​​内部​

​addAll()​

​​方法是基于其​

​add()​

​方法来实现的。

public boolean addAll(Collection<? extends E> c) {
    boolean modified = false;
    for (E e : c)
        if (add(e))
            modified = true;
    return modified;
}      

关键是​

​HashSet​

​​的​

​addAll()​

​​方法是它的​

​add()​

​​方法实现的,这种自用性是实现细节,不是承诺,不能保证以后不会发生改变。因此,基于这个细节实现的​

​InstrumentedHashSet​

​​是非常脆弱的。

导致子类脆弱的一个相关的原因是,它们的父类在后续的版本中可以获得新的方法。比如继承了某个集合,子类在其每个新增方法中都增加了某种判断,若该集合添加了新的方法,该新方法就没有这种判断,导致子类出错。

幸运的是,可以通过组合的方式来避免上述所有的问题。因为现有的类变成了新类的一个组件,新类中的每个实例方法可以调用组合类的对应方法,并返回结果。这被称为转发,新类中的方法被称为转发方法。

其实门面模式就是利用了组合的方式,供一个统一的接口(方法)去访问多个子系统的多个不同的接口(方法)。

只有当子类真正是父类的子类型时,才适合用继承。

对于两个类A和B,只有当两者之间的确存在"is-a"关系的时候,类B才可以继承A,如果不确定,应该问问自己:每个B确实是A吗?

还有,对于你正试图继承的类,它的API中没有缺陷吗?如果有,是否愿意把那些缺陷传播到子类的API中?

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

该类必须有文档说明它可覆盖性的方法的自用性。

为了继承而设计的类,唯一的测试方法就是编写子类。

为了允许继承,构造器不能调用可被覆盖的方法。

无论是​

​clone​

​​还是​

​readObject​

​,都不可以调用可覆盖的方法,不管是以直接还是间接的方式。

对于并非为了安全进行继承而设计的类,要禁止继承。

总之,除非知道真正需要子类,否则最好将类声明为​

​final​

​,或确保没有可访问的构造器来禁止类被继承。

接口优于抽象类

Java只允许单继承,所以用抽象类作为类型定义受到了限制。

接口允许构造非层次结构的类型框架。

通过对接口提供一个抽象的骨架实现类,可以把接口和抽象类的优点结合起来。接口负责定义类型,或者还提供一些缺省方法,而骨架实现类则负责实现除基本类型接口方法之外,剩下的非基本类型接口方法。按照惯例,骨架实现类被称为​

​AbstractInterface​

​​,这里的​

​Interface​

​是指所实现的接口名字。

接口通常是定义允许多个实现的类型的最佳途径,如果你导出了一个重要的接口,就应该考虑同时提供骨架实现类。而且,还应该尽可能地通过缺省方法在接口中提供骨架实现,以便接口的所有实现类都能使用。

编写骨架实现类比较简单,但是过程有点乏味。实现,必须确定接口中的哪些方法是最为基本的,其他方法可以根据它们来实现。这些基本方法将成为骨架实现类中的抽象方法。接下来,在接口中为所有可以在基本方法上直接实现的方法提供缺省方法,但是,不能为​

​Object​

​方法提供缺省方法。如果基本方法和缺省方法覆盖了接口,那么就不需要骨架实现类了。否则,就编写一个类,声明实现接口,并实现所有剩下的方法。这个类中可以包含任何非公有的属性,以及适合该任务的任何方法。

以​

​Map.Entry​

​​接口为例,举个简单的例子。明显的基本方法是​

​getKey​

​​、​

​getValue​

​​和​

​setValue​

​​(可选的)。接口定义了​

​equals​

​​和​

​hashCode​

​​的行为,并有一个明显的​

​toString​

​​实现。由于允许给​

​Object​

​方法提供缺省实现,这些实现都放在骨架实现类中:

package com.java.effective.classandinterface;

import java.util.Map;
import java.util.Objects;

/**
 * @Author: Yinjingwei
 * @Date: 2019/4/21/021 16:02
 * @Description:
 */
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 that) {
        if (that == this) {
            return true;
        }
        if (!(that instanceof Map.Entry)) {
            return false;
        }
        Map.Entry<K,V> e = (Map.Entry<K, V>) that;
        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();
    }
}      

为后代设计接口

Java8添加了缺省方法,目的就是允许给现有的接口添加方法。该方法的声明中包含一个缺省实现,这是给实现了该接口但没有实现默认方法的类使用的。

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

比如,以​

​removeIf​

​​方法为例,它在Java8中被添加到了​

​Collection​

​接口,这个方法用来根据条件来移除元素。缺省实现是通过迭代器来遍历集合:

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;
}      
我的源码中的该函数实现是这样的,感觉比作者贴的实现代码写的可读性强了很多。比如removed的定义,让人一看就明白是什么意思,作者中的该变量名称为result。

这是适用于​

​removeIf​

​​方法的最佳通用实现,但它在某些集合实现中会出错,比如,在​

​org.apache.commons.collections.collection.SynchronizedCollection​

​中(该类竟然没有泛化),Apache提供了利用客户端提供对象(而不是用集合)进行锁定的功能,换句话说,它是一个包装类,它的所有方法在委托给包装集合之前,都在锁定对象上进行了同步。比如:

/** The collection to decorate */
protected final Collection collection;
/** The object to lock on, needed for List/SortedSet views */
protected final Object lock;
    
...
    
public boolean remove(Object object) {
    synchronized (lock) {
        return collection.remove(object);
    }
}      

若果这个类与Java8结合使用,将会继承​

​removeIf​

​​的缺省实现,该缺省实现不会(实际上也无法)保持这个类的基本承诺:围绕着每个方法调用执行自动同步。缺省实现根本不知道有同步这回事,也无权访问包含该锁定对象的域。如果客户端在​

​SynchronizedCollection​

​​实例上调用​

​removeIf​

​方法,同时另一个线程对该集合进行修改,就可能导致异常发生。

为了证明作者的观点,我写了个程序测试了下:

public class SynchronizedCollectionDemo {
    public static void main(String[] args) {
        Collection collection = SynchronizedCollection.decorate(IntStream.of(1,2,3,4,5,6,7,8).boxed().
                collect(Collectors.toList()));
        while (true) {
            new Thread(() -> collection.add(10)).start();
            new Thread(() -> collection.removeIf(e-> (Integer) e % 2 == 0)).start();
        }
    }
}      

果然,没跑多久就抛出了异常:

Exception in thread "Thread-1" java.util.ConcurrentModificationException
  at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
  at java.util.ArrayList$Itr.remove(ArrayList.java:865)
  at java.util.Collection.removeIf(Collection.java:415)
  at com.java.effective.classandinterface.SynchronizedCollectionDemo.lambda$main$2(SynchronizedCollectionDemo.java:22)
  at java.lang.Thread.run(Thread.java:748)      

为了避免发生异常,API维护人员必须覆盖类似默认的​

​removeIf​

​实现。

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

因此,在发布程序之前,测试每个新的接口就显得尤其重要。程序员应该以不同的方法实现每一个接口。最起码不应少于三种实现。编写多个客户端程序,利用每个新接口的实例来执行不同的任务。

接口只用于定义类型

当类实现接口时,接口就充当可以引用这个类的实例的类型。因此,类实现了接口,表明客户端可以对这个类的实例实施某些动作。为了任何其他目的而定义接口时不恰当的

比如常量接口。常量接口模式是对接口的不良使用。

如果要导出常量,可以有几种合理的选择方案。

  • 如果这些常量与某个现有的类或接口紧密相关,则把这些常量添加到这个类或接口中。如​

    ​Integer​

    ​​和​

    ​Double​

  • 如果这些常量最好被看作枚举类型的成员,就应该用枚举类型来导出这些常量
  • 否则应该使用不可实例化的工具类

比如:

public class PhysicalConstants {
    private PhysicalConstants(){}
    public static final double AVOGADROS_NUMBER = 6.022_140_875e23;
}      

注意,在数字字面量中使用下划线,对值没有任何影响,而且增加了可读性。通常是每隔3个数字一个下划线。

类层次优于标签类

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

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;
    }

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

这种标签类有很多缺点。充斥着样板代码,包括枚举类型、标签域以及条件语句。破坏了可读性。

标签类过于冗长、容易出错,且效率低下。

幸运的是,Java提供了其他更好的方法来定义能表示多种风格对象的单个数据类型:子类化(subtyping)。标签类正是对类层次的一种简单的效仿。

下面通过类层次实现同样的功能:

abstarct 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;
    }
}      

总之,标签类很少有适用的时候。当你想要编写一个包含显示标签域的类时,应该考虑一下,

这个标签是否可以取消,这个类是否可以用类层次来代替。

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

嵌套类是指定义在另一个类的内部的类。嵌套类存在的目的只是为它的外围内提供服务。

嵌套类有四种:静态成员类、非静态成员类、匿名类和局部类。除了静态成员类外,其他三种都称为内部类。

《Effective Java 3rd》读书笔记——类和接口

静态成员类

静态成员类申明在外围类的内部,可以访问外围类的所有静态成员,包括私有成员,不能访问非静态成员。静态成员类是外围类的一个静态成员,如果它被声明为私有的,它就只能在外围类的内部才可以被访问。

不需要外围类实例就可以创建静态成员类的实例。

常见用法是作为公有的辅助类,只有与它的外部类一起使用才有意义。比如作为Builder:

public class Hero {
    /**
     * 名称
     */
    private final String name;
    /**
     * 角色(AD,SUP,JUG等等)
     */
    private final String roles;
    /**
     * 等级,最高18级
     */
    private final int level;

    private Hero(Builder builder) {
        this.level = builder.level;
        this.name = builder.name;
        this.roles = builder.roles;
    }


    @Override
    public String toString() {
        return "Hero{" +
                "name='" + name + '\'' +
                ", roles='" + roles + '\'' +
                ", level=" + level +
                '}';
    }

    public static void main(String[] args) {
        Hero.Builder builder = new Builder("JUG","Graves");
        builder.level(18);

        Hero hero = builder.build();

        System.out.println(hero);
    }
    
    public static class Builder {
        //必传参数
        private final String roles;
        private final String name;
        //可选参数
        private int level = 0;
        public Builder(String roles,String name) {
            this.roles = roles;
            this.name = name;
        }

        public Builder level(int level) {
            this.level = level;
            return this;
        }

        public Hero build() {
            return new Hero(this);
        }


    }

}      

私有静态成员类的一种常见用法是代表外围类所代表的对象的组件。以​

​Map​

​​实例为例,它把键和值关联起来。许多​

​Map​

​​实现的内部都有一个​

​Entry​

​​对象,对应于​

​Map​

​​中的每个键值对。虽然每个​

​entry​

​​都与一个​

​Map​

​​关联,但是​

​entry​

​​上的方法​

​getKey/getVaue/setValue​

​​并不需要访问该​

​Map​

​​。因此,使用非静态成员类来表示​

​entry​

​是浪费的:私有的静态成员类是最佳的选择。

非静态成员类

从语法上讲,静态成员类和非静态成员类之间的唯一区别是,静态成员类的声明中包含修饰符​

​static​

​。

非静态成员类的每个实例都隐含地与外围类的一个外围实例相关联,在非静态成员类的内部可以调用外围实例上的方法,或者通过修饰过的​

​this​

​构造获得外围实例的引用。

如果嵌套类的实例可以在它外围类的实例之外独立存在,这个嵌套类必须是静态成员类:在没有外围实例的情况下,要想创建非静态成员类的实例是不可能的。

当非静态成员类的实例被创建的时候,它和外围类实例之间的关联关系也随之被建立起来,并且这种关联关系不能修改。当在外围类的某个实例方法的内部调用非静态成员类的构造器时,这关联关系被自动建立起来。使用表达式​

​enclosingInstance.new MemberClass(args)​

​来建立这种关系也是可能的,但是很少使用。

非静态成员类的一种常见用法是定义一个适配器,允许外部类的实例被看作是另一个不相关的类的实例。例如,​

​Map​

​​接口的实现往往使用非静态成员类来实现它们的集合视图,这些集合视图是由​

​Map​

​​的​

​keySet​

​​、​

​entrySet​

​​和​

​values​

​​方法返回的。同样地,诸如​

​Set​

​​和​

​List​

​这种集合接口的实现往往也使用非静态成员类来实现它们的迭代器:

//非静态成员类的典型应用
public class MySet<E> extends AbstractSet<E> {
    ...
    @Override
    public Iterator<E> iterator() {
        return new MyIterator();
    }
    
    private class MyIterator implements Iterator<E> {
        ...
    }
}      

如果声明成员类不要求访问外围实例,就要始终把修饰符​

​static​

​放在它的声明中,使它成为静态成员类,而不是非静态成员类。如果不小心省略了​

​static​

​修饰符,则每个实例都将包含一个额外的指向外围对象的引用,由此造成的内存泄露常常难以发现。

匿名类

没有名字的类。它不是外围类的一个成员,它并不与其他的成员一起被声明,而是在使用的时候同时被声明和实例化。

匿名类的运行受到了很多的限制,除了在它们被声明的时候之外,是无法将它们实例化的。不能执行​

​instanceof​

​测试,或者做任何需要命名类的其他事情;无法声明一个匿名类来实现多个接口,或继承一个类,并同时继承类和实现接口。

匿名类可以出现在代码中任何允许存在表达式的地方。当且仅当匿名类出现在非静态的环境中时,才有外围实例。但是即使出现在静态的环境中,也不可能拥有任何静态成员,而是拥有常数变量。

Java中增加的lambda表达式后,可以优先考虑使用它来替代匿名类。

局部类

是四种嵌套类中使用最少的类。在任何可以声明局部变量的地方,都可以声明局部类。

只有当局部类是在非静态环境中定义的时候,才有外围实例,它们也不能包含静态成员。

总之,每一种嵌套类都有自己的用途。如果一个嵌套类需要在单个方法之外仍然是可见的,或者它太长了,不适合放在方法内部,就应该使用成员类。如果成员类的每个实例都需要一个指向其外围实例的引用,就要把成员类做成非静态的;否则,就做成静态的。 假设这个嵌套类属于一个方法的内部,如果你只需要在一个地方创建实例,并且已经有了一个预置的类型可以说明这个类的特征,就要把它做成匿名类;否则,就做成局部类。

限制源文件为单个顶级类

虽然编译器允许在一个源文件中定义多个顶级类,但是这样做并没有什么好处,只会带来巨大的风险。

因为在一个源文件中定义多个顶级类,可能导致给一个类提供多个定义。哪一个定义被用到,取决于源文件被传输给编译器的顺序。