Java中的类与接口是很重要的组成部分,以下是JDK作者Bloch多年的经验总结,非常具有参考价值,现将重点部分总结出来,更多细节参考原书
Item15-使类和成员的可访问性最小化
- 设计良好的组件很好的对外部其它组件隐藏了其内部数据结构和实现细节
- 信息隐藏可以有效的解除组件之间的耦合关系,方便组件独立的开发、测试、维护、优化、理解和修改;有利于模块化开发,提高软件的可重用性
- 合理使用Java提供的访问修饰符,隐藏信息, 让类或成员的可访问性尽量小,对外曝露越少越好,否则维护不易
- 如果一个类是public修饰,你就有责任永远维护它,保持兼容,它的成员如无必要应该都是private;只有同包下的另一个类确实需要才提高到default级别,如Thread和ThreadLocal;protected修饰的也是导出API的一部分,需要谨慎;实例域是决对不可以用public来修饰的,一是你完全丧失控制权,二是这样的类通常是线程不安全的
public static final修饰的常量可以对外曝露,但不要修饰引用,引用虽然不可变但它指向的对象是可以修改的,下面的方法可以保护数据的对象不被修改
//有安全漏洞,数组中的对象引用是能替换的
public static final Thing[] VALUES = { ... };
//正确做法
private static final Thing[] PRIVATE_VALUES = { ... };
//如有修改抛异常
public static final List VALUES =
Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
//或者
private static final Thing[] PRIVATE_VALUES = { ... };
//如有修改,修改的是克隆数组中的对象引用,不会影响原数组中的对象引用
public static final Thing[] values() {
return PRIVATE_VALUES.clone();
}
Item16 在共有类使用访问方法获取共有字段的值
对于重点是字段的数据类,如果在包外可以访问,需要提供访问方法,保持内部实现的灵活性,也就是对字段值的获取和赋值由类自己控制。
public class Point {
private double x;
private double y;
public Point(double x, double y) { this.x = x;this.y = y; }
//重点是拥有对返回值的控制权,而不仅仅是返回其值
public double getX() { return x; }
public double getY() { return y; }
//拥有对字段值设置的权限,可以在不同版本中修改而不影响客户端调用
public void setX(double x) { this.x = x; }
public void setY(double y) { this.y = y; }
}
如果字段是不可变的,还是可以接受的
//immutable fields - questionable
public final class Time {
private static final int HOURS_PER_DAY = 24;
private static final int MINUTES_PER_HOUR = 60;
public final int hour;
public final int minute;
public Time(int hour, int minute) {
if (hour < 0 || hour >= HOURS_PER_DAY)
throw new IllegalArgumentException("Hour: " + hour);
if (minute < 0 || minute >= MINUTES_PER_HOUR)
throw new IllegalArgumentException("Min: " + minute);
this.hour = hour;
this.minute = minute;
}
}
但如果类是包私有的或私有的嵌套类就没必要,可以直接暴露数据字段,因为客户端无法访问,控制权在自己手里,不影响后期的更新维护
Item17 使可变性最小化
不可变类在实例化时就完成所有信息的初始化,而且所有信息在生命周期中不可变,如String、BigInteger。这样的类这易于设计、实现,使用简单。下面是不可变类的设计原则:
- 字段最好使用private final修饰
- 不提供改变字段信息的方法
- 不可继承, 私有的构造器或final修饰类
- 不曝露可变组件
不可变类的优点:
- 线程安全,可以自由共享,甚至是内部信息,如BigInteger的取负,其数值部分与新对象是共享的,仅改变符号部分
- 不可变对象因字段不可变,其hash值和equal方法都是稳定的,所以作为Map或Set的键不会有意外,之所以感觉不到有意外是因为我们常用的键是String,是不可变的,如使用自定义的对象,一定要注意这点。中文版这段的翻译含义不清,没能表达作者的原意
缺点在于每个不同的值都要有一个单独的对象,虽然对象不能重复使用,对于大部分对象,性能都不是问题,如果产生一个最终对象需要太多中间结果或性能损耗,考虑提供一个可变配套类,如String的StringBuilder
私有构造器的类是不可继承的,所以可以使用私有的构造器和静态工厂方法让一个类不可变,如下所示。好处在于灵活,可以使用多个包级私有的实现类,还可以通过缓存提高性能
public class Complex {
//对于常用的对象,提供可重用实例
public static final Complex ONE=new Complex(1,0);
private final double re;
private final double im;
private Complex(double re, double im) {
this.re = re;
this.im = im;
}
public static Complex valueOf(double re, double im) {
return new Complex(re, im);
}
//函数式方法,返回一个结果,不改变类的状态
public Complex minus(Complex c){
return new Complex(re-c.re,im-c.im);
}
...
}
如果一个不可变类不是final的,如BigInteger,那么其作为参数的时候,就需要在安全性方面做更多检查
public static BigInteger safeInstance(BigInteger val) {
return val.getClass() == BigInteger.class ?
val : new BigInteger(val.toByteArray());
}
在实际开发中,不可能字段都是private final的,原则是对象状态的改变尽量对外不可见,如无必要不要每个get方法都提供set方法。将可变的部分限制到最小,不仅有利于分析对象的行为还能降低出错的可能。
在构造器中完全初始化对象,如无必要,不要在构造器之外提供公有的初始化方法,其带来的代价大于便利。
Item18优先使用组合
同一包中的类可以使用继承,因为都在同一作者控制范围内,或者是有着良好的文档说明,除此之外,最好不要通过继承来实现代码复用。继承使子类和父类耦合性较强,若父类在不同版本之间可能有变更,则子类会受到牵连,推荐使用组合,尤其是下面这种包装类转发的方式
public class ForwardingSet implements Set {
private final Set s;
public ForwardingSet(Set s) { this.s = s; }
public void clear() { s.clear(); }
public boolean contains(Object o) { return s.contains(o); }
public boolean isEmpty() { return s.isEmpty(); }
public int size() { return s.size(); }
...
}
Item19无文档不继承
继承是Java的一大功能特性,它让程序员无需白手起家,继承某个父类修改一番即可为我所用,如果只是自娱自乐不会出什么岔子。但如果你的代码是面向公众的,就必须考虑其中的类是否面向继承,对可覆盖的方法要有清晰的说明,客户端一旦继承,你必须提供一致性。
如果设计的类不打算被继承,一定要设计成final的,否则不仅需要提供文档说明,还需要找其他人试用并测试。有一些基本技巧需要注意
- 绝对不要在构造方法中调用可被覆盖的方法,clone方法和readObject方法和构造方法类似,所以同样适用此规则。
- 如果需要调用可以覆盖方法中的代码,将其移到私有方法中供复用。
Item20接口优先于抽象类
- 一个类或接口可以实现或继承多个接口,而一个类只能继承一个类,所以一个类可以随便增加一个新接口却不能随便继承一个类。
- 接口不会让子类陷于某个层级中,而抽象类与子类就有层级关系。
- 接口适合定义类型,如果每个实现类都从零实现,难免出现代码冗余,尤其是一些通用基础方法,这个时候就适合用抽象类实现接口的基础方法完成骨架部分,这样的抽象类还是限定在接口定义的范围,你可以用顶级类继承完成其他部分,也可以完全从零实现接口,同时用内部类继承抽象骨架类,将接口的调用转发到此内部类,间接的实现了多继承。
- 接口虽然可以有默认方法,但不允许提供Object类方法,如hashCode,equals、toString的默认实现,它们通常在骨架实现类中实现
Item21向后设计接口
Java 8之前如果在接口中添加方法会导致实现类编译错误,在Java 8接口中却可以添加default关键字修饰的方法实现,而不会导致客户端编译错误,但却可能引起运行时错误,而且对现有方法的影响很难评估。默认方法通常用来实现标准方法,减少实现接口的负担,所以强烈建议如无必要不要给接口添加默认方法,如果添加需要从不同维度实现接口并测试
Item22 接口只用来定义类型
接口用来规范实现类的行为,但因为其字段都是常量,容易被滥用成常量接口,即接口里面几乎全都是常量。想要导出常量,如果想被看成枚举类型成员,使用枚举类型,否则使用不可实例化的工具类。
public class PhysicalConstants {
private PhysicalConstants() { } // 防止实例化
//这里使用下划线代替千分符,这种书面表达自java 7以后是合法的
public static final double AVOGADROS_NUMBER = 6.022_140_857e23;
public static final double BOLTZMANN_CONST =1.380_648_52e-23;
public static final double ELECTRON_MASS = 9.109_383_56e-31;
}
接口只用来定义类型,而不能仅仅用来导出常量
Item23 类层次优于标签类
编程初期,我们喜欢把一堆可以用标签区分的东西都揉到一个类中,然后通过switch语句区分,
class Figure {
enum Shape { RECTANGLE, CIRCLE };
// 标签字段,决定这个图形的形状
final Shape shape;
// 这些字段只有图形在矩形时才有用
double length;
double width;
//图形是圆形时才有用
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);
}
}
}
这样做开始很直观,但随着项目的发展这个类会变得臃肿、含糊而难以维护,它的字段太多而且可能只有少部分初始化,浪费内存还容易出错。这种情况最好使用类层次来替代:将标签要表达的共性部分抽象出来,子类型实现这些共性部分并添加自己的特性
abstract class Figure {
abstract double area();
}
class Circle extends Figure {
final double radius;
Circle(double radius) { this.radius = radius; }
@Override 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;
}
@Override double area() { return length * width; }
}
重构之后不再有冗余字段和方法,每个类的内容都是有用的、明确的、简单的,方便维护和扩展
Item24 静态成员类优于非静态
如果一个类嵌套在其它类里面就是嵌套类,嵌套类优势在于可以访问外围类所有字段,也只服务于外围类,如果嵌套类的作用更广,应该设计为顶级类。有四种嵌套类
- 静态成员类,行为和其它static关键字修饰的字段类似
- 非静态成员类
- 匿名类
- 局部类
静态成员类可以看作是声明在其它类中的普通类,其他三个都是内部类。非静态成员类隐式的持有外围类的引用,它在实例化的时候就与外围类建立了关联,这种关联占据了非静态成员类实例的空间还有构造时间,是有成本的。
非静态成员类常见用法是定义一个适配器,将外围类以不同视图展示,如HashMap的KeySet,Values,分别展现的就是Map的键和值,在Java不支持多继承的情况下,这是一种很好的弥补。基于内部类持有外部类引用的优势,对视图的操作都是转发给外围类处理的,所以对视图的修改是直接影响原数据结构的,如remove方法。
final class KeySet extends AbstractSet {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator iterator() { return new KeyIterator(); }
public final boolean contains(Object o) { return containsKey(o); }
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
...
}
如果你的成员类不需要持用外围类的引用,一定要加上static关键字变成静态成员类,因为这种对外围类的隐式引用是有成本的,而且可能导致外围类的泄漏,典型的就是在Android的Activity页面使用非静态的Thread类,如果这个线程无法结束将导致其隐式引用的Activity泄漏。HashMap中的Node实现了Map.Entry,它的操作不需要持有外部引用,所以使用了static修饰。这个静态内部类是包级访问权限,控制权还在作者手里,如果是public或protected修饰的,说明是导出的API,那么后续若想变动就难了
HashMap.java
static class Node implements Map.Entry {
final int hash;
final K key;
V value;
Node next;
Node(int hash, K key, V value, Node next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
}
匿名类没有名字,它在使用的时候被同时声明和实例化,因为没有名字所以不能使用instanceOf判断,也不能实现或继承接口,只能调用从父类继承的方法,它多数的用武之地在于创建小型函数对象,如Android中的按钮点击
btnOne.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this, "通过匿名内部类实现点击事件监听", Toast.LENGTH_SHORT).show();
tvShow.setText("通过匿名内部类实现点击事件监听");
}
});
也常用在静态工厂方法中应用,减少类定义,结构简单
static List intArrayAsList(int[] a) {
Objects.requireNonNull(a);
return new AbstractList<>() {
@Override public Integer get(int i) {
return a[i];
}
@Override public Integer set(int i, Integer val) {
int oldVal = a[i];
a[i] = val;
return oldVal;
}
@Override public int size() {
return a.length;
} };
}
四种嵌套类的使用场景总结如下:
方法外 | 方法内 |
---|---|
需要外部引用:non static | 只在一个地方创建实例并有预置类型特征:匿名类 |
不需要:static | 其它情况:局部类 |
item 25 一个源文件只放单个顶级类
在一个源文件中放两个顶级的public类是编译不通过的
Test1.java
public class Test1 {
static final String name="one";
}
public class Test2 {
static final String name="two";
}
//错误: 类Test2是公共的, 应在名为 Test2.java 的文件中声明
去掉Test2类的public修饰符或者两个都去掉是可以编译通过的
Test1.java
public class Test1 {
static final String name="one";
}
class Test2 {
static final String name="two";
}
但强烈建议不要把多个顶级类放到一个源文件中,否则编译的顺序会影响运行结果,比如还有一个源文件Test2.java同样定义了两个顶级类
Test2.java
class Test1 {
static final String name="three";
}
class Test2 {
static final String name="four";
}
分别编译都没有出错,和书上看到的不一样
编译 | 运行结果 |
---|---|
javac Test.java Test1.java | onetwo |
javac Test.java Test2.java | onetwo |
javac Test2.java Test.java | threefour |
Test.java
public class Test {
public static void main(String[] args) {
System.out.println(Test1.name+Test2.name);
}
}
为了避免这种编译顺序不一样导致运行结果的不确定性,不要把多个顶级类定义在一个源文件中,如有必要使用静态成员类。
Test.java
public class Test {
public static void main(String[] args) {
System.out.println(Test1.NAME + Test2.NAME); }
private static class Test1 {
static final String NAME = "one";
}
private static class Test2 {
static final String NAME = "two";
}
}