EffectiveJava-3-类和接口

224 阅读9分钟

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

- 封装(信息隐藏):隐藏实现细节,将它的API与内部实现隔离,降低模块间的耦合性,易于独立开发,测试,优化等,提高软件的可重用性,

- 访问控制的方式:使用访问修饰符(private,public,protected)

- 尽可能的使每个类或者成员不能被外界访问

- 不能为了方便测试,而将类,接口或者成员变成包的导出API的一部分(即不要将权限大于package-private(default))

- 包含公有可变域的类不是线程安全的,即 public &&(非final || 引用可变对象),同样也适用于静态域
例如:

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

如果类可以在它所在的包外进行访问,就提供访问方法(getter)和设值方法(setter),以保留将来改变该类内部表示法的灵活性,而且方便对其添加限制条件

使可变性最小化

不可变类:其实例不能被修改,每个实例所有信息在创建实例时提供,并且在对象的整个生命周期内固定不变,如String,基本类型的包装类,BigInteger,BigDecimal,不可变类比可变类更易于设计,实现和使用,且更加不易出错,是线程安全,可以被自由的共享,充分利用现有实例;

使类不可变的五条规则:
1. 不提供任何修改对象状态的方法
2. 保证类不会被扩展:用final修饰或将所有构造方法私有化并提供公有的静态工厂方法
3. 使所有域都是final的(实际上只要没有方法能够对域产生外部可见的改变即可,如延迟初始化,懒汉单例等,都不能让该域是final的)
4. 使所有域都是私有的
5. 确保对于任何可变组件的互斥访问:对于指向可变对象的域,必须确保客户端无法获得指向这些对象的引用,并且永远不使用客户端提供的对象引用来初始化这样的域;在构造器,访问方法,readObject方法中使用保护性拷贝技术

如果一个类的安全性依赖于参数(来源于客户端)的不可变性,就必须进行检查,确保是真正的不可变类,而不是不可信任的子类实例,如果是后者,就必须进行保护性拷贝,例如:

除非有需要,不要为getter方法编写对应的setter方法,以保证类的不可变,getter方法可以返回保护性拷贝对象;

只有当确认有必要实现令人满意的性能时,才应该为不可变类提供公有的可变配套类,如String的可变配套类StringBuilder和StringBuffer;

构造器/静态工厂方法应该创建完全初始化的对象,并建立起所有的约束关系,不要再提供额外的公有初始化方法,或重新初始化方法;

复合优先于继承

继承是实现代码重用的有力手段,但并非永远是最佳方案(跨包边界的继承,可能处于不同程序员的控制下,是非常危险的,父类随着发行版本的不同可能发生变化,子类就有可能遭到破坏),例如下面代码中所提到的问题:

只有当A,B两个类之间存在"B is A"关系,且A类本身不存在缺陷时,才应该使用继承;

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

- 该类必须有文档说明它可覆盖方法的自用性,方法或构造器中调用了哪些可覆盖方法,每个调用如何影响后续处理过程

- 好的API文档应该描述一个给定方法做了什么工作,而不是描述它是如何做到的

- 可以使用受保护(protected)的方法或域,提供钩子(hook),以便子类能够获取更多的权限/功能

- 对于为了继承而设计的类,唯一的测试方法就是编写子类,以此查找遗漏,和判断是否应该私有,一般编写3个子类测试即可

- 构造器决不能调用可被覆盖的方法,无论直接或是间接(因为超类的构造方法先于子类构造方法执行,而可被覆盖的方法可能在子类重写时依赖子类构造方法初始化的数据,这时会导致执行该方法出现问题)(可将可覆盖方法代码体移到一个私有辅助方法中,可覆盖方法和构造器都调用这个私有辅助方法)

- 为继承而设计的类,最好不要实现Cloneable或Serializable接口,如果实现,请不要在clone或readObject方法中调用可覆盖的方法,原因同上

- 对于普通的具体类,若非为了安全的进行子类化而设计,要禁止其子类化(或者完全消除这个类中可覆盖方法的自用性,以确保安全的进行子类化)

接口优于抽象类

- 二者区别:

1. 抽象类允许包含某些方法的实现,接口则不允许

2. (重要)为了实现由抽象类定义的类型,类必须成为抽象类的一个子类,而java只允许单继承,使得抽象类作为类型定义收到类极大的限制

3. 抽象类的演变比接口容易的多,在后续发行中可以在抽象类中增加新的具体方法,并包含合理的默认实现,则该抽象类的所有实现都将具有这个新的方法,而接口则不行

- 现有类可以很容易被更新,以实现新的接口;而抽象类则需要放到类型层次的最高处,无论后续的其他子孙类是否需要

- 接口是定义mixin(混合类型)的理想选择,方便实现某些可供选择的行为

- 接口允许我们构造非层次结构的类型框架,如一个coder接口和一个gamer接口,然而一个人可能即是程序员又是游戏玩家

- 可以通过对重要接口提供一个抽象的骨架实现类,把接口和抽象类的优点结合起来,如常见的集合类中List接口和AbstractList抽象类等等(骨架实现类为例继承的目的而设计,有助于接口的实现,而又不是必须的)

- 接口一旦公开发行,并被广泛实现,再想改变这个接口几乎是不可能的,因此必须在设计之初就保证接口的正确性

接口只用于定义类型

- 当类实现接口时,接口就充当可以引用这个类的实例的类型,任何为了其他目的而定义的接口是不恰当的;

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

1.常量接口暴露了类的实现细节;
2.后续子类的命名空间会被接口中的常量污染

- 常量接口的替代方案:

1.添加到类或接口中;

2.使用枚举类型导出常量;

3.使用不可实例化的工具类

类层次优先于标签类

- 标签类:可以有多种风格的实例的类,并包含表示实例风格的标签域

- 这种标签类中充斥着样板代码,包含枚举或静态域标签,以及条件语句,破坏可读性和单一职责原则,过于冗长,容易出错,效率底下,占用内存增加;
- 如一个形状类通过一个type字段来判断要创建出的实例是矩形,圆,还是三角形;最好是创建一个形状抽象类,再分别实现代表矩形,圆,三角形的不同子类;

用函数对象表示策略 (策略模式)

函数对象:如果一个类仅仅执行这样一个方法,此方法执行其他对象(这些对象被显示对传递给这些方法)上的操作,它的实例实际上等同于一个指向该方法的指针,这样的实例成为函数对象,如下:

优先考虑静态成员类

- 嵌套类:指被定义在另一个类的内部的类,嵌套类存在的目的应该只是为它的外围类提供服务;
如果嵌套类将来可能用于其他某个环境,它就应该是顶层类;

- 嵌套类分为:静态成员类,非静态成员类,匿名类,和局部类,后三种都称为内部类

- 静态成员类是最简单的嵌套类,把它看作普通类即可,常见的用法:
1. 公有静态成员类:作为公有的辅助类,仅当与它的外部类一起使用才有意义
2. 私有静态成员类:用来代表外围类所代表对象的部分组件,如汽车的轮子,Map中的Entry代表一组键值对

- 静态成员类(独立)与非静态成员类(依赖)的唯一区别是:内部类实例是否依赖外围类的实例

- 匿名类:只需要在一个地方创建实例时使用,使用时被声明和实例化,可以出现在任何允许存在表达式的地方,应尽量简短(10行左右)以维持可读性,常见用法:

1. 动态的创建函数对象,如Comparator比较器;
2. 创建过程对象,如进行接口回调,Runnable,Thread等;
3. 用在静态工厂方法内部返回匿名内部类创建的实例;

- 局部类:用的最少,在任何可以声明局部变量的地方

我是今阳,如果想要进阶和了解更多的干货,欢迎关注公众号”今阳说“接收我的最新文章