EffectiveJava第四章:类和接口

第15条:使类和成员的可访问性最小化

对于成员(域、方法、嵌套类、嵌套接口)有四种级别,按递增顺序如下:

  1. 私有的(private):只有在声明该成员的顶层类内部才可以访问这个成员
  2. 包级私有的(package-private):声明该成员的包内部的任何类都可以访问这个成员。从技术上讲,它被称为“缺省”(default)访问级别,如果没有为成员指定访问修饰符,就采用这个访问级别(接口成员除外,它们的默认的访问级别是公有的)
  3. 受保护的(protected):声明该成员的类的子类可以访问这个成员(但有一些限制),并且声明该成员的包内部的任何类也可以访问这个成员
  4. 公有的(public):在任何地方都可以访问该成员

注意:

  1. 尽可能的使每个类或者成员不被外界访问
  2. 公有类的实例域决不能是公有的,类里面的属性必须是private,方法那些可以public。如果实例域是非final的或者是一个指向可变对象的final引用,一旦这个域成为公有的,就等于放弃了对存储在这个域中的值进行限制的能力;这意味着也放弃了强制这个域不可变的能力。同时,当这个域被修改的时候,也失去了对它采取任何行动的能力。因此,包含公有可变域的类通常并不是线程安全的
  3. 长度非0的数组总是可变的,所以让类具有公有的静态final数组域或者返回这种域的访问方法,都是错误的。如果类具有这样的域或者访问方法,客户端将能够修改数组中的内容,这是安全漏洞的常见来源:
public static final Thing[] VALUES = {...};

许多IDE产生的访问方法会返回指向私有数组域的引用,正好导致了这个问题。解决办法:

1、使公有数组变成私有的,并增加一个公有的不可变列表

private static final Thing[] PRIVATE_VALUES = {...};

public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

2、使数组变成私有的,并添加一个公有方法,它返回私有数组的一个拷贝

    private static final Thing[] PRIVATE_VALUES = {...};
    
    public static final Thing[] values(){
        return PRIVATE_VALUES.clone();
    }

 

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

  1. 如果类可以在它所在的包之外进行访问,就提供访问方法
  2. 如果类是包级私有的,或者是私有的嵌套类,直接暴露它的数据域并没有本质的错误
  3. 公有类永远都不应该暴露可变的域,尽量使用private的访问级别

 

第17条:使可变性最小化

为了使类成为不可变,遵循原则:

  1. 不要提供任何会修改对象状态的方法(也称设值方法)
  2. 保证类不会被扩展。这样可以防止粗心或者恶意的子类假装对象的状态已经改变,从而破坏该类的不可变行为。为了防止子类化,一般做法是声明这个类成为final的。
  3. 声明所有的域都是final的
  4. 声明所有的域都是私有的
  5. 确保对于任何可变组件的互斥访问

最简单的一点:类声明为final class,属性声明为private final thing;

 

不可变对象的优点:

  1. 不可变对象比较简单
  2. 不可变对象本质上是线程安全的,它们不要求同步。当多个线程并发访问这样的对象时,它们不会遭到破坏(线程安全)。所以,不可变对象可以被自由的共享,也可以共享它们的内部信息。
  3. 不可变对象为其他对象提供了大量的构件
  4. 不可变对象无偿的提供了失败原子性(失败的方法调用应该使对象保持在被调用之前的状态)。

缺点:不可变类对于每个不同的值都需要一个单独的对象。

建议:

  1. 尽量把类设为不可变的类
  2. 如果做不成不可变的类,那就尽量限制它的可变性
  3. 每个属性(域)都尽量做成private final的
  4. 构造器应该创建完全初始化的对象,并建立起所有的约束关系

 

第18条:复合优先于继承

1、只有当子类真正是超类的子类型,两个类A和B存在“is-a”的关系时,才适合用继承。与方法调用不同的是,继承打破了封装性。

2、所以使用复合的设计:不扩展现有的类,在新的类中增加一个私有域,它引用现有类的一个实例,因为现有的类变成了新类的一个组件。

3、转发:新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回它的结果,新类中的方法被称为转发方法。

这样得到的类会非常稳固,它不依赖于现有类的实现细节。即使现有的类添加了新的方法,也不会影响新的类。

简单点说:(这个理解应该是错误的,不理解这个复合)

1)原来的类构造器要有参数,参数为其本身

2)把原来的类作为新类的一个属性private final OldEntity oldEntity;

3)新类的构造器也要有参数,参数为原类

 

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

  1. 该类必须有文档说明它可覆盖(overridable)的方法的自用性
  2. 类必须以精心挑选的受保护的(protected)方法的形式,提供适当的钩子(hook),以便进入其内部工作中
  3. 对于为了继承而设计的类,唯一的测试方法就是编写子类,必须在发布类之前先编写子类对类进行测试
  4. 构造器决不能调用可被覆盖(@Override)的方法
  5. 无论是clone还是readObject,都不可以调用可覆盖的方法,不管是以直接还是间接的方式
  6. 为了继承而设计类,对这个类会有一些实质性的限制
  7. 对于那些并非为了安全的进行子类化而设计和编写文档的类,要禁止子类化

 

第20条:接口优于抽象类

  1. 现有的类可以很容易被更新,实现新的接口
  2. 接口是定义mixin(混合类型)的理想选择。mixin类型是指:类除了实现它的“基本类型”之外,还可以实现这个mixin类型,以表明它提供了某些可供选择的行为。Comparable是一个mixin接口,是因为它允许任选的功能可被混合到类型的主要功能中。抽象类不能被用于定义mixin是因为它们不能被更新到现有的类中:类不可能有一个以上的父类
  3. 接口允许构造非层次结构的类型框架,即定义新的接口继承多个旧的接口,来完成多个功能
  4. 因为包装类模式的存在,接口使得安全的增强类的功能成为可能
  5. 通过对接口提供一个抽象的骨架实现类(skeletal implementation),可以把接口和抽象类的优点结合起来。接口负责定义类型,或许还提供一些缺省方法,而骨架实现类则负责实现除基本类型接口方法之外,剩下的非基本类型接口方法。扩展骨架实现占了实现接口之外的大部分工作。这就是模板方法(Template Method)模式。

骨架实现类被称为AbstractInterface,这里的Interface是指所实现的接口的名字。而且注意要编写好的注释文档。

简单来说:骨架实现类就是实现了通用的方法,从而不需要定义多个接口。骨架实现类也不强加“抽象类被用作类型定义时”所持有的严格限制,从而不用特定的定义为abstract类。算是一种抽象类和接口的集大成的混合体

 

第21条:为后代设计接口

  1. Java类库的缺省方法是高品质的通用实现,它们在大多数情况下都能正常使用。缺省方法的声明中包括一个缺省实现(default implementation),这是给实现了 该接口但没有实现默认方法的所有类使用的。但是,并非每一个可能的实现的所有变体,始终都可以编写出一个缺省方法
  2. 有了缺省方法,接口的现有实现就不会出现编译时没有报错或警告,运行时却失败的情况
  3. 尽管缺省方法现在已经是Java平台的组成部分,但谨慎设计接口仍然是至关重要的
  4. 发布程序之前,测试每一个新的接口很重要。程序员应该以不同的方法实现每一个接口(至少三种实现)。编写多个客户端程序,利用每个新接口的实例来执行不同的任务也很重要,这有助于在接口发布之前及时发现其中的缺陷。

简单的说:缺省方法就是继承或实现接口之后,方法直接提供默认实现,不会报错、标红或者警告。

 

第22条:接口只用于定义类型

  1. 当类实现接口时,接口就充当可以引用这个类的实例的类型(type),因此类实现了接口,就表明客户端可以对这个类的实例实施某些动作
  2. 常量接口不包含任何方法,只包含静态的final域,每个域导出一个常量。使用这些常量的类实现这个接口,以避免用类名来修饰常量名
  3. 常量接口模式是对接口的不良使用。类在内部使用某些常量,这纯粹是实现细节。实现常量接口会导致把这样的实现细节泄露到该类的导出API中
  4. 如果要导出常量
  1. 如果这些常量与某个现有的类或者接口紧密相关,就应该把这些常量添加到这个类或者接口中
  2. 如果这些常量最好被看作枚举类型的成员,就应该用枚举类型。否则,应该使用不可实例化的工具类(utility class)来导出常量。
  3. 要导出的常量最好使用静态导入(static import)
  4. 如果导出常量是很长的一串数字,就每隔三位以下划线_分开。从Java7开始,下划线的使用已经合法,它对数字字面量的值没有影响

 

第23条:类层次优先于标签类

  1. 很多时候标签类过于冗长、容易出错,并且效率低下。因为标签类充斥着样板代码,包括枚举声明、标签域以及条件语句
  2. 子类型化(subtyping)表示多种风格对象的单个数据类型,标签类正是对类层次的一种简单的仿效
  3. 标签类很少有适用的时候。当你想要编写一个包含显示标签域的类时,应该考虑这个标签是否可以取消,这个类是否可以用类层次来代替。当你遇到一个包含标签域的现有类时,就要考虑将它重构到一个层次结构中去

简单的说:标签类就是专门用来定义某些对象的类,如果在标签类里面使用了太多的样板代码,就很容易出错,所以不推荐使用标签类。应该使用类层次来划分对象,多个对象公有的部分提取出来,私有的独立存在类自身

 

第24条:静态成员类优于非静态成员类

  1. 嵌套类(nested class)是指定义在另一个类的内部的类。嵌套类的目的应该只是为它的外围类(enclosing class)提供服务。如果嵌套类将来可能会用于其他的某个环境中,它就应该是顶层类(top-level class)
  2. 嵌套类有四种:静态成员类(static member class)、非静态成员类(nonstatic member class)、匿名类(anonymous class)和局部类(local class)。除了静态成员类,其他三种都称为内部类(inner class)
  3. 静态成员类是最简单的一种嵌套类。最好把它看作是普通类,只是碰巧被声明在另一个类的内部而已,它可以访问外围类的所有成员,包括那些声明为私有的成员。静态成员类是外围类的一个静态成员,与其他的静态成员一样,也遵守同样的可访问性规则。如果它被声明为私有的,它就只能在外围类的内部才可以被访问
  4. 静态成员类的一种常见用法是作为公有的辅助类,只有与它的外部类一起使用才有意义
  5. 从语法上讲,静态成员类和非静态成员类唯一的区别是静态成员类的声明中包含修饰符static。非静态成员类的每个实例都隐含的与外围类的一个外围实例(enclosing instance)相关联。在非静态成员类的实例方法内部,可以调用外围实例上的方法,或者利用修饰过的this(qualified this)构造获得外围实例的引用。如果嵌套类的实例可以在它外围类的实例之外独立存在,这个嵌套类就必须是静态成员类:在没有外围实例的情况下,要想创建非静态成员类的实例是不可能的
  6. 非静态成员类的实例被创建的时候,它和外围实例之间的关联关系也随之被建立起来,而且这种关联关系以后不能被修改。通常情况下,外围类的某个实例方法的内部调用非静态成员类的构造器也会自动创建这种关联关系。这种关联关系需要消耗非静态成员类实例的空间,并且会增加构造的时间开销
  7. 非静态成员类的另一种常见用法是定义一个Adapter,它允许外部类的实例被看作是另一个不相关的类的实例。如果声明成员类不要求访问外围实例,就要始终把修饰符static放在它的声明中,使它成为静态成员类,而不是非静态成员类。如果省略了static修饰符,则每个实例都将包含一个额外的指向外围对象的引用
  8. 私有静态成员类的常见用法是代表外围类所代表的对象的组件
  9. 匿名类没有名字,它不是外围类的一个成员,不与其他成员一起被声明,而是在使用的同时被声明和实例化。匿名类可以出现在代码中任何允许存在表达式的地方。当且仅当匿名类出现在非静态的环境中时,它才有外围实例。但是即使它们出现在静态的环境中,也不可能拥有任何静态成员,而是拥有常数变量,常数变量是final基本类型,或者被初始化成常量表达式的字符串域
  10. 匿名类是动态的创建小型函数对象和过程对象的最佳方式,或者将其用在静态工厂方法的内部(但是现在已经使用lambda代替)
  11. 在任何可以声明局部变量的地方,都可以声明局部类。局部类与成员类一样有名字,可以被重复使用。与匿名类一样只有当局部类是在非静态环境中定义的时候才有外围实例,它们也不能包含静态成员。与匿名类一样必须简短,以便不影响可读性

总的来说,如果一个嵌套类需要在单个方法之外仍然是可见的,或者它太长了,不适合放在方法内部,就应该使用成员类。如果成员类的每个实例都需要一个指向其外围实例的引用,就要把成员类做成非静态的,否则就做成静态的。

假设这个嵌套类属于一个方法的内部,如果你只需要在一个地方创建实例,并且已经有了一个预置的类型可以说明这个类的特征,就要把它做成匿名类,否则做成局部类

 

第25条:限制源文件为单个顶级类

  1. 如果有多个顶级类,就应该把它们做成静态成员类(包含在一个大类里面)
  2. 不要把多个顶级类或者接口放在一个源文件中,这样可以确保编译时一个类不会有多个定义,反过来也能确保编译产生的类文件以及程序结果的行为,都不会受到源文件被传给编译器时的顺序的影响
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值