类和接口相关

22 篇文章 0 订阅
18 篇文章 0 订阅

15. 使类可访问性最小化

尽可能降低程序元素的可访问性,尤其注意引用可变对象,尽量降低可访问性(尽管有时不容易),以保证线程安全性。
可变元素引用公有化合理方式是:

// 维护一个不可变长的备份
private static final Thing[] PRIVATE_VALUES = { ... };
public static final List<Thing> VALUES =
	Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
// 返回copy
private static final Thing[] PRIVATE_VALUES = { ... };
public static final Thing[] values() {
	return PRIVATE_VALUES.clone();
}

16. 隐藏公有域

共有类永远不要暴露可变的域,可以通过get和set方法暴露接口。内部类暴露有时候便于使用。

17. 使可变性最小化

对象不可变会带来很多好处,其中最显而易见的是一致性——既然无法修改其状态,那它就是一致的,安全的。
让类变为不可变对象(尽可能)并不是很容易,涉及到规范,优点,实现,最佳实践。

不可变对象的规则

  • 不要提供任何修改状态的方法。
  • 保证类不会被拓展。子类可以通过屏蔽的方式,提供可改变的状态,因而应该防止子类化。方法有两个,final类 or 私有化构造器。
  • 声明所有域为final和private,final确保本身不被篡改,private隐藏域,确保域引用的对象不被篡改。
  • 确保对于任何可变组件访问都是互斥的。需要返回内部域的时候,可以复制一份。

不过事实上,这是比较严格的要求,在实践中,我们事实上是:保证没有一个方法能够对对象产生外部可见的改变。举个例子,在计算hashCode的时候,为了效率,我们通常会在对象中缓存hashCode,虽然状态(hashCode)改变了,当却对外不可见。

不可变对象的好处

不可变对象的好处都有什么,为什么要尽可能使可变性最小化。

  • 简单。状态只有一个,通常在初始化之后就不需要额外的逻辑进行复杂维护。
  • 线程安全,不要求同步。关键在于,它的状态就不会改变,也就没有所谓的一致性问题。
  • 自由共享。还记得静态工厂中缓存优化吗,不可变方法因为不可变,所以可以安全的进行缓存共享的改进。
  • 不可变对象共享内部数据。
public BigInteger negate() {
    // mag为内部数组,不可变,可以在两个不同对象中共享
    return new BigInteger(this.mag, -this.signum);
}
  • 安全地作为其他对象的构件。作为其他类的组件的时候无需担心状态的问题
  • 无偿提供一致性,原子性。

性能问题

然后,事实上,不可变对象的唯一的问题是性能——每次状态改变都需要多一个copy。比如,你使用一个几百万位的大整数做大量运算的时候,就会生成大量的copy,影响性能,这非常好理解。

实现方式

// 声明final避免被子类化
public final class Complex {
    // final 和 private
    private final double re;
    private final double im;
    // 构造器中初始化状态
    public Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }
    // 返回copy
    public Complex plus(Complex c) {
        return new Complex(re + c.re, im + c.im);
    }
}

防止子类化,final class是一种方式,另一种更好的方式是静态工厂方法。直接私有化构造器防止子类化,静态工厂方法还可以提供灵活性,缓存优化等等。

public class Complex {
    private final double re;
    private final double im;
    private Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }
    public static Complex valueOf(double re, double im) {
        return new Complex(re, im);
    }
}

过于性能问题的解决思路。
方法一,如上述提到的大整数类大量运算的情况,可以通过基本类型对其中运算进行优化。
方法二,提供可变配套类,解决效率问题,尽管可变配套类的使用需要注意更多问题,但这些可以通过封装隐藏细节,提供安全性。典型的例子是String的可变配套类StringBuilder。

最佳实践

  • 除非有充足理由,否则类应该就是不可变的。我们也知道了,不可变类有诸多好处。
  • 除非有充足理由,否则域应该是private和final
  • 尽可能保证只在构造器中初始化状态,不要提供reset的方法,这种优化没有必要。例如CountDownLatch,它虽然是可变类(猜测是内部资源状态改变),但是它不提供任何重置资源的手段。

18. 复合优先于继承

继承是实现代码重用的强大手段,但是继承会破坏封装性,也会带来诸多坏处。理解“复合优先于继承”为什么更好,先来理解继承为什么不好。

继承的缺陷

  • 父类拓展容易破坏子类的规约。本质原因是子类对于父类的约束实际上仅仅通过覆盖来实现的,如果父类拓展了新方法子类未覆盖,则新方法可能破坏子类的规约。
  • 继承会暴露过多父类的细节。客户端可以通过父类引用直接访问内部细节。例如JDK,Stack继承于Vector,但这是不合理的,使用Vector引用我可以访问到Stack的底层数组。这一条实际上和第一条是一样的,这种方式可以破坏子类预设的规约。同时,如果父类API本身有缺陷,这些缺陷也会一并继承。
  • 覆盖和动态分派引起一些未预期的错误

正因为上述缺点,在大部分情况下,复合都要优先考虑,它更加健壮,灵活,功能也更加强大。但继承也不是完全不能使用:

只有当子类真正是超类的子类型时,才适用于继承,这种关系是is-a。反例就是Stack的实现,Stack并不是一种Vector,所以更好的实现方式是复合,而不是继承。

19. 为继承而设计的类的规则

特别地,编写为继承而设计的类的时候,需要注意:

  • 提供文档,说明调用了那些覆盖方法(自调用),会造成什么影响,有什么约束。特别是制定的规则和约束,需要通过大量测试确保周全,因为这意味这之后的所有子类都需要遵守
  • 构造器,clone,序列化方法,绝对不能调用可覆盖的方法。因为子类初始化之前会调用父类初始化,如果调用被子类覆盖的方法,有可能使用到子类未初始化的域,这时候会造成错误或异常。
  • 提供protected方法,为子类化提供合适的工具。有些实现不放心子类实现,也为了便于子类化,很多时候可以编写protected方法,给子类提供复用。protected的原因,猜测是限制 父类.method()这种情况,绕过子类的约束直接修改内部。

总而言之,如果不是有必要,那么少用继承,如果不是有必要,那么使用继承的时候不要调用任何可覆盖的方法,这可以保证避免自调用未来某天给程序带来“惊喜”。

20. 接口优于抽象类

由于Java是单继承的语言,所以理所当然,接口之于抽象类,体现的就是灵活性,同时回避继承的种种问题。

  • mixin是理想的选择。mixin是指类除了实现基本的父类外,实现mixin接口,混合使用,这通常是一种最佳实践。
  • 接口支持多继承,合并成大接口。
  • 接口其实可以避免组合爆炸的现象。——你可以利用组合来实现接口的功能,但是这样依赖,你不得不编写n个基本组件,以及可能形成2^n中组合方式,尽管这些类的功能,形式基本一样
  • 为重要的接口提供骨架类实现。接口用于定义基本方法,以及有时候可能提供基本实现。但这些通常无法满足基本父类的抽象实现,这时候可以编写抽象骨架类——编写抽象父类,实现接口中的可复用方法,或提供缺省方法。方便子类化

21. 接口缺省方法设计需要谨慎

JDK 1.8提供接口的默认方法的实现,方便了接口实现以及lambda函数式接口的实现。但这实现实际上是注入实现类中的,它在实现的时候并不知道实现类的情况,因而可能会在后期引起一些问题。

22. 接口只用于定义类型

主要是有些实现喜欢用接口来定义常量,导出常量。下面来分析一下这个问题:

// Constant interface antipattern - do not use!
public interface PhysicalConstants {
    // jdk 1.7之后允许数字字面量添加_,没有任何影响,增加可读性
    // Avogadro's number (1/mol)
    static final double AVOGADROS_NUMBER = 6.022_140_857e23;
    // Boltzmann constant (J/K)
    static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23;
    // Mass of the electron (kg)
    static final double ELECTRON_MASS = 9.109_383_56e-31;
}

常量接口的缺点

很多时候为什么好需要为什么不好来定义,所以知道为什么不好很重要。

  • 泄漏实现细节到导出API。实现常量接口,从而使用这些常量,但问题在于,内部使用什么东西都是细节实现问题,这种形式,使得这些实现细节都会泄漏到API中。
  • 它还代表一种承诺:即使未来的版本中不再需要使用这些常量,它依然需要实现这些老接口以保证兼容性(接口可以看作定义类型,不能随意取消)。

导出常量的最佳实践

  • 与紧密关联的类或接口绑定。例如Integer.MAX_VALUE
  • 使用枚举类型
  • 常量类要保证不可实例化,例如私有化构造器抛出异常。
  • 使用静态导入避免大量类名修饰常量。

23. 层次化优于标签类

应该使用层次化(继承)来取代标签类。所谓的标签类就是指:

// Tagged class - vastly inferior to a class hierarchy!
class Figure {
    enum Shape { RECTANGLE, CIRCLE };
    // Tag field - the shape of this figure
    final Shape shape;
    // These fields are used only if shape is RECTANGLE
    double length;
    double width;
    // This field is used only if shape is CIRCLE
    double radius;
}
  • 冗长,可读性差,内存浪费,效率低下
  • 容易出错

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

书中建议如果声明的成员类不要求访问外部类的成员,那么就应该始终把static加上。
主要还是理解为什么静态成员类优于非静态成员类:
核心原因只有一条,静态成员类没有指向外围类的引用。

  • 不必要的消耗。这种引用其实增加了不必要的消耗。这种有时候并不像看起来那么微弱,例如map中如果Entry的内部类声明使用非静态内部类,那么每个entry就都持有对外部类的引用,这是没有必要的。
  • 内存泄漏。更重要的是,它妨碍了外部类对象的回收,有可能造成内存泄漏。

https://www.yuque.com/kdlin/lioesk/zhr3x0

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

Java中允许在一个源文件中添加多个顶级类。
但因为不同源文件中可以存在重复的类名,这时候选择顺序是根据编译顺序决定的,这是我们无法接受的。所以建议一个源文件只写一个顶级类。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值