【读书笔记】《Effective Java》(3)-- 类和接口

这一章的笔记时隔略久,因为开学了,有事情要做了。现在正值中秋佳节,放假了,有时间补完这篇笔记。
现在正在看下一章——泛型,发现难度更高一点,估计更新下一篇读书笔记的时间会更长。

类和接口

13. 使类和成员的访问性最小化

  • 这样做的原因:

    1. 信息隐藏:将内部数据和其他实现细节隐藏,有助于模块之间解耦,加快开发速度、让程序员专注于自己的代码
    2. 公有和受保护的成员将会是导出API的一部分,这意味着该类对于外界的承诺,保证这些成员的实现不会改变,这对于编写来说,亦是负担
  • 规则:

    1. 顶层的类或者接口,只有公有和包级私有两种可能的访问级别,能将其做成包级私有的就不要公有
    2. 如果一个包级私有的顶层类只在某一个类的内部被用到,就应该考虑将其变成这个类的私有内部类
    3. 实例域决不能是公有的,这让类放弃了对实例域的管理能力,失去了改变内部数据表示方法的灵活性,同时包含公有可变域的类不是线程安全的,这一条也适用于公有静态可变域
    4. 对于final域,公有亦使它失去了改变内部数据表示方法的灵活性
  • 注意点:

    1. 如果类实现了Serializable接口,那么私有成员、保护成员也有可能被泄露到导出的API(参考以后序列化部分修改)
    2. 如果不可变域final所包含的是可变对象的引用,那么它就有和可变域一样的缺点,虽然引用本身不能改变,但引用的对象却可以任意改变
    3. 不要为了测试,而将类、接口的访问级别降低
    4. 长度为0的数组域总是可变的,基于第二条,类具有公有的静态final数组域,或者返回这种域的访问方法几乎总是错误的做法,有两种改进的方法:

      (以下面这段代码为例)

          public static final Thing[] VALUES={...};

      a. 将公有静态final数组改为私有,并且增加一个公有的不可变列表:

          private static final Thing[] PRIVATE_VALUES={...};
          public static final List<Thing> VALUES=
              Collections.unmodifiableList(Array.asList(PRIVATE_VALUES));

      b. 将公有数组改为私有,并增加一个返回数组拷贝的公有方法

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

14. 在公有类中使用访问方法而不是公有域

  • 除非是包级私有或者嵌套的私有类,否则即便是简单的方法都要封装起来,使用getter、setter等方法访问,而不是直接暴露公有域
  • Java平台违反该建议的类:java.awt下的Point和Dimension类

15. 使可变性最小化

  • 使类成为不可变的,有以下规则需要遵守

    1. 不要提供任何会修改对象状态的方法
    2. 保证类不会被扩展:方法是把类做成final,或者使用私有构造器配以公有的静态工厂
    3. 使所有的域都为final(关于这一条限制,在实际编写代码过程中为了提高性能可以放松,改为 没有一个方法可以产生外部可见的更改)
    4. 使所有的域都是私有的:防止客户代码直接修改对象的可变域,同时不建议将可变域设为公有final的,这样放弃了以后更改对象内部表示的灵活性
    5. 确保对于任何可变组建的互斥访问:不用客户代码的可变对象引用初始化对象的域,不要从任何方法返回不可变对象内部域的引用;在构造器、访问方法、readObject方法使用保护性拷贝
  • 不可变对象的优点:

    1. 线程安全,不要求被同步,可以被自由地共享
    2. 不需要保护性拷贝,不需要clone方法或者拷贝构造器
    3. 不仅对象自己可以共享,对象内部的数据也可以共享。eg:BigInteger类内部使用数组实现数值的表示,negate方法会产生一个它的相反数,这个相反数的数值部分就是共享的原对象内部的数组
    4. 不可变对象为其他对象提供了大量的构件:维护一个复杂对象内部的不可变的对象组件要不可变组件容易许多
  • 不可变对象的缺点:

    1. 对于每一个不同的值都需要一个单独的对象:这对于一个大型对象来说代价会比较高

      对于这个缺点,一个解决方法是引入一个包级私有的可变配套类,用来加速复杂的多级操作

      在不能预测客户代码会如何使用不可变类,进而不能提供包级私有的配套类的时候,可以提供公有的可变配套类,比如String类就有StringBuilder(以及基本已经废弃的StringBuffer)作为配套类,BigSet在一定程度上也可以认为是BigInteger的可变配套类。

  • Java平台不遵守该建议的类:

    1. String类作为不可变类依旧提供了拷贝构造器,应当尽量少使用它
    2. BigInteger和BigDecimal在设计之初没有想到要把类的域设计成final的,这在以后版本中为了兼容没有修改回来
  • 注意点:

    1. 如果不可变类要实现序列化接口Serializable,并且这个类本身包含一个或多个指向可变对象的域,那么必须编写显式的readObject或者readResolve方法,或者调用时使用ObjectOutputStream.writeUnshared和ObjectInputStream.readUnshared方法,否则,攻击者可以从不可变类中创建可变的实例
    2. 不要为不可变类提供构造器和静态工厂之外的公有初始化方法,或者重新初始化方法(让类回归新创建时状态的方法),这会破坏不可变类的初衷,同时与增加的复杂性相比,重新初始化方法并没有带来更多的性能优势
    3. 不要为每一个getter编写对应的setter

16. 复合优先于继承

  • (1)在包的内部使用继承是安全的,因为这时子类和超类都在同一个程序员的控制之下;(2)对于专门为了继承而设计、并且有良好文档说明的类,继承也是安全的;(3)但是,对于普通的具体类进行跨越包边界的集成则是危险的

  • 继承的缺点:

    1. 继承打破了封装:子类需要依靠超类的实现细节,而超类的实现可能会随着时间而变化,这会使子类遭到破坏
    2. 超类可能会在后续版本中新增方法,轻易打破子类自定义的规则
    3. 超类可能会在后续版本中增加方法,这些方法可能会与子类自定义的方法重名(这让我想起了JS一些失策的扩展库)
  • 使用复合替代继承:使用私有域引用一个需要继承的类的实例(包装起来,这被称为Decorator模式)

    1. 复合扩展接口之后,可以将任何这个接口的类包装起来,而继承只能扩展某个具体的单个类
    2. 复合类(又称包装类)内部调用需要继承的类的方法并返回结果,这被称为转发
  • 注意点:

    1. 包装类不适合于回调框架,因为回调框架需要将自身的引用传给其他对象,而这个“自身”并不知道自己外部有包装类,这就导致了包装类功能的失效
    2. 只有当子类真的是父类的子类型时(is-a),再使用继承
  • Java平台没有遵守该建议的类:

    1. Stack扩展了Vector,这并不合适
    2. Properties扩展了HashTable,这导致了一些问题:Properties设计者希望客户代码只设置字符串作为键和值,但是通过调用超类的set可以设置非字符串,这违反了使用这个类的约定

17. 要么为继承而设计,并提供文档说明,要么就禁止继承

  • 专为继承设计的类必须精确描述覆盖每个方法所带来的影响,即说明它的可覆盖的方法的自用性,什么情况下他将会调用那些可能会被覆盖的方法

    尽管这违反了“好的API应该描述要给方法做了什么工作,而不是如何做到的”这一理念,但是为了类可以被安全的子类话,这是必须的

  • 为了让程序员设计更有效的子类,专为继承而设计的类必须通过某种形式提供合适的钩子(hook),以便能够进入这种专为继承而设计的类的内部工作流程,这种钩子一般是受保护的方法或者受保护的域。

    以java.util.AbstractList的removeRange,这个方法对于List的最终客户代码而言没有意义,但对于继承这个类的子类而言,这个方法提供了改善子列表快速clear的方法。

  • 注意:

    1. 在构造器中绝对不要调用可能会被覆盖的方法
    2. 对于专为继承而设计的类,唯一的测试方法就是编写子类
    3. 如果专为继承而设计的类需要扩展Cloneable和Serializable接口的话,需要特别注意,clone以及readObject方法行为上和构造器类似,所以这两个方法也不能调用可能会被覆盖的方法
    4. 如果要实现Serializable接口,且类拥有readResolve或者writeReplace方法,则这两个方法需要设置为protected的,设成private的话子类会忽略重写它们,这是“为了继承将内部实现变为类的API的一部分”这一不好的行为的体现
    5. 对于普通类而言,应尽可能不继承它们,如果一定要继承具体的类的话,一定要确保这个具体类内部不会调用任何可能被覆盖的类并在文档中说明,一个解决方法是将一定要调用的方法集中改写为私有的方法域,然后在可能被覆盖的方法中调用这些私有方法

18. 接口优先于抽象类

  • 接口和继承的区别:

    1. 继承可以包含某些方法的实现,而接口只提供方法签名
    2. 为了实现继承,类必须成为某一个抽象类的子类,而扩展接口只要实现方法,并且遵守通用约定就可以。
    3. Java只允许单继承,这使得抽象类最为类型定义受到了很大的限制
  • 接口的优点:

    1. 现有类可以很容易地被更新,以实现新的接口:扩展接口只要增加接口定义的方法即可,而继承抽象类不能更新现有的类扩展新的抽象类;如果有多个类要扩展一个抽象类的话,需要将这个抽象类放到类层次的高层,这会导致以后所有后代类都扩展这个类
    2. 接口是定义混合类型(mixin)的理想选择:接口可以允许可选的功能混入类型的主要功能中,而因为抽象类是单继承的,这样是不行的
    3. 接口允许我们构建非层次结构的类型框架:如果使用继承可能会导致一些情况下的“类臃肿”
  • 接口的缺点:

    1. 针对优点的第一条:接口一旦公布,再次增删方法就变得很困难了。接口一旦增加方法,现有类如果不做改变,将会面临在此编译不通过的问题,而抽象类新增一个非抽象方法的话,它的子类不会受到任何影响。
    2. 接口虽然更加灵活,但是更不易更改,即不易演变,接口一旦公布,并且被广泛使用,在想改变这个接口几乎是不可能的。
  • 接口和继承混合,结合两者的优点:

    • 使用接口定义方法,并且使用一个抽象类实现类的骨架实现,这种方法在很流行,一般这样的抽象类被称作Abstract** ,或者Skeletal** 。
    • 还可以使用如下的 模拟多重继承 来利用多重继承的优点,且避免相应的缺陷:
      一个实现了接口的类将对接口方法的调用转发到内部私有类上,而这个私有内部类扩展了抽象的骨架类。

      在这种方法中,子类扩展骨架类是一个明显的选择,但是,如果觉得骨架类无法满足自己的需求,扩展接口并且手动完成自定义的类也是可行的。


19. 接口只用于定义类型

  • 接口的正确使用方法:接口等于新引入了一个类型,这允许客户代码对这个类的实例实施相应的操作,这是接口的正确用途。

  • 接口的非正常用途举例:

    1. 常量接口:整个接口中不包含任何方法,仅仅包含静态的final域,里面是常量。
  • 接口非正常用途的不良影响:

    1. 对扩展该接口的类本身没有任何作用,反而,扩展常量接口会暴露这个类的内部实现细节,污染类的命名空间,甚至,如果这个接口的常量做了更改,依赖于这些常量的类可能不可用。
  • 导出常量的正确途径:

    1. 将常量包含入现有类或者接口:如果常量和某一个类或者接口密切相关,建议将常量包含如类或者接口,而不是将这些常量放到一个集中的,相关的接口中。
    2. 使用不可实例化的工具类,然后在调用时可以使用Java1.5的新特性静态导入,避免使用类名而使用常量名。

20. 类层次优先于标签类

  • 背景知识:所谓标签类即一个类中的相关代码可以表示多种风格,或者说是多个类的特性的类,其中充斥着大量的if-else、switch语句来判断这个实例当前到底表示具体哪个类的特性或者行为。

  • 标签类的缺点:

    1. 充斥着样板代码,包括枚举生命、条件语句
    2. 因为有多个实现拥挤在一个类中,类内部冗杂,破坏了可读性
    3. 内存占用增加,因为类中有其他风格的不相关的域
    4. 域不能做成final的,除非构造器初始化了不相关的域,这样会产生更多样板代码
  • 类层次的优点:

    1. 可以清晰的反映类之间的关系
    2. 没有不相关的域,可以将域定义为final,减少了代码冗余
    3. 可扩展性更强,多个程序员可以独立扩展层次结构

21. 用函数对象表示策略

  • 使用方法:

    1. 利用函数指针(对象引用)来实现策略模式,需要首先声明一个接口来表示一个抽象的策略,然后为每个具体的策略(具体的操作)声明一个实现了该接口的类
    2. 当调用时,如果仅仅使用几次,可以使用匿名函数的形式实例化这个具体的策略类,传参如调用的函数中
    3. 如果使用频繁,建议使用这个策略类的类使用一个静态私有域将策略类实现为一个静态的内部私有类,并通过公有的静态final域导出,类型为策略接口而不是策略类。
  • 举例:

    1. Comparator接口(区别于Comparable接口),这是一个策略接口,String类内部有一个私有的Comparator的实现类,通过它实现了忽略大小写的字符串比较器。

22. 优先考虑静态成员类

  • 嵌套类的种类:

    1. 静态成员类:很一个普通的类很像,只不过声明在类的内部,可以为private的,可以访问外围类的所有属性。另外,静态内部类可以独立于外围类(不依靠于,或者说没外围类)的实例存在。

      用途一般是公有的辅助类,仅当与它的外部类一起使用时有意义。第二个用法是作为外围类所代表的对象的组件,例子是Map中的Entry对象,每一个Map中有一个静态的Entry对象

    2. 非静态成员类:和静态成员类很像,不过因为是非静态的,因此每一个类的实例持有一个外围类的对象的引用,可以调用外围类的实例上的方法,非静态内部类必须依托于外围类的实例而存存在。

      用途一般是作为定义一个适配器,将外部类制作成一个与自己不相关的类。例子是Map的集合视图(keySet,entrySet和Values方法)

    3. 匿名类:动态创建的类,没有类名,必须要扩展某一个类或者接口。存在诸多限制:比如无法扩展多个接口,无法执行instanceof测试,无法定义使用扩展或继承得到的方法以外的方法,以及各种需要类名的工作。而且应当尽量保持简短,否则会影响可读性。

      用途一般是动态地创建函数对象(21条)、创建过程对象(比如Runnable、Thread以及TimerTask的实例)以及用在静态工厂方法内部。

    4. 局部类:在方法中定义的类,具有方法内变量一样的生命周期。使用得最少。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值