UP | HOME

类和接口

Table of Contents

使可见性最低

信息隐藏

设计良好的模块会隐藏所有的实现细节,把接口和实现清晰的分离开来。模块之间通过它们之间的接口进行调用,单个模块不需要知道其他模块的内部情况

  • 有效的解除系统模块间的耦合,模块可以独立的开发、测试、优化、使用、理解和修改
  • 加快系统开发的速度,模块可以并发开发
  • 减轻了维护的负担,更快的理解代码,并且调试一个模块不影响其他的模块
  • 虽然信息隐藏本身无论是对内还是对外,都不会带来更好的性能,但是可以有效的方便优化性能:一旦完成一个系统,并通过剖析哪些模块影响了系统性能,可以进一步优化,而不影响到其他模块的正确性
  • 信息隐藏提高了可重用性,因为模块间并不紧密耦合,除了开发这些模块所使用的坏境之外,模块在其他坏境中往往也可用
  • 信息隐藏降低了构建大型系统的风险,因为即使整个系统不可用,但是独立的子模块却有可能是可用的

封装规则

  • 实例成员绝不能是public,包含public可变成员的类是线程不安全的
  • public静态的final数组成员几乎总是错误的
  • 使共有数组变成私有的,并返回一个公有的不可变List
    private static final Thing[] PRIVATE_VALUES = {...};
    public static final List<Thing> VALUES =
        Collecations.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
    //或者
    private static final Thing[] PRIVATE_VALUES = {...};
    public static final Thing[] values() {
        return PRIVATE_VALUES.clone;
    }
    
  • 如果类可以在它所在的包的外部被访问,就提供访问私有成员的方法
  • 如果类是包私有的,或者私有的嵌套类,直接暴露他的数据成员并不是本质的错误

使可变性最小

不可变类的规则

  1. 不要提供任何可修改对象状态的方法
  2. 保证类不会被扩展
  3. 使所有的成员都是final
  4. 使所有的成员都成为private
  5. 确保对于任何可变组件的互斥访问

不可变类的优点

  • 不可变对象本质上是线程安全的,不要求同步
  • 不可变对象可以自由地共享,甚至可以共享对象的内部信息
  • 永远不需要进行保护性拷贝
  • 不可变对象对其他对象提供了大量的构件(building blocks)

函数式风格

修改对象状态的方法永远返回一个新的不可变对象

public final class Complex {
    private final double re;
    private final double im;

    public Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }
    // Accessors with no corresponding mutators
    public double realPart() { return re; }
    public double imaginaryPart() { return im; }
    public Complex add(Complex c) {
        return new Complex(re + c.re, im + c.im);
    }              
    public Complex subtract(Complex c) {
        return new Complex(re - c.re, im - c.im);
    }

    public Complex multiply(Complex c) {
        return new Complex(re * c.re - im * c.im,
                   re * c.im + im * c.re);
    }

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

调用加减乘除计算方法每次都返回一个新的不可变的复数对象

不可变类的缺点

每一个值都需要一个单独的对象,加重了垃圾回收器的负担

不可变类的总结

  • 除非有很好的理由让类成为可变的类,否则就应该是不可变
  • 尽量使成员变成final
  • 让类的所有构造器都变成私有的或者包级私有的,并添加public静态工厂来替代public构造器
  • 构造器应该创建完全初始化的对象,并建立起所有的约束关系。不要在构造器或者静态工厂之外再提供初始化方法
  • 如果类无法做成不可变的,但也应该尽量限制可变性

组合优先于继承

  • 破化封装:子类依赖于其父类中特定功能的实现细节。而父类的实现有可能会随着发型版本的不同而发生改变,如果真的发生了变化,子类可能会遭到破坏,即使子类的代码完全没有改变
  • 暴露实现:继承的子类必须了解父类的实现细节,甚至可能无意识破坏父类的约束
//如果想要修复bug,就必须了解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;
    }

    public static void main(String[] args) {
        InstrumentedHashSet<String> s
            = new InstrumentedHashSet<>();
        s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
        //it should be 3, but is 6 !
        //the InstrumentedHashSet.addAll -> HashSet.add -> InstrumentedHashSet.add 
        System.out.println(
            String.format("the elemet number of instrumented hash set : %d",
                      s.getAddCount()));
    }
}
  • 妨碍优化:继承限制了父类的实现,往往使得父类的性能优化变得极其困难
  • is-a原则:只有当子类真正是父类的子类型(subtype)时,才适合用继承

组合扩展

不扩展现有的类,而是在新类中增加一个私有成员,这个私有成员引用现有类一个实例

public class WrappedInstrumentedSet<E> {
    private final Set<E> s;
    private int addCount = 0;

    public WrappedInstrumentedSet(Set<E> s) {
        this.s = s;
    }

    public boolean add(E e) {
        addCount++;
        return s.add(e);
    }

    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return s.addAll(c);
    }

    public int getAddCount() {
        return addCount;
    }

    public static void main(String[] args) {
        WrappedInstrumentedSet<String> s
            = new WrappedInstrumentedSet<>(new HashSet<>());
        s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
        //the element number of instrumented set is 3
        System.out.println(
            String.format("the element number of wrapped instrumented set is %d",
                      s.getAddCount()));
    }
}

继承必须提供文档

好的接口文档应该描述一个方法做了什么工作,而不是描述是如何做到的。但是如果设计时候允许被继承,则必须给出足够扩展的实现细节,否则就禁止继承

  • 面向继承的文档必须精确地描述覆盖每个方法所带来的影响。对于每个public或受protected的方法或构造器,文档必须指明调用了哪些允许子类覆盖的方法,是以什么顺序调用的,每个调用的结果又是如何影响后续的处理过程的
  • 可以被继承的类往往提供适当的钩子(hook),以便子类能够进入父类的内部工作流程中,文档中必须说明这些精心选择的protected钩子方法

接口优先于抽象类

接口的优点

抽象类作为类型定义拥有极大的限制

  • 已经实现的类可以更方便地实现新的接口。假设现有的类A继承于类C,而现有的类B继承于类D,要为A和B添加同一个抽象类E,就必须先让C和D继承于E,这会间接伤害类的层次关系
  • 接口是定义混合类型(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();
    }
    
  • 使用接口可以通过组合模式更安全地增强类的功能,完全不用受到抽象类实现细节的干扰

接口的缺点

  • 接口一旦被公开发行,并且被广泛实现,再想改变这个接口几乎是不可能的
  • 抽象类的修改比接口的修改要容易的多。抽象类可以增加非abstract的方法,而接口往往就需要在每个实现类实现同一个新增的方法(Java8之后接口也可以用default修饰符来添加某个方法的实现代码)

抽象骨架实现类

设计接口时候可以先确定哪些方法是最基本的(primitive),其他的方法可以根据他们来实现。基本方法将成为抽象骨架类(abstract skeletal implementation)中的抽象方法,其他方法则在抽象骨架类中提供默认实现。子类通常只需要继承这个抽象骨干类并实现这些抽象方法

总结

接口通常是定义允许多个实现的类型的最佳途径。如果演变的容易性比灵活性和功能更为重要的时候,应当选用抽象类,前提是必须理解并且可以接受这些局限性。考虑为每个重要接口都提供一个抽象的骨架实现类

接口只声明方法

  • 常量接口是对接口的不良使用
  • 如果可能使用枚举定义常量
  • 如果不能使用枚举,请使用单例模式的工具类。使用static import,避免用类名修饰常量名

不要使用标签类

标签类冗长,易错,低效

// Tagged class - vastly inferior to a class hierarchy!
class Figure {
    enum Shape { RECTANGLE, CIRCLE };
    // Tag field - the shape of this figure
    final Shape shape;
    // These fields are used only if shape is RECTANGLE
    double length;
    double width;
    // This field is used only if shape is CIRCLE
    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();
        }
    }
}

使用类层次来替代标签类

// Class hierarchy replacement for a tagged class
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; }
}

用函数对象表达策略

Java没有提供函数指针,lambda表达式等方式来调用函数,而是使用“函数对象”来模拟函数指针(Java8后开始支持lambda表达式)

函数对象

通常情况下调用某个对象的方法是为了作用于这个对象。但是同样可以定义一个类,它有且仅有一个方法,这个方法是作用于传递给它的对象上,这种类就被称为“函数对象”

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.compare这个方法的函数指针。换种说法StringLengthComparator是某种特定“字符串比较”策略的具体实现类

  • 函数对象应该是无状态的
  • 函数对象最好是单例的

策略接口

提供给客户端调用的接口中需要一个类似函数指针的策略接口,而不是一个具体的函数对象类

// Strategy interface
public interface Comparator<T> {
    public int compare(T t1, T t2);
}

客户端实现某种具体策略

class StringLengthComparator  implements Comparator<String> { 
    private StringLengthComparator() { }

    public static final StringLengthComparator INSTANCE =
        new StringLengthComparator();

    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
}

内部匿名类

具体的策略类往往使用匿名内部类定义

Arrays.sort(stringArray,new Comparator<String>() {
        @Override
        public int compare(String o1, String o2) {
            return 0;
        }
    });

但是使用匿名类会导致每次调用都创建新的class对象和实例对象,如果频繁调用的话开销会比较大

静态成员类

使用静态成员类来缓存频繁调用的具体策略对象,可以有更好的性能

// Exporting a concrete strategy
class Host {
    private static class StrLenCmp
        implements Comparator<String>, Serializable {
        public int compare(String s1, String s2) {
            return s1.length() - s2.length();
        }
    }

    // Returned comparator is serializable
    public static final Comparator<String>
        STRING_LENGTH_COMPARATOR = new StrLenCmp();
    // Bulk of class omitted
}

优先考虑静态成员类

嵌套类是指被定义在另一个类的内部的类。嵌套类存在的目的应该只是为他的外围类提供服务。如果嵌套类将来可能会用于其他的某个环境中,它就应该是顶层类

嵌套类有4种,除了第四种都被称为内部类

  1. 静态成员类
  2. 非静态成员类
  3. 匿名类
  4. 局部类

静态成员类

作为一个static属性被定义在一个类中,在这个类中可以访问外围类所有的(包括private)成员和方法。静态成员类的访问权限受到定义的修饰符的限制,比如定义为private static成员类,那它就不能被外围类之外的所有类访问

  • public静态成员类通常被用来定义对外暴露的但只能作用于这个外围类的常量类,比如可以为某个Calculator类定义只作用于它的public Operator枚举类,客户端可以用来传递Calculator.Operation.PLUS
  • private静态成员类被用来定义外围类的组件,比如HashMap的私有静态成员Entry类

非静态成员类

非静态成员类和静态成员类的区别

  • 非静态成员类是和外围类的一个实例对象关联的,这种关联是在外围类实例被创建的时候就建立的,而静态成员类是和整个外围类关联的
  • 非静态成员类可以引用外围类对象的this,静态成员类不可以
  • 每次创建外围类实例都会创建非静态成员类的class和实例对象,存储和垃圾回收器的开销很大,而静态成员类在jvm载入class代码时候就被创建,且只会被创建一次

非静态成员类通常被用作adatper对象,使得外围类的实例对象可以被当成另一个类的实例对象来使用。比如java的集合类某个具体Set, List通常都会提供一个静态成员类Iterator来被外部当作iterator操作

// Typical use of a nonstatic member class
public class MySet<E> extends AbstractSet<E> {
    // Bulk of the class omitted
    public Iterator<E> iterator() {
        return new MyIterator();
    }
    private class MyIterator implements Iterator<E> {
        //...
    }
}

内部匿名类

匿名类没有名字,也不是外围类的一个成员。内部匿名类可以被定义在任何合法的代码内

内部匿名类通常被用于创建函数对象,Java8后往往被lambda表达式替代

内部匿名类的限制

  • 在代码被调用的时候同时完成声明和初始化,无法在被声明的地方外初始化实例对象
  • 只有在非static的上下文中匿名类才能够访问外围类的this引用
  • 即使在static的上下文中,它也不能定义自己的static成员
  • 无法使用instance of
  • 无法同时实现多个接口,或者实现一个接口和扩展一个类

局部类

局部类被定义在某个方法内,和匿名类唯一的区别就是它有自己的名字而已,可以被多次用来创建不同的实例对象

嵌套类总结

  1. 如果内部类需要被当作一个成员从外部访问或者代码长到不能包含在一个方法内,就定义为成员类
  2. 如果成员类每个实例对象都不需要引用某个外围类具体的实例对象,就定义为静态成员类,反之则非静态
  3. 如果内部类被定义在某个方法中,并且只需要一次创建实例对象,就定义为内部匿名类,反之则局部类

Next:范型

Previous:通用方法

Home:目录