EFFECTIVE JAVA 3RD EDITION — 第四章 类与接口(2)

/ dev

Item 21 : Design interfaces for posterity

为后续维护、使用者设计好的接口

Java8之后允许interface提供默认方法,但是如果你要在一个已经发布的接口添加默认方法需要认证考虑带来的影响,所有实现类是否能够提供正确的结果,
虽然默认方法实现类都不需要提供实现,但是需要注意虽然编译时候不会报错,但是可能运行后就不是你想要的结果。

比如Java8里 Collection 接口的removIf方法:

1
2
3
4
5
6
7
8
9
10
11
default boolean removeIf(Predicate<? super E> filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}

Collections的内部类SynchronizedCollection就实现了Collection接口,并且重写了removeIf方法,因为这是一个同步集合,需要加锁:

1
2
3
4
@Override
public boolean removeIf(Predicate<? super E> filter) {
synchronized (mutex) {return c.removeIf(filter);}
}

试想如果没有重写该方法,多线程调用该方法的时候会出现什么结果?

所以尽量不要提供默认方法特别是已经存在的接口,除非有特别的需要或者必须,仔细思考对已有实现的影响。

Item 22 : Use interfaces only to define types

接口只用于定义实现类

接口(interface)只能用于定义实现类型,而不能用来定义常量,虽然说用起来是非常方便,
如果用于定义常量,容易导致api泄露,试想如果不小心实现了一个常量接口,常量值就无法保证不变了。

比如 java.io.ObjectStreamConstants就是一个反例,但是其常量都用final声明了。
如果非要导出常量,推荐下面几种方式:

1
2
3
4
5
6
7
// Constant utility class
public class PhysicalConstants {
private PhysicalConstants() { } // Prevents instantiation
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;
}

java7之后允许基础类型数据出现下划线,所以对于int,double,float都最好使用下划线来隔开长数字,方便阅读。

总之接口只用来定义实现类型,而不是常量。

Item 23 : Prefer class hierarchies to tagged classes

抽象类层次结构

尽量的去抽象类的层次结构,用一个书中的例子来进行说明:

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
// Tagged class - vastly inferior to a class hierarchy!
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(shape);
}
}
}

上面的类就称为标签类(tagged classes)比如一个形状可能有矩形和圆形,他们共有的属性有面积,但是上面的结构有下面不足:

  1. 类结构复杂并且凌乱
  2. 代码可读性差
  3. 内存占用大
  4. 构造方法复杂,多种形状需要多个构造器
  5. final关键字使用麻烦,需要在构造方法中初始化
  6. 扩展性差,如果你不修改源码根本没办法添加形状,就算修改了也要添加相应的构造方法和switch…case
  7. 根据类名根本无法区分类型,抽象的不够好
    但Java面向对象的思想提供了一个层级的方式来抽象一个形状,
    先抽象出形状共有的属性和行为,比如面积,然后扩展这个类,用子类来表示不同的形状,在子类中定义各自的特有属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Class hierarchy replacement for a tagged class
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; }
}

上面的结构就清楚很多,扩展性也强,其实就是强调了Java抽象和封装的概念。

Item 24 : Favor static member classes over nonstatic

尽量使用静态成员类作为嵌套类

这一节主要介绍的是四种嵌套类:

Item 25 : Limit source files to a single top-level class

不要在一个文件中定义多个顶级类

1
2
3
4
5
6
7
// Two classes defined in one file. Don't ever do this!
class Utensil {
static final String NAME = "pan";
}
class Dessert {
static final String NAME = "cake";
}

比如上面两个类,又在另外一个文件中定义了,那么最终使用的顺序是取决于类传入编译器的先后顺序的,所以尽量避免这种情况的出现。