《Effective Java》- 类和接口

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

模块设计可以从该模块对于外部模块是否隐藏了内部数据以及实现细节来区别好坏,模块之间隐藏内部细节以及数据,通过 API 来互相通信。这个概念叫做信息隐藏,这也是高内聚,低耦合的基础。这样做有一下几点好处:

  • 模块可以独立的开发,测试,优化,使用
  • 修改一个模块时,由于内部细节是隐藏的,所有使用了该模块的其他模块不需要修改,也不用关心该模块是否修改过,提高了系统的可维护性
  • 由于模块足够独立,不需要依赖其他模块,所有也提高了模块的可用性,把该模块放到其他运行环境中依旧可以正常使用

一个创建类的原则:尽可能地使每个类或者成员不被外界访问

  • 如果类或者接口能做成包级私有的,就应该做成包级私有的,这样它实际是包的一部分,而不是该包导出 API的一部分,以后对该类或接口进行增删改也不会影响到调用者。如果做成公有的,开发者就有责任一直维护它,保持它的兼容性
  • 如果一个类或者接口只在一个类中被用到,应该考虑把它做成那个类的私用内部类,这样就可以把该类的访问范围限制到了使用它的那个类中
  • Java 中的继承和实现限制了降低函数可访问性的能力,因为接口中声明的函数都是公有的,而继承了超类的子类在覆盖超类函数时,访问级别必须要高于超类中声明的,这是因为要保证在使用超类的地方都能用子类
  • 可变的引用类型的类变量绝对不能是公有的
  • 由于长度非零的数组总是可变的,所以类不能包含公有静态的数组变量,或者提供返回这类数组的函数
  • 类中能够对外部暴露的变量类型只有静态 final 常量,这些常量要么是基本类型,要么是不可变类型

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

其实这就是我们经常为 JavaBean 提供的 getset 函数组合

Q:为什么不把类的变量声明为公有的呢?
A:首先为每个变量提供访问方法这种做法保留了将来改变该类内部表示法的可能性,在外部调用不做任何改变的情况下,可以通过修改内部表示法来满足需求

如果是内部嵌套类的变量,可以声明为公有的,因为内部嵌套类的变化范围被限制到了外围类中

使可变性最小化

为了让类变成不可变,需要遵循以下原则:

  • 不提供任何会修改对象状态的方法
  • 保证类不会被扩展 - 为了防止子类化,常见做法是使这个类变成 final,或者私有化所有构造函数,添加公有的静态工厂
  • 保证所有的属性都是 final 的
  • 保证所有的属性都是 private 的
  • 保证所有引用了可变对象的属性的互斥访问 - 需要确保使用该类的客户端无法从该类中获取到该可变对象的引用,进而破坏类的不可变性;并且永远不要用客户端提供的可变对象初始这样的属性,也不要提供任何的访问函数;构造器、访问函数以及 readObject 方法中使用 保护性拷贝 技术

由于不可变对象的特征,它本质上就是线程安全的,可以被自由分享,同时它也具有一个唯一的缺点就是对于不同的值都需要一个单独的对象,如果这个对象占用内存很低则影响不会太大,但是对于有大型对象频繁使用的项目,这个缺点是灾难性的。

在这种情况下如果能够精确预测出客户端想要在不可变类上做哪些复杂的多阶段操作,就需要提供一个 包级私有可变配套类,如果不可预测,最后的办法就是提供一个 公有可变配套类,就如同 String 和 StringBuilder 以及特定环境下的 BigInteger 和 BitSet

如果类实在不能做成不可变,就尽量限制它的可变性,除非有特别的理由要使属性变成非 final,否则一律都是 final

不可变对象可以提供一些静态工厂函数,对象内部缓存频繁被请求的实例,如果有请求满足该实例,则不用创建对象直接返回即可,降低内存占用和不必要的回收,典型例子就是 Boolean#valueOf

复合优于继承

首先需要知道的一点是继承打破了封装性:
如果子类依赖父类的某个功能的实现,在不停的版本迭代中,如果父类的该功能发生变化,那么子类同时也会遭到破坏,既然它没有任何变化,同样的如果父类在某个版本中增加了一个和子类某个函数签名相同但是返回类型不同的函数,这样程序就无法通过编译

解决此类问题有一个很好的方法 - 复合,不用扩展现有的类,只用在新的类中增加一个属性引用现有类的一个实例即可,这样的类也叫做包装类。此时被包装的类增加函数,不会影响到包装类;包装类也隐藏了被包装类的细节,并且可以基于被包装类随意扩展

要么为继承而设计,并提供文档说明,要么就禁止继承

如果想要设计一个可被继承的类,必须要满足以下几点:

  • 完善的文档 - 对于公有的或者受保护的函数或者构造器,文档必须指明该函数或者构造器调用了那些可覆盖函数

接口优于抽象类

由于 Java 中只允许单继承,抽象类作为类型定义就受到了极大的限制。使用接口的好处:

  • 现有的类可以很容易被更新 - 因为可以实现多个接口,所以当有一个新的接口时,只需要增加几个该接口定义的函数即可。如果是继承的话,现有的类很难扩展抽象类,而且如果有两个类需要扩展同一抽象类,就必须把该抽象类放到类层次的高处,这样做又间接的伤害到了类层次,迫使所有子类都要扩展这个抽象类

  • 接口是定义 mixin(混合类型)的理想选择 - mixin 是指:类除了实现它的基本类型之外,还可以实现这个 mixin 类型,表明该类具有哪些可供选择的行为。例如 comparable 就是一个 mixin 接口,它允许类表明它的实例可以与其他可对比的类进行比较排序,由于抽象类不能被更新到现有的类中,所有抽象类不用由于定义 minin

  • 接口允许我们构建非层次结构的类型框架

    1
    2
    3
    4
    5
    6
    7
    public interface Singer{
    void sing();
    }

    public interface Songwriter{
    Song compose();
    }

    对一个既是歌唱家又是作曲家的对象,让该对象同时实现这两个接口即可,甚至可以再定义一个接口:

    1
    2
    3
    4
    public interface SingerSongwriter extends SingerSongwriter{
    void sing();
    Song compose();
    }

    如果是继承的话,这种情况就不太好处理

接口只能定义函数,不能包含函数的实现,但是这并不影响接口作为定义类型并提供实现上的帮助,这个时候就可以通过对导出的每个重要接口提供一个抽象的骨架实现类,把接口和抽象类的优点结合起来,接口还是用于定义类型,骨架实现类接管了所有与接口实现相关的工作

按照惯例,骨架实现类都被称为 AbstractInterface,其中 Interface 是指接口的名字,例如 AbstractSetAbstractList

使用骨架实现类有一个明显的优势就是:抽象类的演变比接口的演变要容易的多,如果后期在抽象类中添加新的函数,始终可以有默认实现,不用子类做任何改变。如果是接口,则所有实现该接口的子类就必须实现该函数,有了骨架实现类,只需要在骨架实现类中增加该函数即可,一定程度上减少了对结构的破坏

当演变的容易性比灵活性和功能更加重要的时候,这种情况下使用抽象类定义类型,前提是必须理解并且可以接受这些局限性。如果导出了一个重要的接口,就应该坚决考虑同时提供骨架实现类

接口只用于定义类型

有很多情况下都会定义一个常量接口这样的类,这种模式是对接口的不良使用。实现常量接口,对于客户端来说没有任何意义,而且在以后版本迭代中如果不需要这些常量时,依然必须实现这个接口,以确保兼容性

如果需要导出常量,推荐使用 枚举类型 或者不可实例化的 工具类 来导出

类层次优于标签类

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

这样一个标签类,即表示圆形也表示矩形,如果想要另外增加一个形状,就必须要修改源代码,而且多种形状的实现都混合在一个类中,极大破坏了可读性;实例所占用的内存也随之提高,因为它承担着属于其他形状的属性字段;而且实例本身也没有提供任何的关于形状的线索;标签类过于冗长,容易出错,效率低下

将标签类转化为类层次是一个很好的方式,首先需要定义一个包含抽象方法的抽象类,然后为每种标签类定义具体子类,并且每个子类中只包含该类型的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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; }
}

这样做有几个好处:

  • 具有较强的可读性,代码简单清晰
  • 每个子类不受到其他不相关数据的拖累
  • 所有属性都可以是 final
  • 每种类型都是相互独立的,具有独立的数据,允许程序员指明变量类型,限制变量
  • 可以用来反映类型之间本质上的层次关系,有助于增强灵活性,并进行更好的编译时类型检测

1
2
3
4
5
class Square extends Rectangle {
Square(double side) {
super(side, side);
}
}

这样就可以反映出来正方形也是一种特殊的矩形这一事实,尽量不使用标签类,用类层次代替

用函数对象表示策略

声明一个接口表示该策略,具体策略实现该接口,如何一个具体策略只被使用一次,通常使用匿名类来实例化。如果需要多次复用,通常要被实现为私有的静态成员类,并通过公有的静态 final 属性导出,导出类型为策略接口

优先考虑静态成员类

  • 非静态成员类默认持有外部类的引用,如果静态成员类生命周期比外部类长,就会造成内存泄漏
  • 嵌套类需要独立于外部类存在,必须是静态内部类
  • 如果嵌套类不访问外部类实例,需要把它变成静态内部类,否则每一个内部类实例都持有一个外部类的引用,浪费时间空间
《Effective Java》- 泛型 《Effective Java》- 对于所有对象都通用的方法

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×