《Effective Java》笔记18-22:类和接口

第18条:接口优于抽象类

Java 提供两种机制用来定义允许多个实现的类型:接口和抽象类。

接口的优势

  • 现有的类可以很容易被更新,以实现新的接口。当需要增加方法的时候只需要 implements 具体的接口即可,非常方便,而如果通过抽象类来实现,则需要在抽象类里新增方法,而这会导致其他继承该抽象类的类也被强制加上额外的方法!

  • 接口是定义mixin(混合类型)的理想选择。mixin 是指主要的类型:类除了实现它的"基本类型"之外,还可以实现 mixin 类型(利用实现多个接口可以达到混合类型的目的,而利用抽象类只能继承一个类,则不能达到混合类型的效果)

  • 接口允许我们构造非层次结构的类型框架。类型层次对于组织某些事物是非常合适的,但是其他有些事物并不能被整齐地组织成一个严格的层次结构。例如,假设我们有一个接口代表一个 Singer(歌唱家),另一个接口代表一个 SongWriter(作曲家)。

骨架实现

在 Java 的 Collections Framework 中存在一组被称为"骨架实现"(skeletal implementation)的抽象类,如 AbstractCollection、AbstractSet 和 AbstractList 等。如果设计得当,骨架实现可以使程序员很容易的提供他们自己的接口实现。这种组合还可以让我们在设计自己的类时,根据实际情况选择是直接实现接口,还是扩展该抽象类。

和接口相比,骨架实现类还存在一个非常明显的优势,既如果今后为该骨架实现类提供新的方法,并提供了默认的实现,那么他的所有子类均不会受到影响,而接口则不同,由于接口不能提供任何方法实现,因此他所有的实现类必须进行修改,为接口中新增的方法提供自己的实现,否则将无法通过编译。

简而言之,接口通常是定义允许多个实现的类型的最佳途径。这条规则有个例外,即当演变的容易性比灵活性更为重要的时候。在这种情况下,应该使用抽象类来定义类型,但前提是必须理解并且可以接受这些局限性。如果你导出了一个重要的接口,就应该坚决考虑同时提供骨架实现类。

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

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

public interface PhysicalConstants {
    static final double AVOGADROS_NUMBER = 6.02214199e23;
}

常量接口模式是对接口的不良使用。类在内部使用某些常量,这纯粹是实现细节。实现常量接口,会导致把这样的实现细节泄露到该类的导出 API 中。类实现常量接口没有什么价值。如果在将来的发行版本中,这个类被修改了,它不再需要使用这些常量了,它依然必须实现这个接口,以确保二制兼容性。如果非 final 类实现了常量接口,它的所有子类的命名空间也会被接口中的常量所“污染”。Java 平台类库中有几个常量接口,例如 java.io.ObjectStreamConstants,被认为是反面例子,不值得效仿。

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

  • 如果这些常量与某个现有的类或者接口紧密相关,就应该把这些常量添加到这个类或者接口。例如,在 Java 平台类库中所有的数值包装类,如 Integer 和 Double,都导出了 MIN_VALUE 和 MAX_VALUE 常量。
  • 如果这些常量最好被看作枚举类型的成员,就应该使用枚举类型(见第30条)来导出这些常量。
  • 否则,应该使用不可实例化的工具类(见第4条)来导出这些常量,下面是前面的 PhysicalConstants 例子的工具类翻版:
public class PhysicalConstants {
    private PhysicalConstants() { }  // 私有构造器
    public static final double AVOGADROS_NUMBER = 6.02214199e23;
}

简而言之,接口应该只被用来定义类型,它们不应该被用来导出常量。

第20条:类层次优于标签类

有时候,可能会遇到带有两种甚至更多风格(功能)的实例的类,并包含表示实例风格的标签域,例如:

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

标签类的缺点非常明显,当你要表现的风格非常多样的时候,你需要写大量的判断语句,非常容易出错,而且当你需要修改某一个风格的时候,你需要在一大堆代码里找出你要改的地方,很有可能引入 bug,非常难以维护。

这个时候,将标签类转变成类层次就非常方便了:

abstract class Figure {
    abstract double area();
}

class Circle extends Figure {
    private final double radius;
    Circle(double radius) {
        this.radius = radius;
    }
    double area() {
        return Math.PI * (radius * radius);
    }
}

class Rectangle extends Figure {
    private final double length;
    private final double width;
    Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }
    double area() {
        return length * width;
    }
}

类层次的另一好处在于,它们可以用来反映类之间本质上的层次关系,有助于后期的扩充。假设现有加一个正方形有,标签类就需要修改源码,而利用类层次结构只需新加一个正方形类,并继承自长方形,

第21条:用函数对象表示策略

Java 没有提供函数指针,但是可以用对象引用实现同样的功能。调用对象上的方法通常是执行该对象(that Object)上的某项操作。然而,我们也可能定义这样一种对象,它的方法执行其他对象(other Objects)上的操作。如果一个类仅仅导出这样的一个方法,它的实例实际上就等同于一个指向该方法的指针。这样的实例被称为函数对象(Function Object),如 JDK 中 Comparator,我们可以将该对象看做是实现两个对象之间进行比较的"具体策略对象",如:

public class StringLengthComparator {
    public int comare(String s1, String s2) {
        return s1.length() - s2.length();
    }
}

作为典型的具体策略类,StringLengthComparator 类是无状态的:它没有域,所以,这个类的所有实例在功能上都是相互等价的。因此,它作一个 Singleton 是非常合适的,可以节省不必要的对象创建开销(见第3与第5条):

public class StringLengthComparator {
    private StringLengthComparator(){}
    public static final StringLengthComparator INSTANCE = new StringLengthComparator();
    public int comare(String s1, String s2) {
        return s1.length() - s2.length();
    }
}

为了把 StringLengthComparator 实例传递给方法,需要适当的参数类型,直接使用 StringLengthComparator 并不好,因为客户端将无法传递任何其他的比较策略,即不能随时动态的改变比较性为。此时,我们可以定义一个比较策略接口,这个接口在 java.util.Comparator 就已提供。

使用匿名类

具体的策略类往往使用匿名类(见第22条)来声明,下面的语句根据长度对一个字符串数组进行排序:

Arrays.sort(strArr, new Comparator<String>(){
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }});

但是注意,以这种方式使用匿名类时,将会在每次执行调用的时候创建一个新的实例。如果它被重复执行,考虑将函数对象存储到一个私有的静态 final 域时重复使用它。

使用私有内部类

下面的例子使用静态成员类,而不是匿名类,这样允许我们的具体的策略类实现第二个接口 Serializable:

public class Outer {
    private static class StrLenCmp implements Comparator<String>, Serializable {
        public int compare(String s1, String s2) {
            return s1.length() - s2.length();
        }
    }
    public static final Comparator<String> STRING_LEN_CMP = new StrLenCmp();
    //...
}

String 类利用这种模式,通过它的 CASE_INSENSITIVE_ORDER 域,实现了一个不区分大小写的字符串比较器。

public static final Comparator<String> CASE_INSENSITIVE_ORDER
                                         = new CaseInsensitiveComparator();
private static class CaseInsensitiveComparator
        implements Comparator<String>, java.io.Serializable {
    // use serialVersionUID from JDK 1.2.2 for interoperability
    private static final long serialVersionUID = 8575799808933029326L;

    public int compare(String s1, String s2) {
        int n1 = s1.length();
        int n2 = s2.length();
        int min = Math.min(n1, n2);
        for (int i = 0; i < min; i++) {
            char c1 = s1.charAt(i);
            char c2 = s2.charAt(i);
            if (c1 != c2) {
                c1 = Character.toUpperCase(c1);
                c2 = Character.toUpperCase(c2);
                if (c1 != c2) {
                    c1 = Character.toLowerCase(c1);
                    c2 = Character.toLowerCase(c2);
                    if (c1 != c2) {
                        // No overflow because of numeric promotion
                        return c1 - c2;
                    }
                }
            }
        }
        return n1 - n2;
    }

    /** Replaces the de-serialized object. */
    private Object readResolve() { return CASE_INSENSITIVE_ORDER; }
}

总结

简而言之,函数的主要用途就是实现策略模式。为了在 Java 中实现这种模式,要声明一个接口来表示该策略,并且为每个具体策略声明一个实现了该接口的策略类。

当一个具体策略只被使用一次时,通常使用匿名类来声明和实例化这个具体匿名策略类。

当一个具体策略是设计用来重复使用的时候,它的类通常要被实现为私有的静态成员类,并通过公有的静态 final 域被导出,其类型为该策略接口。

第22条:优先考虑静态成员类(四种嵌套类中)

嵌套类(nested class)是指被定义在另一个类的内部的类。嵌套类存在的目的应该只是为了它的外围类提供服务。

静态成员类

不称之为内部类(inner class)。可以看为普通的类,可以访问外围类的所有成员,和其他的静态成员一样,也遵循同样的可访问行规则(如果为私有,则只能在外围类的内部被访问)

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

非静态成员类

非静态成员类的实例都隐含持有一个外部类的实例(enclosing instance)

这不仅仅会消耗更多的空间,还可能会导致外部类的实例泄漏,内存泄漏,而静态成员类并不会。

匿名类

不能拥有任何静态成员,通常用于创建函数对象(见21条),比如 Thread,Runnable

局部类

最少使用的类,在任何"可以声明局部变量"的地方,都可以声明局部类,并且局部类也遵守同样的作用域规则。

总结

如果一个嵌套类需要在单个方法之外仍然是可见的,或者它太长了,不适合于放在方法内部,就该使用成员类。

如果成员类的每个实例都需要一个指向其外围实例的引用,就要把成员类做成非静态的,否则做成静态的。

假设这个嵌套类属于一个方法的内部,如果你只需要在一个地方创建实例,并且已经有了一个预置的类型接口可以说明这个类的特征,就要把它做成匿名类,否则,就做成局部类吧。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 bin07280@qq.com

文章标题:《Effective Java》笔记18-22:类和接口

文章字数:3k

本文作者:Bin

发布时间:2018-10-12, 21:06:22

最后更新:2019-08-06, 00:43:14

原始链接:http://coolview.github.io/2018/10/12/Effective-Java/%E3%80%8AEffective%20Java%E3%80%8B%E7%AC%94%E8%AE%B018-22%EF%BC%9A%E7%B1%BB%E5%92%8C%E6%8E%A5%E5%8F%A3/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录