Item 13: 使类及其成员的可访问性最小
除非真的有必要, 否则不要让外界能访问这个类或者成员变量。 因此, 再复习一下Java之中几种访问级别:
作用域 | 当前类 | 同一package | 子孙类 | 其他package |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | × |
package | √ | √ | × | × |
private | √ | × | × | × |
没有修饰符的时候, 默认为package级别。
当我们覆盖了父类的某个方法的时候, 不能使其访问级别提高。 比如原来是protected, 不能升级成为public。
一种不太好的做法:
可以声明一个跟父类的方法名一样的方法, 但是不加上
@Override
,即只是方法名一样, 但是并不是真实的覆盖父类的方法。光有覆盖之名却无覆盖之实。
一个在JDK8修复的安全漏洞:
在原书编写时的 最新JDK(5 or 6), 如果定义了一个公开的静态数组域, 在类的外部是可以进行修改的。
但是在笔者JDK8 的环境之中进行测试的时候, IDE 直接报错! 测试代码如下:
复用第二章的一个Student
类 :
1 2 3 4 5 6 7 8 9 10 11 |
public class Student { public String name; public Integer age; // 按照书上的说法, 在JDK5 or 6 的时候,此变量可以被外部修改 public static final Integer[] HIST_SCORE_ARR = {1,2,3,4}; @Override public String toString() { return String.format("name: %s, age: %s", name, age); } } |
测试的Test.class :
1 2 3 4 |
Student s = new Student(); s.HIST_SCORE_ARR = new Integer[]{2,3,4,5,6}; // IDE throws errors Student.HIST_SCORE_ARR = new Integer[]{2,3,4,5,6}; // IDE throws errors, too |
本文原创, 转摘注明:http://www.flyml.net/2017/02/16/effective-java-ch4-class-interface/
Item 14: 对公有类使用访问方法而不是直接公有域
翻译不是很好, 原文是:
In public classes, use accessor methods, not public fields
比如上面的Student类, 只是为了简单的测试而直接将域成员变量name
跟age
直接设置成public
, 这是非常不好的习惯。
比较好的做法是使用getters and setters, 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package skyaid.sparkstat; public class Student { String name; Integer age; public static final Integer[] HIST_SCORE_ARR = {1,2,3,4}; // getters and setters public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } @Override public String toString() { return String.format("name: %s, age: %s", name, age); } } |
如果是不可变的成员变量, 也可以直接暴露出来,比如学生的ID是不可变的, 那么可以采用如下方式:
1 2 3 4 5 6 7 8 |
public class Student { public final String stuID; public Student(String id) { this.stuID = id; } // getters and setters and other methods ommit } |
即在初始化的时候, 就对这个类成员变量进行初始化, 之后已经不可变了, 相当于只有只读级别。
文中提到, JDK之中的Dimension
与Point
类都直接抛出了公有成员变量,这是一种不好的做法。 我看了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, 因为没有线程同步的开销
很多情况下, 是可以把你的类变成不可变的类的, 但是总有一些情况不行。 这种情况下, 也尽量减少可变性,比如:
- 将可以不改变的那一部分成员变量变成final
- 除非真的需要, 否则只提供getter不提高setterr
- 甚至使用
final
修饰类, 使其无法子类化
可以参考TimerTask
虽然它不是一个不可变类, 但是可变部分被限制的很小。
本文原创, 转摘注明:http://www.flyml.net/2017/02/16/effective-java-ch4-class-interface/
Item 16: 组合(composition)优先于继承
PS: 中文版将Composition翻译成“复合”, 个人觉得翻译成“组合”更好理解。
PSS: 这一点我其实是非常同意的, 在实际的工程实践之中也是这样做的。比较无奈的反而是每次出Java相关的校招笔试题, 继承的各种考察反而挺多的。可能这也是因为继承在各个语言之中的特性不太一样, 在Java之中, 使用不当也很容易出错。
PSSS: 书中强调, 这个继承, 仅仅只是类继承, 而不是“接口实现”
继承为什么不推荐呢?
因为继承打破了类的封闭性。比如你继承了父类的一个方法,
- 最好能大概了解里面的实现细节, 是否真的跟你设想的一样。
- 同时, 如果父类的实现方法做了修改,可能你还得关心是否会对你有影响。
- 即使你新实现了某个方法, 但是如果一不小心父类在未来也实现了并且返回值跟你还不太一样,编译就直接挂了
继承并非一无是处
Stackoverflow 上面的总结很恰当:
- Composition means
HAS A
- Inheritance means
IS A
举例:
Car has an Engine and Car is a Vehicle
1 2 3 4 5 6 7 8 9 10 |
class Engine {} // The engine class. class Vehicle {} // Vehicle class which is parent to Car class. // Car is an Automobile, so Car class extends Automobile class. class Car extends Vehicle{ // Car has a Engine so, Car class has an instance of Engine class as its member. private Engine engine; } |
上面这种方法(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 4 5 6 7 8 9 |
// Constant interface antipattern - do not use! public interface PhysicalConstants { // Avogadro's number (1/mol) static final double AVOGADROS_NUMBER = 6.02214199e23; // Boltzmann constant (J/K) static final double BOLTZMANN_CONSTANT = 1.3806503e-23; // Mass of the electron (kg) static final double ELECTRON_MASS = 9.10938188e-31; } |
感觉无论中文翻译还是英文原文, 我的原文的表述理解都不是非常清楚, 下面是摘抄的一段说明:
- 接口是不能阻止被实现或继承的,也就是说子接口或实现中是能够覆盖掉常量的定义,这样通过父、子接口(或实现) 去引用常量是可能不一致的;
- 同样的,由于被实现或继承,造成在继承树中可以用大量的接口、类或实例去引用同一个常量,从而造成接口中定义的常量污染了命名空间;
- 接口暗含的意思是:它是需被实现的,代表着一种类型,它的公有成员是要被暴露的API,但是在接口中定义的常量还算不上API。
From: http://www.howardliu.cn/constant-interface-anti-pattern/
再说说我的个人理解, 除了上面的不好的地方, 还有:
比如我们实现了上面的常量接口PhysicalConstants
, 那么即使我们只需要AVOGADROS_NUMBER
但是也会无意中引入了另外两个我们不需要的变量:BOLTZMANN_CONSTANT
跟ELECTRON_MASS
如果当前的类被继承, 那么子类也会被这几个变量污染。
文中推荐的方式是使用枚举类型或者不可实例化的工具类, 参考下面修改过之后的PhysicalConstants
:
1 2 3 4 5 6 |
public class PhysicalConstants { private PhysicalConstants() { } // Prevents instantiation public static final double AVOGADROS_NUMBER = 6.02214199e23; public static final double BOLTZMANN_CONSTANT = 1.3806503e-23; public static final double ELECTRON_MASS = 9.10938188e-31; } |
如果想一次性的全部导入, 也可以通过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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
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(); } } } |
正常的重构方法, 就是:
- 将Rectangle、Circle 分别各自实现
- 将他们的公共部门抽取出来, 可以是抽象类, 也可以是接口 因为在这个例子之中, 两个类其实都是Figure(图形)的一种, 使用抽象类也是一种很自然而然的做法
看看作者重构之后推荐的做法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
abstract class Figure { abstract double area(); } class Circle extends Figure { final double radius; Circle(double radius) { this.radius = radius; } double area() { return Math.PI * (radius * radius); } } class Rectangle extends Figure { final double length; final double width; Rectangle(double length, double width) { this.length = length; this.width = width; } double area() { return length * width; } } class Square extends Rectangle { Square(double side) { super(side, side); } } |
本文原创, 转摘注明: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来初始化就好了。 更详细的内容, 我们可以后面另外开一篇文章来讨论。
书中之所以提到策略模式, 是因为书中举的例子比较适合用策略模式:
- 首先定义一个Comparator接口
- 然后根据不同的策略/比较方法定义具体的实现Comparator接口的类比如书中是通过字符串的长度进行比较, 有如下定义:
123class StringLengthComparator implements Comparator<String> {... // class body is identical to the one shown above}
即使在引入了lambda表达式的JDK8, 在策略模式中,匿名函数/匿名类的使用也是很广泛的,比如下面的例子:
1 2 3 4 5 6 |
// sort的第二个参数值是一个匿名类 Arrays.sort(stringArray, new Comparator<String>() { public int compare(String s1, String s2) { return s1.length() - s2.length(); } }); |
但是这种做法不好的地方在于:每次用到都要新创建跟销毁对象。
推荐下面的这种做法:
1 2 3 4 5 6 7 8 9 10 11 |
// Exporting a concrete strategy class Host { private static class StrLenCmp implements Comparator<String>, Serializable { public int compare(String s1, String s2) { return s1.length() - s2.length(); } } // Returned comparator is serializable public static final Comparator<String> STRING_LENGTH_COMPARATOR = new StrLenCmp(); // Bulk of class omitted ... } |
- 首先第3行定义了一个具体的实现类,
- 倒数第3行则定义了一个内部的final变量以后每次使用的时候, 都是使用相同的StrLenCmp() 实例了
补充说明:在JDK8 里面我们可以如何更方便的sort 呢?
充分利用lambda表达式:
1 |
Arrays.sort(months, Comparator.comparingInt(String::length)); |
第二种方式:
1 2 |
Arrays.sort(months, (String a, String b) -> a.length() - b.length()); |
可以省略类型说明, 让JDK自己处理:
1 |
Arrays.sort(months, (a, b) -> a.length() - b.length()); |
本文原创, 转摘注明: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种类别:
- static member classes (静态成员类)
- non-static member classes (非静态成员类)
- anonymous classes 匿名类
- 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/
文章评论