Effective Java 类和接口 (19-25 小节)

374次阅读  |  发布于1年以前

大家好 ,effective java 进度+1

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

这一节主要在说,设计父类时,文档方法要写全,写完整,特别是方法之间有相互调用的,不然子类只重写其中一个,可能会有 bug

这里举了 remove 方法的例子,它里面调用到了 iterator 方法,并且在方法注释上有详细的说明

正例

而我们上文提到的 addAll 方法中注释没有写好,没有提到会调用 add 方法,容易导致子类在重写过程中犯错(上文算出 6 的例子)

反例

protected?

接着,作者便说了这么一句话

a class may have to provide hooks into its internal workings in the form of judiciously chosen protected methods

类必须以某种形式提供 hook,以便能够进入它的内部工作流程,比如 protected 方法

我思考了很久,结合作者提到的 removeRange 方法例子,大致应该是说,父类会有 protected 方法,但是方法效率可能不好,需要子类结合自身去优化,得到更好的方法。

注意 protected 只有子类才可以调用,如果我们的客户端不是子类是无法调用到这个方法的。

这里提到这个方法的时间复杂度达到 O(n^2),思索了很久后,发现这个主要是针对 ArrayList 这种 动态数组 形式的,这里 remove 时,需要移动后面的 n 个元素,时间复杂度 O(n), 会导致效率很差。

在这里,还发现 removeRange 方法在 jdk8 和 11 中是不同的,为啥升级了呢,请看看这个例子

public class SuperClass<E> extends ArrayList<E>{
    public static void main(String[] args) {
        SuperClass<String> list = new SuperClass<>();
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.add("e");
        list.add("f");

        System.out.println("before remove : " + list);

        list.removeRange(3, 1);

        System.out.println("after remove (3, 1) : " + list); //[a, b, c, b, c, d, e, f]
    }
}

这里调用居然没报错的~ (jdk8)

// jdk8
protected void removeRange(int fromIndex, int toIndex) {
    modCount++;
    int numMoved = size - toIndex;
    System.arraycopy(elementData, toIndex, elementData, fromIndex,
                     numMoved);

    // clear to let GC do its work
    int newSize = size - (toIndex-fromIndex);
    for (int i = newSize; i < size; i++) {
        elementData[i] = null;
    }
    size = newSize;
}

从源码可以发现,这里并没有限制这个 fromIndex 必须小于 toIndex 。

难怪 removeRange 调用后数据还多了

// jdk11
protected void removeRange(int fromIndex, int toIndex) {
    if (fromIndex > toIndex) {
        throw new IndexOutOfBoundsException(
                outOfBoundsMsg(fromIndex, toIndex));
    }
    modCount++;
    shiftTailOverGap(elementData, fromIndex, toIndex);
}

/** Erases the gap from lo to hi, by sliding down following elements. */
private void shiftTailOverGap(Object[] es, int lo, int hi) {
    System.arraycopy(es, hi, es, lo, size - hi);
    for (int to = size, i = (size -= hi - lo); i < to; i++)
        es[i] = null;
}

用 jdk11 的版本会抛出 IndexOutOfBoundsException 异常。

其他几点

小结

看完感觉……对父类多了一点点了解~

  1. 在父类的构造器,或者实现 Cloneable , Serializable 接口的父类,不能在其中调用可以被重写的方法
  2. 可以恰当地提供 protected 方法,供子类去实现,优化性能
  3. 通过 final 和 私有化构造器 可以禁止子类化
  4. 写好注释

20. 接口优于抽象类

这个就是 单继承,多实现 了。

特点

  public interface Singer {
    AudioClip sing(Song s);
}
public interface Songwriter {
    Song compose(int chartPosition);
}

public interface SingerSongwriter extends Singer, Songwriter{
    AudioClip strum();
    void actSensitive();
}

骨架实现类

骨架实现类 (skeletal implementation) 被称为 AbstractInterface , 从名字就可以看出,是 抽象+接口 的组合。

比如 AbstractCollection, AbstractSet, AbstractList, AbstractMapAbstractQueue

好的骨架设计例子

将 int[] 转成 Integer List。这里利用匿名内部类 AbstractList,很轻易的实现了这个功能

static List<Integer> intArrayAsList(int[] a) {
        Objects.requireNonNull(a);

// The diamond operator is only legal here in Java 9 and later
// If you're using an earlier release, specify <Integer>

        return new AbstractList<Integer>() {
            @Override public Integer get(int i) {
                return a[i]; // Autoboxing (Item 6)
            }
            @Override public Integer set(int i, Integer val) {
                int oldVal = a[i];
                a[i] = val; // Auto-unboxing
                return oldVal; // Autoboxing
            }
            @Override public int size() {
                return a.length;
            }
        };
    }

骨架的简单实现

这里举了 AbstractMap.SimpleEntry 这个例子,它的最大特点就是 :不是抽象的 ,简单高效。

小结

接口最大的优点便是灵活了,也不用过多介绍了

这里最大的收获便是知道了 骨架实现类 这个概念,以及对集合类的层次设计多了一些认识。

骨架实现类 (skeletal implementation) ,又称 AbstractInterface ,例子:AbstractCollection, AbstractSet, AbstractList, AbstractMapAbstractQueue

21. 为后代设计接口

这里主要介绍了 Java8 的新特点,default 方法

以及它可能带来的风险

比如 Collection 接口中新增了 removeIf 方法

但是apache 工具包中的org.apache.commons.collections4.Collection.SynchronizedCollection 工具类还没有覆盖到。

4.3版本

这就可能导致一些并发问题,抛出异常。

所以在 4.4 版本中就得紧急修复了

4.4 版本

再比如,如果我之前刚好定义了个方法,和 新增的 default 方法一样,但是可访问性缩小,那么就会出现编译期的异常

小结

default 方法虽然可以很方便的在接口中添加实现,但是要注意它带来的风险。

22. 接口只用于定义类型

这个我不服 哈哈哈。在工作中,已经习惯了把这些常量抽出来,对这几个接口进行管理,挺方便的

但作者认为 常量接口是对接口的不正确使用 ,还举了 java.io.ObjectStreamConstants 充当反例

作者认为 导出常量 的好方法

  1. 工具类
  2. 枚举

额,我还是觉得用 常量接口 就行了

23. 类层次优于标签类

标签类例子

package effectivejava.chapter4.item23.taggedclass;

// Tagged class - vastly inferior to a class hierarchy! (Page 109)
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(shape);
        }
    }
}

可以看出它的特点是:有多种类型,还有一个字段表示它是什么类型。

这种写法有很多弊端:可读性差,浪费内存,扩展性差

这应该是面向对象的基本功叭,该拆分的就拆分出来。

过~

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

这一节主题是嵌套类 ,主要是在说

  1. 何时使用哪种嵌套类
  2. 为什么使用嵌套类

嵌套类有四种,后三种是内部类

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

静态成员类

比如:ArrayList 中的 ArrayListSpliterator ,这个在 Stream 源码篇章出现过,调用 stream() 方法时,会先去创建这个 Spliterator。

特点:可以访问外部类的所有属性

非静态成员类

比如 SubList

非静态成员类和静态成员类的不同

  1. static 关键字
  2. 是否需要访问外部实例,如果不需要,就加这个 static
  3. 非静态成员的实例都隐含地与其外部实例相关联,创建时需要额外的空间开销

匿名类

这里引用上面 好的骨架设计例子 ,可以看到它在使用时,就被声明和实例化

static List<Integer> intArrayAsList(int[] a) {
        Objects.requireNonNull(a);

// The diamond operator is only legal here in Java 9 and later
// If you're using an earlier release, specify <Integer>

        return new AbstractList<Integer>() {
            @Override public Integer get(int i) {
                return a[i]; // Autoboxing (Item 6)
            }
            @Override public Integer set(int i, Integer val) {
                int oldVal = a[i];
                a[i] = val; // Auto-unboxing
                return oldVal; // Autoboxing
            }
            @Override public int size() {
                return a.length;
            }
        };
    }

局部类

这里也在 stream 源码篇章中有提到,makeRef 方法内部,有一个 ReducingSink 局部类。

四种类的应用场景:

  1. 如果一个嵌套类需要在一个方法之外可⻅,或者代码太⻓,使用成员类
  2. 如果一个成员类的每个实例都需要保留其宿主实例的引用,则使用非静态类,否则,使用静态类
  3. 如果一个嵌套类在方法内部,而你只需要在一个地方创建实例,同时类也提前定义好了,那直接使用匿名类,否则用局部类

25. 限制源文件为单个级类

就是不要将多个 class 放在一起,就像这样 ,如果其他文件也有类似的类,会导致编译出错。

package effectivejava.chapter4.item25;

// Two classes defined in one file. Don't ever do this! (Page 115)
//class Utensil {
//    static final String NAME = "pot";
//}
//
//class Dessert {
//    static final String NAME = "pie";
//}

如果一定要放一起,要改成 静态成员类

总结

19 小节:了解到 父类 的使用过程中,一定要注意 方法重写 带来的风险,不能在它的 构造器,或者 Clone,序列化 之类的方法中添加方法。以及注释的重要性,protected 方法的 hook 属性

20 小节:便是 骨架实现类 这个概念了 (skeletal implementation) ,又称 AbstractInterface ,更好地了解了集合类的架构,例子:AbstractCollection, AbstractSet, AbstractList, AbstractMapAbstractQueue

21 小节:了解到 default 可能带来的风险。

22 小节:还是喜欢用 常量接口 哈哈,枚举类和工具类也在用

23 小节:面向对象的基本功~

24 小节:对 静态内部类,非静态内部类,匿名类,局部类 这四种嵌套类多了些了解,在 stream 源码中有 N 多例子参考

25 小节:一个文件一个类,非要挤一起考虑 静态内部类。

终于又看完一章了,啊~

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8