博客
关于我
effectivejava第三章类和接口总结
阅读量:344 次
发布时间:2019-03-04

本文共 5520 字,大约阅读时间需要 18 分钟。

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

设计良好的组件会隐藏所有的实现细节,把API与实现清晰的分割开来,这个概念也叫做封装。封装可以解耦,使得维护,测试更加轻松。本节内容讲的就是 Java 的访问机制 (private, default, protected, public)。其中有几点建议值得学习:


(1)在同一个包中时,只有当另一个类确实需要访问某一个成员时,才应该删除 private 修饰符,使它变为包级私有。若这样的操作经常被执行,则可能该考虑重写设计此类了。严格的控制私有成员可以防止“API 泄露”。


(2)对于重写的方法,子类中该方法访问级别不允许低于父类中的级别(继承自接口的方法则必须声明为 public 的)。


(3)除公有静态 final 域(常量类)的情形外,共有类都不应该包含公有域,且要确保公有静态 final 域所引用的对象都是不可变的。如公有静态 final 数组域,就不能直接返回它的引用,因为这样客户将也能修改数组中的内容,应该像下面这样修改:

// 不好的做法
public static final String[] ARR = new String[]{"a", "b", "c"};
public static String[] getArr() {
return ARR;
}

//好的做法

public static final String[] ARR = new String[]{"a", "b", "c"};
public static String[] getArr() {
return new String[]{"a", "b", "c"};
}

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

这节说的是,我们不要直接在类中使用公有域,而是应该使用私有域但提供 getter, setter 方法。唯一例外的是,若类是包级私有的,或者是内部类,那么直接暴露它的数据倒也可以。


17、使可变性最小化

不可变类是指其实例不可修改的类,它们每个实例的信息在创建该实例的时候就提供了,并在整个对象的生命周期内固定不变。保证类是不可变的有以下5点条件:


(1)不要提供任何修改对象的方法(如 setter

(2)声明所有的域都是 final

(3)声明所有的域都为私有

(4)确保对任何可变组件的互斥访问(即确保客户端无法获取类中可变对象的域的引用)


18、复合优先于继承

这一节所说的继承是指某一个具体类继承另一个具体类,是涉及具体(实现)类之间的继承,而且是 public 的那种实现类之间的继承。那么这种继承可能引起子类父类方法冲突,因子类重写父类方法但考虑不全,导致出现逻辑错误等各种奇怪现象。为了解决这种问题,作者建议使用复合。

复合即不扩展现有类(就是不继承现有类),而是在新的类(开一个新类)中增加一个私有域,它引用现有类的一个实例。而新类的每个方法都可以调用被包含的现有类实例。举个栗子注意看下面两种情况:

第一种情况:直接继承 HashSet

public class HashSet extends HashSet {
public void addAll(Collection
c) {
addCount += c.size();
for (E e : c) {
add(e);
}
}
}

在这种情况下,当我们调用 addAll 方法时,不管是调用 HashSetaddAll 还是 HashSetaddAll,都会执行我们重写的 add 方法中的 addCount++,所以可能会导致 addCount 错误地增加。

第二种情况:使用转发类

public class ForwardingSet
implements Set
{
private final Set
set;
public ForwardingSet(Set
set) {
this.set = set;
}
@Override
public boolean add(T t) {
return set.add(t);
}
// 其他方法同样简单地转发到 `set` 实例
}

在这种情况下,当我们调用 add 方法时,直接调用转发类中的 set 实例的 add 方法,不会有任何问题。

总结:只有当子类和超类间确实存在子类型关系时,使用继承才是恰当的。即便如此,如果子类和超类处在不同的包中,并且超类并不是为了继承而设计的,那么继承将会导致脆弱性(可能产生 bug),因此可以用复合和转发机制代替继承,尤其是存在适当的接口可以实现包装类的时候。


19、要么设计要么继承并提供说明文档,要么禁止继承

对于普通的实现类,不要随意再继承它了。那么对于专门设计为继承的类(这里指专为继承而设计的“普通实现类”)而言,又有什么规矩呢?

(1)该类必须要对所有可覆盖方法进行说明,说明它在哪些地方被调用了,如果被修改后会对哪些结果有影响

(2)类必须以精心挑选的受保护的方法的形式,让它成为修改内部工作的入口

(3)对于为了继承而设计的类,唯一的测试方法就是编写子类

(4)为了允许继承,构造器绝不能调用可被覆盖的方法。因为超类的构造器在子类的构造器之前运行,所以,子类中覆盖版本的方法将会在子类的构造器运行之前先被调用。如果该覆盖版本的方法依赖于子类构造器所执行的任何初始化工作的话,该方法将不会如预期般执行。


20、接口优于抽象类

由于 Java 单继承的限制,使用接口更容易扩展,更加灵活。接口允许构造非层次结构的类型框架,就是说接口可以 extends 多个不同接口,然后组合成一个“混合接口”。但接口中不允许存在实例域或者非公有的静态成员。因此为了结合继承和接口的长处,可以为接口提供一个抽象的骨架实现类,接口负责定义类型,骨架实现类则负责实现除了基本类型接口方法之外,剩下的非基本类型接口方法。

总结:就是为接口提供一个骨架实现类(就是将接口先实现了一次的 abstract 类)之后就使用骨架实现类。


21、为后代设计接口

在 Java 8 之后,为接口中增加了 default 方法,这种方法子类不重写它,编译也不会报错。

示例

public interface MyInterface {
void doSomething();
default void doSomethingElse() {
System.out.println("默认方法执行");
}
}

但作者建议不要利用缺省方法在现有接口上添加新的方法,因为缺省方法的实现是否会破坏现有的接口实现,然而,在创建接口的时候,用缺省方法提供标准的方法实现是非常方便的。

总结:接口要经过良好的设计,测试,谨慎使用缺省方法。


22、接口只用于定义类型

不包含任何方法,只含静态 final 域的接口,称为常量接口。事实上常量接口是对接口的不良使用,因为在类的内部使用某些常量,这纯粹是实现细节,实现这样的常量接口会导致把这样的实现细节泄露到该类导出的 API 中,我们应该用工具类或枚举类型替代常量接口。

总结:工作中基本没怎么见过有人用常量接口,看来这一块的坑被潜意识的规避掉了哈。


23、类层次优于标签类

有时一个具体类可能带有两种风格,并包含表示实例风格的标签域。如下面这个例子:

public class Figure {
enum Shape { RECTANGLE, CIRCLE }
final Shape shape;
double length;
double width;
double radius;
Figure(double radius) {
shape = Shape.CIRCLE;
this.radius = radius;
}
Figure(double length, double width) {
shape = Shape.RECTANGLE;
this.length = length;
this.width = width;
}
double area() {
switch (shape) {
case CIRCLE:
return Math.PI * (radius * radius);
case RECTANGLE:
return length * width;
default:
throw new AssertionError(shape);
}
}
}

这个类中我们通过构造方法程序就可以知道当前是哪个图形,然后不同的图形有不同的功能实现。但这种标签类有许多缺点,不如这种类中充满很多样板代码,包括枚举声明,标签域及条件语句。这样多个实现都放在单个类中,破坏了可读性,而且内存占用也增加了。

为了解决以上问题,作者建议使用子类型化。请看下面的示例:

abstract class Figure2 {
abstract double area();
}
class Circle extends Figure2 {
final double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
double area() {
return Math.PI * (radius * radius);
}
}
class Rectangle extends Figure2 {
final double length;
final double width;
Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
@Override
double area() {
return length * width;
}
}

上面代码就实现了子类化,这种类就没有受到不相关数据域的拖累,而且它也反映类型之间本质上的层次关系,有助于增加灵活性,便于更好的进行编译时的类型检查。

总结:标签类很少有适用的时候,当你用到带有显式标签域的类时,可以思考是否可以优化它,用类层次来代替。当你遇到一个包含标签域的现有类时,就考虑将它重构到一个层次结构中去。


24、静态成员类优于非静态成员类

嵌套类是指定义在另一个类内部的类,它存在的目的应该只是为外围类提供服务。嵌套类有4种,静态成员类,非静态成员类,匿名类,局部类。除第一种外,其余三种也被叫做内部类。静态成员类:它可以看作普通的类,只是恰好被声明在了一个类的内部而已。它可以访问外部类的所以成员。它也当作是外部类的一个成员,与其他静态成员一样,也遵守可访问性规则。它的一种常见用法是作为公有的辅助类,只有与它的外部类一起使用才有意义。

非静态成员类:它隐含一个外部类实例,它的创建依赖于外部类。这种类在安卓中如 adapter, handler 等用到。需要注意的是它的内存泄露问题。

总而言之,如果一个嵌套类需要在某个函数外可见,或者它不太适合方在函数内部,那么可以将它做为成员类,而且除非它的实例需要用到外部类实例,就做成非静态的,否则就做成静态的。假设它属于一个函数的内部,且已有一个预置类型的类时,我们可以直接在方法中 new一个它的对象出来,这就是匿名类,而局部类就是给匿名类重新一个新名字(即用新类实现它)而已。


25、限制源文件为单个顶级类

虽然 Java 编译器允许在一个源文件中定义多个顶级类,但这么做并没有什么好处,反而会带来很多风险。因为在一个源文件中定义多个顶级类,可能导致给一个类提供多个定义,哪一个定义会被用到,取决于源文件被传递给编译器的顺序。举个例子就是:

public class A {
public void methodA() {}
}
public class B {
public void methodB() {}
}

从代码上看,在 main 方法中它们都是调用的 Utensil, Dessert 两个类中的 name,但如果你不注意看,这两个类是来自哪个源文件的话,就不知道到底是哪个源文件下的两个类。所以不要在单个源文件中定义多个顶级类。


结论

本章很多内容其实在日常开发中已经潜意识下就规避了,但了解一下前人踩坑的历史也是有好处的。

转载地址:http://lepe.baihongyu.com/

你可能感兴趣的文章
Netty源码解读
查看>>
Netty的Socket编程详解-搭建服务端与客户端并进行数据传输
查看>>
Netty相关
查看>>
Netty遇到TCP发送缓冲区满了 写半包操作该如何处理
查看>>
Netty:ChannelPipeline和ChannelHandler为什么会鬼混在一起?
查看>>
Netty:原理架构解析
查看>>
Network Dissection:Quantifying Interpretability of Deep Visual Representations(深层视觉表征的量化解释)
查看>>
Network Sniffer and Connection Analyzer
查看>>
Network 灰鸽宝典【目录】
查看>>
NetworkX系列教程(11)-graph和其他数据格式转换
查看>>
Networkx读取军械调查-ITN综合传输网络?/读取GML文件
查看>>
network小学习
查看>>
Netwox网络工具使用详解
查看>>
Net与Flex入门
查看>>
net包之IPConn
查看>>
Net操作配置文件(Web.config|App.config)通用类
查看>>
Neutron系列 : Neutron OVS OpenFlow 流表 和 L2 Population(7)
查看>>
New Relic——手机应用app开发达人的福利立即就到啦!
查看>>
NFinal学习笔记 02—NFinalBuild
查看>>
NFS
查看>>