[读书笔记]Effective Java 第4章-类与接口

2017年02月16日 9625点热度 0人点赞 0条评论

Item 13: 使类及其成员的可访问性最小

除非真的有必要, 否则不要让外界能访问这个类或者成员变量。 因此, 再复习一下Java之中几种访问级别:

作用域 当前类 同一package 子孙类 其他package
public
protected ×
package × ×
private × × ×

没有修饰符的时候, 默认为package级别。

当我们覆盖了父类的某个方法的时候, 不能使其访问级别提高。 比如原来是protected, 不能升级成为public。

一种不太好的做法:

可以声明一个跟父类的方法名一样的方法, 但是不加上@Override,即只是方法名一样, 但是并不是真实的覆盖父类的方法。光有覆盖之名却无覆盖之实。

一个在JDK8修复的安全漏洞:

在原书编写时的 最新JDK(5 or 6), 如果定义了一个公开的静态数组域, 在类的外部是可以进行修改的。

但是在笔者JDK8 的环境之中进行测试的时候, IDE 直接报错! 测试代码如下:

复用第二章的一个Student类 :

 

测试的Test.class :

本文原创, 转摘注明:http://www.flyml.net/2017/02/16/effective-java-ch4-class-interface/

 

 

Item 14: 对公有类使用访问方法而不是直接公有域

翻译不是很好, 原文是:

In public classes, use accessor methods, not public fields

比如上面的Student类, 只是为了简单的测试而直接将域成员变量nameage直接设置成public, 这是非常不好的习惯。

比较好的做法是使用getters and setters, 如下:

 

如果是不可变的成员变量, 也可以直接暴露出来,比如学生的ID是不可变的, 那么可以采用如下方式:

 

即在初始化的时候, 就对这个类成员变量进行初始化, 之后已经不可变了, 相当于只有只读级别。

文中提到, JDK之中的DimensionPoint类都直接抛出了公有成员变量,这是一种不好的做法。 我看了jdk8的Point的代码, 这个问题还是没有修复。 估计造成的影响也不算很大。。。

本文原创, 转摘注明:http://www.flyml.net/2017/02/16/effective-java-ch4-class-interface/

Item 15: 使可变性最小

不可变类(immutable class)

在这里,书中提到一个不可变类。所谓不可变类,是指:instance 不可被修改。

好处: 在设计与实现上更简单, 同时更难以出错、更安全。

最常见的例子就是String 。 不可变类, 本质上是线程安全的, 他们不要求同步。

书中举了一个复数(Complex)的例子, 并且提供了相应的四则运算的方法。 重点在于:每个方法的返回值是创建一个新的Complex实例, 而不是修改当前的instance!

具体什么样的类, 才能算是好的不可变类, 可以直接看书。讲得比较清晰~ 不赘述了

在面试、校招之中, 一个很常见的问题就是 StringBuffer/StringBuilder 与String的区别是什么?

这里就讲了比较根本的原因: String是不可变的类, 如果不断的进行字符串操作,最终要的只是最后的结果, 那么性能就会比较差,因为每次都要新创建String对象。 而StringBuffer/StringBuilder是String的可变包装类,允许只修改某个部分,而不是新创建对象, 因此在性能上面要好很多。

值得注意的是, 书中有这么一句话:

The main example of this approach in the Java platform libraries is the String class, whose mutable companion is StringBuilder (and the largely obsolete StringBuffer)

注意上面括号之中的话: 更不要说已经快要被抛弃的StringBuffer。 从作者的态度来看, StringBuilder要完胜StringBuffer, 甚至在平时使用的时候, 可以直接不考虑StringBuffer只用StringBuilder

可能这种说法偏颇了一些, 他们有不同的应用场景:

  • 多线程环境下,使用StringBuffer比如XML拼接, Http参数解析
  • 单线程环境下, 使用StringBuilder这是从JDK5.0 才引入的东西, 单线程情况下性能好于StringBuffer, 因为没有线程同步的开销

很多情况下, 是可以把你的类变成不可变的类的, 但是总有一些情况不行。 这种情况下, 也尽量减少可变性,比如:

  1. 将可以不改变的那一部分成员变量变成final
  2. 除非真的需要, 否则只提供getter不提高setterr
  3. 甚至使用final修饰类, 使其无法子类化

可以参考TimerTask 虽然它不是一个不可变类, 但是可变部分被限制的很小。

本文原创, 转摘注明:http://www.flyml.net/2017/02/16/effective-java-ch4-class-interface/

Item 16: 组合(composition)优先于继承

PS: 中文版将Composition翻译成“复合”, 个人觉得翻译成“组合”更好理解。

PSS: 这一点我其实是非常同意的, 在实际的工程实践之中也是这样做的。比较无奈的反而是每次出Java相关的校招笔试题, 继承的各种考察反而挺多的。可能这也是因为继承在各个语言之中的特性不太一样, 在Java之中, 使用不当也很容易出错。

PSSS: 书中强调, 这个继承, 仅仅只是类继承, 而不是“接口实现”

继承为什么不推荐呢?

因为继承打破了类的封闭性。比如你继承了父类的一个方法,

  1. 最好能大概了解里面的实现细节, 是否真的跟你设想的一样。
  2. 同时, 如果父类的实现方法做了修改,可能你还得关心是否会对你有影响。
  3. 即使你新实现了某个方法, 但是如果一不小心父类在未来也实现了并且返回值跟你还不太一样,编译就直接挂了

继承并非一无是处

Stackoverflow 上面的总结很恰当:

  1. Composition means HAS A
  2. Inheritance means IS A

举例:

Car has an Engine and Car is a Vehicle

 

上面这种方法(Car之中包含了Engine这个成员变量)是一种很常见的组合模式, 另外一种书中的例子的方式,是转发(forwarding)的方式, 感觉用起来稍微麻烦一些。

一个题外话:JDK8 之中接口也可以有方法体!

这是JDK8 接口的新特性: default methods

本文原创, 转摘注明:http://www.flyml.net/2017/02/16/effective-java-ch4-class-interface/

Item 17: 要么禁止继承, 要么专门为继承而设计并提供说明

对继承暂时不感兴趣, 先PASS

脑子里面记住Item 16 就OK啦

Item 18: 接口优于抽象类

先说一下文中提到的minxin的意思。

参考廖雪峰的官方网站 :

在设计类的继承关系时,通常,主线都是单一继承下来的,例如,Rooster(公鸡)继承自Bird。但是,如果需要“混入”额外的功能,通过多重继承就可以实现,比如,让Rooster除了继承自Bird外,再同时继承Runnable。这种设计通常称之为Mixin。

PS: 原文是针对python2.7 做的说明与举例, 但是思想上跟java是一样的, 只是具体的实现方式不完全一样。

另外可以参考上面引用的文章里面用图画解释的类爆炸/组合爆炸。非常的简单明了, 这里就不重复累赘说明了。

特别说明一下:接口可以多重继承(只能继承接口), 类只能单一继承

以前我还不知道接口也能继承, 而且还能多重继承, ⊙﹏⊙b汗

父类成为superclass, 相应的,被继承的接口称为 superinterface

本章的理解并不是非常深刻, 以后再回过头来看看

同时, 有些东西已经部分失效了, 比如文中提到,接口公布之后, 几乎就不能动。 但是在JDK8出来之后, 一个良好设计的接口其实反而更重要了,因为不好好设计的话, 维护起来反而会更麻烦。

本文原创, 转摘注明:http://www.flyml.net/2017/02/16/effective-java-ch4-class-interface/

Item 19: 接口只能用于定义类型

PS:感觉更详细的是说, 不要使用常量接口的形式来使用接口

接口有一种不好的使用方式: 常量接口(constant interface), 这种接口没有定义任何方法, 只定义了一系列的静态final成员, 比如下面的例子:

 

感觉无论中文翻译还是英文原文, 我的原文的表述理解都不是非常清楚, 下面是摘抄的一段说明:

  1. 接口是不能阻止被实现或继承的,也就是说子接口或实现中是能够覆盖掉常量的定义,这样通过父、子接口(或实现) 去引用常量是可能不一致的;
  2. 同样的,由于被实现或继承,造成在继承树中可以用大量的接口、类或实例去引用同一个常量,从而造成接口中定义的常量污染了命名空间;
  3. 接口暗含的意思是:它是需被实现的,代表着一种类型,它的公有成员是要被暴露的API,但是在接口中定义的常量还算不上API。

From: http://www.howardliu.cn/constant-interface-anti-pattern/

再说说我的个人理解, 除了上面的不好的地方, 还有:

比如我们实现了上面的常量接口PhysicalConstants, 那么即使我们只需要AVOGADROS_NUMBER 但是也会无意中引入了另外两个我们不需要的变量:BOLTZMANN_CONSTANTELECTRON_MASS

如果当前的类被继承, 那么子类也会被这几个变量污染。

文中推荐的方式是使用枚举类型或者不可实例化的工具类, 参考下面修改过之后的PhysicalConstants :

 

如果想一次性的全部导入, 也可以通过import static package_name.*;的方式

衍生阅读: Constants should not be defined in interfaces

本文原创, 转摘注明:http://www.flyml.net/2017/02/16/effective-java-ch4-class-interface/

Item 20: 类层次优于标签类

首先, 不知道tagged class 标签类是不是作者自己创造的一个词语, 百度、google相关的搜索资料都很少。 不是非常明白具体什么才是tagged class

不过文中给出来的反例确实看起来就很不合适: 不同的类别(RECTANGLE、CIRCLE)被强行糅合到一个单独的类之中, 通过构造函数的参数进行区分具体是什么类别。

反例:

 

正常的重构方法, 就是:

  1. 将Rectangle、Circle 分别各自实现
  2. 将他们的公共部门抽取出来, 可以是抽象类, 也可以是接口 因为在这个例子之中, 两个类其实都是Figure(图形)的一种, 使用抽象类也是一种很自然而然的做法

看看作者重构之后推荐的做法:

本文原创, 转摘注明:http://www.flyml.net/2017/02/16/effective-java-ch4-class-interface/

 

 

Item 21: 使用函数对象(function objects)表示策略

首先这里也提到一个适合单例模式的场景:

当类是无状态的(stateless)的时候, 即它没有域成员变量, 所有的实例在功能上都是等价的。

做成单例, 就能节省创建跟销毁对象的消耗。

然后, 文中花了好一些篇幅讲了策略模式,我们可以参考一下这篇文章, 讲得比较简单明了:

http://www.cnblogs.com/java-my-life/archive/2012/05/10/2491891.html

策略模式整体架构非常清晰, 如下图:

在实际使用的时候, 只需要根据具体的Strategy来初始化就好了。 更详细的内容, 我们可以后面另外开一篇文章来讨论。

书中之所以提到策略模式, 是因为书中举的例子比较适合用策略模式:

  1. 首先定义一个Comparator接口
  2. 然后根据不同的策略/比较方法定义具体的实现Comparator接口的类比如书中是通过字符串的长度进行比较, 有如下定义:

     

     

即使在引入了lambda表达式的JDK8, 在策略模式中,匿名函数/匿名类的使用也是很广泛的,比如下面的例子:

 

但是这种做法不好的地方在于:每次用到都要新创建跟销毁对象

推荐下面的这种做法:

 

  1. 首先第3行定义了一个具体的实现类,
  2. 倒数第3行则定义了一个内部的final变量以后每次使用的时候, 都是使用相同的StrLenCmp() 实例了

补充说明:在JDK8 里面我们可以如何更方便的sort 呢?

充分利用lambda表达式:

 

第二种方式:

 

可以省略类型说明, 让JDK自己处理:

本文原创, 转摘注明:http://www.flyml.net/2017/02/16/effective-java-ch4-class-interface/

 

 

Item 22: 优先考虑静态成员类(static member class)

nested class: 嵌套类

嵌套类是定义在另外一个类之中的类, 他的存在仅仅只是用于服务包含他的外围类(enclosing class)。如果作用不仅于此, 那么就应该将其设计为顶层类(top-level class)

PS: 其实这个标准不是那么容易把握, 比如前面提到的Builder模式, Builder 类可以是内部静态类, 也可以是跟调用者平级的一个类。 只是一般将其设计为内部静态类。

嵌套类具体有3种类别:

  1. static member classes (静态成员类)
  2. non-static member classes (非静态成员类)
  3. anonymous classes 匿名类
  4. local classes 内部类

其中,#2 #3 #4 统称inner classes (内部类)

我们逐个的来讲讲。

static member classes 静态成员类

主要作用: public helper class, 仅对外部调用类的连接有帮助。(具体作用参考Builder模式的代码)

比如在Calculator类之中有一个静态成员类Operation. 并且其中有静态final变量, 那么我们可以直接调用:Calculator.Operation.MINUS

【剩下的看的不是非常明白。。。 因为我自己很少使用嵌套类。。。】

本文为原创文章,转载请注明出处
原文链接:http://www.flyml.net/2017/02/16/effective-java-ch4-class-interface/

RangerWolf

保持饥渴的专注,追求最佳的品质

文章评论