《Effective Java》学习笔记(三)——类和接口

《Effective Java》学习笔记

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

信息隐藏或封装,可以有效地解除组成系统的各模块之间的耦合关系,使得这些模块可以独立地开发、测试、优化、使用、理解和修改。

Java程序设计语言提供了许多机制来协助信息隐藏。访问控制机制决定了类、接口和成员的可访问性。

第一规则:尽可能地使每个类或者成员不被外界访问。

对于顶层的(非嵌套的)类和接口,只有两种可能的访问级别:包级私有的(package-private)和公有的(public)。如果一个包级私有的的顶层类或者接口只是在某一个类的内部被用到,就应该考虑使它成为唯一使用它的那个类的私有嵌套类。

对于成员(域、方法、嵌套类和嵌套接口)有四种可能的访问级别,下面按照可访问性的递增顺序罗列出来:

  • 私有的(private)——只有在声明该成员的顶层类内部才可以访问这个成员;
  • 包级私有的(package-private)——声明该成员的包内部的任何类都可以访问这个成员。从技术上讲,它被称为“缺省(default)访问级别”,如果没有为成员指定访问修饰符,就采用这个访问级别;
  • 受保护的(protected)——声明该成员的类的子类可以访问这个成员,并且声明该成员的包内部的任何类也可以访问这个成员;
  • 公有的(public)——在任何地方都可以访问该成员。

对于公有类的成员,当访问级别从包级私有变成保护级别时,会大大增强可访问性。

如果方法覆盖了超类中的一个方法,子类中的访问级别就不允许低于超类中的访问级别。这样可以确保任何可使用超类的实例的地方也都可以使用子类的实例。一种特殊情形:如果一个类实现了一个接口,那么接口中所有的类方法在这个类中也都必须被声明为公有的。

实例域决不能是公有的。包含公有可变域的类并不是线程安全的。

静态域也同样决不能是公有的。只有一种例外情况:假设常量构成了类提供的整个抽象中的一部分,可以通过公有的静态final域来暴露这些常量。
长度非零的数组总是可变的,所以,类具有公有的静态final数组域,或者返回这种域的访问方法,这几乎总是错误的。

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

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

但是,如果类是包级私有的,或者是私有的嵌套类,直接暴露它的数据域并没有本质的错误。

让共有类直接暴露域从来都不是种好办法,但是如果域是不可变的,这种做法的危害就比较小一些。

使可变性最小化

不可变类只是其实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内固定不变。

为了使类成为不可变,要遵循下面五条规则:

  1. 不要提供任何会修改对象状态的方法(也成为mutator)。
  2. 保证类不会被扩展。为了防止子类化,一般做法是使这个类成为final的。
  3. 使所有的域都是final的。
  4. 使所有的域都成为私有的。
  5. 确保对于任何可变组件的互斥访问。

不可变对象本质上是线程安全的,它们不要求同步。不可变对象可以被自由地共享。

不可变的类可以提供一些静态工厂,它们把频繁被请求的实例缓存起来,从而当现有实例可以符合请求的时候,就不必创建新的实例,从而降低内存占用和垃圾回收的成本。

“不可变对象可以被自由地共享”导致的结果是,永远也不需要进行保护性拷贝。实际上根本无需做任何拷贝,因为这些拷贝始终等于原始的对象。

不仅可以共享不可变对象,甚至可以共享它们的内部信息。

不可变对象为其他对象提供了大量的构件(building blocks),无论是可变的还是不可变的对象。

不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象。

如果你执行一个多步骤的操作,并且每个步骤都会产生一个新的对象,除了最后的结果之外其他的对象最终都会被丢弃,此时性能问题就会显露出来。如果能够精确地预测出客户端将要在不可变的类上执行哪些复杂的多阶段操作,这种包级私有的可变配套类的方法就可以工作的很好;如果无法预测,最好的办法就是提供一个公有的可变配套了。在Java平台类库中,这种方法的主要例子是String类,它的可变配套类是StringBuilder。

让不可变的类变成final的另一种方法是,让类的所有构造器都变成私有的或者包级私有的,并添加公有的静态工厂来代替公有的构造器。

坚决不要为每个get方法编写一个相应的set方法,除非有很好的理由要让类成为可变的类,否则就应该是不可变的。

如果类不能被做成不可变的,仍然应该尽可能地限制它的可变性。除非有令人信服的理由要使域变成非final的,否则要使每个域都是final的。

复合优先于继承

在包的内部使用继承是非常安全的,在那里,子类和超类的实现都处在同一个程序员的控制之下。

与方法调用不同的是,继承打破了封装性。

只有当子类和超类之间确实存在子类型关系时,使用继承才是恰当的。即便如此,如果子类和超类处在不同的包中,并且超类并不是为了继承而设计的,那么继承将会导致脆弱性。为了避免这种脆弱性,可以用复合和转发机制来代替继承。

不用扩展现有的类,而是在新的类中增加一个私有域,它引用现有类的一个实例。这种设计被称作“复合(composition)”,因为现有的类变成了新类的一个组件。新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回它的结果,这被称为转发(forwarding),新类中的方法被称为转发方法。——这也正是Decorator模式。

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

对于专门为了继承而设计并且具有良好文档说明的类而言,该类的文档必须精确地描述覆盖每个方法所带来的影响。该类必须有文档说明它可覆盖的自用性。

关于程序文档有句格言:好的API文档应该描述一个给定的方法做了一个什么工作,而不是描述它是如何做到的。

为了继承而进行的设计不仅仅涉及自用模式的文档设计。为了使程序员能够编写出更加有效的子类,而无需承受不必要的痛苦,类必须通过某种形式提供适当的钩子(hook),以便能够进入到它的内部工作流程中,这种形式可以是精心选择的受保护的方法,也可以是受保护的域,后者比较少见。

对于为了继承而设计的类,唯一的测试方法就是编写子类。如果遗漏了关键的受保护成员,尝试编写子类就会使遗漏所带来的痛苦变得更加明显。

为了允许继承,类还必须遵守其他一些约束:构造器决不能调用可被覆盖的方法,无论是直接调用还是间接调用。

如果你决定在一个为了继承而设计的类中实现Serializable,并且该类有一个readResolve或者writeReplace方法,就必须是readResolve或者writeReplace成为受保护的方法,而不是私有的方法。

对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化。

接口优于抽象类

Java程序设计语言提供了两种机制,可以用来定义允许多个实现的类型:接口和抽象类。这两种机制之间最明显的区别在于,抽象类允许包含某些方法的实现,但是接口则不允许。因为Java只允许单继承,所以抽象类作为类型定义收到了极大的限制。

现有的类可以很容易被更新,以实现新的接口。

接口是定义mixin(混合类型)的理想选择。

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

通过包装类模式,接口使得安全地增强类的功能成为可能。

通过对你导出的每个重要接口都提供一个抽象的骨架实现类(AbstractInterface),把接口和抽象类的优点结合起来。接口的作用仍然是定义类型,但是骨架实现类接管了所有与接口实现相关的工作。

设计公有的接口要非常谨慎。接口一旦被公开发行,并且已被广泛实现,再想改变这个接口几乎是不可能的。

接口只用于定义类型

当类实现接口时,接口就充当可以引用这个类的实例的类型。因此类实现了接口,就表明客户端可以对这个类的实例实施某些动作。为了任何其他目的而定义接口是不恰当的。

常量接口:这种接口没有包含任何方法,只包含静态的final域,每个域导出一个常量。使用这些常量的类实现这个接口,以避免用类名来修饰常量名。常量接口模式是对接口的不良使用。

如果要导出常量:如果这些常量与某个现有的类库或者接口紧密相关,就应该把这些常量添加到这个类或者接口中;如果这些常量是最好被看作枚举类型的成员,就应该用枚举类型来导出这些常量;否则,应该使用不可实例化的工具类来导出这些常量。

工具类通常要求客户端要用类名来修饰这些常量名,如果大量利用工具类导出的常量,可以通过利用静态导入机制,避免用类名来修饰常量名。

类层次优于标签类

有时候,可能会遇到带有两种甚至更多种风格的实例的类,并包含表示实例风格的标签域。但是标签类过于冗长、容易出错,并且效率低下。

Java提供了其他更好的方法来定义能表示多种风格对象的单个数据类型:子类型化。标签类是类层次的一种简单的仿效。

为了将标签类转变成类层次,首先要为标签类中的每个方法都定义一个包含抽象方法的抽象类,这每个方法的行为都依赖于标签值。接下来,为每种原始标签类都定义根类的具体子类,在每个子类中都包含特定于该类型的数据域,同时在每个子类中还包括针对根类中每个抽象方法的相应实现。

类层次的另一种好处在于,它们可以用来反映类型之间本质上的层次关系,有助于增强灵活性,并进行更好的编译时类型检查。

用函数对象表示策略

Java没有提供函数指针,但是可以用对象引用实现同样的功能。调用对象上的方法通常是执行该对象上的某些操作。然而,我们也可能定义这样一种对象,它的方法执行其他对象(这些对象被显式传递给这些方法)上的操作。如果一个类仅仅导出这样的一个方法,它的实例实际上就等同于一个指向该方法的指针。这样的实例被称为函数对象。

函数指针的主要用途就是实现策略模式。为了在Java中实现这种模式,要声明一个接口来表示该策略,并且为每个具体策略声明一个实现了该接口的类。当一个具体策略只被使用一次时,通常使用匿名类来声明和实例化这个具体策略类。当一个具体策略是设计用来重复使用的时候,它的类通常就要被实现为私有的静态成员类,并通过公有的静态final域被导出,其类型为该策略接口。

优先考虑静态成员类

嵌套类是指被定义在另一个类内部的类。嵌套类存在的目的应该只是为它的外围类提供服务。如果嵌套类将来可能会用于其他的某个环境中,它就应该是顶层类。嵌套类有四种:静态成员类、非静态成员类、匿名类和局部类。除了第一种之外,其他三种都被称为内部类。

静态成员类是最简单的一种嵌套类。最好把它看作是普通的类,只是碰巧被声明在另一个类的内部而已,它可以访问外围类的所有成员,包括那些声明为私有的成员。静态成员类是外围类的一个静态成员,与其他的静态成员一样,也遵守同样的可访问性规则。如果它被声明为私有的,它就只能在外围类的内部才可以被访问。

静态成员类的一种常见用法是作为公有的辅助类,仅当与它的外部类一起使用时才有意义。

从语法上讲,静态成员类和非静态成员类之间唯一的区别是,静态成员类的声明中包含修饰符static。非静态成员类的每个实例都隐含着与外围类的一个外围实例相关联。在非静态成员类的实例方法内部,可以调用外围实例上的方法,或者利用修饰过的this构造获得外围实例的引用。如果嵌套类的实例可以在它外围类的实例之外独立存在,这个嵌套类就必须是静态成员类:在没有外围实例的情况下,要想创建非静态成员类的实例是不可能的。

非静态成员类的一种常见用法是定义一个Adapter,它允许外部类的实例被看作是另一个不相关的类的实例。

如果声明成员类不要求访问外围实例,就要始终把static修饰符放在它的声明中,使它成为静态成员类,而不是非静态成员类。如果省略了static修饰符,则每个实例都将包含一个额外的指向外围对象的引用。

私有静态成员类的一种常见用法是用来代表外围类所代表的对象的组件。

匿名类没有名字,不是外围类的一个成员,并不与其他的成员一起被生命,而是在使用的同时被声明和实例化。匿名类可以出现在代码中任何允许存在表达式的地方。当且仅当匿名类出现在非静态的环境中时,它才有外围实例。

匿名类的一种常见用法是动态地创建函数对象。匿名类的另一种常见用法是创建过程对象,比如Runnable、Thread或者TimerTask实例。第三种常见的用法是在静态工厂方法的内部。