Chapter 4 Classes and Interfaces

Item 13: Minimize the accessibility of classes and members

一个好的模块设计会封装所有实现细节,模块之间只能通过他们公开的API进行交流。因为这些模块互不影响(loosely coupled),所以可以进行单独测试等等。所以为了封装,我们应该把每一个class或member弄得越inaccessible越好。顶层的class和interface只能是public或者default,如果它只是用作实现,那么就应该是default的,如果它只被一个类使用,可以考虑把它换成这个类的private nested class。BTW,private和default的field可能会通过Serializable泄露到公开API中。protected级别的members要尽量少。但是,Override基类方法的时候,不允许你指定更inaccessible的修饰符,这是因为要满足“基类的实例能用的地方,子类的实例都能用”。还有实现interface里面的方法就只能声明成public的。如果想便于测试,最多弄成default级别的,不能再高了。Instance fields绝不应该是public的,因为如果写成public的话,你就缺少了很多灵活(因为当你发布给用户后,然后你又想给这个field换一个数据类型,就没办法了,并且当这个field被修改的时候,你也没法验证什么的)。static fields也一样,唯有一个例外:static final fields可以是public的,但是一定要确保这些fields指向primitive或者immutable对象(绝不能是数组),如果想用数组,你可以用点技巧:

private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES =
Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
//或者:
private static final Thing[] PRIVATE_VALUES = { ... };
public static final Thing[] values() {
return PRIVATE_VALUES.clone();
}

Item 14: In public classes, use accessor methods, not public fields

如果你这么做了,这个public fields就相当于对外的API,如果你想换个数据类型,你不得不改API(which会破坏client代码),你的fields值可能会随时被别人改变,你的fields在被获取之前也不能做一些辅助工作。所以说,如果是public class,用getter和setter吧。

但如果是default的或者private的内部类,这样做其实没啥坏处。如果是immutable的fields,直接public出去也许不会“那么的坏”,但是也是受到质疑的。

Item 15: Minimize mutability

想让一个class是immutable的,请遵守以下几点:

一.不要提供任何可以修改其状态的方法。

二.确保它不能被extend。

因为如果可以被extend的话,它的子类可能会无意或有意地修改自己的状态,这就违背了immutable。有两种方法:一是把class声明成final的;二是让它只有private的constructor,然后用static factory methods。第二种方法更好,详情见item1。注意:BigInteger和BigDecimal不是final的,所以使用的话要注意安全。

三.所有的fields都应该是final的。

不是“那么的绝对”,比如可以定义一些“cache fileds”来提升性能,因为final的fields只能在类被创建的时候就被初始化,之后就再也不能被赋值,而这些cache fileds(比如可以cache哈希码)可以在第一次被请求计算的时候,被赋值。

四.所有的fields都应该是private的。

五.如果有指向mutable objects的field,确保clients无法获得这些objects的引用,在constructor,getter和readObject方法中使用defensive copies(Item 76)。

functional approach就是每个方法都会返回一个新的实例(比如String),还有procedural or imperative approach书上一句带过,所以也不知何意。immutable对象的好处在于:

一.被创建的时候它的状态就确定了并且会永远保持不变,然而mutable的对象可能有复杂的状态变化,如果没有可靠的文档,可能很难正确地使用。

二.天生线程安全。所以可以毫无顾忌地在任何地方复用它的某个instance。可以用static factory methods来实现(item 1)。并且完全不需要clone方法和“copy constructor”(Java中的String虽然提供了,但千万别用)。

三.有时候immutable对象的的内部东西也可以复用,比如BigInteger内部有个int数组表示绝对值,一个int表示符号,如果你想得到一个符号相反但是绝对值相等的BigInteger,只要返回一个新的BigInteger 它内部的int数组还是指向同一个数组,但是它内部的int变一下就行。

四.Immutable objects可以很好地作为其他对象的组成部分。比如作为HashMap的key和HashSet的元素。

唯一的缺点就是 每一个值都需要一个新的对象,导致性能受损。解决方法请参考String和Stringbuilder。

有一个注意事项:如果你的immutable对象中有fields指向mutable对象,那么如果要实现Serializable,必须另加手段(比如显示写一个readObject),否则坏人可能通过某种手段把你的object变成可变的。

总结一下,如果能让class是immutable的,尽量这样做,特别是对一些小的值类型(而java.util.Date却是mutable的,是个反面教材)。如果一个class不能是immutable,尽量让它尽可能多的fields是final的,让这个class的对象的状态越少越好。

Item 16: Favor composition over inheritance

继承其实会破坏封encapsulation。在同一个package内用inheritance是安全的,但是在不同的package间用inheritance就很惨了,注这里的inheritance只是指extends class而不包括implement(extends) interface。比如基类的实现者是类库作者,然后client继承了它,然后子类的行为可能是依赖于某些基类的方法的,而基类的方法可能随着一次次的release而改变,这样就会破坏client的子类了(除非基类的作者很好地写了文档,并说明如何继承该类)。下面举个反例,假设我们想有这么一个HashSet,再每次有元素被放进去的时候,就记录一下,统计一共有几次放入元素的操作:

// Broken - Inappropriate use of inheritance!
public class InstrumentedHashSet<E> extends HashSet<E> {
// The number of attempted element insertions
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);
}
public int getAddCount() {
return addCount;
}
}

然后如果你加了三个元素进去:

InstrumentedHashSet<String> s = new InstrumentedHashSet<String>();
s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));

那么你会发现得到的addCount是6,显然与你设计的意图不符。这是因为在HashSet内部的addAll方法会调用add方法,所以相当于先加了3,然后对每一个元素又加了1。那我们不override addAll方法了行吗?虽然可以暂时“解决问题”,但是其实我们作了一个假设就是:在allAll方法内部会调用add。但是这是个实现细节,可能会随着新版本的发布而改变,所以你的子类还是不靠谱儿。

再比如,如果你的子类不override任何方法,只是新增一些方法。那么你可能运气不好,以后的新版本的基类里刚好新增了一个签名和 你的新增方法 一模一样的方法,那默就吃瘪。

总之就是如果基类新增或修改了它的方法,由于某些你的假定,可能会对子类造成影响。那么怎么办呢?其实你可以设置一个private的field,并指向一个“基类”的实例:

public class InstrumentedSet<E> {
private final Set<E> s;
private int addCount = 0;
@Override public boolean add(E e) {
addCount++;
return s.add(e);
}
@Override public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return s.addAll(c);
}
public int getAddCount() {
return addCount;
}
}

现在由于没有了继承关系,所以不会产生多态,所以调用addAll不会调用add了。然后你可以把所有Set的方法都写一个对应版本的方法,然后只要“s.对应方法()”就行了。为了更好的代码复用,比如你又想实现一个别的类,它也拥有Set的这些行为,那么你又要再重新写一遍这些对应方法就很麻烦,所以你可以直接写一个通用基类:

// Reusable forwarding class
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) {
this.s = s;
}
public boolean add(E e) {
return s.add(e);
}
...//其他方法
}

然后再把刚才那个InstrumentedSet<E>直接继承这个类,再稍微改动一点就好了,比如把s.add()改成super.add()。这种方法我们叫做forwarding,这个新类里面的方法我们叫做forwarding方法。想继承Set的话,直接继承它(上面这个ForwardingSet)就好了,但本质上是“composition ”。

相比继承,这种方法更加灵活,比如继承的时候你只能选择是继承HashSet还是TreeSet,这里的话你可以随时改变你内部的那个“component”:

Set<Date> s = new InstrumentedSet<Date>(new TreeSet<Date>());
Set<E> s2 = new InstrumentedSet<E>(new HashSet<E>());

这个InstrumentedSet我们叫它wrapper class,因为它wrapp了另一个Set instance。这也叫Decorator pattern,因为它给Set装饰了一个新的特性。有时候也被叫delegation,虽然严格定义上不正确(但是delegation的严格定义是什么不用纠结了)。

这种方法的缺陷很少,比如不适合callback,虽然书上没具体说,但我觉得你可以想一下观察者模式,被wrap的类假设是个观察者,然后被创建的时候把自己注册给了某个Subject,那么当它收到通知的时候,是直接跳过它的wrapper的。

那么什么时候用继承?你一定要先仔细想想是不是有“is-a”的关系,如果有那么一点感觉不是,就别用。Java库里就有很多这样的错误,比如Stack居然继承了Vector,Properties居然继承了HashTable,p.getProperty(key)和p.get(key)会得到不同结果,因为后者来自HashTable,而且Properties的设计者的本意是只能将String作为key,但是如果你access underlying的HashTable的话,是可以放别的类型的key进去的,这样的话某些Properties的API就不能用了。而且继承还有不好的地方就是会把有缺陷的基类的方法带到子类来,而composition可以隐藏掉这些缺陷。

Item 17: Design and document for inheritance or else prohibit it

一个类必须准确地把 “自己的public或者protected的方法 会调用哪些其他overridable的方法,以何种顺序,每一次调用的结果对后续处理的影响;以及何时可能会调用这些个overridable方法(比如来自后台线程或者static initializers的调用)”写入文档。比如上一个item中的HashSet的addAll就应该说明自己在内部会调用add。By convention,这个说明一般是在文档中的最后以“This implementation...”开头,意思就是接下来的描述是关乎这个方法的内部情况的,但不会随着release的新版本而改变(也就是你做了个承诺,你承诺这种self-use pattern(也就是调用同一个类中的其他方法,如addAll会调用all)永远不会改变)。举个例子,java.util.AbstractCollection的public boolean remove(Object o)的文档中的最后一段是:

This implementation iterates over the collection looking for the specified element.然后说明了会用iterator的remove方法来移除一个元素。

所以说这段说明告诉了我们:override iterator方法会影响remove这个方法。但是这种做法岂不是违背了“we should describe what a given method does and not how it does it”,确实如此,但是没办法,因为“inheritance violates encapsulation”。想要给一个 可以被安全地继承的class 写文档,就必须描述这些实现细节。

另外,为了subclass的实现者考虑,你还要说明某个protected的方法(偶尔也可以是field),是怎么用的,可以怎么用,会被谁用。比如java.util.AbstractList的

protected void removeRange(int fromIndex, int toIndex)

的文档就说明了:这个方法会移除fromIndex到toIndex范围内的所有元素。并且,注意重点来了,这个方法会被这个list或其sublists的clear方法调用。所以你可以override这个方法,来提升clear的效率。虽然这个说明对 不需要继承这个class的end-user 来说没用,但写这本书的时候,还没有什么办法来把这种说明和普通api文档分开。

当你写完一个class designed for inheritance后,一般需要写三个subclass来进行测试,并根据测试决定是否要把你的class的某些成员从private变为protected或反之。

当允许继承的时候,constructors必须不能调用overridable的方法,因为基类的constructor在子类的constructor之前运行,所以说如果你这时候去调用一些子类中的overriding的方法,也许这些overriding的方法是基于某些子类中需要初始化的变量的,那么这时候这些变量还没有被初始化。我觉得很好想象了,具体例子就不写了。同理,clone和readObject(序列化里面的)方法也要遵循同样的原则。

一个class designed for inheritance最好不要实现Cloneable和Serializable接口,否则会给subclass的实现者很大压力,参考item11和item 74。BTW,如果你的class designed for inheritance实现了Serializable,记得把readResolve和writeReplace(如果有的话)声明为protected(rather than private),否则会被subclass默默地忽略。

如果一个普通的类,如果它is not designed and documented to be safely subclassed,那就别继承它(或者从类库实现者的角度就是,让它不能被继承)。顺便一提,如果你完全消除了一个类的self-use of overridable methods,那么它就是可以被安全地继承的,因为override一个方法绝不会影响到其他方法,比如你可以这么做:先把每一个overridable的方法中的代码移到一个对应的private的“helper method”里去,然后把这个overridable的方法改成直接调用对应的“helper method”,然后把每一个对overridable方法的self-use都替换成对 对应的“helper method” 的调用。

Item 18: Prefer interfaces to abstract classes

已经存在的class可以很轻松地被新增一个接口实现,但是却不能给一个已经继承了别的类的类新增一个abstract class(因为只能继承一个类)。

接口可以很好地实现“nonhierarchical types”,比如定义两个接口Singer和Songwriter,他俩是没有hierarchical上的关系的。然后你就可以写一个类同时实现者两个接口。但是如果换成是两个抽象类就不行,因为你只能继承其中一个,如果你一定要让一个类同时拥有Singer和Songwriter的能力,那你只能再新定义一个类比如叫SingerAndSongwirter,就很白痴了。

你可以用一种叫skeletal implementation的方法来结合interface的好处和抽象类的可以提供默认实现代码的功能。举个例子:AbstractCollection, AbstractSet, AbstractList, AbstractMap。这样的话,你就不需要自己实现interface中的所有方法了。

但是用抽象类比接口的好的地方就在于,如果你想给接口新增一个方法,那么你本来已经实现这个接口的类就都编译不通过了,但是用抽象类的话你只要提供一个默认实现就行了,所以说在设计接口的时候一定要仔细。

但是,Java 8支持接口中的默认方法了,就不存在以上问题了,我个人感觉连那个什么skeletal implementation都不需要了。

Item 19: Use interfaces only to define types

意思就是,接口是用来定义类型的,你可以用这个类型来refer to instances of the class(that implements the Interface),这应该是接口的唯一的用法和作用,而不应该把接口用作别的目的。

关于接口的用法有个反例,就是这个接口只包含常量(放在interface里的field自动static+final),而没有任何方法,比如:

// Constant interface antipattern - do not use!
public interface PhysicalConstants {
static final double AVOGADROS_NUMBER = 6.02214199e23;// Avogadro's number (1/mol)
static final double ELECTRON_MASS = 9.10938188e-31;// Mass of the electron (kg)
}

这个接口的意义仅仅就是为了:如果实现这个接口,那么在类里面就可以直接用,比如AVOGADROS_NUMBER,而不需要用PhysicalConstants.AVOGADROS_NUMBER。作者说这是一种非常不好的做法,因为,“某个类需要这些常量”这件事是一个实现细节,不应该通过接口而暴露成API出来,这个类的client并不需要知道这个interface,相反这个interface还会让他们疑惑。而且你一旦发布,在以后的版本里如果想去掉 实现这个interface,都不行。接口的作用应该是规定了:client用这个接口类型的变量能做些什么。而不是为了“减少代码长度”。

那么应该怎么做?如果这些常量只跟某个类有关,那么就应该直接放到那个类里面去,比如Integer就有MIN_VALUE和MAX_VALUE常量。或者如果有很多类都需要,那你应该放到一个noninstantiable的utility class里去,然后用static import就能“减少代码长度”了。

Item 20: Prefer class hierarchies to tagged classes

tagged class就是一种很傻的实现,比如说定义了一个类叫Figure(形状),然后初始化或者调用某些方法的时候根据某个变量进行switch case,比如:enum Shape { RECTANGLE, CIRCLE },然后如果是RECTANGLE,那么就初始化一些RECTANGLE独有的fields(比如边长),如果是CIRCLE就初始化半径什么的。反正这种做法的坏处一大堆。正确做法就是定义一个Figure类,再定义一个Rectangle类继承Figure,Circle同理。本来看完这条item我都不想写,因为觉得这是最基本的OO的多态,但后面想想自己可能确实无意中写出这样类似的“switch case代码“。

Item 21: Use function objects to represent strategies

一个只有一个方法的instance常常被叫做function object,也可以叫“一个concrete strategy”(strategy pattern),对应C#中的委托实例。这样的instance最好被弄成singleton,因为你只是需要一个方法而已。然后这个instance的class需要实现某个接口,这个接口里面就只有一个方法,对应C#委托中的委托类型。当你要传一个function object给一个方法时,比较老的方法是可以new一个匿名类,但是这样的缺点是如果你多次new,就会构造多个对象,是不必要的,所以也许你可以把它存起来,然后复用:

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

我用eclipse做了个测试,private的内部类即使有个public的constructor,在其Outer class之外的地方也不能new一个这个内部类,但是请看以上代码,compare方法由于是interface里面的,所以必须是public的,而在Host(上面代码中的类名)外部,想要用这个strategy的时候,只要是需要一个Comparator<String>的地方,就可以传一个Host.STRING_LENGTH_COMPARATOR进去,也就可以用它的compare方法了(内部类虽然是private的,但他实现的接口是public的)。

Java中的String类就用这个方法,可以返回一个CASE_INSENSITIVE_ORDER。

当然,Java8中的Lambda表达式不知道有没有上面说的缺陷。

Item 22: Favor static member classes over nonstatic

一个static member class不过就是个普通的class,只不过恰好被声明在另一个类里面,并且可以access这个类的所有成员。static member class的一种用法是定义一些 只跟自己的outer class有关系的 public helper class,比如在Calculator这个类中定义一个static public enum Operation{PLUS,MINUS},然后Calculator这个类的client就可以用,比如Calculator.Operation.PLUS了。

而每一个nonstatic member class的实例都必须和一个instance of its outer class相关联。一般是这么用:在一个outer class的instance method中new一个nonstatic member class,这时候这种“关联”会自动建立,比如:

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

如果你的member class不需要一个其outer class的instance引用的话,就把它定义为static的。比如上面的MyIterator内部需要access一些MySet中的instance field,所以只能定义成nonstatic的。但如果不需要outer class的一个实例,你却还是定义成nonstatic的,那么就会导致额外的不必要的开销,比如必须先new一个outer class才能new一个这个member class,再比如可能outer class的对象已经可以被GC了,但是你的member class实例还保存着对它的引用导致无法GC。

private static member class经常被用作其外部类的“components”,比如Map中的Entry。

Anonymous class有很多限制,比如它不能有static的成员。你也不能对他们用instanceof(因为没类名)或者定义constructor什么的。也不能让它实现多个接口。为了可读性,anonymous classes必须尽量简洁。

Local classes是最少用的,它们的声明和作用域和local variables一样。

《Effective Java》读书笔记 - 4.类和接口的更多相关文章

  1. Effective Java 读书笔记之三 类和接口

    一.使类和成员的可访问性最小化 1.尽可能地使每个类或者成员不被外界访问. 2.实例域决不能是共有的.包含公有可变域的类不是线程安全的. 3.除了公有静态final域的特殊情形之外,公有类都不应该包含 ...

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

    1.使类和成员的可访问性最小化不指定访问级别,就是包私有.protected = 包私有 + 子类一般private不会被访问到,如果实现了Serializable,可能会泄露.反射.final集合或 ...

  3. Effective java读书笔记

    2015年进步很小,看的书也不是很多,感觉自己都要废了,2016是沉淀的一年,在这一年中要不断学习.看书,努力提升自己 计在16年要看12本书,主要涉及java基础.Spring研究.java并发.J ...

  4. Effective Java读书笔记完结啦

    Effective Java是一本经典的书, 很实用的Java进阶读物, 提供了各个方面的best practices. 最近终于做完了Effective Java的读书笔记, 发布出来与大家共享. ...

  5. 《Effective Java》笔记 使类和成员的可访问性最小化

    类和接口 第13条 使类和成员的可访问性最小化 1.设计良好的模块会隐藏所有的实现细节,把它的API与实现清晰的隔离开来,模块之间只通过它们的API进行通信,一个模块不需要知道其他模块的内部工作情况: ...

  6. [Effective Java]第四章 类和接口

    声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...

  7. 初读"Thinking in Java"读书笔记之第九章 --- 接口

    抽象类和抽象方法 abstract void f();抽象方法是仅有声明而没有方法体的方法. 包含抽象方法的类叫做抽象类,如果一个类包含了一个抽象方法,则该类必须限定为抽象类. 抽象类和抽象方法可以使 ...

  8. Effective Java 读书笔记(一):使用静态工厂方法代替构造器

    这是Effective Java第2章提出的第一条建议: 考虑用静态工厂方法代替构造器 此处的静态工厂方法并不是设计模式,主要指static修饰的静态方法,关于static的说明可以参考之前的博文&l ...

  9. Effective Java 读书笔记

    创建和销毁对象 >考虑用静态工厂方法替代构造器. 优点: ●优势在于有名称. ●不必再每次调用他们的时候都创建一个新的对象. ●可以返回原返回类型的任何子类型的对象. ●在创建参数化类型实例的时 ...

随机推荐

  1. python中常见的一些错误异常类型

    python提供了两个非常重要的功能来处理python程序在运行中出现的异常和错误.你可以使用该功能来调试python程序. 什么是异常? 异常即是一个事件,该事件会在程序执行过程中发生,影响了程序的 ...

  2. java 线程池 - ThreadPoolExecutor

    1. 为什么要用线程池 减少资源的开销 减少了每次创建线程.销毁线程的开销. 提高响应速度 ,每次请求到来时,由于线程的创建已经完成,故可以直接执行任务,因此提高了响应速度. 提高线程的可管理性 ,线 ...

  3. luogu P1587 [NOI2016]循环之美

    传送门 首先要知道什么样的数才是"纯循环数".打表可以发现,这样的数当且仅当分母和\(k\)互质,这是因为,首先考虑除法过程,每次先给当前余数\(*k\),然后对分母做带余除法,那 ...

  4. 手把手 教你把H5页面打造成windows 客户端exe 软件

    序言: 好久没有更新博客了,最近在工作中碰到这种需求,由于没有做过,中间碰到好多坑,最后在一位贵人帮助的情况下,最终还是搞定了. 第一步,先安装 cefpython3 pip install cefp ...

  5. 微软撤出 Windows断供华为!

    华为被美国列入“实体名单”后,从硬件到软件再到技术标准,华为对外联系纷纷被掐断,其中软件系统方面,Google安卓系统已经停止与华为合作,Mate 20 Pro也被从安卓Q 10.0的尝鲜名单中移除. ...

  6. DTM/DEM/DSM/DOM/DLG

    一.DTM (Digital Terrain Model) 数字地面模型是利用一个任意坐标系中大量选择的已知x .y .z 的坐标点对连续地面的一个简单的统计表示,或者说,DTM 就是地形表面形态属性 ...

  7. lvs工作方式和调度算法

    LVS工作原理可以简单理解为: Lvs工作在内核空间,本身工作在input链上,与iptable不能同时用. LVS: ipvsadm :管理集群服务的工具,用来写规则 Ipvs 工作在内核. 工作原 ...

  8. json格式字符串转字典

    //json格式字符串转字典+ (NSDictionary *)dictionaryWithJsonString:(NSString *)jsonString {        if (jsonStr ...

  9. Java并发编程实战 第2章 线程安全性

    编写线程安全的 代码,核心在与对共享的和可变的对象的状态的访问. 如果多个线程访问一个可变的对象时没有使用同步,那么就会出现错误.在这种情况下,有3中方式可以修复这个问题: 不在线程之间共享该状态变量 ...

  10. python-进程、线程与协程

    基础概念 进程 是一个执行中的程序,即将程序装载到内存中,系统为它分配资源的这一过程.进程是操作系统资源分配的基本单位. 每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text regio ...