《Effective Java》读书笔记 —— 类和接口
1.使类和成员的可访问性最小化
访问修饰符:
- private
- protected
- public
顶层的(非嵌套)类和接口,两种访问级别:
- 包级私有(package-private)
- public
成员(域、方法、嵌套类和嵌套接口)
- private:只有在声明该成员的顶层类内部才可以访问
- package-private:声明该成员的包内部的任何类都可以访问,是默认访问级别
- protected:在声明类和子类中可以访问
- public:任何地方可以访问
规则一:尽可能使每个类或者成员不被外界访问
如果一个包级私有的顶层类只是在某一个类的内部使用,就应该考虑使它成为唯一使用它的那个类的私有嵌套类。
规则二:如果方法覆盖了超类中第一个方法,子类中的访问级别就不允许低于超类的访问级别,确保任何可使用超类的地方都可以使用子类
规则三:接口中的所有方法都必须是public
规则四:实例域不能是公有的
如果域时非final的,或者是一个指向可变对象的final引用,那么一旦使这个域称为公有,就放弃了在这个域中的值进行限制的能力,也就放弃了这个域的不可变能力
包含公有可变域的类并不是线程安全的。
规则四:静态域不要是公有的(除了暴露静态常量)
要对外暴露静态域,必须是基本类型的值,或者是不可变对象。
长度非零的数组总是可变的,所以,类具有公有的静态final数组域,或者返回这种域的方法,总是不正确的。
以下错误:
public static final Tings[] VALUES = {...};
解决方案:公有数组私有化,并增加一个公有的不可变列表
private static final Tings[] VALUES = {...};
public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(VALUES));
或者:添加公有方法,返回私有数组的拷贝
private static final Tings[] VALUES = {...};
public static final Tings[] values() {
return VALUES.clone;
}
2.在公有类中使用访问方法而非公有域
公有类永远不要暴露可变的类。
3.使可变性最小化
不可变类:其实例不能被修改的类。具体来说,每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内固定不变。
Java 平台类库中的不可变类:String、基本类型的包装类、BigInteger、BigDecimal。不可变类对应配套的可变类:StringBuilder、BitSet。本应该是不可变,但却是可变的类:Date、Point。
不可变类的优点:
- 易于设计、实现和使用
- 不可变对象很简单、只有一种状态,即被创建时的状态
- 不容易出错、更加安全
- 线程安全,不要求同步,可以被自由的共享
不可变类的缺点:
- 在特定的情况下,存在潜在的性能问题,比如执行一个多步骤操作,每个步骤都会产生一个新的对,但除了最后的结果之外其他的对象最终都会被丢弃,就会有性能问题
- 所以应该使一些小的值对象成为不可变的
- 如果发生了性能问题,才应该为不可变的类提供公有的可变配套版
String对象不可变性的优缺点
- 字符串常量池的需要.
- 线程安全考虑
- 类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载
- 支持hash映射和缓存
使类成为不可变,遵循的规则:
- 不要提供任何会修改对象状态(属性)的方法
- 保证类不会被扩展,不会有子类,破坏该类的不可变行为
- 如果类可以被继承会破坏类的不可变性机制,只要继承类覆盖父类的方法并且继承类可以改变成员变量值,那么一旦子类以父类的形式出现时,不能保证当前类是否可变。使所有的域都是final的。
- 使所有的域(属性)都是final
- 使所有的域都是private,防止客户端获得访问被域引用的可变对象的权限,并防止客户端直接修改这些对象
- 确保对于任何可变组件的互斥访问
- 如果类具有指向可变对象的域,必须确保该类的客户端无法获得执行这些对象的引用
- 在构造器中,永远不要用客户端提供的对象引用来初始化这样的域
- 在访问方法中,也不要返回该对象引用
- 普通对象,直接new 一个新的对象
- 数组这类复杂对象,可使用 clone方法
- 在构造器、访问方法和 readObject 方法中请使用保护性拷贝技术
通过构造器初始化所有成员,构造器初始化成员时,需要进行深浅拷贝,如果构造器传入的对象直接赋值给成员变量,还是可以通过对传入对象的修改进而导致改变内部变量的值
public final class ImmutableDemo {
private final int[] myArray;
public ImmutableDemo(int[] array) {
this.myArray = array; // wrong
}
}
这种方式不能保证不可变性,myArray和array指向同一块内存地址,用户可以在ImmutableDemo之外通过修改array对象的值来改变myArray内部的值。
为了保证内部的值不被修改,可以采用深度copy来创建一个新内存保存传入的值。正确做法:
public final class MyImmutableDemo {
private final int[] myArray;
public MyImmutableDemo(int[] array) {
this.myArray = array.clone();
}
}
不可变类的设计
对于访问方法,一般会返回一个新的实例,而不是修改这个实例,大多数不可变类使用这种模式,称为函数的做法。
不可变的类一般会提供一些静态工厂,它们把频繁被请求的实例缓存起来,使客户端之间可以共享这些实例,而不用创建新的实例,降低内存占用和垃圾回收成本。所有基本类型的包装类和BigInteger都有这样的静态工厂。
不可变对象可以被自由共享,所以根本不需要做任何拷贝,因为拷贝始终等于原始的对象,所以不需要为不可变的类提供clone
方法或者拷贝构造器。
不仅可以共享不可变对象,也可以共享它们的内部信息。
不可变对象为其他对象提供了大量的构件。
有关序列化,如果让自己的不可变类实现序列化,就必须显式提供 readObject 或者 readResolve,否则反序列化可能会产生新的实例。
尽量使用不可变类,不要为每个get
方法编写一个相应的set
方法
举例
说明:这个类表示一个复数,加减运算都是返回一个新的实例,而不是在原来的实例上修改,称为函数的做法。
public final class Complex {
private final double re;
private final double im;
public Complex(double re, double im) {
this.re = re;
this.im = im;
}
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 sub(Complex c) {
return new Complex(re - c.re, im - c.im);
}
}
将构造函数改为私有的,并添加静态工厂来替代公有构造器
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);
}
...
}
4.复合优先于继承
与方法调用不同的是,继承打破了封装性。子类依赖于其超类中特定功能的实现细节。所以子类必须要跟着其超类的更新而演变,导致子类很脆弱。
继承
- 继承打破了封装性
- 父类内部细节对于子类是可见的,继承的代码复用是一种白盒式代码复用,如果基类的实现发生改变,那么派生类也将随之改变,导致子类的行为不可预知
- 只有两者存在is-a的关系,才使用继承,如果不是,则使用组合
组合
- 在新的类中增加一个私有域,它引用现有类的一个实例
- 继承必须在编译器确定继承哪个类,组合可以采用面向接口编程,类的组合关系可以在运行期确定
5.要么为继承而设计,并提供文档说明,要么就禁止继承
类的文档必须精确描述覆盖每个方法所带来影响,也就是说,覆盖的方法必须说明其自用性
类必须通过某种形式提供适当的钩子(hook),以便能够进入它的内部工作流程中,这种形式可以是精心选择的受保护的方法
6.接口优于抽象类
接口优点
- 现有的类可以很容易被更新,以实现新的接口
- 接口是定义mixin(混合类型)的理想选择
- 类不可能有一个以上的父类,类层次结构中也没有适当的地方插入mixin
- 接口允许我们构造非层次接口的类型框架
接口和抽象类区别
- 接口里不能定义静态方法;抽象类里可以定义静态方法。
- 接口里不包含构造器,抽象类可以包含构造器。抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。
- 接口里不能包含初始化块,但抽象类可以包含初始化块。
- 接口里不包含已经提供实现的方法,只能包含抽象方法,;抽象类则完全可以包含普通方法。
- 接口里只能定义静态常量,不能定义其他变量。抽象类既可以定义普通变量,也可以定义静态常量。
- 注意:在接口里定义的接口、枚举类、变量默认都采用public static两个修饰符,不管定义时是否指定这两个修饰符,系统都会自动使用public static对他们进行修饰,同理,在抽象类里,会默认使用public abstract修饰方法。
骨架实现类
虽然接口不允许包含默认实现,但是,可通过对你导出的每个重要接口都提供一个抽象的骨架实现类,把接口和抽象类的优点结合起来。接口的作用仍然是定义类型,但是骨架实现类接管了所有与接口实现相关的工作
在选择抽象类和接口时,并不是二选一的答案,或干脆枪毙掉抽象类。其实,你可以把接口和抽象类的优点结合起来,对于你希望导出(对外提供)的每一个重要接口都提供一个抽象类(骨架实现类)。接口的作用仍然是定义类型,骨架实现类负责所有与接口实现相关的工作
7.接口只用于定义类型
当类实现接口时,接口就充当可以引用这个类的实例的类型。类实现了接口,就表明客户端对这个类的实例实施某些动作。
接口应该只被用来定义类型,不应该被用来导出常量
接口用于导出常量
常量接口:只包含静态的final域。导出常量的一种形式。常量接口模式是对接口的不良使用。
缺点:
- 实现常量接口,会导致把这样的实现细节泄漏到该类导出的API中
- 如果非final类实现了常量接口,它的所有子类的命名空间也会被接口中的常量所”污染“。
常量接口的例子
public interface PhysicalConstants {
static final double AAA = 0.1;
static final double BBB = 0.1;
static final double CCC = 0.1;
}
导出常量的合理方案:
- 使用枚举类型导出
- 使用不可实例化的工具类导出
工具类的方式:
public class PhysicalConstants {
private PhysicalConstants() {};
static final double AAA = 0.1;
static final double BBB = 0.1;
static final double CCC = 0.1;
}
8.类层次优于标签类
有时,可能遇到带有两种甚至更多风格的实例的类,并包含表示实例风格的标签域。
下面例子,此类表示圆形或者矩形
demo标签类缺点:
- 充斥样板代码,包括枚举声明、标签域以及条件语句
- 破坏了可读性
- 内存占用也增加
- 实例承担着其他风格不相关的域
解决方案:子类化
定义一个包含抽象方法的抽象类,公共方法定义在抽象类
demo9.用函数对象表示策略(策略模式)
函数指针(引用)的主要用途是实现策略模式,在Java中实现策略模式,要声明一个接口来表示该策略,并且为每个具体策略声明一个实现了该接口的类。当一个具体策略只被使用一次时,通常使用匿名类来声明和实例化这个具体策略类,当一个具体策略是设计用来重复使用的时候,它的类通常就要被实现为私有的静态成员类,并且通过公有的静态final域被导出,其类型为该策略接口。
举例:比较器函数代表一种为元素排序的策略。
Java没有提供函数指针,可以用对象引用实现此功能。
比较器实例
StringLengthComparator 实例就是用于字符串长度比较的具体策略。
StringLengthComparator 是无状态的(没有域),所以单例比较合适。
class StringLengthComparator implements Comparator<String> {
private StringLengthComparator() {};
private static final StringLengthComparator INSTANCE = new StringLengthComparator();
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}
定义一个策略接口
public interface Comparator<T> {
public int compare(T t1, T t2);
}
使用比较策略
Arrays.sort(stringArray, new Comparator(String)(){
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
})
10.优先考虑静态成员类
嵌套类:被定义在另一个类的内部的类。嵌套类存在的目的只是为了它的外围类提供服务。
嵌套类包括:
- 静态成员类(内部类)
- 非静态成员类(内部类)
- 匿名类
- 局部类
静态成员类
最简单的一种嵌套类,可看作是普通的类,可以访问外围类的所有成员,包括私有成员。
公有静态成员类常见用法,是作为公有的辅助类,仅当与它的外部类一起使用时才有意义。
私有静态成员类常见用法,用来代表外围类所代表的对象的组件。例如,Map实例,Map的内部都有一个Entry对象,对应于Map的key和value。
非静态成员类(内部类)
必须和外围类的一个实例相关联,可以用this来访问
常见用法:Adapter
如果成员类不要求访问外围实例,就要声明成静态成员类,不然每个实例都会包含一个额外的指向外围对象的引用