文章目录

  • 《Effective Java》读书笔记
    • 第一章 引言
    • 第二章 创建和销毁对象
      • 1 考虑使用静态工厂方法替代构造方法**
      • 2 当构造方法参数过多时使用builder(建造者) 模式**
      • 3 使用私有构造方法或枚类实现Singleton 属性*
      • 4 使用私有构造方法执行非实例化*
      • 5 依赖注入优于硬连接资源(hardwiring resources)**
      • 6 避免创建不必要的对象*
      • 7 消除过期的对象引用*
      • 8 避免使用Finalizer 和Cleaner 机制**
      • 9 使用try-with-resources 语句替代try-finally 语句**
    • 第三章 对于所有对象都通用的方法
      • 10 重写equals 方法时遵守通用约定*
      • 11 重写equals 方法时同时也要重写hashcode 方法*
      • 12 始终重写toString 方法*
      • 13 谨慎地重写clone 方法**
      • 14 考虑实现Comparable 接口***
    • 第四章 类和接口
      • 15 使类和成员的可访问性最小化*
      • 16 在公共类中使用访问方法而不是公共属性*
      • 17 最小化可变性*
      • 18 组合优于继承***
      • 19 要么设计继承并提供文档说明,要么禁用继承**
      • 20 接口优于抽象类**
      • 21 为后代设计接口**
      • 22 接口只用于定义类型*
      • 23 类层次优于标签类***
      • 24 静态成员类优于非静态成员类**
      • 25 限制原文件为单个顶级类***
    • 第五章 泛型
      • 第26 条 请不要使用原生态类型**
      • 第27 条 消除非受检的警告**
      • 第28 条 列表优于数组**
      • 第29 条 优先考虑泛型**
      • 第30 条 优先考虑泛型方法**
      • 第31 条 利用有限制通配符来提升API 的灵活性***
      • 第32 条 谨慎并用泛型和可变参数**
      • 第33 条 优先考虑类型安全的异构容器***
    • 第六章 枚举和注解
      • 枚举
      • 第34 条 用enum 代替int 常量**
      • 第35 条 用实例域代替序数**
      • 第36 条 用EnumSet 代替位域***
      • 第37 条 用EnumMap 代替序数索引***
      • 第38 条 用接口模拟可扩展的枚举***
      • 第39 条 注解优先于命名模式**
      • 第40 条 坚持使用Override 注解**
      • 第41 条 用标记接口定义类型**
    • 第七章 Lambda和Stream
      • 第42 条 Lambda 优先于匿名类**
      • 第43 条 方法引用优先于Lambda**
      • 第44 条 坚持使用标准的函数接口**
      • 第45 条 谨慎使用Stream***
      • 第46 条 优先选择Stream 中无副作用的函数***
      • 第47 条 Stream 要优先用Collection 作为返回类型***
      • 第48 条 谨慎使用Stream 并行**
    • 第八章 方法
      • 第49 条 检查参数的有效性*
      • 第50 条 必要时进行保护性拷贝**
      • 第51 条 谨慎设计方法签名*
      • 第52 条 慎用重载*
      • 第53 条 慎用可变参数*
      • 第54 条 返回零长度的数组或者集合,而不是null**
      • 第55 条 谨慎返回optinal**
      • 第56 条 为所有导出的API 元素编写文档注释**
    • 第九章 通用编程
      • 第57 条 将局部变量的作用域最小化*
      • 第58 条 for-each 循环优先于传统的for 循环*
      • 第59 条 了解和使用类库***
      • 第60 条 如果需要精确的答案,请避免使用float 和double**
      • 第61 条 基本类型优先于装箱基本类型*
      • 第62 条 如果其他类型更适合,则尽量避免使用字符串**
      • 第63 条 了解字符串连接的性能**
      • 第64 条 通过接口引用对象**
      • 第65 条 接口优先于反射机制***
      • 第66 条 谨慎地使用本地方法**
      • 第67 条 谨慎地进行优化***
      • 第68 条 遵守普遍接受的命名惯例*
    • 第十章 异常
      • 第69 条 只针对异常的情况才使用异常**
      • 第70 条 对可恢复的情况使用受检异常,对编程错误使用运行时异常**
      • 第71 条 避免不必要地使用受检异常**
      • 第72 条 优先使用标准的异常*
      • 第73 条 抛出与抽象对应的异常**
      • 第74 条 每个方法抛出的所有异常都要建立文档**
      • 第75 条 在细节消息中包含失败-捕获信息***
      • 第76 条 努力使失败保持原子性**
      • 第77 条 不要忽略异常*
    • 第十一章 并发
      • 第78 条 同步访问共享的可变数据**
      • 第79 条 避免过度同步***
      • 第80 条 executor 、task 和stream 优先于线程***
      • 第81 条 并发工具优先于wait 和notify***
      • 第82 条 线程安全性的文档化**
      • 第83 条 慎用延迟初始化**
      • 第84 条 不要依赖于线程调度器***
    • 第十二章 序列化
      • 第85 条 其他方法优先于Java 序列化**
      • 第86 条 谨慎地实现Serializable 接口**
      • 第87 条 考虑使用自定义的序列化形式***
      • 第88 条 保护性地编写readObject 方法***
      • 第89 条 对于实例控制,枚举类型优先于readResolve**
      • 第90 条 考虑用序列化代理代替序列化实例***

《Effective Java》读书笔记

第一章 引言

  • 为什么要学习这本书?

《Effective Java》一书的核心写作目的就一个:如何写出更好(看)的Java代码。它可以帮助我们去构建更加健壮的Java代码,减少我们在写程序中有可能遇到的一些潜在的bug,或者说让我们在写程序的时候更加注重OOP思想,增加程序的灵活性、复用性、可扩展性。在写一个应用程序时,将这本书提出的建议运用到编码过程中可能不会立即看到代码的优越性,但是在未来对应用进行维护和产品迭代时就会发现代码的健壮性会为我们节省很多的时间与精力。

在各式各样的Java新技术迅速衍生的同时,我们也需要更加关注Java代码的可读性、健壮性和扩展性。追求新技术的同时,也要追求写具有美感的Java代码。(追求Java新技术 < 追求Java稳定性)

我把这本书上的90个建议分成三类:

第一类:本来就能看懂的。(易:*)

第二类:本来不懂的看完之后懂的。(中:**)

第三类:看完也不怎么懂的。(难:***)

第二章 创建和销毁对象

  本章的主题是创建和销毁对象:何时以及如何创建对象,何时以及如何避免创建对象,如何确保它们能够适时地销毁,以及如何管理对象销毁之前必须进行的各种清理动作。

1 考虑使用静态工厂方法替代构造方法**

静态工厂方法的一个优点是,与构造方法不同,它们是有名字的。
静态工厂方法的第二个优点是,与构造方法不同,它们不需要每次调用时都创建一个新对象。(实例控制类)
静态工厂方法的第三个优点是,与构造方法不同,它们可以返回其返回类型的任何子类型的对象。
静态工厂的第四个优点是返回对象的类可以根据输入参数的不同而不同。
静态工厂的第五个优点是,在编写包含该方法的类时,返回的对象的类不需要存在。(服务提供者框架)

只提供静态工厂方法的主要限制是,没有公共或受保护构造方法的类不能被子类化。
静态工厂方法的第二个缺点是,程序员很难找到它们(它们一般在API文档的最后)。

总之,静态工厂方法和公共构造方法都有它们的用途,并且了解它们的相对优点是值得的。通常,静态工厂更可取,因此避免在没有考虑静态工厂的情况下直接选择使用公共构造方法。

2 当构造方法参数过多时使用builder(建造者) 模式**

简而言之,重载构造方法模式是有效的,但是当有很多参数时,客户端代码将会变得很难编写,而且很难读懂它。

JavaBeans 模式,在这种模式中,调用一个无参的构造方法来创建对象,然后调用setter方法来设置每个必需的参数和可选参数,但是这种模式并不安全。因为Java Beans 模式使得把类做成不可变的可能性不复存在。

Builder 模式则结合了可伸缩构造方法模式的安全性和JavaBean模式的可读性。

总而言之,当设计类的构造方法或静态工厂的参数超过几个时,Builder 模式是一个不错的选择,特别是如果许多参数是可选的或相同类型的。builder 模式客户端代码比使用重载构造方法更容易读写,并且builder 模式JavaBeans 更安全。

3 使用私有构造方法或枚类实现Singleton 属性*

有两种常⻅的方法来实现单例:私有构造和枚举。这两者都基于保持构造方法私有和导出公共静态成员以提供对唯一实例的访问。

4 使用私有构造方法执行非实例化*

这个比较简单,因为如果不写构造器,java会提供一个默认构造器,还是可以进行实例化的,所以为了保证某各类绝对不会被实例化,可以写一个private的构造器进去,外部无法访问这个构造器,也就无法进行实例了。

只有当类不包含显式构造方法时,才会生成一个默认构造方法,因此可以通过包含一个私有构造方法来实现类的非实例化。因为显式构造方法是私有的,所以在类之外是不可访问的。

这个用法有点违反编码习惯,好像构造方法就是设计成不能调用的一样。因此,如前面所示,添加注释是种明智的做法。但是这种习惯仍然有一个副作用,它阻止了类的子类化。所有的构造方法都必须显式或隐式地调用父类构造方法,而子类则没有可访问的父类构造方法来调用。

5 依赖注入优于硬连接资源(hardwiring resources)**

该模式的一个优点是将资源工厂传递给构造方法。工厂是可以重复调用以创建类型实例的对象,这种工厂体现了工厂方法模式。

尽管依赖注入极大地提高了灵活性和可测试性,但它可能使大型项目变得混乱,这些项目通常包含数千个依赖项。

总之,不要使用单例或静态的实用类来实现一个类,该类依赖于一个或多个底层资源,这些资源的行为会影响类的行为,并且不让类直接创建这些资源。相反,将资源或工厂传递给构造方法(或静态工厂或builder 模式)。这种称为依赖注入的实践将极大地增强类的灵活性、可重用性和可测试性。

6 避免创建不必要的对象*

在每次需要时重用一个对象而不是创建一个新的相同功能对象通常是恰当的。重用可以更快更流行。如果对象是不可变的(详⻅第17 条),它总是可以被重用。除了重用不可变对象,如果知道它们不会被修改,还可以重用可变对象。

比如调用一次String.matches 方法就会创建一个Pattern实例(代价大)。

比如每次调用keySet 都必须创建一个新的Set 实例,但是对给定Map 对象的keySet 的每次调用都返回相同的Set实例。虽然创建keySet 视图对象的多个实例基本上是无害的,但这是没有必要的,也没有任何好处。

另一种创建不必要的对象的方法是自动装箱(autoboxing),它允许程序员混用基本类型和包装的基本类型,根据需要自动装箱和拆箱。优先使用基本类型而不是装箱的基本类型,也要注意无意识的自动装箱。

可以在一个池中维护一定数量的对象,在需要的时候拿出来用。但是要记住,除非池中的对象非常重量级,否则通过维护自己的对象池来避免对象创建是一个坏主意。对象池的典型例子就是数据库连接。因为建立连接的成本非常高,因此重用这些对象是有意义的。但是,一般来说,维护自己的对象池会使代码混乱,增加内存占用,并损害性能。现代JVM 实现具有高度优化的垃圾收集器,它们在轻量级对象上轻松胜过此类对象池。

未能在需要的情况下防御性复制会导致潜在的错误和安全漏洞;而不必要地创建对象只会影响程序的⻛格和性能。

7 消除过期的对象引用*

JVM虽然可以自动回收垃圾,但是这并不代表你不需要考虑内存管理。

一个常见的内存泄漏:如果一个栈增⻓后收缩,(Stack类管理自己的内存)那么从栈弹出的对象不会被垃圾收集,即使使用栈的程序不再引用这些对象。这是因为栈维护对这些对象的过期引用(obsolete references)。过期引用简单来说就是永远不会解除的引用。这类问题的解决方法很简单:一旦对象引用过期,将它们设置为null。取消过期引用的另一个好处:如果它们随后被错误地引用,程序立即抛NullPointerException异常,而不是悄悄地做继续做错误的事情。尽可能快地发现程序中的错误是有好处的。程序的最后清空对象引用应该是例外而不是规范。

因此,一般来说,当一个类自己管理内存时,程序员应该警惕内存泄漏问题。每当一个元素被释放时,元素中包含的任何对象引用都应该被清除。

另一个常⻅的内存泄漏来源是缓存。一旦将对象引用放入缓存中,很容易忘记它的存在,并且在它变得无关紧要之后,仍然保留在缓存中。

第三个常⻅的内存泄漏来源是监听器和其他回调。如果你实现了一个API,其客户端注册回调,但是没有显式地撤销注册回调,除非采取一些操作,否则它们将会累积。确保回调是垃圾收集的一种方法是只存储弱引用。

因为内存泄漏通常不会表现为明显的故障,所以它们可能会在系统中保持多年。通常仅在仔细的代码检查或借助堆分析器(heap profiler)的调试工具才会被发现。因此,学习如何预⻅这些问题,并防止这些问题发生,是非常值得的。

8 避免使用Finalizer 和Cleaner 机制**

Finalizer 机制是不可预知的,往往是危险的,而且通常是不必要的。它们的使用会导致不稳定的行为,糟糕的性能和移植性问题。Java 9 中Cleaner 机制代替了Finalizer 机制。Cleaner 机制不如Finalizer 机制那样危险,但仍然是不可预测,运行缓慢并且通常是不必要的。

Finalizer 和Cleaner 机制的一个缺点是不能保证他们能够及时执行。

一位同事调试了一个⻓时间运行的GUI 应用程序,这个应用程序正在被一个OutOfMemoryError 错误神秘地死掉。分析显示,在它死亡的时候,应用程序的Finalizer 机制队列上有成千上万的图形对象正在等待被终结和回收。不幸的是,Finalizer 机制线程的运行优先级低于其他应用程序线程,所以对象被回收的速度低于进入队列的速度。

语言规范并不保证哪个线程执行Finalizer 机制,因此除了避免使用Finalizer 机制之外,没有轻便的方法来防止这类问题。在这方面,Cleaner 机制比Finalizer 机制要好一些,因为Java 类的创建者可以控制自己cleaner 机制的线程,但cleaner 机制仍然在后台运行,在垃圾回收器的控制下运行,但不能保证及时清理。

Java 规范不能保证Finalizer 和Cleaner 机制能及时运行;它甚至不能能保证它们是否会运行。

Finalizer 机制的另一个问题是在执行Finalizer 机制过程中,未捕获的异常会被忽略,并且该对象的Finalizer 机制也会终止。

总之,除了作为一个安全网或者终止非关键的本地资源,不要使用Cleaner 机制,或者是在Java 9 发布之前的finalizers 机制。即使是这样,也要当心不确定性和性能影响。

9 使用try-with-resources 语句替代try-finally 语句**

从以往来看,try-finally 语句是保证资源正确关闭的最佳方式,即使是在程序抛出异常或返回的情况下,但是当添加第二个资源时,情况会变得非常糟糕。

Java 类库和第三方类库中的许多类和接口现在都实现或继承了AutoCloseable 接口。如果你编写的类表示必须关闭的资源,那么这个类也应该实现AutoCloseable 接口。

结论很明确:在处理必须关闭的资源时,使用try-with-resources 语句替代try-finally 语句。生成的代码更简洁,更清晰,并且生成的异常更有用。try-with-resources 语句在编写必须关闭资源的代码时会更容易,也不会出错,而使用try-finally 语句是办不到的。

第三章 对于所有对象都通用的方法

  尽管Object 是一个具体类,但设计它主要是为了扩展。它所有的非final 方法( equals 、hashCode 、t oString 、clone 和finalize )都有明确的通用约定( general contract),因为它们设计成是要被覆盖( overri de )的。任何一个类,它在覆盖这些方法的时·候,都有责任遵守这些通用约定;如果不能做到这一点,其他依赖于这些约定的类(例如HashMap和HashSet )就无法结合该类一起正常运作。

  本章将讲述何时以及如何覆盖这些非final 的Object 方法。本章不再讨论finalize方法,因为第8 条已经讨论过这个方法了。而Comparable . compareTo 虽然不是Object 方法,但是本章也将对它进行讨论,因为它具有类似的特征。

10 重写equals 方法时遵守通用约定*

重写equals 方法看起来很简单,但是有很多方式会导致重写出错,其结果可能是可怕的。避免此问题的最简单方法是不覆盖equals 方法,在这种情况下,类的每个实例只与自身相等。

什么时候需要重写equals 方法呢?如果一个类包含一个逻辑相等(logical equality)的概念,此概念有别于对象标识(object identity),而且父类还没有重写过equals 方法。这通常用在值类(value classes)的情况。值类只是一个表示值的类,例如Integer 或String 类。

当你重写equals 方法时,必须遵守它的通用约定。Object 的规范如下:equals 方法实现了一个等价关系(equivalence relation)。它有以下这些属性:自反性、对称性、传递性、一致性、非空性。

综合起来,以下是编写高质量equals 方法的配方(recipe):

1.使用== 运算符检查参数是否为该对象的引用。如果是,返回true。这只是一种性能优化,但是如果这种比较可能很昂贵的话,那就值得去做。

2.使用instanceof 运算符来检查参数是否具有正确的类型。如果不是,则返回false。通常,正确的类型是equals 方法所在的那个类。有时候,改类实现了一些接口。如果类实现了一个接口,该接口可以改进equals 约定以允许实现接口的类进行比较,那么使用接口。集合接口(如Set,List,Map 和Map.Entry)具有此特性。

3.参数转换为正确的类型。因为转换操作在instanceof 中已经处理过,所以它肯定会成功。

4.对于类中的每个「重要」的属性,请检查该参数属性是否与该对象对应的属性相匹配。如果所有这些测试成功,返回true,否则返回false。如果步骤2 中的类型是一个接口,那么必须通过接口方法访问参数的属性;如果类型是类,则可以直接访问属性,这取决于属性的访问权限。

以下是一些最后提醒:

1.当重写equals 方法时,同时也要重写hashCode 方法(详⻅第11 条)。

2.不要让equals 方法试图太聪明。如果只是简单地测试用于相等的属性,那么要遵守equals 约定并不困难。如果你在寻找相等方面过于激进,那么很容易陷入麻烦。一般来说,考虑到任何形式的别名通常是一个坏主意。例如,File 类不应该试图将引用的符号链接等同于同一文件对象。幸好File 类并没这么做。

3.在equal 时方法声明中,不要将参数Object 替换成其他类型。对于程序员来说,编写一个看起来像这样的equals 方法并不少⻅,然后花上几个小时苦苦思索为什么它不能正常工作:在equal 时方法声明中,不要将参数Object 替换成其他类型。

编写和测试equals(和hashCode)方法很繁琐,生的代码也很普通。替代手动编写和测试这些方法的优雅的手段是,使用谷歌AutoValue 开源框架,该框架自动为你生成这些方法,只需在类上添加一个注解即可。在大多数情况下,AutoValue 框架生成的方法与你自己编写的方法本质上是相同的。

总之,除非必须:在很多情况下,不要重写equals 方法,从Object 继承的实现完全是你想要的。如果你确实重写了equals 方法,那么一定要比较这个类的所有重要属性,并且以保护前面equals 约定里五个规定的方式去比较。

11 重写equals 方法时同时也要重写hashcode 方法*

在每个类中,在重写equals 方法的时侯,一定要重写hashcode 方法。如果不这样做,你的类违反了hashCode的通用约定,这会阻止它在HashMap 和HashSet 这样的集合中正常工作。根据Object 规范,以下是具体约定:

1.当在一个应用程序执行过程中,如果在equals 方法比较中没有修改任何信息,在一个对象上重复调用hashCode 方法时,它必须始终返回相同的值。从一个应用程序到另一个应用程序的每一次执行返回的值可以是不一致的。

2.如果两个对象根据equals(Object) 方法比较是相等的,那么在两个对象上调用hashCode 就必须产生的结果是相同的整数。(重写equals方法必须重写hashcode方法的根本原因)

3.如果两个对象根据equals(Object) 方法比较并不相等,则不要求在每个对象上调用hashCode 都必须产生不同的结果。但是,程序员应该意识到,为不相等的对象生成不同的结果会提高散列表(hash tables)的性能。(一个好的hash 方法趋向于为不相等的实例生成不相等的哈希码。)

总之,每次重写equals 方法时都必须重写hashCode 方法,否则程序将无法正常运行。你的hashCode 方法必须遵从Object 类指定的常规约定,并且必须执行合理的工作,将不相等的哈希码分配给不相等的实例。如果使用AutoValue 框架,这很容易实现。如条目10 所述,AutoValue 框架为手动编写equals 和hashCode 方法提供了一个很好的选择,IDE 也提供了一些这样的功能。

12 始终重写toString 方法*

toString 通用约定「建议所有的子类重写这个方法」。如此简单,excellent!如果未能重写toString,则消息可能是无用的。

虽然它并不像遵守equals 和hashCode 约定那样重要(条目10 和11),但是提供一个良好的toString 实现使你的类更易于使用,并对使用此类的系统更易于调试。当对象被传递到println、printf、字符串连接操作符或断言,或者由调试器打印时,toString 方法会自动被调用。

13 谨慎地重写clone 方法**

如果一个类实现了Cloneable 接口,那么Object 的clone 方法将返回该对象的逐个属性(field-by-field)拷⻉;否则会抛出CloneNotSupportedException 异常。

clone 方法的通用规范:创建并返回此对象的副本。「复制(copy)」的确切含义可能取决于对象的类。一般意图是,对于任何对象x,表达式x.clone()!= x 返回true,并且x.clone().getClass()== x.getClass() 也返回true,但它们不是绝对的要求,但通常情况下,x.clone().equals(x) 返回true,当然这个要求也不是绝对的。

14 考虑实现Comparable 接口***

通过实现Comparable 接口,一个类表明它的实例有一个自然顺序(natural ordering)。对实现Comparable 接口的对象数组排序非常简单,如: Arrays.sort(a);如果你正在编写具有明显自然顺序(如字⺟顺序,数字顺序或时间顺序)的值类,则应该实现Comparable 接口。compareTo 方法是Comparable 接口中的唯一方法,它的通用约定与equals 相似:

将此对象与指定的对象按照排序进行比较。返回值可能为负整数,零或正整数,因为此对象对应小于,等于或大于指定的对象。如果指定对象的类型与此对象不能进行比较,则引发ClassCastException 异常。

  • 实现类必须确保所有x 和y 都满足sgn(x.compareTo(y))== -sgn(y. compareTo(x))。

  • 实现类还必须确保该关系是可传递的:(x. compareTo(y)> 0 && y.compareTo(z)> 0) 意味着x.compareTo(z)> 0。

  • 对于所有的z,实现类必须确保x.compareTo(y)== 0 意味着sgn(x.compareTo(z))==sgn(y.compareTo(z))。

  • 强烈推荐(x.compareTo(y)== 0)== (x.equals(y)),但不是必需的。如果违反这一条,顺序关系被认为与equals 不一致。其compareTo 方法施加与equals 不一致顺序关系的类虽然仍然有效,但包含该类元素的有序集合可能不服从相应集合接口(Collection,Set 或Map)的一般约定。这是因为这些接口的通用约定是用equals 方法定义的,但是排序后的集合使用compareTo 强加的相等性测试来代替equals。如果发生这种情况,虽然不是一场灾难,但仍是一件值得注意的事情。

总而言之,无论何时实现具有合理排序的值类,你都应该让该类实现Comparable 接口,以便在基于比较的集合中轻松对其实例进行排序,搜索和使用。比较compareTo 方法的实现中的字段值时,请避免使用「<」和「>」运算符。相反,使用包装类中的静态compare 方法或Comparator 接口中的构建方法。

第四章 类和接口

  类和接口是Java 编程语言的核心,它们也是Java 语言的基本抽象单元。Java 语言提供了许多强大的基本元素,供程序员用来设计类和接口。本章阐述的一些指导原则,可以帮助你更好地利用这些元素,设计出更加有用、健壮和灵活的类和接口。

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

所以尽量不要使用Lombok组件,它会将所有属性暴露,以给别人访问。

一个设计良好的组件隐藏了它的所有实现细节,干净地将它的API 与它的实现分离开来。然后,组件只通过它们的API 进行通信,并且对彼此的内部工作一无所知。这一概念,被称为信息隐藏或封装,是软件设计的基本原则。

信息隐藏很重要有很多原因,其中大部分来源于它将组成系统的组件分离开来,允许它们被独立地开发,测试,优化,使用,理解和修改。这加速了系统开发,因为组件可以并行开发。它减轻了维护的负担,因为可以更快速地理解组件,调试或更换组件,而不用担心损害其他组件。虽然信息隐藏本身并不会导致良好的性能,但它可以有效地进行性能调整:一旦系统完成并且分析确定了哪些组件导致了性能问题,则可以优化这些组件,而不会影响别人的正确的组件。信息隐藏增加了软件重用,因为松耦合的组件通常在除开发它们之外的其他环境中证明是有用的。最后,隐藏信息降低了构建大型系统的⻛险,因为即使系统不能运行,各个独立的组件也可能是可用的。

总而言之,应该尽可能地减少程序元素的可访问性(在合理范围内)。在仔细设计一个最小化的公共API 之后,你应该防止任何散乱的类,接口或成员成为API 的一部分。除了作为常量的公共静态final 字段之外,公共类不应该有公共字段。确保public static final 字段引用的对象是不可变的。

16 在公共类中使用访问方法而不是公共属性*

总之,公共类不应该暴露可变属性。公共类暴露不可变属性的危害虽然仍然存在问题,但其危害较小。然而,有时需要包级私有或私有内部类来暴露属性,无论此类是否是可变的。

17 最小化可变性*

不可变类简单来说是它的实例不能被修改的类。包含在每个实例中的所有信息在对象的生命周期中是固定的,因此不会观察到任何变化。Java 平台类库包含许多不可变的类,包括String 类,基本类型包装类以及BigInteger类和BigDecimal 类。有很多很好的理由:不可变类比可变类更容易设计,实现和使用。他们不太容易出错,更安全。

要使一个类不可变,请遵循以下五条规则:

  • 不要提供修改对象状态的方法(也称为mutators)。

  • 确保这个类不能被继承。

  • 把所有属性设置为final。

  • 把所有的属性设置为private。

  • 确保对任何可变组件的互斥访问。

不可变对象本质上是线程安全的; 它们不需要同步。被多个线程同时访问它们时并不会被破坏。这是实现线程安全的最简单方法。由于没有线程可以观察到另一个线程对不可变对象的影响,所以不可变对象可以被自由地共享。不可变类应鼓励客户端尽可能重用现有的实例。一个简单的方法是为常用的值提供公共的静态final常量。

不仅可以共享不可变的对象,而且可以共享内部信息。

不可变对象为其他对象提供了很好的构件(building blocks),无论是可变的还是不可变的。如果知道一个复杂组件的内部对象不会发生改变,那么维护复杂对象的不变量就容易多了。这一原则的特例是,不可变对象可以构成Map 对象的键和Set 的元素,一旦不可变对象作为Map 的键或Set 里的元素,即使破坏了Map 和Set 的不可变性,但不用担心它们的值会发生变化。

不可变对象提供了免费的原子失败机制。

不可变类的主要缺点是对于每个不同的值都需要一个单独的对象。创建这些对象可能代价很高,特别是如果是大型的对象。

两种方法保证一个类的不可变性,第一种:将类设置为final;第二种更灵活:构造方法私有,添加静态工程方法访问。它允许使用多个包级私有实现类。除了允许多个实现类的灵活性以外,这种方法还可以通过改进静态工厂的对象缓存功能来调整后续版本中类的性能。

构造方法应该创建完全初始化的对象,并建立所有的不变性。除非有令人信服的理由,否则不要提供独立于构造方法或静态工厂的公共初始化方法。

总而言之,坚决不要为每个属性编写一个get 方法后再编写一个对应的set 方法。除非有充分的理由使类成为可变类,否则类应该是不可变的。

18 组合优于继承***

组合模式让对象更加有层次,将对象的划分更加清晰,特别是树形结构的层次,利用组合模式会更加简化。

不要继承一个现有的类,而应该给你的新类增加一个私有属性,该属性是现有类的实例引用,这种设计被称为组合(composition),因为现有的类成为新类的组成部分。

总之,继承是强大的,但它是有问题的,因为它违反封装。只有在子类和父类之间存在真正的子类型关系时才适用。即使如此,如果子类与父类不在同一个包中,并且父类不是为继承而设计的,继承可能会导致脆弱性。为了避免这种脆弱性,使用合成和转发代替继承,特别是如果存在一个合适的接口来实现包装类。包装类不仅比子类更健壮,而且更强大。

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

对于不是为了继承而设计并且没有文档说明的“外来”类进行子类化是多么危险。那么对于专门为了继承而设计并且具有良好文档说明的类而言,这又意味着什么呢?首先,该类的文档必须精确地描述覆盖每个方法所带来的影响。换句话说, 该类必须有文档说明它可覆盖( overridable )的方法的自用性( self-use ) 。

好的API 文档应该描述一个给定的方法做了什么工作,而不是描述它是如何做到的。

为了继承而进行的设计不仅仅涉及自用模式的文档设计。为了使程序员能够编写出更加有效的子类,而无须承受不必要的痛苦, 类必须以精心挑选的受保护的( protected )方法的形式,提供适当的钩子( hook ),以便进入其内部工作中。

对于为了继承而设计的类,唯一的测试方法就是编写子类。如果遗漏了关键的受保护成员,尝试编写子类就会使遗漏所带来的痛苦变得更加明显。

简而言之,专门为了继承而设计类是一件很辛苦的工作。你必须建立文档说明其所有的自用模式,并且一旦建立了文档,在这个类的整个生命周期中都必须遵守。如果没有做到,子类就会依赖超类的实现细节,如果超类的实现发生了变化,它就有可能遭到破坏。为了允许其他人能编写出高效的子类,还你必须导出一个或者多个受保护的方法。除非知道真正需要子类,否则最好通过将类声明为final ,或者确保没有可访问的构造器来禁止类被继承。

20 接口优于抽象类**

骨架实现类是为了继承的目的而设计的,所以应该遵从第四条中介绍的所有关于设计和文挡的指导原则。为了简洁起见,上面例子中的文档注释部分被省略掉了,但是对于骨架实现类而言,好的文档绝对是非常必要的, 无论它是否在接口或者单独的抽象类中包含了缺省方法。

总而言之,接口通常是定义允许多个实现的类型的最佳途径。如果你导出了一个重要的接口,就应该坚决考虑同时提供骨架实现类。而且,还应该尽可能地通过缺省方法在接口中提供骨架实现,以便接口的所有实现类都能使用。也就是说,对于接口的限制,通常也限制了骨架实现会采用的抽象类的形式。

21 为后代设计接口**

Java 8 在核心集合接口中增加了许多新的缺省方法,主要是为了便于使用lambda。

有了缺省方法,接口的现有实现就不会出现编译时没有报错或警告,运行时却失败的情况。

建议尽量避免利用缺省方法在现有接口上添加新的方法,除非有特殊需要,但就算在那样的情况下也应该慎重考虑:缺省的方法实现是否会破坏现有的接口实现。然而,在创建接口的时候,用缺省方法提供标准的方法实现是非常方便的,它简化了实现接口的任务。

尽管缺省方法现在已经是Java 平台的组成部分, 但谨慎设计接口仍然是至关重要的。虽然缺省方法可以在现有接口上添加方法,但这么做还是存在着很大的风险。就算接口中只有细微的缺陷都可能永远给用户带来不愉快;假如接口有严重的缺陷, 则可能摧毁包含它的API 。

在发布程序之前,测试每一个新的接口就显得尤其重要。程序员应该以不同的方法实现每一个接口。最起码不应少于三种实现。编写多个客户端程序,利用每个新接口的实例来执行不同的任务,这一点也同样重要。这些步骤对确保每个接口都能满足其既定的所
有用途起到了很大的帮助。它还有助于在接口发布之前及时发现其中的缺陷,使你依然能够轻松地把它们纠正过来。或许接口程序发布之后也能纠正,但是千万别指望它啦。

22 接口只用于定义类型*

当类实现接口时,接口就充当可以引用这个类的实例的类型(type ) 。因此,类实现了接口,就表明客户端可以对这个类的实例实施某些动作。为了任何其他目的而定义接口是不恰当的。

常量接口模式是对接口的不良使用。类在内部使用某些常量,这纯粹是实现细节。

简而言之,接口应该只被用来定义类型, 它们不应该被用来导出常量。

23 类层次优于标签类***

一句话, 标签类过于冗长、容易出错,并且效率低下。

这个类层次纠正了前面提到过的标签类的所有缺点。这段代码简单且清楚,不包含在原来的版本中见到的所有样板代码。类层次的另一个好处在于,它们可以用来反映类型之间本质上的层次关系,有助于增强灵活性,并有助于更好地进行编译时类型检查。

简而言之,标签类很少有适用的时候。当你想要编写一个包含显式标签域的类时,应该考虑一下,这个标签是否可以取消,这个类是否可以用类层次来代替。当你遇到一个包含标签域的现有类时,就要考虑将它重构到一个层次结构中去。

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

如果声明成员类不要求访问外围实例,就要始终把修饰符static 放在它的声明中, 使它成为静态成员类,而不是非静态成员类。

总而言之,共有四种不同的嵌套类,每一种都有自己的用途。如果一个嵌套类需要在单个方法之外仍然是可见的,或者它太长了,不适合放在方法内部,就应该使用成员类。如果成员类的每个实例都需要一个指向其外围实例的引用,就要把成员类做成非静态的;否则,就做成静态的。假设这个嵌套类属于一个方法的内部,如果你只需要在一个地方创建实例, 并且已经有了一个预置的类型可以说明这个类的特征,就要把它做成匿名类; 否则, 就做成局部类。

25 限制原文件为单个顶级类***

永远不要把多个顶级类或者接口放在一个源文件中。遵循这个规则可以确保编译时一个类不会有多个定义。这么做反过来也能确保编译产生的类文件,以及程序结果的行为,都不会受到源文件被传给编译器时的顺序的影响。

第五章 泛型

  从Java 5 开始,泛型( generic )已经成了Java 编程语言的一部分。在没有泛型之前,从集合中读取到的每一个对象都必须进行转换。如果有人不小心插入了类型错误的对象,在运行时的转换处理就会出错。有了泛型之后,你可以告诉编译器每个集合中接受哪些对象类型。编译器自动为你的插入进行转换,并在编译时告知是否插入了类型错误的对象。这样可以使程序更加安全,也更加清楚, 但是要享有这些优势(不限于集合)有一定的难度。本章就是教你如何最大限度地享有这些优势,又能使整个过程尽可能简单化。

https://blog.csdn.net/qq_41822345/article/details/104967036

第26 条 请不要使用原生态类型**

为便于参考,在下表中概括了本章所介绍的术语:

每一种泛型都定义一个原生态类型,即不带任何实际类型参数的泛型名称。例如,与List < E>相对应的原生态类型是List 。原生态类型就像从类型声明中删除了所有泛型信息一样。它们的存在主要是为了与泛型出现之前的代码相兼容。
出错之后应该尽快发现, 最好是编译时就发现。

总而言之,使用原生态类型会在运行时导致异常,因此不要使用。原生态类型只是为了与引人泛型之前的遗留代码进行兼容和互用而提供的。

让我们做个快速的回顾:Set是个参数化类型,表示可以包含任何对象类型的一个集合; Set <?>则是一个通配符类型,表示只能包含某种未知对象类型的一个集合; Set 是一个原生态类型,它脱离了泛型系统。前两种是安全的,最后一种不安全。

第27 条 消除非受检的警告**

要尽量消除非受检的警告。如果无法消除警告,同时可以证明引起警告的代码是类型安全的,(只有在这种情况下)才可以用一个@Suppress Warnings ( “ unchecked " )注解来禁止这条警告。每当使用SuppressWarnings ( “ unchecked " )注解时,都要添加一条注释,说明为什么这么做是安全的。

总而言之,非受检警告很重要,不要忽略它们。每一条警告都表示可能在运行时抛出ClassCastException 异常。要尽最大的努力消除这些警告。如果无法消除非受检警告,同时可以证明引起警告的代码是类型安全的就可以在尽可能小的范围内使用@Suppress Warnings( “unchecked ”)注解禁止该警告。要用注释把禁止该警告的原因记录下来。

第28 条 列表优于数组**

数组与泛型相比,有两个重要的不同点。首先,数组是协变的。

数组与泛型之间的第二大区别在于,数组是具体化的。因此数组会在运行时知道和强化它们的元素类型。泛型只在编译时强化它们的类型信息,并在运行时丢弃(或者擦除)它们的元素类型信息。

总而言之,数组和泛型有着截然不同的类型规则。数组是协变且可以具体化的;泛型是不可变的且可以被擦除的。因此,数组提供了运行时-的类型安全,但是没有编译时的类型安全,反之,对于泛型也一样。一般来说,数组和泛型不能很好地混合使用。如果你发现自己将它们混合起来使用,并且得到了编译时错误或者警告,你的第一反应就应该是用列表代替数组。

第29 条 优先考虑泛型**

总而言之,使用泛型比使用需要在客户端代码中进行转换的类型来得更加安全,也更加容易。在设计新类型的时候,要确保它们不需要这种转换就可以使用。这通常意味着要把类做成是泛型的。只要时间允许,就把现有的类型都泛型化。这对于这些类型的新用户来说会变得更加轻松,又不会破坏现有的客户端。

第30 条 优先考虑泛型方法**

总而言之,泛型方法就像泛型一样,使用起来比要求客户端转换输入参数并返回值的方法来得更加安全,也更加容易。就像类型一样,你应该确保方法不用转换就能使用,这通常意味着要将它们泛型化。并且就像类型一样,还应该将现有的方法泛型化,使新用户使用起来更加轻松,且不会破坏现有的客户端(详见第26 条) 。

第31 条 利用有限制通配符来提升API 的灵活性***

为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。如果某个输入参数既是生产者,又是消费者,那么通配符类型对你就没有什么好处了: 因为你需要的是严格的类型匹配,这是不用任何通配符而得到的。

总而言之,在API 中使用通配符类型虽然比较需要技巧,但是会使API 变得灵活得多。如果编写的是将被广泛使用的类库, 则一定要适当地利用通配符类型。记住基本的原则: prod u ce r-ex tends,consumer 寸uper (PECS ) 。还要记住所有的comparable 和comparator 都是消费者。

第32 条 谨慎并用泛型和可变参数**

总而言之,可变参数和泛型不能良好地合作,这是因为可变参数设施是构建在顶级数组之上的一个技术露底,泛型数组有不同的类型规则。虽然泛型可变参数不是类型安全的,但它们是合法的。如果选择编写带有泛型(或者参数化)可变参数的方法,首先要确保该方法是类型安全的,然后用@ SafeVarargs 对它进行注解,这样使用起来就不会出现不愉快
的情况了。

第33 条 优先考虑类型安全的异构容器***

总而言之,集合API 说明了泛型的一般用法, 限制每个容器只能有固定数目的类型参数。你可以通过将类型参数放在键上而不是容器上来避开这一限制。对于这种类型安全的异构容器,可以用Class 对象作为键。以这种方式使用的Class 对象称作类型令牌。你也可以使用定制的键类型。例如,用一个DatabaseR ow 类型表示一个数据库行(容器),用泛型Column作为它的键。

第六章 枚举和注解

  Java 支持两种特殊用途的引用类型: 一种是类, 称作枚举类型( enum type ) ; 一种是接口,称作注解类型( annotation type ) 。本章将讨论这两个新类型的最佳使用实践。

枚举

枚举类型( enum type )是指由一组固定的常量组成合法值的类型,例如一年中的季节、太阳系中的行星或者一副牌中的花色。

https://blog.csdn.net/javazejian/article/details/71333103

枚举原理

枚举实现原理:实际上在使用关键字enum创建枚举类型并编译后,编译器会为我们生成一个相关的类,这个类继承了Java API中的java.lang.Enum类,也就是说通过关键字enum创建枚举类型在编译后事实上也是一个类类型,而且该类继承自java.lang.Enum类。

枚举的常见方法

Enum抽象类常见方法如下表:

另外, Enum类内部会有一个构造函数,该构造函数只能有编译器调用,我们是无法手动操作的。

//由编译器调用
protected Enum(String name, int ordinal) {this.name = name;this.ordinal = ordinal;}

values()方法和valueOf()方法

继承了Enum抽象类的枚举子类另有两个方法:values()和valueOf(String name), 它们是由编译器生成的static方法,所以在Enum类中并没有出现这两个方法 。values()方法的作用就是获取枚举类中的所有变量,并作为数组返回,而valueOf(String name)方法与Enum类中的valueOf方法的作用类似根据名称获取枚举变量,只不过编译器生成的valueOf方法更简洁些只需传递一个参数。

枚举的进阶用法

实际上使用关键字enum定义的枚举类,除了不能使用继承(因为编译器会自动为我们继承Enum抽象类而Java只支持单继承,因此枚举类是无法手动实现继承的),可以把enum类当成常规类,也就是说我们可以向enum类中添加方法和变量,甚至是mian方法。

想enum类中添加构造方法和方法

enum类中可以像定义常规类一样声明变量或者成员方法。但是我们必须注意到,如果打算在enum类中定义方法,务必在声明完枚举实例后使用分号分开,倘若在枚举实例前定义任何方法,编译器都将会报错,无法编译通过。同时即使自定义了构造函数,我们也无法手动调用构造函数创建枚举实例,毕竟这事只能由编译器执行。

public enum Day {MONDAY("星期一"),TUESDAY("星期二"),WEDNESDAY("星期三"),THURSDAY("星期四"),FRIDAY("星期五"),SATURDAY("星期六"),SUNDAY("星期日");  //记住要用分号结束private String desc;//中文描述private Day(String desc){this.desc=desc;}public String getDesc(){return desc;}public static void main(String[] args){for (Day day:Day.values()) {System.out.println("name:"+day.name()+",desc:"+day.getDesc());}}/**输出结果:name:MONDAY,desc:星期一name:TUESDAY,desc:星期二name:WEDNESDAY,desc:星期三name:THURSDAY,desc:星期四name:FRIDAY,desc:星期五name:SATURDAY,desc:星期六name:SUNDAY,desc:星期日*/
}

enum类覆盖父类Enum方法

既然enum类跟常规类的定义没什么区别(实际上enum还是有些约束的),那么覆盖父类的方法也不会是什么难说,可惜的是父类Enum中的定义的方法只有toString方法没有使用final修饰,因此只能覆盖toString方法。

enum类中定义抽象方法

enum类允许我们为其定义抽象方法,然后使每个枚举实例都实现该方法,以便产生不同的行为方式。

public enum EnumDemo3 {FIRST{@Overridepublic String getInfo() {return "FIRST TIME";}},SECOND{@Overridepublic String getInfo() {return "SECOND TIME";}};   //分号结束public abstract String getInfo();  //abstract抽象方法//测试public static void main(String[] args){System.out.println("F:"+EnumDemo3.FIRST.getInfo());System.out.println("S:"+EnumDemo3.SECOND.getInfo());/**输出结果:F:FIRST TIMES:SECOND TIME*/}
}

enum类与接口

由于Java单继承的原因,enum类并不能再继承其它类,但并不妨碍它实现接口 。

枚举与接口

枚举实例也可以作为case条件。 枚举与switch是个比较简单的话题,使用switch进行条件判断时,条件参数一般只能是整型,字符型,而枚举型确实也被switch所支持,在java 1.7后switch也对字符串进行了支持。

枚举与单例模式

关于单例,我们总是应该记住:线程安全,延迟加载,序列化与反序列化安全,反射安全是很重重要的。

public enum  SingletonEnum {INSTANCE;private String name;public String getName(){return name;}public void setName(String name){this.name = name;}
}

枚举序列化是由jvm保证的,每一个枚举类型和定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定:在序列化时Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的并禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,从而保证了枚举实例的唯一性

EnumMap

EnumMap是专门为枚举类型量身定做的Map实现,虽然使用其它的Map(如HashMap)也能完成相同的功能,但是使用EnumMap会更加高效,它只能接收同一枚举类型的实例作为键值且不能为null,由于枚举类型实例的数量相对固定并且有限,所以EnumMap使用数组来存放与枚举类型对应的值,毕竟数组是一段连续的内存空间,根据程序局部性原理,效率会相当高。

EnumSet

EnumSet是与枚举类型一起使用的专用 Set 集合,EnumSet 中所有元素都必须是枚举类型。 EnumSet在内部实现是位向量(稍后分析),它是一种极为高效的位运算操作,由于直接存储和操作都是bit,因此EnumSet空间和时间性能都十分可观,足以媲美传统上基于 int 的“位标志”的运算,重要的是我们可像操作set集合一般来操作位运算,这样使用代码更简单易懂同时又具备类型安全的优势。

第34 条 用enum 代替int 常量**

int 枚举模式的程序是十分脆弱的。因为工nt 枚举是编译时常量,它们的int 值会被编译到使用它们的客户端中。如果与int 枚举常量关联的值发生了变化,客户端必须重新编译。如果没有重新编译,客户端程序还是可以运行,不过其行为已经不再准确。

String 枚举模式虽然为这些常量提供了可打印的字符串,但是会导致初级用户直接把字符串常量硬编码到客户端代码中,而不
是使用对应的常量字段( field )名。一旦这样的硬编码字符串常量中包含书写错误,在编译时不会被检测到,但是在运行的时候却会报错。而且它会导致性能问题,因为它依赖于字符串的比较操作。

Java 枚举类型的基本想法非常简单:这些类通过公有的静态final 域为每个枚举常量导出一个实例。枚举类型没有可以访问的构造器,所以它是真正的final 类。客户端不能创建枚举类型的实例,也不能对它进行扩展,因此不存在实例,而只存在声明过的枚举常量。换句话说,枚举类型是实例受控的,它们是单例( Singleton) 的泛型化,本质上是单元素的枚举。

枚举中的常量值并没有被编译到客户端代码中,而是在int 枚举模式之中。最终,可以通过调用toString 方法,将枚举转换成可打印的字符串。枚举类型还允许添加任意的方法和域,并能实现任意的接口。

为了将数据与枚举常量关联起来,得声明实例域,并编写一个带有数据并将数据保存在域中的构造器。

一般来说,枚举通常在性能上与int 常量相当。与int 常量相比,枚举有个小小的性能缺点, 即装载和初始化枚举时会需要空间和时间的戚本,但在实践中几乎注意不到这个问题。那么什么时候应该使用枚举呢? 每当需要一组固定常量.并且在编译时就知道其成员的时候,就应该使用枚举。

总而言之,与int 常量相比,枚举类型的优势是不言而喻的。枚举的可读性更好, 也更加安全,功能更加强大。许多枚举都不需要显式的构造器或者成员,但许多其他枚举则受益于属性与每个常量的关联以及其行为受该属性影响的方法。只有极少数的枚举受益于将多种行为与单个方法关联。在这种相对少见的情况下,特定于常量的方法要优先于启用自有值的枚举。如果多个(但非所有)枚举常量同时共享相同的行为,则要考虑策略枚举。

第35 条 用实例域代替序数**

永远不要根据枚举的序数导出与它关联的值, 而是要将它保存在一个实例域中。如下:

public enum Ensemble {MONDAY(1),TUESDAY(2),WEDNESDAY(3),THURSDAY(4),FRIDAY(5),SATURDAY(6),SUNDAY(7);private final int days;Ensemble(int size){this.days = size;}public int daysOfWeek() { return days;}
}

Enum 规范中谈及ordinal ()方法时写道:“大多数程序员都不需要这个方法。它是设计用于像EnumSet 和EnumMap 这种基于枚举的通用数据结构的。”除非你在编写的是这种数据结构,否则最好完全避免使用ordinal() 方法。

第36 条 用EnumSet 代替位域***

总而言之, 正是因为枚举类型要用在集合中,所以没有理由用位域来表示它。EnumSet类集位域的简洁和性能优势及第34 条中所述的枚举类型的所有优点于一身。实际上EnumSet 有个缺点,即截止Java 9 发行版本,它都无法创建不可变的EnumSet ,但是这一点很可能在即将发布的版本中得到修正。同时,可以用Collections . unmodifiableSet将EnumSet 封装起来,但是简洁性和性能会受到影响。

第37 条 用EnumMap 代替序数索引***

总而言之, 最好不要用序数来索引数组,而要使用EnumMap。如果你所表示的这种关系是多维的,就使用EnumMap< … , EnumMap< … >> 。应用程序的程序员在一般情况下都不使用Enurn.ordinal 方法,仅仅在极少数情况下才会使用,因此这是一种用未情况(详见第35 条) 。

第38 条 用接口模拟可扩展的枚举***

虽然枚举类型( BasicOperation )不是可扩展的,但接口类型( Operat i on )却是可扩展的,它是用来表示API 中的操作的接口类型。你可以定义另一个枚举类型,它实现这个接口,并用这个新类型的实例代替基本类型。

总而言之, 虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型来对它进行模拟。这样允许客户端编写自己的枚举(或者其他类型)来实现接口。如果API 是根据接口编写的,那么在可以使用基础枚举类型的任何地方,也都可以使用这些枚举。

第39 条 注解优先于命名模式**

命名模式的两个缺点:文字拼写错误会导致失败,且没有任何提示;无法确保它们只用于相应的程序元素上;未提供将参数值与程序元素关联起来的好方法。

既然有了注解,就完全没有理由再使用命名模式了。所有的程序员都应该使用Java 平台所提供的预定义的注解类型(详见第40 条和第27 条) 。还要考虑使用IDE 或者静态分析工具所提供的任何注解。这种注解可以提升由这些工具所提供的诊断信息的质量。

第40 条 坚持使用Override 注解**

应该在你想要覆盖超类声明的每个方法声明中使用Overide 注解。IDE和编译器可以确保你无一遗漏地覆盖任何你想要覆盖的方法。

总而言之,如果在你想要的每个方法声明中使用Override 注解来覆盖超类声明,编译器就可以替你防止大量的错误,但有一个例外。在具体的类中,不必标注你确信覆盖了抽象方法声明的方法(虽然这么做也没有什么坏处) 。

第41 条 用标记接口定义类型**

标记接口( marker interface )是不包含方法声明的接口,它只是指明(或者“标明”) 一个类实现了具有某种属性的接口。例如:Serializable 接口。

相对于标记注解(第39条)标记接口有两个优点:标记接口定义的类型是由被标记类的实例实现的,标记注解则没有定义这样的类型;标记接口胜过标记注解的另一个优点是,它们可以被更加精确地进行锁定。

标记注解胜过标记接口的最大优点在于,它们是更大的注解机制的一部分。

那么什么时候应该使用标记注解,什么时候应该使用标记接口呢?很显然,如果标记是应用于任何程序元素而不是类或者接口,就必须使用注解,因为只有类和接口可以用来实现或者扩展接口。如果标记只应用于类和接口,就要问问自己:我要编写一个还是多个只接受有这种标记的方法呢?如果是这种情况,就应该优先使用标记接口而非注解。这样你就可以用接口作为相关方法的参数类型,它可以真正为你提供编译时进行类型检查的好处。

总而言之,标记接口和标记注解都各有用处。如果想要定义一个任何新方法都不会与之关联的类型,标记接口就是最好的选择。如果想要标记程序元素而非类和接口,或者标记要适合于已经广泛使用了注解类型的框架,那么标记注解就是正确的选择。如果你发现自己在编写的是目标为ElementType . TYPE 的标记注解类型,就要花点时间考虑清楚,它是否真的应该为注解类型,想想标记接口是否会更加合适。

第七章 Lambda和Stream

  在Java 8 中,增加了函数接口( functional interface )、Lambda 和方法引用( method reference ),使得创建函数对象(且function object )变得很容易。与此同时,还增加了Stream API,为处理数据元素的序列提供了类库级别的支持。在本章中,将讨论如何最佳地利用这些机制。

  • Java8新特性:https://github.com/liuwen766/MyRecruitment

第42 条 Lambda 优先于匿名类**

“带有单个抽象方法的接口是特殊的,值得特殊对待”,这些接口现在被称作函数接口( functional interface), Java 允许利用Lambda 表达式( Lambdaexpression ,简称Lambda )创建这些接口的实例。

与方法和类不同的是, Lambda 没有名称和文档;如果一个计算本身不是自描述的, 或者超出了几行, 那就不要把它旋在一个Lambda 中。对于Lambda而言,一行是最理想的, 三行是合理的最大极限。如果违背了这个规则,可能对程序的可读性造成严重的危害。

总而言之,从Java 8 开始, Lambda 就成了表示小函数对象的最佳方式。除非必须创建非函数接口的类型的实例,否则尽量不要给函数对象使用匿名类。同时,还要记住, Lambda 使得表示小函数对象变得如此轻松,因此打开了之前从未实践过的在Java 中进行函数编程的大门。

  • 匿名内部类:

     Comparator<Integer> comparator = new Comparator<Integer>() {@Overridepublic int compare(Integer o1, Integer o2) {return Integer.compare(o1,o2);}};
    
  • Lambada表达式:

    Comparator<Integer> comparator = (o1,o2)->Integer.compare(o1,o2);
    

    甚至(第43条):

    Comparator<Integer> comparator1 = Integer::compare;
    

缺点:可读性越来越差。

第43 条 方法引用优先于Lambda**

与匿名类相比,Lambda 的主要优势在于更加简洁。Java 提供了生成比Lambda 更简洁函数对象的方法: 方法引用( method reference ) 。比如:System.out::println。但要注意的是:方法引用并不一定就比Lambada表达式更加简单。

五种方法引用类型:

总而言之,方法引用常常比Lambda 表达式更加简洁明了。只要方法引用更加简洁、清晰,就用方法引用;如果方法引用并不简洁,就坚持使用Lambda 。

第44 条 坚持使用标准的函数接口**

只要标准的函数接口能够满足需求,通常应该优先考虑,而不是专门再构建一个新的函数接口。

java.util.function中共有43个函数式接口,其中六个基本常用接口如下:

总而言之,既然Java 有了Lambda ,就必须时刻谨记用Lambda 来设计API 。输入时接受函数接口类型,并在输出时返回。一般来说,最好使用java.util.function.Function 中提供的标准接口,但是必须警惕在相对罕见的几种情况下,最好还是自己编写专用的函数接口。

第45 条 谨慎使用Stream***

Stream API大大简化了串行或并行(如fork/join)的大批量操作。如下:

    @Testpublic void test(){Instant start = Instant.now();long reduce = LongStream.rangeClosed(0, 50000000000L)
//                .sequential()      //串行   Java串行流总耗时:65251毫秒.parallel()        //并行  Java并行流总耗时:5128毫秒.reduce(0, Long::sum);System.out.println(reduce);Instant end = Instant.now();System.out.println("Java并行流总耗时:"+Duration.between(start,end).toMillis()+"毫秒");}

但是滥用Stream 会使程序代码更难以读懂和维护。

Stream pipeline 利用函数对象(一般是Lambda 或者方法引用)来描述重复的计算,而迭代版代码则利用代码块来描述重复的计算。

下列工作只能通过代码块,而不能通过函数对象来完成:

  • 从代码块中,可以读取或者修改范围内的任意局部变量;从Lambda 则只能读取final 或者有效的final 变量,并且不能修改任何local 变量。
  • 从代码块中,可以从外围方法中return 、break 或continue 外围循环,或者抛出该方法声明要抛出的任何受检异常;从Lambda 中则完全无法完成这些事情。

Stream则可以使得完成这些工作变得易如反掌:

  • 统一转换元素的序列
  • 过滤元素的序列
  • 利用单个操作(如添加、连接或者计算其最小值)合并元素的顺序
  • 将元素的序列存放到一个集合中,比如根据某些公共属性进行分组
  • 搜索满足某些条件的元素的序列

总之,有些任务最好用Stream 完成,有些则要用迭代。而有许多任务则最好是结合使用这两种方法来一起完成。具体选择用哪一种方法,并没有硬性、速成的规则,但是可以参考一些有意义的启发。在很多时候,会很清楚应该使用哪一种方法;有些时候,则不太明显。如果实在不确定用Stream 还是用迭代比较好,那么就两种都试试,看看哪一种更好用吧。

第46 条 优先选择Stream 中无副作用的函数***

总而言之,编写Stream pipeline 的本质是无副作用的函数对象。这适用于传入Stream及相关对象的所有函数对象。终止操作中的forEach 应该只用来报告由Stream 执行的计算结果,而不是让它执行计算。为了正确地使用Stream ,必须了解收集器。最重要的收集器工厂是toList 、toSet 、toMap 、groupingBy 和joining 。

第47 条 Stream 要优先用Collection 作为返回类型***

总而言之,在编写返回一系列元素的方法时,要记住有些用户可能想要当作Stream 处理,而其他用户可能想要使用迭代。要尽量两边兼顾。如果可以返回集合,就返回集合。如果集合中已经有元素,或者序列中的元素数量很少,足以创建一个新的集合,那么就返回一个标准的集合,如ArrayList 。否则,就要考虑实现一个定制的集合,如幂集( power set)范例中所示。如果无法返回集合,就返回Stream 或者lterable ,感觉哪一种更自然即可。如果在未来的Java 发行版本中, Stream 接口声明被修改成扩展了Iterable 接口,就可以放心地返回Stream 了,因为它们允许进行Stream 处理和迭代。

第48 条 谨慎使用Stream 并行**

总而言之,尽量不要并行Stream pipeline ,除非有足够的理由相信它能保证计算的正确性,并且能加快程序的运行速度。如果对Stream 进行不恰当的并行操作,可能导致程序运行失败,或者造成性能灾难。如果确信并行是可行的,并发运行时一定要确保代码正确,并在真实环境下认真地进行性能测量。如果代码正确,这些实验也证明它有助于提升性能,只有这时候,才可以在编写代码时并行Stream 。

第八章 方法

  本章要讨论方法设计的几个方面:如何处理参数和返回值,如何设计方法签名,如何为方法编写文档。本章大部分内容既适用于构造器,也适用于普通的方法。与第4 章一样,本章的焦点也集中在可用性、健壮性和灵活性上。

第49 条 检查参数的有效性*

请不要由本条目的内容得出这样的结论: 对参数的任何限制都是件好事。相反,在设计方法时,应该使它们尽可能通用, 并符合实际的需要。假如方法对于它能接受的所有参数值都能够完成合理的工作,对参数的限制就应该是越少越好。然而,通常情况下,有些限制对于被实现的抽象来说是固有的。

简而言之,每当编写方法或者构造器的时候,应该考虑它的参数有哪些限制。应该把这些限制写到文档中,并且在这个方法体的开头处,通过显式的检查来实施这些限制。养成这样的习惯是非常重要的。只要有效性检查有一次失败,你为必要的有效性检查所付出的努力便都可以连本带利地得到偿还了。

第50 条 必要时进行保护性拷贝**

即使在安全的语言中,如果不采取一点措施,还是无法与其他的类隔离开来。假设类的客户端会尽其所能来破坏这个类的约束条件,因此你必须保护性地设计程序。

为了保护Period 实例的内部信息避免受到这种攻击对于构造器的每个可变参数进行保护性拷贝( defensivecopy )是必要的, 并且使用备份对象作为Period 实例的组件,而不使用原始的对象。保护性拷贝是在检查参数的有效性(详见第49 条)之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始的对象。

简而言之,如果一个类包含有从客户端得到或者返回到客户端的可变组件,这个类就必须保护性地拷贝这些组件。如果烤贝的成本受到限制,并且类信任它的客户端不会不恰当地修改组件,就可以在文档中指明客户端的职责是不得修改受到影响的组件,以此来代替保护性拷贝。

第51 条 谨慎设计方法签名*

谨慎地选择方法的名称。

不要过于追求提供便利的方法。

避免过长的参数列表。目标是四个参数或者更少。

对于参数类型,要优先使用接口而不是类。

对于boolean 参数,要优先使用两个元素的枚举类型。

第52 条 慎用重载*

对于重载方法的选择是静态的,而对于被覆盖的方法的选择则是动态的。选择被覆盖的方法的正确版本是在运行时进行的,选择的依据是被调用方法所在对象的运行时类型。

简而言之,“能够重载方法”并不意味着就“应该重载方法” 。一般情况下,对于多个具有相同参数数目的方法来说,应该尽量避免重载方法。在某些情况下,特别是涉及构造器的时候,要遵循这条建议也许是不可能的。在这种情况下,至少应该避免这样的情形:同一组参数只需经过类型转换就可以被传递给不同的重载方法。如果不能避免这种情形,例如,因为正在改造一个现有的类以实现新的接口,就应该保证: 当传递同样的参数时,所有重载方法的行为必须一致。如果不能做到这一点,程序员就很难有效地使用被重载的方法或者构造器,同时也不能理解它为什么不能正常地工作。

第53 条 慎用可变参数*

在重视性能的情况下,使用可变参数机制要特别小心。每次调用可变参数方法都会导致一次数组分配和初始化。如果凭经验确定无法承受这一成本,但又需要可变参数的灵活性,还有一种模式可以让你如愿以偿。假设确定对某个方法95% 的调用会有3 个或者更少的参数,就声明该方法的5 个重载,每个重载方法带有0 至3 个普通参数,当参数的数目超过3 个时,就使用一个可变参数方法。如下:

    public void foo(){}public void foo(int val1){}public void foo(int val1,int val2){}public void foo(int val1,int val2,int val3){}public void foo(int val1,int val2,int val3,int... vals){}

简而言之,在定义参数数目不定的方法时,可变参数方法是一种很方便的方式。在使用可变参数之前, 要先包含所有必要的参数,并且要关注使用可变参数所带来的性能影响。

第54 条 返回零长度的数组或者集合,而不是null**

简而言之,永远不要返回null ,而不返回一个零长度的数组或者集合。如果返回null,那样会使API 更难以使用,也更容易出错,而且没有任何性能优势。

第55 条 谨慎返回optinal**

在Java 8 之前,要编写一个在特定环境下无法返回任何值的方法时,有两种方法:要么抛出异常,要么返回null (假设返回类型是一个对象引用类型) 。在Java 8 中,还有第三种方法可以编写不能返回值的方法。Optinal<T >类代表的是一个不可变的容器,它可以存放单个非null 的T 引用,或者什么内容都没有。不包含任何内容的optional 称为空( empty )。

Optional.empty()返回一个空的optional, Optional .of(value)返回一个包含了指定非null 值的optional 。将null 传入Optional.of(value)是一个编程错误。如果这么做,该方法将会抛出NullPointerException 。Optional.ofNullable(value)方法接受可能为null 的值,当传入null 值时就返回一个空的optional 。永远不要通过返回Optional 的方法返回null :因为它彻底违背了optional 的本意。

总而言之,如果发现自己在编写的方法始终无法返回值,并且相信该方法的用户每次在调用它时都要考虑到这种可能性,那么或许就应该返回一个optional 。但是,应当注意到与返回optional 相关的真实的性能影响;对于注重性能的方法,最好是返回一个null ,或者抛出异常。最后,尽量不要将optional 用作返回值以外的任何其他用途。

第56 条 为所有导出的API 元素编写文档注释**

Javadoc 利用特殊格式的文档注释( documentationcomment ,通常被写作doc comment ),根据源代码自动产生API 文挡。

为了正确地编写API 文档,必须在每个被导出的类、接口、构造器、方法和域声明之前增加一个文档注释。方法的文档注释应该简洁地描述出它和客户端之间的约定。除了专门为继承而设计的类中的方法(详见第四条)之外,这个约定应该说明这个方法做了什么,而不是说明它是如何完成这项工作的。

为了完整地描述方法的约定,方法的文档注释应该让每个参数都有一个@ pa ram 标签,以及一个@ return 标签(除非这个方法的返回类型为void ),以及对于该方法抛出的每个异常,无论是受检的还是未受检的都应有一个@ throws 标签(详见第74 条) 。如果@ return标签中的文本与方法的描述一致,就允许省略,具体取决于你所遵循的编码标准。

Javadoc 生成的文档是 HTML 格式,而这些 HTML 格式化的标识符并不是 javadoc 加的,而是我们在写注释的时候写上去的。比如,需要换行时,不是敲入一个回车符,而是写入
,如果要分段,就应该在段前写入

。因此,格式化文档需要在文档注释中添加相应的 HTML 标识。文档注释的正文并不是直接复制到输出文件 (文档的 HTML 文件),而是读取每一行后,删掉前导的 * 号及 * 号以前的空格,再输出到文档的。

简而言之,要为API 编写文档,文档注释是最好、最有效的途径。对于所有可导出的API 元素来说,使用文档注释应该被看作是强制性的要求。要采用一致的风格来遵循标准的约定。记住,在文档注释内部出现任何HTML 标签都是允许的,但是HTML 元字符必须要经过转义。

第九章 通用编程

  本章主要讨论Java 语言的细枝末节,包含局部变量的处理、控制结构、类库的用法、各种数据类型的用法,以及两种不是由语言本身提供的机制(反射机制和本地方法)的用法。最后讨论了优化和命名惯例。

第57 条 将局部变量的作用域最小化*

本条目与第15 条使类和成员的可访问性最小化本质上是类似的。将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性。

要使局部变量的作用域最小化,最有力的方法就是在第一次要使用它的地方进行声明。

几乎每一个局部变量的声明都应该包含一个初始化表达式。如果你还没有足够的信息来对一个变量进行有意义的初始化,就应该推迟这个声明,直到可以初始化为止。

第58 条 for-each 循环优先于传统的for 循环*

在数组或者集合中,迭代器和索引变量都会造成一些混乱一一而你需要的只是元素而已。

for循环相比于while循环它可以使局部变量更小化,同时程序更加简短增强了可读性。

for-each循环在简洁性和预防bug方面有着传统for循环无法比拟的优势,并且没有性能损失,所以我们应该尽可能使用for-each循环

for-each循环通过完全隐藏迭代器或者索引变量,避免了混乱和出错的可能。当见到冒号(:)时,可以把它读作“在… …里面” 。for(Element e : elements) { } 可以读作“对于元素集合elements 中的每一个元素e ”。

遗憾的是,有三种常见的情况无法使用for- each 循环:解析过滤(遍历的时候删除一些元素)、转换(遍历的时候修改一些元素)、平行迭代(平行遍历多个集合)。

总而言之,与传统的for 循环相比, for- each 循环在简洁性、灵活性以及出错预防性方面都占有绝对优势,并且没有性能惩罚的问题。因此,当可以选择的时候, for- each 循环应该优先于for 循环。

第59 条 了解和使用类库***

Java 必知必会的 20 种常用类库和 API:https://mp.weixin.qq.com/s/23kYogWMtirrEmIWAqeR5Q 该文总结了日志、JSON解析、单测、XML解析、字节码处理、数据库连接池、集合类、邮件、加密、嵌入式SQL数据库、JDBC故障诊断以及序列化等20个方面的常用类库。都是你日常开发经常可能要用到的,现在不用不要紧,但是你要知道有这么一篇文章可以供你参考。

使用标准类库的第一个好处是,通过使用标准类库,可以充分利用这些编写标准类库的专家的知识,以及在你之前的其他人的使用经验。比如现在选择随机数生成器时,大多使用 ThreadLocalRandom。它会产生更高质量的随机数,并且速度非常快。

使用标准类库的第二个好处是,不必浪费时间为那些与工作不太相关的问题提供特别的解决方案。就像大多数程序员一样,应该把时间花在应用程序上,而不是底层的细节上。

使用标准类库的第三个好处是, 它们的性能往往会随着时间的推移而不断提高,无须你做任何努力。

使用标准类库的第四个好处是,它们会随着时间的推移而增加新的功能。

使用标准类库的最后一个好处是,可以使自己的代码融入主流。这样的代码更易读、更易维护、更易被大多数的开发人员重用。

既然有那么多的优点,使用标准类库机制而不选择专门的实现,这显然是符合逻辑的,然而还是有相当一部分的程序员没有这样做。为什么呢?可能他们并不知道有这些类库机制的存在。在每个重要的发行版本中,都会有许多新的特性被加入到类库中,所以与这些新特性保持同步是值得的。每当Java 平台有重要的发行时,都会发布一个网页来说明新的特性,这些网页值得好好读一读。

这些标准类库太庞大了,以至于不可能学完所有的文档,但是每个程序员都应该熟悉java. lang 、java.util 、java.io 及其子包中的内容。比如Collections Framework (集合框架)和Stream 类库值得一读。

总而言之,不要重复发明轮子。如果你要做的事情看起来是十分常见的,有可能类库中已经有某个类完成了这样的工作。如果确实是这样,就使用现成的;如果还不清楚是否存在这样的类,就去查一查。一般而言,类库的代码可能比你向己编写的代码更好一些,并且会随着时间的推移而不断改进。这并不是在质疑你作为一个程序员的能力。从经济角度的分析表明:类库代码受到的关注远远超过大多数普通程序员在同样的功能上所能给予的投入。

第60 条 如果需要精确的答案,请避免使用float 和double**

float 和double 类型主要是为了科学计算和工程计算而设计的。它们执行二进制浮,或运算( binary floating-point arithmetic ),这是为了在广泛的数值范围上提供较为精确的快速近似计算而精心设计的。然而,它们并没有提供完全精确的结果,所以不应该被用于需要精确结果的场合。float 和double 类型尤其不适合用于货币计算,因为要让一个float 或者double 精确地表示0.1 (或者10的任何其他负数次方值)是不可能的。解决这个问题的正确方法是使用BigDecimal 、int 或者long 进行货币计算。

总而言之,对于任何需要精确答案的计算任务,请不要使用float 或者double 。如果你想让系统来处理十进制小数点,并且不介意因为不使用基本类型而带来的不便,就请使用BigDecimal 。使用BigDecimal 还有一些额外的好处,它允许你完全控制舍人,每当一个操作涉及舍入的时候,你都可以从8 种舍入模式中选择其一 。如果你正通过合法强制的舍入行为进行商务计算,使用BigDecimal 是非常方便的。如果性能非常关键,并且你又不介意自己处理十进制小数点,而且所涉及的数值又不太大,就可以使用int 或者long 。如果数值范围没有超过9 位十进制数字,就可以使用工int ;如果不超过18 位数字,就可以使用long ;如果数值可能超过18 位数字,就必须使用BigDecimal 。

第61 条 基本类型优先于装箱基本类型*

在基本类型和装箱基本类型之间有三个主要区别。第一,基本类型只有值, 而装箱基本类型则具有与它们的值不同的同一性。换句话说,两个装箱基本类型可以具有相同的值和不同的同一性。第二,基本类型只有函数值,而每个装箱基本类型则都有一个非函数值,除了它对应基本类型的所有函数值之外,还有个null 。最后一点区别是,基本类型通常比装箱基本类型更节省时间和空间。

对装箱基本类型运用 == 操作符几乎总是错误的。如果需要用比较器描述一个类型的自然顺序,应该避免同一性比较,比如应该避免比较new Integer(42)和new Integer(42),这会返回不是我们想要的结果。

几乎在任何一种情况下, 当在一项操作中混合使用基本类型和装箱基本类型时,装箱基本类型就会自动拆箱,这种情况无一例外。如果null 对象引用被自动拆箱,就会抛出一个NullPointerException 异常。

总而言之,当可以选择的时候,基本类型要优先于装箱基本类型。基本类型更加简单,也更加快速。如果必须使用装箱基本类型,要特别小心! 自动装箱减少了使用装箱基本类型的烦琐性,但是并没有减少它的风险。当程序用 == 操作符比较两个装箱基本类型时,它做了个同一性比较,这几乎肯定不是你所希望的。当程序进行涉及装箱和拆箱基本类型的混合类型计算时,它会进行拆箱, 当程序进行拆箱时,会抛出NullPointerException 异常。最后,当程序装箱了基本类型值时,会导致较高的资源消耗和不必要的对象创建。

第62 条 如果其他类型更适合,则尽量避免使用字符串**

字符串不适合代替其他的值类型。

字符串不适合代替枚举类型。

字符串不适合代替聚合类型。

字符串也不适合代替能力表( capabilities )。

总而言之,如果可以使用更加合适的数据类型,或者可以编写更加适当的数据类型,就应该避免用字符串来表示对象。若使用不当字符串会比其他的类型更加笨拙、更不灵活、速度更慢,也更容易出错。经常被错误地用字符串来代替的类型包括基本类型、枚举类型和聚合类型。

第63 条 了解字符串连接的性能**

要想产生单独一行的输出,或者构造一个字符串来表示-个较小的、大小固定的对象,使用连接操作符是非常合适的,但是它不适合运用在大规模的场景中。为连接n 个字符串而重复地使用字符串连接操作符,需要n 的平方级的时间。为了获得可以接受的性能,请用Str ingBuilder 代替String 。

原则很简单: 不要使用字符串连接操作符来合并多个字符串,除非性能无关紧要。否则,应该使用StringBuilder 的append 方法。另一种做法是使用字符数组,或者每次只处理一个字符串,而不是将它们组合起来。

第64 条 通过接口引用对象**

第51 条建议:应该使用接口而不是类作为参数类型。更通俗来讲,应该优先使用接口而不是类来引用对象。如果有合适的接口类型存在,那么对于参数、返回值、变量和域来说,就都应该使用接口类型进行声明。只有当你利用构造器创建某个对象的时候,才真正需要引用这个对象的类。

如果养成了用接口作为类型的习惯,程序将会更加灵活。当你决定更换实现时,所要做的就只是改变构造器中类的名称。

如果没有合适的接口存在,完全可以用类而不是接口来引用对象。以值类(value class)为例,比如String 和Biginteger 。

不存在适当接口类型的第二种情形是,对象属于一个框架,而框架的基本类型是类,不是接口。

不存在适当接口类型的最后一种情形是,类实现了接口但它也提供了接口中不存在的额外方法。

以上这些例子并不全面,而只是代表了一些“适合于用类来引用对象”的情形。实际上,给定的对象是否具有适当的接口应该是很显然的。如果是,用接口引用对象就会使程序更加灵活。如果没有适合的接口,就用类层次结构中提供了必要功能的最小的具体类来引用对象吧。

第65 条 接口优先于反射机制***

如果只是以非常有限的形式使用反射机制,虽然也要付出少许代价,但是可以获得许多好处。许多程序必须用到的类在编译时是不可用的,但是在编译时存在适当的接口或者超类,通过它们可以引用这个类(详见第64 条)。如果是这种情况,就可以用反射方式创建实例,然后通过它们的接口或者超类,以正常的方式访问这些实例。

类对于在运行时可能不存在的其他类、方法或者域的依赖性,用反射法进行管理是合理的,但是很少使用。如果要编写一个包,并且它运行的时候就必须依赖其他某个包的多个版本,这种做法可能就非常有用。具体做法就是,在支持包所需要的最小环境下对它进行编译,通常是最老的版本,然后以反射方式访问任何更加新的类或者方法。如果企图访问的新类或者新方法在运行时不存在,为了使这种方法有效你还必须采取适当的动作。所谓适当的动作,可能包括使用某种其他可替换的办法来达到同样的目的,或者使用简化的功能进行处理。

总而言之,反射机制是一种功能强大的机制,对于特定的复杂系统编程任务,它是非常必要的,但它也有一些缺点。如果你编写的程序必须要与编译时未知的类一起工作,如有可能,就应该仅仅使用反射机制来实例化对象,而访问对象时则使用编译时已知的某个接口或者超类。

第66 条 谨慎地使用本地方法**

使用本地方法来提高性能的做法随着JVM的发展已经不值得提倡。

使用本地方法有一些严重的缺陷。因为本地语言不是安全的(详见第50 条),所以使用本地方法的应用程序也不再能免受内存毁坏错误的影响。

总而言之,在使用本地方法之前务必三思。只有在极少数情况下需要使用本地方法来提高性能。如果你必须要使用本地方法来访问底层的资獗,或者遗留代码库,也要尽可能少用本地代码,并且要全面进行测试。本地代码中只要有一个Bug 都可能破坏整个应用程序。

第67 条 谨慎地进行优化***

不要为了性能而牺牲合理的结构。要努力编写好的程序而不是快的程序。如果好的程序不够快,它的结构将使它可以得到优化。

要努力避免那些限制性能的设计决策。

要考虑API 设计决策的性能后果。

总而言之,不要费力去编写快速的程序一-一应该努力编写好的程序,速度自然会随之而来。但在设计系统的时候,特别是在设计API 、交互层协议和永久数据格式的时候,一定要考虑性能的因素。当构建完系统之后,要测量它的性能。如果它足够快,你的任务就完成了。如果不够快,则可以在性能剖析器的帮助下,找到问题的根源,然后设法优化系统中相关的部分。第一个步骤是检查所选择的算法:再多的低层优化也无法弥补算法的选择不当。必要时重复这个过程,在每一次修改之后都要测量性能,直到满意为止。

第68 条 遵守普遍接受的命名惯例*

包名、模块名要有层次且小写。类和接口要驼峰命名表达且首字母大写。方法和域也是驼峰命名但首字母小写。常量域要全部大写且下划线分割。

包或者模块 org.junit.jupiter.api, com. google.common.collect

类或者接口 Stream, FutureTask , LinkedHashMap , HttpClien

方法或者域 remove , groupingBy , getCrc

常量域 MIN VALUE , NEGATIVE INFINITY

局部变量 i, denom , houseNum

类型参数 T, E, K, V, X , R, U, V, Tl , T2

总而言之,把标准的命名惯例当作一种内在的机制来看待,并且学着用它们作为第二特性。字面惯例是非常直接和明确的;语法惯例则更复杂,也更松散。

第十章 异常

  充分发挥异常的优点,可以提高程序的可读性、可靠性和可维护性。如果使用不当,它们也会带来负面的影响。本章提供了一些关于有效使用异常的指导原则。

第69 条 只针对异常的情况才使用异常**

顾名思义异常应该只用于异常的情况下;它们永远不应该用于正常的控制流。

如果对象将在缺少外部同步的情况下被并发访问,或者可被外界改变状态,就必须使用optional 返回值或者可识别的返回值,因为在调用“状态测试’方法和调用对应的“状态相关”方法的时间间隔之中,对象的状态有可能会发生变化。如果单独的“状态测试”方法必须重复“状态相关”方法的工作,从性能的角度考虑,就应该使用可被识别的返回值。如果所有其他方面都是等同的,那么“状态测试”方法则略优于可被识别的返回值。它提供了稍微更好的可读性,对于使用不当的情形可能更加易于检测和改正: 如果忘了去调用状态测试方法,状态相关的方法就会抛出异常,使这个Bug 变得很明显;如果忘了去检查可识别的返回值,这个Bug 就很难被发现。optional 返回值不会有这方面的问题。
总而言之,异常是为了在异常情况下使用而设计的。不要将它们用于普通控制流,也不要编写迫使它们这么做的API 。

第70 条 对可恢复的情况使用受检异常,对编程错误使用运行时异常**

在决定使用受检异常或是未受检异常时,主要的原则是: 如果期望调用者能够适当地恢复, 对于这种情况就应该使用受检异常。用运行时异常来表明编程错误。

运行时异常和错误。在行为上两者是等同的:它们都是不需要也不应该被捕获的可抛出结构。

实现的所有未受检的抛出结构都应该是RuntimeException 的子类(直接的或者间接的) 。不仅不应该定义Error 子类,甚至也不应该抛出AssertionError 异常。

总而言之,对于可恢复的情况,要抛出受检异常;对于程序错误,要抛出运行时异常。不确定是否可恢复,则抛出未受检异常。不要定义任何既不是受检异常也不是运行时异常的抛出类型。要在受检异常上提供方法,以便协助恢复。

第71 条 避免不必要地使用受检异常**

消除受检异常最容易的方法是,返回所要的结果类型的一个optional (详见第55 条) 。这个方法不抛出受检异常,而只是返回一个零长度的optional 。这种方法的缺点是,方法无法返回任何额外的信息,来详细说明它无法执行你想要的计算。相反,异常应该是具有描述性的类型,并且能够导出方法,以提供额外的信息。

总而言之,在谨慎使用的前提之下,受检异常可以提升程序的可读性;如果过度使用,将会使API 使用起来非常痛苦。如果调用者无法恢复失败,就应该抛出未受检异常。如果可以恢复,并且想要迫使调用者处理异常的条件,首选应该返回一个optional 值。当且仅当万一失败时,这些无法提供足够的信息,才应该抛出受检异常。

第72 条 优先使用标准的异常*

专家级程序员与缺乏经验的程序员一个最主要的区别在于,专家追求并且通常也能够实现高度的代码重用(不要重复造轮子)。

经常被重用的异常有:IllegalArgumentException;IllegalStateException;NullPointerException;IndexOutOfBoundsException;ConcurrentModificationException;UnsupportedOperationException。它们的使用场景如下:

不要直接重用Exception 、RuntimeException , Throwable 或者Error 。对待这些类要像对待抽象类一样。你无法可靠地测试这些异常,因为它们是一个方法可能抛出的其他异常的超类。

第73 条 抛出与抽象对应的异常**

更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常。这种做法称为异常转译。

总而言之,如果不能阻止或者处理来自更低层的异常,一般的做法是使用异常转译,只有在低层方法的规范碰巧可以保证“它所抛出的所有异常对于更高层也是合适的”情况下,才可以将异常从低层传播到高层。异常链对高层和低层异常都提供了最佳的功能:它允许抛出适当的高层异常,同时又能捕获低层的原因进行失败分析(详见第75 条) 。

第74 条 每个方法抛出的所有异常都要建立文档**

始终要单独地声明受检异常, 并且利用Javadoc 的@ throws 标签, 准确地记录下抛出每个异常的条件。如果一个公有方法可能抛出多个异常类,则不要使用“快捷方式”声明它会抛出这些异常类的某个超类。永远不要声明一个公有方法直接“ throws Exception “,或者更糟糕的是声明它直接“ throws Throwable ”,这是非常极端的例子。

虽然Java 语言本身并没有要求程序员为一个方法声明它可能会抛出的未受检异常,但是,如同受检异常一样,仔细地为它们建立文档是非常明智的。使用Javadoc 的@throws 标签记录下一个方法可能抛出的每个未受检异常,但是不要使用throws 关键字将未受检的异常包含在方法的声明中。

应该注意的是,为每个方法可能抛出的所有未受检异常建立文档是很理想的,但是在实践中并非总能做到这一点。

如果一个类中的许多方法出于同样的原因而抛出同一个异常,在该类的文档注释中对这个异常建立文档,这是可以接受的, 而不是为每个方法单独建立文档。

总而言之,要为你编写的每个方法所能抛出的每个异常建立文档。对于未受检异常和受检异常,以及抽象的方法和具体的方法一概如此。这个文档在文档注释中应当采用@throws 标签的形式。要在方法的throws 子句中为每个受检异常提供单独的声明,但是不要声明未受检的异常。如果没有为可以抛出的异常建立文档,其他人就很难或者根本不可能有效地使用你的类和接口。

第75 条 在细节消息中包含失败-捕获信息***

异常类型的toString 方法应该尽可能多地返回有关失败原因的信息,这一点特别重要。换句话说,异常的字符串表示法应该捕获失败,以便于后续进行分析。为了捕获失败,异常的细节信息应该包含“对该异常有贡献”的所有参数和域值。

由于在诊断和修正软件问题的过程中,许多人都可以看见堆战轨迹,因此千万不要在细节消息中包含密码、密钥以及类似的信息!

异常的细节消息不应该与“用户层次的错误消息”混为一谈,后者对于最终用户而言必须是可理解的。与用户层次的错误消息不同,异常的字符串表示法主要是让程序员或者网站可靠性工程师用来分析失败的原因。因此,信息的内容比可读性要重要得多。用户层次的错误消息经常被本地化,而异常的细节消息则几乎没有被本地化。

为了确保在异常的细节消息中包含足够的失败- 捕捉信息, 一种办法是在异常的构造器而不是字符串细节消息中引人这些信息。

提供这样的访问方法对受检的异常,比对未受检异常更为重要,因为失败一捕获信息对于从失败中恢复是非常有用的。程序员希望通过程序的手段来访问未受检异常的细节,这很少见(尽管也是可以想象的) 。然而,即使对于未受检异常,作为一般原则提供这些访问方法也是明智的(详见第12 条) 。

第76 条 努力使失败保持原子性**

当对象抛出异常之后,通常我们期望这个对象仍然保持在一种定义良好的可用状态之中, 即使失败是发生在执行某个操作的过程中间。对于受检异常而言,这尤为重要,因为调用者期望能从这种异常中进行恢复。一般而言,失败的方法调用应该使对象保持在被调用之前的状态。具有这种属性的方法被称为具有失败原子性。

有几种途径可以实现这种效果。

  • 最简单的办法莫过于设计一个不可变的对象,如果对象是不可变的,失败原子性就是显然的。

  • 对于在可变对象上执行操作的方法,获得失败原子性最常见的办法是,在执行操作之前检查参数的有效性。

  • 一种类似的获得失败原子性的办法是,调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生。

  • 第四种获得失败原子性的办法是,在对象的一份临时拷贝上执行操作,当操作完成之后再用临时拷贝中的结果代替对象的内容。

  • 最后一种获得失败原子’性的办法远远没有那么常用,做法是编写一段恢复代码(recovery code ),由它来拦截操作过程中发生的失败,以及便对象回滚到操作开始之前的状态上。

总而言之,作为方法规范的一部分,它产生的任何异常都应该让对象保持在调用该方法之前的状态。如果违反这条规则, API 文档就应该清楚地指明对象将会处于什么样的状态。遗憾的是,大量现有的API 文档都未能做到这一点。

第77 条 不要忽略异常*

本条目中的建议同样适用于受检异常和未受检异常。不管异常代表了可预见的异常条件,还是编程错误,用空的catch 块忽略它,都将导致程序在遇到错误的情况下悄然地执行下去。然后,有可能在将来的某个点上,当程序不能再容忍与错误源明显相关的问题时,它就会失败。正确地处理异常能够彻底避免失败。只要将异常传播给外界,至少会导致程序
迅速失败,从而保留了有助于调试该失败条件的信息。

第十一章 并发

  线程机制允许同时进行多个活动。并发程序设计比单线程程序设计要困难得多,因为有更多的东西可能出锚,也很难重现失败。但是你无法避免并发,因为我们所做的大部分事情都需要并发,而且并发也是能否从多核的处理器中获得好的性能的一个条件,这些现在都是很平常的事了。本章阐述的建议可以帮助你编写出清晰、正确、文档组织良好的并发程序。

Java并发学习:https://blog.csdn.net/qq_41822345/article/details/104620428

第78 条 同步访问共享的可变数据**

关键字synchronized 可以保证在同一时刻,只有一个线程可以执行某一个方法,或者某一个代码块。许多程序员把同步的概念仅仅理解为一种互斥( mutual exclusion )的方式,即,当一个对象被一个线程修改的时候,可以阻止另一个线程观察到对象内部不一致的状态。

为了在线程之间进行可靠的通信,也为了互斥访问,同步是必要的。这归因于Java 语言规范中的内存模型( memory model ),它规定了一个线程所做的变化何时以及如何变成对其他线程可见。

只同步写方法还不够! 除非读和写操作都被同步,否则无法保证同步能起作用。有时候,会在某些机器上看到只同步了写(或读)操作的程序看起来也能正常工作,但是在这种情况下,表象具有很大的欺骗性。

总而言之, 当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步。如果没有同步,就无法保证一个线程所做的修改可以被另一个线程获知。未能同步共享可变数据会造成程序的活性失败( liveness failure )和安全性失败( safety failure ) 。这样的失败是最难调试的。它们可能是间歇性的,且与时间相关,程序的行为在不同的虚拟机上可能根本不同。如果只需要线程之间的交互通信,而不需要互斥, vo latile 修饰符就是一种可以接受的同步形式,但要正确地使用它可能需要一些技巧。

第79 条 避免过度同步***

为了避免活性失败和安全性失败,在一个被同步的方法或者代码块中,永远不要放弃对害户端的控制。换句话说,在一个被同步的区域内部,不要调用设计成要被覆盖的方法,或者是由客户端以函数对象的形式提供的方法。

通常来说,应该在同步区域内做尽可能少的工作。获得锁,检查共享数据,根据需要转换数据,然后释放锁。

在这个多核的时代,过度同步的实际成本并不是指获取锁所花费的CPU 时间;而是指失去了并行的机会,以及因为需要确保每个核都有一个一致的内存视图而导致的延迟。过度同步的另一项潜在开销在于,它会限制虚拟机优化代码执行的能力。

如果正在编写一个可变的类,有两种选择:省略所有的同步,如果想要并发使用,就允许客户端在必要的时候从外部同步,或者通过内部同步,使这个类变成是线程安全的(详见第82 条),你还可以因此获得明显比从外部锁定整个对象更高的并发性。java.util 中的集合(除了已经废弃的Vector 和Hash table 之外)采用了前一种方法,而java.util.concurrent 中的集合则采用了后一种方法(详见第81 条) 。

在Java 平台出现的早期,许多类都违背了这些指导方针。例如, StringBuf fer 实例几乎总是被用于单个线程之中,而它们执行的却是内部同步。为此, StringBuffer 基本上都由StringBuilder 代替,它是一个非同步的StringBuf fer 。同样地,java.util.Random中线程安全的伪随机数生成器,被java.util.co 口current.ThreadLocalRandom 中非同步的实现取代,主要也是出于上述原因。当你不确定的时候,就不要同步类,而应该建立文档,注明它不是线程安全的。

总而言之,为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法。更通俗地讲,要尽量将同步区域内部的工作量限制到最少。当你在设计一个可变类的时候,要考虑一下它们是否应该自己完成同步操作。在如今这个多核的时代,这比永远不要过度同步来得更重要。只有当你有足够的理由一定要在内部同步类的时候,才应该这么做,同时还应该将这个决定清楚地写到文档中(详见第82 条) 。

第80 条 executor 、task 和stream 优先于线程***

为特殊的应用程序选择executor service 是很有技巧的。如果编写的是小程序,或者是轻量负载的服务器,使用Executors.newCachedThreadPool 通常是个不错的选择,因为它不需要配置,并且一般情况下能够正确地完成工作。但是对于大负载的服务器来说,缓存的线程池就不是很好的选择了!在缓存的线程池中,被提交的任务没有排成队列,而是直接交给线程执行。如果没有线程可用,就创建一个新的线程。如果服务器负载得太重,以致它所有的CPU 都完全被占用了,当有更多的任务时,就会创建更多的线程,这样只会使情况变得更糟。因此,在大负载的产品服务器中,最好使用Executors.newFixedThreadPool ,它为你提供了一个包含固定线程数目的线程池,或者为了最大限度地控制它,就直接使用ThreadPoolExecutor 类。

在Executor Framework 中,工作单元和执行机制是分开的。现在关键的抽象是工作单元,称作任务( task ) 。任务有两种:
Runnable 及其近亲Callable (它与Runnab le 类似,但它会返回值,并且能够抛出任意的异常) 。执行任务的通用机制是executor service 。如果你从任务的角度来看问题,并让一个executor service 替你执行任务,在选择适当的执行策略方面就获得了极大的灵活性。从本质上讲, Executor Framework 所做的工作是执行, Collections Fram巳work 所做的工作是聚合(aggregation ) 。

第81 条 并发工具优先于wait 和notify***

java.util.concurre 口t 中更高级的工具分成三类: Executor Framework 、并发集合( Concurrent Collection )以及同步器( Synchronizer)。这些 Java 平台提供的更高级的并发工具,它们可以完成以前必须在wait 和notify 上手写代码来完成的各项工作。既然正确地使用wait 和notify 比较困难,就应该用更高级的并发工具来代替。

并发集合并发集合导致同步的集合大多被废弃了。比如, 应该优先使用ConcurrentHashMap ,而不是使用Collections.synchronizedMap 。仅仅只一步就可以极大的提升并发应用程序的性能。

同步器( Synchronizer )是使线程能够等待另一个线程的对象,允许它们协调动作。最常用的同步器是CountDownLatch 和Semaphore 。较不常用的是CyclicBarrier 和Exchanger 。功能最强大的同步器是Phaser 。

wait 方法被用来使线程等待某个条件。它必须在同步区域内部被调用,这个同步区域将对象锁定在了调用wait 方法的对象上。

为了唤醒正在等待的线程,你应该使用notify 方法还是notifyAll方法(回忆一下, notify 方法唤醒的是单个正在等待的线程,假设有这样的线程存在,而notifyAll 方法唤醒的则是所有正在等待的线程) 。一种常见的说法是,应该始终使用notifyAll 方法。这是合理而保守的建议。它总会产生正确的结果,因为它可以保证你将会唤醒所有需要被唤醒的线程。你可能也会唤醒其他一些线程,但是这不会影响程序的正确性。这些线程醒来之后,会检查它们正在等待的条件,如果发现条件并不满足,就会继续等待。

简而言之,直接使用wait 方法和notify 方法就像用‘Ji-发汇编语言’进行编程一样,而java.util.concurre 口t 则提供了更高级的语言。没有理由在新代码中使用wait方法和notify 方法,即使有,也是极少的。如果你在维护使用wait 方法和ηotify方法的代码,务必确保始终是利用标准的模式从while 循环内部调用wait 方法。一般情况下,应该优先使用notifyAll 方法,而不是使用notify 方法。如果使用notify 方法,请一定要小心,以确保程序的活性。

第82 条 线程安全性的文档化**

当一个类的方法被并发使用的时候,这个类的行为如何,是该类与其客户端程序建立的约定的重要组成部分。如果你没有在一个类的文档中描述其行为的并发性情况,使用这个类的程序员将不得不做出某些假设。如果这些假设是错误的,所得到的程序就可能缺少足够的同步(详见第78 条),或者过度同步(详见第79 条) 。

一个类为了可被多个钱程安全地使用,必须在文档中清楚地说明它所支持的线程安全性级别。下述分项概括了线程安全性的几种级别:不可变的( immutable )、无条件的钱程安全、有条件的结程安全、非线程安全、线程对立。

在文档中描述一个有条件的线程安全类要特别小心。你必须指明哪个调用序列需要外部同步,还要指明为了执行这些序列,必须获得哪一把锁(极少的情况下是指哪几把锁) 。通常情况下,这是指作用在实例自身上的那把锁,但也有例外。

简而言之,每个类都应该利用字斟句酌的说明或者线程安全注解,清楚地在文档中说明它的线程安全属性。synchronized 修饰符与这个文档毫无关系。有条件的线程安全类必须在文档中指明“哪个方法调用序列需要外部同步,以及在执行这些序列的时候要获得哪
把锁” 。如果你编写的是无条件的线程安全类,就应该考虑使用私有锁对象来代替同步的方法。这样可以防止客户端程序和子类的不同步干扰,让你能够在后续的版本中灵活地对并发控制采用更加复杂的方法。

第83 条 慎用延迟初始化**

延迟初始化就像一把双刃剑。它降低了初始化类或者创建实例的开销,却增加了访问被延迟初始化的域的开销。根据延迟初始化的域的哪个部分最终需要初始化、初始化这些域要多少开销,以及每个域多久被访问一次,延迟初始化(就像其他的许多优化一样)实际上降低了性能。

如果出于性能的考虑而需要对静态域使用延迟初始化,就使用lazy initialization holder class 模式。这种模式(也称作initialize-on-demand holder class idiom )保证了类要到被用到的时候才会被初始化。

private static class FiledHolder{static final FiledType filed = new FiledType();
}
private static FiledType getFiled(){return FiledHolder.filed;
}

如果出于性能的考虑而需要对实例域使用延迟初始化, 就使用双重检查模式( doub lecheck idiom ) 。这种模式避免了在域被初始化之后访问这个域时的锁定开销。

private volatile FiledType field;private FiledType getFieldType(){//temp这个变量的作用是确保field只在已经被初始化的情况下读取一次。FiledType temp = field;if(temp == null){synchronized (this){if(field == null){field= temp = new FiledType();}}}return temp;
}

有时可能需要延迟初始化-个可以接受重复初始化的实例域。如果处于这种情况,就可以使用双重检查模式的一个变量,它负责分配第二次检查。没错,它就是单重检查模式。

    private volatile FiledType field;private FiledType getFieldType(){FiledType temp = field;if(temp == null){field= temp = new FiledType();}return temp;}

总而言之,大多数的域应该正常地进行初始化,而不是延迟初始化。如果为了达到性能目标,或者为了破坏有害的初始化循环,而必须延迟初始化一个域,就可以使用相应的延迟初始化方法。对于实例域,就使用双重检查模式( double-check idiom );对于静态域, 则使用lazy initialization holder class idiom 。对于可以接受重复初始化的实例域,也可以考虑使用单重检查模式( single-check idiom )。

第84 条 不要依赖于线程调度器***

任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的。

要编写出健壮、响应良好、可移植的多线程应用程序,最好的办法是确保可运行线程的平均数量不明显多于处理器的数量。这使得线程调度器没有更多的选择:它只需要运行这些可运行的线程,直到它们不再可运行为止。

线程不应该一直处于忙一等( busy-wait )的状态,应反复地检查一个共享对象,以等待某些事情发生。

总而言之,不要让应用程序的正确性依赖于线程调度器。否则,得到的应用程序将既不健壮,也不具有可移植性。同样,不要依赖Thread.yield 或者线程优先级。这些机制都只是影响到调度器。线程优先级可以用来提高一个已经能够正常工作的程序的服务质量,但永远不应该用来“修正” 一个原本并不能工作的程序。

第十二章 序列化

  本章讨论对象序列化( object serialization ),它是Java 的一个框架,用来将对象编码成字节流(序列化),并从字节流编码中重新构建对象(反序列化) 。一旦对象被序列化后,它的编码就可以从一台正在运行的虚拟机被传递到另一台虚拟机上,或者被存储到磁盘上,供后续反序列化时使用。本章主要关注序列化的风险,以及如何将风险降到最低。

第85 条 其他方法优先于Java 序列化**

序列化的根本问题在于,其攻击面( attack surface)过于庞大,无法进行防护,并且它还在不断地扩大:对象图是通过在ObjectiputStream 上调用readObject 方法进行反序列化的。这个方法其实是个神奇的构造器,它可以将类路径上几乎任何类型的对象都实例化,只要该类型实现了Serializable 接口。在反序列化字节流的过程中,该方法可以执行以上任意类型的代码,因此所有这些类型的代码都是攻击面的一部分。

攻击面包括Java 平台类库中的类、第三方类库如Apache Commons Collections 中的类,以及应用本身的类。

攻击者和安全研究员都在研究Java 类库和常用的第三方类库中可序列化的类型,寻找在进行潜在危险活动的反序列化期间被调用的方法。这些方法被称作指令片段(gadget) 。 多个指令片段可以一起使用,形成一个指令片段链。指令片段链的功能十分强大,允许攻击者在底层硬件中执行任意的本机代码。

如果不使用任何指令片段,对于需要长时间进行反序列化的简短字节流,只要引发反序列化,就可以轻松地展开一次拒绝服务攻击。这样的字节流被称作反序列化炸弹

避免序列化攻击的最佳方式是永远不要反序列化任何东西。

如果无法完全避免Java 序列化,或许是因为需要在Java 序列化的遗留系统环境中工作,下一步最好永远不要反序列化不被信任的数据。尤其是永远不应该接受来自不信任资源的RMI 通信。如果无法避免序列化,又不能绝对确保被反序列化的数据的安全性,就应利用Java9 中新增的对象反序列化过滤。

为了避免Java 序列化的诸多风险,还有许多其他机制可以完成对象和字节序列之间的转化,它们同时还能带来很多便利,诸如跨平台支持、高性能、一个大型的工具生态系统,以及一个广阔的专家社区。本书把这些机制作称跨平台的结构化数据表示法

最前沿的跨平台结构化数据表示法是JSON (基于文本的,用于浏览器和服务器之间的通信)和Protocol Buffers(基于二进制的,用于服务器之间保存和交换)。

总而言之,序列化是很危险的,应该予以避免。如果是重新设计一个系统,一定要用跨平台的结构化数据表示法代替,如JSON 或者protobuf。不要反序列化不被信任的数据。如果必须这么做,就要使用对象的反序列化过滤,但要注意的是,它并不能确保阻止所有的攻击。不要编写可序列化的类。如果必须这么做一定要倍加小心地进行试验。

第86 条 谨慎地实现Serializable 接口**

实现Serializable 接口而付出的最大代价是,一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性。如果一个类实现了Serializable 接口,它的字节流编码(或者说序列化形式)就变成了它的导出的API 的一部分。

换句话说, 如果接受了默认的序列化形式,这个类中私有的和包级私有的实例域将都变成导出的API 的一部分,这不符合“可访问性最小”的实践准则。

实现Serializable 的第一个代价是:如果接受了默认的序列化形式,并且以后又要改变这个类的内部表示法, 则结果可能导致序列化形式的不兼容。

序列化会使类的演变受到限制,这种限制的一个例子与流的唯一标识符( stream unique identifier )有关,通常称它为序列版本UID。如果你没有声明一个显式的序列版本UID ,系统会自动产生UID,该值会受到类名称、它所实现的接口的名称,以及所有公有的和受保护的成员的名称所影响。因此当类信息改变时就会使得兼容性遭到破坏,在运行时导致InvalidClassException 异常。

实现Serializable 的第二个代价是,它增加了出现Bug 和安全漏洞的可能性。序列化机制是一种语言之外的对象创建机制,反序列化机制( deserialization )都是一个“隐藏的构造器”,具备与其他构造器相同的特点。

序列化就如同另外一个构造函数,只不过是有由stream进行创建的。如果字段有一些条件限制的,特别是非可变的类定义了可变的字段会反序列化可能会有问题。可以在readObject方法中添加条件限制,也可以在readResolve中做。

实现Serializable 的第三个代价是,随着类发行新的版本,相关的测试负担也会增加。

实现序列化接口可能会带来很大的开销,因此需要权衡。根据经验,Biginteger 和Instant 等值类应该实现Serializable 接口,大多数的集合类也应该如此。代表活动实体的类,比如线程池( thread pool ), 一般不应该实现Serializable接口。

为了继承而设计的类(详见第19 条)应该尽可能少地去实现Serializable 接口,用户的接口也应该尽可能少继承Serializable 接口。如果违反了这条规则,扩展这个类或者实现这个接口的程序员就会背上沉重的负担。

简而言之,千万不要认为实现Serializable 接口会很容易。除非一个类只在受保护的环境下使用,在这里版本之间永远不会交互,服务器永远不会暴露给不可信任的数据,否则,实现Serializable 接口就是个很严肃的承诺,必须认真对待。如果一个类允许继承, 则更要加倍小心。

第87 条 考虑使用自定义的序列化形式***

如果事先没有认真考虑默认的序列化形式是否合适,则不要贸然接受。接受默认的序列化形式是一个非常重要的决定,需要从灵活性、性能和正确性等多个角度对这种编码形式进行考察。一般来讲,只有当自行设计的自定义序列化形式与默认的序列化形式基本相同时A,才能接受默认的序列化形式。

换句话说,默认的序列化形式描述了该对象内部所包含的数据,以及每一个可以从这个对象到达的其他对象的内部数据。它也描述了所有这些对象被链接起来后的拓扑结构。对于一个对象来说,理想的序列化形式应该只包含该对象所表示的逻辑数据,而逻辑数据与物理表示法应该是各自独立的。

如果一个对象的物理表示法等罔于它的逻辑内容, 可能就适合于使用默认的序列化形式。即使你确定了默认的序列化形式是合适的,通常还必须提供一个readObject 方法以保证约束关系和安全性。

实现序列化有两种方式 :Externalizable,该接口是继承于Serializable 。区别在于Externalizable多声明了两个方法readExternal和writeExternal,子类必须实现二者。Serializable是内建支持的也就是直接implement即可,但Externalizable的实现类必须提供readExternal和writeExternal实现。对于Serializable来说,Java自己建立对象图和字段进行对象序列化,可能会占用更多空间。而Externalizable则完全需要程序员自己控制如何写/读,麻烦但可以有效控制序列化的存储的内容。

不管你选择了哪种序列化形式,都要为自己编写的每个可序列化的类声明一个显式的序列版本UID。这样可以避免序列版本UID 成为潜在的不兼容根源。一行代码就可以:

private static final long serialVersionUID = randomLongValue;

总而言之,当你决定要将一个类做成可序列化的时候(详见第86 条),请仔细考虑应该采用什么样的序列化形式。只有当默认的序列化形式能够合理地描述对象的逻辑状态时,才能使用默认的序列化形式;否则就要设计一个自定义的序列化形式,通过它合理地描述对象的状态。你应该分配足够多的时间来设计类的序列化形式,就好像分配足够多的时间来设计它的导出方法一样(详见第51 条) 。正如你无法在将来的版本中去掉导出方法一样,你也不能去掉序列化形式中的域;它们必须被永久地保留下去,以确保序列化兼容性。选择错误的序列化形式对于一个类的复杂性和性能都会有永久的负面影响。

第88 条 保护性地编写readObject 方法***

readObject 方法实际上相当于另一个公有的构造器,如同其他的构造器一样,它也要求警惕同样的所有注意事项。构造器必须检查其参数的有效性(详见第49条),并且在必要的时候对参数进行保护性拷贝(详见第50 条),同样地, readObject 方法也需要这样做。如果readObject 方法无法做到这两者之一,对于攻击者来说,要违反这个类的约束条件相对就比较简单了。

readObject 方法,该方法首先调用defaultReadObject ,然后检查被反序列化之后的对象的有效性。如果有效性检查失败,readObject 方法就抛出一个invalidObjectException 异常,使反序列化过程不能成功地完成。对于每个可序列化的不可变类,如果它包含了私有的可变组件,那么在它的readObject 方法中必须要对这些组件进行保护性拷贝。

如何写出更加健壮的readObject 方法:

  • 对于对象引用域必须保持为私有的类,要保护性地拷贝这些域中的每个对象。不可变类的可变组件就属于这一类别。
  • 对于任何约束条件,如果检查失败,则抛出一个InvalidObjectException 异常。这些检查动作应该跟在所有的保护性拷贝之后。
  • 如果整个对象图在被反序列化之后必须进行验证,就应该使用ObjectInputValidation接口。
  • 无论是直接方式还是间接方式,都不要调用类中任何可被覆盖的方法。

总而言之,在编写readObject 方法的时候,都要这样想:你正在编写一个公有的构造器,无论给它传递什么样的字节流,它都必须产生一个有效的实例。不要假设这个字节流一定代表着一个真正被序列化过的实例。虽然在本条目的例子中,类使用了默认的序列化形式,但是,所有讨论到的有可能发生的问题也同样适用于使用自定义序列化形式的类。

第89 条 对于实例控制,枚举类型优先于readResolve**

非枚举类型单例在实现序列化接口之后就不再是一个单例。任何一个readObject 方法,不管是显式的还是默认的,都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例。readResolve 特性允许你用readObject 创建的实例代替另一个实例。

事实上, 如果依赖readResolve 进行实例控制,带有对象引用类型的所有实例域则都必须声明为transient 。

read Resolve 的可访问性( accessibili ty )很重要。如果把readResolve 方法放在一个final 类上,它就应该是私有的。如果把readResolver 方法放在一个非final 类上,就必须认真考虑它的可访问性。如果它是私有的,就不适用于任何子类。如果它是包级私有的,就只适用于同一个包中的子类。如果它是受保护的或者公有的,就适用于所有没有覆盖它的子类。如果readResolve 方法是受保护的或者是公有的,并且子类没有覆盖它,对序列化过的子类实例进行反序列化,就会产生一个超类实例,这样有可能导致
ClassCastException 异常。

总而言之,应该尽可能地使用枚举类型来实施实例控制的约束条件。如果做不到,同时又需要一个既可序列化又是实例受控的类,就必须提供一个readResolver 方法,并确保该类的所有实例域都为基本类型,或者是瞬时的。

第90 条 考虑用序列化代理代替序列化实例***

决定实现Serializable接口,会增加出错和出现安全问题的可能性,因为它允许利用语言之外的机制来创建实例,而不是用普通的构造器。然而,有一种方法可以极大地减少这些风险。这种方法就是序列化代理模式( serialization proxy pattern ) 。

序列化代理模式相当简单。首先,为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类的实例的逻辑状态。这个嵌套类被称作序列化代理( serialization proxy ),它应该有一个单独的构造器,其参数类型就是那个外围类。

序列化代理模式有两个局限性。它不能与可以被客户端扩展的类相兼容(详见第19条) 。它也不能与对象图中包含循环的某些类相兼容: 如果你企图从一个对象的序列化代理的readResolve 方法内部调用这个对象中的方法,就会得到一个ClassCastException异常,因为你还没有这个对象,只有它的序列化代理。

序列化代理模式所增强的功能和安全性是需要性能开销的。

总而言之,当你发现自己必须在一个不能被客户端扩展的类上编写readObject 或者writeObject 方法时,就应该考虑使用序列化代理模式。要想稳健地将带有重要约束条件的对象序列化时,这种模式可能是最容易的方法。

《Effective Java》真的是一本值得一直去钻研的好书相关推荐

  1. Effective Java 在工作中的应用总结

    简介: <Effective Java>是一本经典的 Java 学习宝典,值得每位 Java 开发者阅读.笔者将书中和平日工作较密切的知识点做了部分总结. 作者 | 宜秋 来源 | 阿里技 ...

  2. Java 程序员们值得一看的好书推荐

    作者:mdcc 链接:https://zhuanlan.zhihu.com/p/23444919 来源:知乎 著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. "学习的最 ...

  3. Java教程-Java 程序员们值得一看的好书推荐

    学习的最好途径就是看书",这是我自己学习并且小有了一定的积累之后的第一体会.个人认为看书有两点好处: 能出版出来的书一定是经过反复的思考.雕琢和审核的,因此从专业性的角度来说,一本好书的价值 ...

  4. Java程序员们值得一看的好书推荐

    学习的最好途径就是看书 "学习的最好途径就是看书",这是我自己学习并且小有了一定的积累之后的第一体会.个人认为看书有两点好处: 1.能出版出来的书一定是经过反复的思考.雕琢和审核的 ...

  5. Google 出品的 Java 编码规范,值得借鉴!

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试资料 译者:Hawstein 原文:google.github.io/s ...

  6. 《Effective java》—–读书笔记

    2015年进步很小,看的书也不是很多,感觉自己都要废了,2016是沉淀的一年,在这一年中要不断学习.看书,努力提升自己!预计在2016年要看12本书,主要涉及java基础.Spring研究.java并 ...

  7. Java编程哪些书值得推荐及计算机语言java中得List[图]

    一.Java编程哪些书值得推荐? 书籍是人类进步的阶梯,在互联网不算发达的十年前(特指09年之前),学编程是要靠各种编程相关的书滴,大部分时间都必须沉迷于图书馆才能学好.现如今不同了,在线教育满天飞的 ...

  8. Effective Java读书笔记五:异常

    第57条:只针对异常的情况才使用异常 异常是为了在异常情况下使用而设计的,不要将它们用于普通的控制流,也不要编写迫使它们这么做的API. 下面部分来自:异常 如果finally块中出现了异常没有捕获或 ...

  9. Effective Java:创建和销毁对象

    前言: 读这本书第1条规则的时候就感觉到这是一本很好的书,可以把我们的Java功底提升一个档次,我还是比较推荐的.本博客是针对<Effective Java>这本书第2章所写的一篇读书笔记 ...

最新文章

  1. 10行代码AC——1016 部分A+B (15分)
  2. php ci 框架 扩展缓存类,CodeIgniter扩展核心类实例详解
  3. 极路由 1S 使用经验
  4. mysql主从安装_MySQL主从详细安装步骤
  5. JAVA基础系列:反射
  6. gcc oracle mysql_[转]Windows下用GCC连接MySQL数据库
  7. 尽快更新!Chrome 修复两个已遭在野利用的 0day
  8. 让django完成翻译,迁移数据库模型
  9. android点击改变字体颜色,Android Textview 动态 部分文字点击 改变颜色
  10. Johnson算法实现流水作业最优调度
  11. HD Audio总线驱动加载失败彻底解决!
  12. 传微信要开直播业务:主流社交工具全面杀入直播战场
  13. 黑马旅游网 —— 登录/注册案例
  14. 颜色空间:RGB,CMY,HSV,HSL,Lab详解
  15. python01g内存读取10g文件并排序_将大文件逐行读取到Python2.7中时的内存使用
  16. pytest框架中setup、teardown和setup_class、teardown_class
  17. python求平均值
  18. mac 上安装 ettercap-gtk,driftnet
  19. 大一学生网页课程作业 南京介绍网页设计 学生家乡网页设计作品静态 HTML网页模板源码 html我的家乡网页作业
  20. 安川服务器显示030,安川伺服驱动器常见故障报警代码解析

热门文章

  1. JS 中的类数组对象如何转换为数组?
  2. 【毕业设计】java社区疫情防控管理系统(源码+论文)
  3. js运算符之三目运算符
  4. python中的pylab_在Python2.7中,在函数级导入pylab的首选方法是什么?
  5. 开个天窗——爆改华硕-飞行堡垒9散热
  6. Mac电脑截图和标注可以一步到位吗?当然可以!
  7. 3.10 创建教学管理数据库的数据表
  8. SVN 树冲突解决详解
  9. amebaZ2 amebaZ II RTL8710CM 8710CM
  10. html浏览器不能自动播放MP3音乐的问题