如何使用Java提供的基本元素来合理设计类和接口。

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

遵循软件设计的基本原则,即封装。隐藏内部实现细节,仅通过API进行通信。

Java提供了许多机制来帮助信息隐藏。访问控制机制指定了类,接口和成员的可访问性。

让每个类或成员尽可能的不可访问

尽可能的缩小可被访问的范围!!从小到大的范围分别如下

当遇到需要调整类、接口、成员的访问控制级别的时候,都应该反思之前是不是设定有问题,要仔细考虑再调整(主要是级别升高的情况)

特殊情况

  1. 继承

    子类复写父类的方法,要求子类中的访问级别不能低于父类中的访问级别

    这对于确保子类的实例在父类的实例可用的地方是可用的(Liskov 替换原则,见条目 15)是必要的。 如果违反此规则,编译器将在尝试编译子类时生成错误消息。 这个规则的一个特例是,如果一个类实现了一个接口,那么接口中的所有类方法都必须在该类中声明为 public。

    反例来想,比如子类是private,而父类是public,那父类在多态时候就无法调用子类的方法了...

    取舍问题,访问控制让步于多态。

  2. 公有类的实例域绝不能是共有的,包含公有可变域的类通常并不是线程安全的

  3. 用final符修饰的内容,如果知识可变对象的引用,则比较危险,比如数组、List等。可以暴露Immutable的List,或者使用方法,每次返回一个clone实例。

    // Potential security hole!
    public static final Thing[] VALUES = { ... };
    
    //you should
    private static final Thing[] PRIVATE_VALUES = { ... };
    public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
    
    //or
    private static final Thing[] PRIVATE_VALUES = { ... };
    public static final Thing[] values() {
        return PRIVATE_VALUES.clone();
    }

关于内部类

对成员变量,通常是private修饰,然后提供public方法访问获取,封装的较好。但是对于类,往往一个public了事。

文中对于类,有一段说明是,如果类的范围明确,尽量缩小,比如在使用类中声明和使用即可。

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

内部类除了能够提供更好的封装以外,还具有一些特殊的性质,如内部类可以直接访问到外部类中的成员,包括私有的成员。

在 Java 的集合框架中大量地使用了内部类,比如集合类中迭代器的实现。这些迭代器实现了同样的接口(Iterator Interface),但是具体的实现又各不相同。外部类也不必关心它们的具体实现方法,只需要按照约定的接口进行访问即可。

//ArrayList
    public Iterator<E> iterator() {
        return new Itr();
    }

    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        Itr() {}

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

内部类分为

16 在公共类中使用访问方法而不是公共属性

解析下标题

就比较常用的封装,属性不能被直接访问,而是通过方法。

**如果一个类在其包之外是可访问的,则提供访问方法来保留更改类内部表示的灵活性。**如果一个公共类暴露其数据属性,那么以后更改其表示形式基本上没有可能,因为客户端代码可以散布在很多地方。

反例是java.awt包中的Point对象

public class Point extends Point2D implements java.io.Serializable {
    /**
     * The X coordinate of this <code>Point</code>.
     * If no X coordinate is set it will default to 0.
     *
     * @serial
     * @see #getLocation()
     * @see #move(int, int)
     * @since 1.0
     */
    public int x;

    /**
     * The Y coordinate of this <code>Point</code>.
     * If no Y coordinate is set it will default to 0.
     *
     * @serial
     * @see #getLocation()
     * @see #move(int, int)
     * @since 1.0
     */
    public int y;
    
    ...//remainder omitted

对于不可变的属性,可以暴露出去,危害比较小

public final 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("Min: " + minute);
        this.hour = hour;
        this.minute = minute;
    }

    ... // Remainder omitted
}

17 最小化可变性

就是在说,设计类的时候,多考虑设计成为不可变类。

不可变类,实际上是说,其实例是不能被修改的类。

Java平台类库有很多不可变类:String,基本类型包装类,BigInteger,BigDecimal

总而言之,不可变类比可变类更易于设计、实现和使用,不容易出错,而且更安全。

不可变类需要遵守的五条规则

  1. 不要提供修改对象状态的方法

  2. 确保这个类不能被继承

  3. 所有字段设置为final

  4. 所有字段设置为private

  5. 确保对任何可变组件的互斥访问

    如果你的类有任何引用可变对象的字段,请确保该类的客户端无法获得对这些对象的引用。 切勿将这样的属性初始化为客户端提供的对象引用,或从访问方法返回属性。 在构造方法,访问方法和 readObject 方法(详见第 88 条)中进行防御性拷贝(详见第 50 条)。

不可变类的方法与函数式编程

举例来说

// Immutable complex number class
public final 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);
    }

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

    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }

        if (!(o instanceof Complex)) {
            return false;
        }

        Complex c = (Complex) o;

        // See page 47 to find out why we use compare instead of ==
        return Double.compare(c.re, re) == 0
                && Double.compare(c.im, im) == 0;
    }

    @Override
    public int hashCode() {
        return 31 * Double.hashCode(re) + Double.hashCode(im);
    }

    @Override
    public String toString() {
        return "(" + re + " + " + im + "i)";
    }
}

上面例子中的方法,没有对入参做修改,而是返回了一个新的实例,这种模式是函数式编程

因为方法返回将操作数应用于函数的结果,而不修改它们。 与其对应的过程式的(procedural)或命令式的(imperative)的方法相对比,在这种方法中,将一个过程作用在操作数上,导致其状态改变。 请注意,方法名称是介词(如 plus)而不是动词(如 add)。 这强调了方法不会改变对象的值的事实。

不可变类的优缺点

优点

缺点

最佳实践

public class FinalBestPractice {
    private final int age;
    private final String name;

    private FinalBestPractice(int age,String name){
        this.age = age;
        this.name = name;
    }

    public static FinalBestPractice getFinalTrue(int age, String name){
        return new FinalBestPractice(age, name);
    }
}

这里注意类本身没有标记为final,但是因为没有提供private级别以上的构造方法,所以不能够被继承。

特别说明

参考资料

  1. [Effective Java (高效 Java) 第三版](https://www.bookstack.cn/books/effective-java-3rd-chinese)
  2. https://blog.csdn.net/cbmljs/article/details/103870881
  3. https://www.jianshu.com/p/7b595ddd9d99
  4. [关于 Java 内部类的小抄](https://blog.jrwang.me/2016/java-inner-class/)
  5. [Java 中的内部类与匿名内部类详解](https://xie.infoq.cn/article/956c85d1cbec5ae5787084a46)
  6. [一篇文章看懂函数式编程与命令式编程](