【EFJava】关于类和接口

Num1:使类和成员的可访问性最小化

要区别设计良好的模块与设计不好的模块,最重要的因素在于,这个模块对于外部的其他模块而言,是否隐藏其内部数据和其他实现细节。设计良好的模块会隐藏所有的实现细节,把它的API与它的实现清晰地隔离开来。也称呼为封装。

所以有这么一句话:尽可能地使每个类或者成员不被外界访问,可以包括实体声明中所出现的访问修饰符共同决定的,有四种访问级别,如下:

  • 私有的,private
  • 包级私有的,default
  • 受保护的,protected
  • 公有的,public

Num2:在公有类中使用访问方法而非公有域

如果类可以在它所在的包的外部进行访问,就提供访问方法,以保留将来改变该类的内部表示法的灵活性。

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
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;
}
}

总之,公有类永远都不应该暴露可变的域。

Num3:使可变性最小化

不可变类只是其实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供了,并在对象的整个生命周期内固定不变。Java平台类库中包含许多不可变的类,其中有String、基本类型的包装类、BigIntegerBigDecimal,存在不可变的类有许多理由:不可变的类比可变类更加易于设计、实现和使用,它们不容易出错,且更加安全。

有五条规则,使得类成为不可变:

  1. 不要提供任何会修改对象状态的方法
  2. 保证类不会被扩展。一般做法是使得这个类成为final
  3. 使所有域都是final的
  4. 使所有的域都成为私有的
  5. 确保对于任何可变组件的互斥访问
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public final class Complex {
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 static Complex valueOfPolar(double r, double theta) {
return new Complex(r * Math.cos(theta), r * Math.sin(theta));
}

public static final Complex ZERO = new Complex(0, 0);
public static final Complex ONE = new Complex(1, 0);
public static final Complex I = new Complex(0, 1);

// Accessors with no corresponding mutators
public double realPart() {
return re;
}

public double imaginaryPart() {
return im;
}

public Complex add(Complex c) {
return new Complex(re + c.re, im + c.im);
}

public Complex subtract(Complex c) {
return new Complex(re - c.re, im - c.im);
}

public Complex multiply(Complex c) {
return new Complex(re * c.re - im * c.im, re * c.im + im * c.re);
}

public Complex divide(Complex c) {
double tmp = c.re * c.re + c.im * c.im;
return new Complex((re * c.re + im * c.im) / tmp, (im * c.re - re
* c.im)
/ tmp);
}

@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Complex))
return false;
Complex c = (Complex) o;

// See page 43 to find out why we use compare instead of ==
return Double.compare(re, c.re) == 0 && Double.compare(im, c.im) == 0;
}

@Override
public int hashCode() {
int result = 17 + hashDouble(re);
result = 31 * result + hashDouble(im);
return result;
}

private int hashDouble(double val) {
long longBits = Double.doubleToLongBits(re);
return (int) (longBits ^ (longBits >>> 32));
}

@Override
public String toString() {
return "(" + re + " + " + im + "i)";
}
}

不可变对象本质上是线程安全的,它们不要求同步。

Num4:复合优先于继承

与方法调用不同的是,继承打破了封装性,换句话说,子类依赖于其超类中特定功能的实现细节。不过有种设计叫做“复合”,在新的类中增加一个私有域,它引用现有类的一个实例,因为现有的类变成了新类的一个组件,新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回它的结果。这样得到的类将会非常稳固,它不依赖于现有类的实现细节,看段代码。

转发类

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;

public ForwardingSet(Set<E> 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();
}

public Iterator<E> iterator() {
return s.iterator();
}

public boolean add(E e) {
return s.add(e);
}

public boolean remove(Object o) {
return s.remove(o);
}

public boolean containsAll(Collection<?> c) {
return s.containsAll(c);
}

public boolean addAll(Collection<? extends E> c) {
return s.addAll(c);
}

public boolean removeAll(Collection<?> c) {
return s.removeAll(c);
}

public boolean retainAll(Collection<?> c) {
return s.retainAll(c);
}

public Object[] toArray() {
return s.toArray();
}

public <T> T[] toArray(T[] a) {
return s.toArray(a);
}

@Override
public boolean equals(Object o) {
return s.equals(o);
}

@Override
public int hashCode() {
return s.hashCode();
}

@Override
public String toString() {
return s.toString();
}
}

继承类

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
public class InstrumentedSet<E> extends ForwardingSet<E> {
private int addCount = 0;

public InstrumentedSet(Set<E> s) {
super(s);
}

@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;
}

public static void main(String[] args) {
InstrumentedSet<String> s = new InstrumentedSet<String>(
new HashSet<String>());
s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
System.out.println(s.getAddCount());
}
}

Set接口的存在使得IntrumentedSet类的设计成为可能,因为Set接口保存了HashSet类的功能特性。除获得了健壮性之外,这种设计也带来了格外的灵活性。

包装类几乎没有什么缺点,需要注意一点的是,包装类不适合用在回调框架(CallBack Framework)中,在回调框架中,对象需要把自身的引用传递给其他的对象,用于后续的调用。

只有当子类真正是超类的子类型的时,才适合用继承,换句话说,对于两个类A和B,只有当两者之间确实存在”is-a”关系的时候,类B才应该扩展类A,如果你打算让类B扩展类A,就应该问问自己:每个B确实也是A吗?如果答案是否定的,通常情况下,B应该包含A的一个私有实例,并且暴露一个较小的,较简单的API:A本质上不是B的一部分,只是它的实现细节而已。

Num5:接口优先于抽象类

Java程序设计中提供了两种机制:接口和抽象类。这两种机制之间最明显的区别在于,抽象类运行包含某些方法的实现,但是接口则不允许。一个更为重要的区别在于,为了实现由抽象类定义的类型,类必须成为抽象类的一个子类。

有三个方式:

  • 现有的类可以很容易被更新,以实现新的接口
  • 接口是定义混合类型的理想选择
  • 接口允许我们构造非层次结构的类型框架

虽然接口不允许包含方法的实现,但是,使用接口来定义类型并不妨碍你成为程序员提供实现上的帮助。通过你导出的每个重要接口都提供一个抽象的骨架来实现类,把接口和抽象类的优点结合起来。接口的作用仍然是定义类型,但是骨架实现类接管了所有与接口实现相关的工作。

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
public class IntArrays {
static List<Integer> intArrayAsList(final int[] a) {
if (a == null)
throw new NullPointerException();

return new AbstractList<Integer>() {
public Integer get(int i) {
return a[i]; // Autoboxing (Item 5)
}

@Override
public Integer set(int i, Integer val) {
int oldVal = a[i];
a[i] = val; // Auto-unboxing
return oldVal; // Autoboxing
}

public int size() {
return a.length;
}
};
}

public static void main(String[] args) {
int[] a = new int[10];
for (int i = 0; i < a.length; i++)
a[i] = i;
List<Integer> list = intArrayAsList(a);

Collections.shuffle(list);
System.out.println(list);
}
}

需要注意的是,接口一旦被公开发行,并且已被广泛实现,再想改变这个接口几乎是不可能的,你必须在初次设计接口的时候就应该保证接口是正确的。总之,接口通常是定义允许多个实现类型的最佳途径。

,