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

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

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

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

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

Java接口使用实现类的变量 java接口实现类调用_经验分享

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

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

17、使可变性最小化

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

(1)不要提供任何修改对象的方法(如setter)
(2)保证类不会被扩展(将类声明为final)
(3)声明所有的域都是final的
(4)声明所有的域都为私有
(5)确保对任何可变组件的互斥访问(即确保客户端无法获取类中可变对象的域的引用)

18、复合优先于继承

这一节所说的继承是指某一个具体类继承另一个具体类,是涉及具体(实现)类之间的继承,而且是public的那种实现类之间的继承。那么这种继承可能引起子类父类方法冲突,因子类重写父类方法但考虑不全,导致出现逻辑错误等各种奇怪现象。为了解决这种问题,作者建议使用复合。复合即不扩展现有类(就是不继承现有类),而是在新的类(开一个新类)中增加一个私有域,它引用现有类的一个实例。而新类的每个方法都可以调用被包含的现有类实例。举个栗子注意看下面两种情况:

Java接口使用实现类的变量 java接口实现类调用_Java接口使用实现类的变量_02


这种情况下,我们继承的是hashset实现类,那么当我们调用addall后,假如添加了3个元素,但我们调用getaddcount方法时,得到的结果却是6。因为addall方法中调的也是add方法,所有会执行我们重写的add方法中的addcount++,故这样的继承有潜在隐患。

Java接口使用实现类的变量 java接口实现类调用_java_03


Java接口使用实现类的变量 java接口实现类调用_java_04


这个情况下,我们使用forwardingset先包装了一层,然后里面获取倒set的实例,里面重写的方法直接调用set对象中相应的方法,这种类也叫做”转发类“。以后我们自己的类都继承自转发类就行了。仔细看情况二现有类中的addall和add方法,它们同样对addCount做了加一操作。但是你调用InstrumentedSet中的add all方法时,却不会出现getaddCount返回6的结果, 原因就是addall中,调用的super.addall方法里,调的是转发类中的相应方法,而转发类又将这个操作转给set本身来做,所有就没有问题啦。

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

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

第18条大概说的意思是,对于普通的实现类,不要随意再继承它了。那么对于专门设计为继承的类(这里指专为继承而设计的“普通实现类”)而言,又有什么规矩呢?
(1)该类必须要对所有可覆盖方法进行说明,说明它在哪些地方被调用了,如果被修改后会对哪些结果有影响
(2)类必须以精心挑选的受保护的方法的形式,让它成为修改内部工作的入口
(3)对于为了继承而设计的类,唯一的测试方法就是编写子类
(4)为了允许继承,构造器绝不能调用可被覆盖的方法。因为超类的构造器在子类的构造器之前运行,所以,子类中覆盖版本的方法将会在子类的构造器运行之前先被调用。如果该覆盖版本的方法依赖于子类构造器所执行的任何初始化工作的话,该方法将不会如预期般执行。

总结:本节传达的信息是尽量不要对普通的具体类进行子类化,否则就要考虑以上几个条件。简言之,如果子类依赖了超类的实现细节,如果超类的实现发生了变化,它就有可能遭到破坏。除非真正需要子类,否则最好将类声明为final,或者确保它没有可访问的构造器来禁止类被继承。

20、接口优于抽象类

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

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

21、为后代设计接口

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

Java接口使用实现类的变量 java接口实现类调用_Java接口使用实现类的变量_05


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

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

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 witdh;
    Rectangle(double length,double witdh){
        this.length = length;
        this.witdh = witdh;
    }
    @Override
    double area() {
        return length*witdh;
    }
}

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

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

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

嵌套类是指定义在另一个类内部的类,它存在的目的应该只是为外围类提供服务。嵌套类有4种,静态成员类,非静态成员类,匿名类,局部类。除第一种外,其余三种也被叫做内部类。静态成员类:它可以看作普通的类,只是恰好被声明在了一个类的内部而已。它可以访问外部类的所以成员。它也当作是外部类的一个成员,与其他静态成员一样,也遵守可访问性规则。它的一种常见用法是作为公有的辅助类,只有与它的外部类一起使用才有意义。非静态成员类:它隐含一个外部类实例,它的创建依赖于外部类。这种类在安卓中如adapter,handler等用到。需要注意的是它的内存泄露问题。

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

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

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

Java接口使用实现类的变量 java接口实现类调用_编程语言_06


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

结论:算是总基本常识,不过对于当年的开发者而言,这也算探索路上学习到的经验教训吧。

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