本文共 5520 字,大约阅读时间需要 18 分钟。
设计良好的组件会隐藏所有的实现细节,把API与实现清晰的分割开来,这个概念也叫做封装。封装可以解耦,使得维护,测试更加轻松。本节内容讲的就是 Java 的访问机制 (private, default, protected, public)。其中有几点建议值得学习:
private 修饰符,使它变为包级私有。若这样的操作经常被执行,则可能该考虑重写设计此类了。严格的控制私有成员可以防止“API 泄露”。public 的)。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"};} 这节说的是,我们不要直接在类中使用公有域,而是应该使用私有域但提供 getter, setter 方法。唯一例外的是,若类是包级私有的,或者是内部类,那么直接暴露它的数据倒也可以。
不可变类是指其实例不可修改的类,它们每个实例的信息在创建该实例的时候就提供了,并在整个对象的生命周期内固定不变。保证类是不可变的有以下5点条件:
setter)final 的这一节所说的继承是指某一个具体类继承另一个具体类,是涉及具体(实现)类之间的继承,而且是 public 的那种实现类之间的继承。那么这种继承可能引起子类父类方法冲突,因子类重写父类方法但考虑不全,导致出现逻辑错误等各种奇怪现象。为了解决这种问题,作者建议使用复合。
复合即不扩展现有类(就是不继承现有类),而是在新的类(开一个新类)中增加一个私有域,它引用现有类的一个实例。而新类的每个方法都可以调用被包含的现有类实例。举个栗子注意看下面两种情况:
HashSetpublic class HashSet extends HashSet { public void addAll(Collection c) { addCount += c.size(); for (E e : c) { add(e); } }} 在这种情况下,当我们调用 addAll 方法时,不管是调用 HashSet 的 addAll 还是 HashSet 的 addAll,都会执行我们重写的 add 方法中的 addCount++,所以可能会导致 addCount 错误地增加。
public class ForwardingSetimplements 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),因此可以用复合和转发机制代替继承,尤其是存在适当的接口可以实现包装类的时候。
对于普通的实现类,不要随意再继承它了。那么对于专门设计为继承的类(这里指专为继承而设计的“普通实现类”)而言,又有什么规矩呢?
由于 Java 单继承的限制,使用接口更容易扩展,更加灵活。接口允许构造非层次结构的类型框架,就是说接口可以 extends 多个不同接口,然后组合成一个“混合接口”。但接口中不允许存在实例域或者非公有的静态成员。因此为了结合继承和接口的长处,可以为接口提供一个抽象的骨架实现类,接口负责定义类型,骨架实现类则负责实现除了基本类型接口方法之外,剩下的非基本类型接口方法。
总结:就是为接口提供一个骨架实现类(就是将接口先实现了一次的 abstract 类)之后就使用骨架实现类。
在 Java 8 之后,为接口中增加了 default 方法,这种方法子类不重写它,编译也不会报错。
public interface MyInterface { void doSomething(); default void doSomethingElse() { System.out.println("默认方法执行"); }} 但作者建议不要利用缺省方法在现有接口上添加新的方法,因为缺省方法的实现是否会破坏现有的接口实现,然而,在创建接口的时候,用缺省方法提供标准的方法实现是非常方便的。
总结:接口要经过良好的设计,测试,谨慎使用缺省方法。
不包含任何方法,只含静态 final 域的接口,称为常量接口。事实上常量接口是对接口的不良使用,因为在类的内部使用某些常量,这纯粹是实现细节,实现这样的常量接口会导致把这样的实现细节泄露到该类导出的 API 中,我们应该用工具类或枚举类型替代常量接口。
总结:工作中基本没怎么见过有人用常量接口,看来这一块的坑被潜意识的规避掉了哈。
有时一个具体类可能带有两种风格,并包含表示实例风格的标签域。如下面这个例子:
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; }} 上面代码就实现了子类化,这种类就没有受到不相关数据域的拖累,而且它也反映类型之间本质上的层次关系,有助于增加灵活性,便于更好的进行编译时的类型检查。
总结:标签类很少有适用的时候,当你用到带有显式标签域的类时,可以思考是否可以优化它,用类层次来代替。当你遇到一个包含标签域的现有类时,就考虑将它重构到一个层次结构中去。
嵌套类是指定义在另一个类内部的类,它存在的目的应该只是为外围类提供服务。嵌套类有4种,静态成员类,非静态成员类,匿名类,局部类。除第一种外,其余三种也被叫做内部类。静态成员类:它可以看作普通的类,只是恰好被声明在了一个类的内部而已。它可以访问外部类的所以成员。它也当作是外部类的一个成员,与其他静态成员一样,也遵守可访问性规则。它的一种常见用法是作为公有的辅助类,只有与它的外部类一起使用才有意义。
非静态成员类:它隐含一个外部类实例,它的创建依赖于外部类。这种类在安卓中如 adapter, handler 等用到。需要注意的是它的内存泄露问题。
总而言之,如果一个嵌套类需要在某个函数外可见,或者它不太适合方在函数内部,那么可以将它做为成员类,而且除非它的实例需要用到外部类实例,就做成非静态的,否则就做成静态的。假设它属于一个函数的内部,且已有一个预置类型的类时,我们可以直接在方法中 new一个它的对象出来,这就是匿名类,而局部类就是给匿名类重新一个新名字(即用新类实现它)而已。
虽然 Java 编译器允许在一个源文件中定义多个顶级类,但这么做并没有什么好处,反而会带来很多风险。因为在一个源文件中定义多个顶级类,可能导致给一个类提供多个定义,哪一个定义会被用到,取决于源文件被传递给编译器的顺序。举个例子就是:
public class A { public void methodA() {}}public class B { public void methodB() {}} 从代码上看,在 main 方法中它们都是调用的 Utensil, Dessert 两个类中的 name,但如果你不注意看,这两个类是来自哪个源文件的话,就不知道到底是哪个源文件下的两个类。所以不要在单个源文件中定义多个顶级类。
本章很多内容其实在日常开发中已经潜意识下就规避了,但了解一下前人踩坑的历史也是有好处的。
转载地址:http://lepe.baihongyu.com/