Effective Java读书笔记--类和接口

1、使类和成员的可访问性最小化
不指定访问级别,就是包私有。protected = 包私有 + 子类
一般private不会被访问到,如果实现了Serializable,可能会泄露。反射。
final集合或者数组,可以返回clone或者使用unmodifiableList等。
java新增2种隐式访问级别,作为模块系统的一部分。一个模块就是一组包。模块内部,可访问性不受导出声明影响,模块中未被到导出的包在模块之外是不可访问的。
2、要在公有类而非公有域中使用访问方法
如果类可以在它所在的包之外进行访问,就提供方法。不要直接访问成员变量,而是通过方法访问。如果是包私有,或者是私有的嵌套类,直接暴露它的数据域并没有本质的错误。
3、使可变性最小化
不可变类:String,基本类型的包装类,BigInteger,BigDecimal.不可变类比可变类更加易于设计、实现和使用。
实现不可变类的5条规则:
1、不要提供任何会修改对象状态的方法。
2、保证类不会被扩展
3、声明所有域都是final
4、声明所有的域都是私有
5、确保对于任何可变组件的互斥访问。在构造器、访问方法和readObject方法中使用保护性拷贝。
纯函数就是不改变入参的值,返回值也不会让外界影响当前对象(比如返回拷贝)。
*一般为了强调这个函数不会改变对象的值,命名一般是介词,或者是动词介词结合。
不可变对象本质上是线程安全的,它们不要求同步。
不可变类可以提供一些静态工厂,把频繁请求的实例缓存起来。
不可变类唯一的缺点是对于每个不同的值都需要一个单独的对象。尤其是大型对象。
防止子类化的方式:本身设置为final;所有构造器都是包私有或者private,然后提供静态工厂方法。
BigInteger,BigDecimal刚被编写出来的时候,对“不可变类必须final”的说话还没被广泛的理解,所以可以被继承,为了保持后向兼容,这个问题无法得到修正。
*稍微弱一点的约束:没有一个方法能够对对象的状态产生外部可见的改变即可。
如果让不可变类实现Serializable接口,并且它包含一个或者或多个指向可变对象的域,就必须显式的readObject或者readResolve方法,或者使用ObjectOutputStream.writeUnshared和ObjectInputStream.readUnshared方法。
如果类不能做成不可变的,仍然应该限制它的可变性。除非有令人信服的理由,否则要使每个域都使private final。
只有在有必要考虑性能的时候,才应该为不可变类提供公有的配套类,比如String的StringBuilder。
不要在构造器或静态工厂方法之外再提供共有的初始化方法,除非有令人信服的理由。
java.util.Date应该是不可变。
4、复合优先于继承(不算接口继承)
在包内部或者使专门为了继承而设计并且具有 很好的文档说明的类来说,继承也是非常安全的。但是跨包边界的继承,则是非常危险的。
继承打破封装性。因为子类得跟着父类的更新而演变。

public class InstrumentedHashSet<E> extends HashSet<E>{
private int addCount = 0;
@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);
}
}
这里调用addAll 会导致addCount 加一次,然后调用到HashSet.addAll(),这里面会调用add方法,从而导致调用被覆写的add方法,addCount 又会加一次。super到父类之后,调用的还是被子类的方法。多态调用的方法得看作用域在哪。
包装类(使用组合去扩展)几乎没缺点,只是不适合用户回调框架。Guava为所有的集合接口提供了转发类。
只有真正是is-a的关系才继承,否则只要不确定是不是,就不应该继承。java有几个不好的实现:stack就不应该继承Vector。properties就不应该继承HashTable。
在复合的地方使用继承会不必要的暴露实现的细节,会永远限定类的性能。暴露细节有可能就会被访问到。
5、要么设计继承并提供文档说明,要么禁止继承。
类必须在文档中说明,在哪些情况它会调用可覆盖的方法。
好的API文档应该是描述一个给定方法做了什么,而不是如何做到。
非API的应该是解释为什么,不是how,也不是what,how看代码。
为了继承而设计的类,唯一的测试方法是编写子类去测试。一般3个大约就可以满足。
*构造器绝不能调用可覆盖的方法。违反可能导致程序失败,因为子类的构造方法调用父类的,父类会调用被覆写的方法,会导致不可预期的结果。构造器调用private,final,static方法是安全的。
如果一个为继承而设计的类实现Cloneable和Serializable接口,就应该意识到clone和readObject方法在行为上非常类似构造器,所以无论clone还是readObject,都不可以调用可覆盖的方法。都是因为覆盖的方法在子类初始化前被运行。
对于那些并非为了安全地进行子类化而设计的和编写文档的类,要禁止子类化。final或者构造器私有。或者不调用可覆盖方法。或者为可覆盖方法编写私有辅助方法。
6、接口优于抽象类
default方法不能覆写Object的方法。
对于骨架实现类而言,好的文档是绝对非常必要的。
7、为后代设计接口
谨慎设计,继承接口,要保证文档说明的行为和所有方法一致,包括继承的缺省方法,否则得覆写。
8、接口只用于定义类型(而不应该用来导出常量)
常量接口模式是对接口的不亮使用(就是接口里放了一堆常量)。代替的方法是使用工具类,或者把常量放到紧密结合的类中。反例:java.io.ObjectStreamConstants,常量类。如果可以用枚举,尽量用枚举。
在数字间使用下划线,是java7开始支持的。
因为接口没法设置final,实现这些接口会被常量污染。
9、类层次(继承)优于标签类(多职责类)
简单来说就是一个类包含过多的标签职责,一个标签就是一个有固定职责的类。
标签类违反了单一职责。
10、静态成员类优于非静态成员类
嵌套类存在的目的应该只是为它的外围类提供服务。如果嵌套类将来可能会用于其他的某个环境中,它就应该是顶层类。
嵌套类:静态成员类,非静态成员类,匿名类,局部类。除了静态成员类,都是内部类。
静态成员类是外围类的一个静态成员,也有可访问性规则。
非静态成员类都隐含地与一个外围实例相关联,这种关联关系不能被修改。可以利用this获得外围实例的引用。通常,当外围类某个实例方法调用非静态成员类的构造器时,这种关联被建立起来。使用实例去调用非静态成员类需要消耗非静态成员类的实例空间,并且增加构造器的时间开销。
非静态成员类的一种常见用法是定义一个Adapter。比如集合的迭代器实现。
private class MyIterator implements Iterator<E>{}
如果声明成员类不要求访问外围实例,就要始终把static放在它的声明中。如果省略static,每个实例都包含一个额外的指向外围对象的引用。保存这份引用要消耗时间和空间(比如对于公共的部分,其实不需要非静态,不需要和外部类关联,非静态会导致每个实例有个单独的成员类关联),会导致外围实例可以回收的时候,却得以保留,导致内存泄漏。
匿名类:同时被声明和实例化。匿名类出现在表达式中,所以要保持简短。
局部类:在任何可以声明局部变量的地方,局部类也遵守作用域的规则。只有局部类在非静态环境才有外围实例,它们也不能包含静态成员。必须简短,否则影响可读性。
11、限制源文件为单个顶级类(一个文件只放一个顶级类)
虽然java编译器允许在一个源文件定义多个顶级类,但是这么做并没有什么好处,只会带来巨大风险。因为如果存在另外一个源文件使用存在一样的顶级类类名,可能会导致程序的结果依赖源文件传给编译器的顺序影响!
实在需要,就把顶级类变成静态成员类。
永远不要把多个顶级类或者接口放在一个源文件中。可以确保编译时,一个类不会有多个定义,也可以保证编译产生的类文件和程序结果不依赖源文件传给编译器时的顺序的影响。 

posted @ 2020-07-07 00:57  DevinDC  阅读(145)  评论(0编辑  收藏  举报