- 创建和销毁对象
- 用静态工厂方法代替构造器
- 静态工厂的优势:
- 有名称
- 不必每次调用时候都创建一个新对象
- 可以返回原返回类型的任何子类型的对象
- 返回的对象的类可以随着每次调用而发生变化,着取决于静态工厂方法的参数值
- 方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在
- 静态工厂的缺点:
- 类如果不含公有的或者受保护的构造器,就不能被子类化
- 程序员很难发现他们
- 静态工厂的优势:
- 遇到多个构造器参数时考虑使用构建器
- Builder模式模拟了具名的可选参数
- Builder模式也适用于类层次结构
- 用私有构造器或者枚举类型强化Singleton属性
- 单元素的枚举类型经常成为实现Singleton的最佳方法,如果Singleton必须扩展一个超类,而不是扩展一个Enum的时候,则不适宜使用这个方法。
- 通过私有构造器强化不可实例化的能力
- 企图通过将类做成抽象类来强制该类不可被实例化是行不通的。
- 优先考虑依赖注入来引用资源
- 不要用Singleton和静态工具类来实现依赖一个或多个底层资源的类,且该资源的行为会影响到该类的行为;也不要用这个类来创建这些资源。而是应该将这些资源或者工厂传给构造器(或者静态工厂,或构建器),通过他们来创建类。
- 避免创建不必要的对象
- 适配器是指这样一个对象:他把功能委托给一个后备对象,从而为后备对象提供一个可以替代的接口。
- 优先使用基本类型而不是装修基本类型
- 消除过期的对象引用
- 只要类是自己管理内存,程序员就要警惕内存泄露问题
- 内存泄露的另一个常见来源是缓存
- 内存泄露的第三个常见来源是监听器和其他回调。
- 避免使用终结方法和清除方法
- Finalizer通常是不可预测的,也是很危险的,一般情况下是不必要的
- 使用终结方法和清除方法有非常严重的性能损失
- try-with-resources优先于try-finally
- 用静态工厂方法代替构造器
- 对于所有对象都通用的方法
- 覆盖equals时请遵守通用约定
- 类的每个实例本质上都是唯一的。
- 类没有必要提供“逻辑相等”的测试功能
- 超类已经覆盖了equals,超类的行为对于这个类也是适合的
- 类是私有的,或者是包级私有的,可以确定他的equals方法永远不会被调用
- Equals方法等价关系:
- 自反性:对于任何非null的引用值x,x.equals(x)必须返回true
- 对称性:对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true
- 传递性:对于任何非null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true
- 一致性:对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致的返回true,或者一致地返回false
- 对于任何非null地引用值x,x.equals(null),必须返回false
- 覆盖equals时总要覆盖hashcode
- 不要企图让equals方法过于智能
- 不要将equals声明中地object对象替换为其他地类型
- 覆盖equals时总要覆盖hashcode
- 始终要覆盖toString
- 在实际应用中,toString方法应该返回对象中包含地所有值得关注地信息
- 谨慎地覆盖clone
- 复制功能最好由构造器或者工厂提供,最好利用clone
- 考虑实现Comparable接口
- 覆盖equals时请遵守通用约定
- 类和接口
- 使类和成员的可访问性最小化
- 尽可能地使每个类或者成员不被外界访问
- 要在公有类而非公有域中使用访问方法
- 公有类永远都不应该暴露可变地域
- 让公有类暴露不可变地域,其危害相对来说比较小
- 使可变性最小化
- 使类成为不可变,要遵循五条规则:
- 不要提供任何会修改对象状态的方法(也称为设值方法)
- 保证类不会被扩展
- 声明所有的域都是final的
- 声明所有的域都为私有的
- 确保对于任何可变组件的互斥访问
- 不可变对象本质上使线程安全的,他们不要求同步
- 使类成为不可变,要遵循五条规则:
- 复合优先于继承
- 包装类不适合用于回调框架
- 要么设计继承并提供文档说明,要么禁止继承
- 构造器绝不能调用可覆盖的方法,无论是直接调用还是间接调用
- 接口优于抽象类
- 为后代设计接口
- 有了缺省方法,接口的实现就不会出现编译时没有报错或警告,运行时却失败的情况
- 接口只用于定义类型
- 常量接口模式是对接口的不良使用
- 类层次优先于标签类
- 嵌套类有四种:静态成员类、非静态成员类、匿名类、局部类
- 静态成员类优于非静态成员类
- 如果一个嵌套类需要在单个方法外仍然是可见的,或者它太长了,不适合放在方法内部,就应该使用成员类。
- 假设这个嵌套类属于一个方法的内部,如果你只需要在一个地方创建实例,并且已经有了一个预置的类型可以说明这个类的特征,就要把它做成匿名类;否则,就做局部类。
- 如果成员类的每个实例都需要一个指向其外围实例的引用,就要把成员类做成非静态的,否则,就要做成静态的。
- 限制源文件为单个顶级类
- 永远不要把多个顶级类或者接口放在一个源文件中。
- 使类和成员的可访问性最小化
- 泛型
- 请不要使用原生态类型
- 消除非受检的警告
- 应该始终在尽可能小的范围内使用SuppressWarnings注解。
- 每当使用Suppress ing Warnings(“unchecked”)注解时,都要添加一条注释,说明什么这么做是安全的。
- 列表优于数组
- 不可具体化的类型是指其运行时表示法包含的信息比它的编译时表示法包含的信息更少的类型。
- 数组协变且可以具体化的;泛型是不可变的且可以被擦除的。
- 优先考虑泛型
- 优先考虑泛型方法
- 利用有限制通配符来提升API的灵活性
- 如果参数化类型表示一个生产者T,就使用<? extends T>;如果它表示一个消费者T,就使用<? super T>
- PECS,producer-extends,consumer-super
- 谨慎并用泛型和可变参数
- 在java7,增加了SafeVarargs注解,它让带泛型vararg参数的方法的设计者能够自动禁止客户端的警告。本质上,SafeVarargs注解是通过方法的设计者做出承诺,声明这是类型安全的。
- 优先考虑类型安全的异构容器
- 枚举和注解
- 用enum代替int常量
- 枚举中得switch语句适合于给外部得枚举类型增加特定于常量得行为
- 用实例域代替序数
- 永远不要根据枚举得序数导出与它关联得值,而是要将它保存在一个实例域中
- 用EnumSet代替位域
- 用EnumMap代替序数索引
- 用接口模拟可扩展的枚举
- 注解优先于命名模式
- @Repeatable元注解对注解得声明进行注解,表示该注解可以被重复的应用给单个元素
- 坚持用override注解
- 用标记接口定义类型
- 标记接口是不包含方法声明得接口,它只指明一个类实现了具有某种属性得接口
- 优点:
- 标记接口定义得类型是由被标记类得实例实现得;标记注解则没有定义这样得类型
- 标记接口胜过标记注解的另一个优点是,他们可以被更加精确地进行锁定
- 标记注解胜过标记接口地最大优点在于,他们是更多地注解机制地一部分
- 用enum代替int常量
- Lambda和Stream
- Lambda优先于匿名类
- 对于lambda而言,一行是最理想地,三行是合理地最大极限。
- Lambda限于函数接口,如果想创建抽象类地实例,可以用匿名类来完成,而不是lambda
- Lambda无法获得对自身地引用,在lambda中,关键字this是指外围实例,这个通常是你想要的
- Lambda与匿名类共享你无法可靠地通过实现来序列化和反序化地属性,因此尽可能不要序列化一个lambda(或者匿名类实例)
- 方法引用优先于Lambda
- 只要方法引用能做的事,就没有lambda不能完成的
- 只要方法引用更加简洁、清晰,就用方法引用;如果法规范引用并不简介,就坚持用lambda
- 方法引用5种类型
- 指向静态方法
- 有限制:本质类似于静态引用,函数对象与被引用方法带有相同参数
- 无限制:接收对象是在运用函数对象时,通过在该方法的声明函数前额外添加一个参数来指定的
- 类构造器
- 数组构造器
- 坚持使用标准的函数接口
- 千万不要用带包装类型的基础函数接口来代替基本函数接口
- 需要自己编写专用函数接口时的场景:
- 通用,并将受益于描述性名称
- 具有与其关联的严格的契约
- 将受益于定制的缺省方法
- 必须始终用@FunctionalInterface注解对自己编写的函数接口进行标注
- 不要在相同的参数位置,提供不同的函数接口来进行多次重载的方法,否则可能在客户端导致歧义
- 谨慎使用Stream
- 滥用Stream会使程序代码更难以读懂和维护。
- 在没有显式类型的情况下,仔细命名Lambda参数,这对于Stream pipeline的可读性至关重要。
- 最好避免利用stream来处理char值
- 下列工作只能通过代码块,而不能通过函数对象来完成:
- 从代码块中,可以读取或者修改范围内的任意局部变量;从lambda则只能读取final或者有效的final变量,并且不能修改任何local变量
- 从代码块中,可以从外围方法中return、break或continue外围循环,或者抛出该方法声明要抛出的任何受检异常;从lambda中则完全无法完成这些事情
- Stream适合场景:
- 统一转换元素的序列
- 过滤元素的序列
- 利用单个操作(如添加、连接或者计算其最小值)合并元素的顺序
- 将元素的序列存放到一个集合中,比如根据某些公共属性进行分组
- 搜索满足某些条件的元素的序列
- 优先选择Stream中无副作用的函数
- 编写Stream pipeline的本质是无副作用的函数对象。
- 收集器:toList、toSet、toMap、groupingBy、joining
- Stream要优先用Collection作为返回类型
- 不要在内存中保存巨大的序列,将它作为集合返回即可
- 谨慎使用Stream并行
- 千万不要任意地并行Stream pipeline,它造成地性能后果可能是灾难性地
- 在Stream上通过并行获得地性能,最好是通过ArrayList、Hash Map、HashSet、Concurrent Hash Map实例,数组,int范围和long范围
- 并行Stream不仅可能降低性能,包括活性失败,还可能导致结果出错,以及难以预计地行为(如安全性失败)
- Lambda优先于匿名类
- 方法
- 检查参数的有效性
- 必要时进行保护性拷贝
- Date已经过时了,不应该在新代码中使用
- 对于参数类型可以被不可信任方子类化的参数,请不要使用clone方法进行保护性拷贝
- 谨慎设计方法签名
- 谨慎选择方法的名称
- 不要过于追求提供便利的方法
- 避免过长的参数列表
- 对于参数类型,要优先使用接口而不是类
- 对于boolean参数,要优先使用两个元素的枚举类型
- 慎用重载
- 安全而保守的策略是,永远不要导出两个具有相同参数数目的重载方法
- 可以给方法起不同的名称,而不使用重载机制
- 不要在相同的参数位置调用带有不同函数接口的方法
- 慎用可变参数
- 返回零长度的数组或者集合,而不是null
- 谨慎返回optional
- 永远不要通过返回Optional的方法返回null,因为这样彻底weibeiloptional的本意
- 容器类型包括集合、映射、stream、数组和optional,都不应该被包装在optional中
- 如果无法返回结果并且没有返回结果时客户端必须执行特殊的处理,那么就应该声明该方法返回Optional<T>
- 几乎永远都不适合用optional作为键、值、或者集合或数组中的元素。
- 为所有导出的api元素编写文档注释
- @implSpec注释是描述方法及其子类之间的约定
- {@literal}用于产生包含html元字符的文档
- 文档注释在源代码和产生的文档中都应该是易于阅读的。如果无法让两者都易读,产生的文档的可读性要优先于源代码的可读性
- 为注解类型编写文档时,要确保在文档中说明所有成员,以及类型本身
- 类或者静态方法是否线程安全,应该在文档中对它的线程安全级别进行说明
- 通用编程
- 将局部变量的作用域最小化
- 要使局部变量的作用域最小化,最有力的方法就是在第一次要使用它的地方进行声明
- 几乎每一个局部变量的声明都应该包含一个初始化表达式。如果你没有足够的信息来对一个变量进行初始化,就应该推迟这个声明,直到可以初始化为止
- for-each循环优先于传统for循环
- 无法使用for-each循环的三种情况:
- 解构过滤:如果需要遍历集合,并删除选定的元素,就需要使用显式的迭代器,以便可以调用它的remove方法。使用Java8的removeIf方法,常常可以避免显式的遍历
- 转换:如果需要遍历列表或者数组,并取代它的部分或者全部元素值,就需要列表迭代器或者数组所以,以便设定元素值
- 平行迭代:如果需要并行地遍历多个集合,就需要显示的控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以同步前进
- 无法使用for-each循环的三种情况:
- 了解和使用类库
- Java7开始就不应该使用Random而是ThreadLocalRandom
- 如果需要精确的答案,请避免使用float和double
- float和double类型主要是为了科学计算和工程计算而设计的。他们执行二进制浮点运算,这是为了在广泛的数值范围上提供较为精确的快速近似计算而精心设计的
- Float和double不适合用于货币计算
- 基本类型优先于装箱基本类型
- 区别:
- 基本类型只有值,而装箱基本类型则具有与他们的值不同的同一性
- 基本类型只有函数值,而每个装箱基本类型则都有一个非函数值,除了对应基本类型的所有函数值之外,还要null
- 基本类型通常比装箱基本类型更节省时间和空间
- 当在一项操作中混合使用基本类型和装箱基本类型时,装箱基本类型就会自动拆箱
- 基本类型要优先于装箱基本类型
- 区别:
- 如果其他类型更适合,则尽量避免使用字符串
- 字符串不适合代替其他的值类型
- 字符串不适合代替枚举类型
- 字符串不适合代替能力表
- 了解字符串连接的性能
- 通过接口引用对象
- 如果有合适的接口类型存在,那么对于参数、返回值、变量和域来说,都应该使用接口类型进行声明
- 如果没有合适的接口,就用类层次结构中提供了必要功能的最小的具体类来引用对象吧
- 接口优先于反射机制
- 反射的缺点:
- 损失了编译时类型检查的优势,包括异常检查
- 执行反射访问所需要的代码非常笨拙和冗长
- 性能损失
- 如果只是以非常有限的形式使用反射机制,虽然也要付出少许代价,但是可以获得许多好处
- 反射的缺点:
- 谨慎地使用本地方法
- 谨慎得进行优化
- 要努力编写好的程序而不是快的程序
- 要努力避免那些限制性能的设计决策
- 要考虑api设计决策的性能后果
- 为获得好的性能而对api进行包装,这是一种非常不好的想法
- 在每次试图做优化之前和之后,要对性能进行测量
- 遵守普遍接受的命名惯例
- 包和模块的名称应该是层次状的,用句号分割每个部分。每个部分都要包括小写字母,极少数情况下还有数字。任何将在你组织外使用的包,其名称都应该以你组织的Internet域名开头,并且顶级域名要放在前面
- 包名称的其余部分应该包括一个或者多个描述该包的组成部分。这些组成部分比较简短,通常不超过8个字符。鼓励使用有意义的缩写形式。只取首字母的缩写形式也是可以接受的。
- 类和接口的名称,包括枚举和注解类型的名称,都应该包括在一个或者多个单词,每个单词的首字母大写。应该避免使用缩写,除非是一些首字母缩写和一些通用的缩写
- 方法和域的名称与类和接口的名称一样,都要遵守相同的字面惯例,只不过方法或者域的名称的第一个字母应该小写
- 常量域的名称应该包含一个或者多个大写的单词,中间用下划线符号隔开
- 局部变量名称的字面命名惯例与成员名称类似,只不过它允许缩写,单个字符和短字符序列的意义取决于局部变量所在的上下文环境
- 类型参数名称通常由单个字母组成。这些字母通常是以一下五种类型之一:T表示任意的类型,E表示集合的元素类型,K和V表示映射的键和值类型,X表示异常。函数的返回类型通常是R
- 可被实例化的类(包括枚举类型)通常用一个名词或者名词短语命名;不可实例化的工具类经常使用复数名词命名;
- 执行某个动作的方法常用动词或者动词短语(包括对象)来命名,例如append或者drawImage
- 对于返回boolean值的方法,其名称往往以单个单词is开头,很少用has,后跟名词或者名词短语,或者任何具有形容词功能的单词或短语
- 如果方法返回被调用对象的一个非boolean的函数或者属性,它通常用名词、名词短语,或者以动词get开头的动词短语来命名
- 转换对象类型的实例方法,他们返回不同类型的独立对象的方法,经常被称为toType
- 返回视图的方法经常被称为asType
- 返回一个与被调用对象同值的基本类型的方法,经常被成为typeValue
- 静态工厂的常用名称包括from、of、valueOf、instance、getInstance、newInstance、getType、newType
- 将局部变量的作用域最小化
- 异常
- 只针对异常的情况才使用异常
- 异常应该只用于异常的情况下;它们永远不应该用于正常的控制流
- 对可恢复的情况使用受检异常,对编程错误使用运行时异常
- 三种可抛出结构:受检异常、运行时异常、错误
- 如果期望调用者能够适当的恢复,这种情况就应该使用受检异常
- 运行时异常来表明编程错误
- 错误往往被jvm保留下来使用,以表明资源不足、约束失败,或者其他使程序无法继续执行的条件
- 异常也是个完全意义上的对象,可以在它上面定义任意的方法。这些方法的主要用途是为捕获异常的代码而提供额外的信息,特别是关于引发这个异常条件的信息。
- 避免不必要地使用受检异常
- 优先使用标准的异常
- 抛出与抽象对应的异常
- 更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常,这种方法称为异常转译。
- 每个方法抛出的所有异常都要建立文档
- 使用javadoc的@throws标签记录下一个方法可能抛出的每个未受检异常,但是不要使用throws关键字将未受检的异常包含在方法的声明中
- 如果一个类中的许多方法出于同样的原因而抛出同一个异常,在该类的文档注释中对这个异常建立文档,这是可以接受的。
- 在细节消息中包含失败-捕获信息
- 为了捕获失败,异常的细节信息应该包括“对该异常有贡献”的所有参数和域的值
- 不要在细节消息中包含密码、密钥以及类信息
- 努力使失败保持原子性
- 一般而言,失败的方法调用应该使对象保持在被调用之前的状态。
- 对于在可变对象上执行操作方法,获得失败原子性最常见的办法是,在执行操作之前检查参数的有效性
- 一种类似的获得失败原子性的办法是,调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生
- 第三种获得失败原子性的办法是,在对象的一份临时拷贝上执行操作,当操作完成之后,再用临时拷贝中的结果代替对象内容
- 最后一种获得失败原子性的办法是,编写一段恢复代码,由它拦截操作过程中发生的失败,以及使对象回滚到操作开始之前的状态
- 不要忽略异常
- 只针对异常的情况才使用异常
- 并发
- 同步访问共享的可变数据
- Java语言规范保证读或者写一个变量是原子的,除非这个变量的类型为long或者double
- 让一个线程短时间内修改一个数据对象,然后与其他线程共享,这样是可以接受的,它只同步共享对象引用的动作。然后其他县城没有进一步的同步也可以读取对象,只要它没有再被修改,这种对象被称作高效不可变。这种对象引用从一个线程传递到其他线程被称作安全发布。
- 安全发布的实现方式:保存在静态域中,作为初始化的一部分;可以将它保存再volatile域、final域或者通过正常锁定访问的域中;或者可以将它放到并发的集合中
- 避免过度同步
- 为了避免活性失败和安全性失败,再一个同步的方法或者代码块中,永远不要放弃对客户端的控制
- 应该再同步区域内做尽可能少的工作
- Executor、task、stream优先于线程
- 并发工具优先于wait和notify
- 对于间歇式的定时,始终应该优先使用System.nanoTime,而不是使用System.currentTimeMillis。因为System.nanoTime更准确,也更精确,它不受系统的实时时钟的调整所影响
- 线程安全性的文档化
- 线程安全的几种级别:
- 不可变的
- 无条件的线程安全
- 有条件的线程安全
- 非线程安全
- 线程对立的
- 私有锁对象模式特别适用于那些专门为继承而设计的类
- 线程安全的几种级别:
- 慎用延迟初始化
- 不要依赖于线程调度器
- 任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能是不可移植的。
- 同步访问共享的可变数据
- 序列化
- 其他方法优先于Java序列化
- 避免序列化攻击的最佳方式是永远不要反序列任何东西
- 谨慎地实现Serializable接口
- 实现Serializable接口的代价:
- 一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性。
- 增加了出现bug和安全漏洞的可能性
- 随着类发行新的版本,相关的测试负担也会增加
- 为了继承而设计的类,应该尽可能少地去实现Serialiazable接口,用户的接口也应该尽可能少继承Serializable接口
- 内部类不应该实现Serializable接口
- 实现Serializable接口的代价:
- 考虑使用自定义的序列化形式
- 如果事先没有认真考虑默认的序列化形式是否合适,则不要贸然接受
- 如果一个对象的物理表示法等同于它的逻辑内容,可能就合适使用默认的序列化形式
- 当一个对象的物理表示法与它的逻辑数据内容有实质性的区别时,使用默认序列化形式会有以下4个缺点:
- 它使这个类的导出api永远地束缚在该类的内部表示法上
- 它小号过多的空间
- 它会消耗过多的时间
- 它会引起栈溢出
- Transient修饰符表明这个实例域将从一个类的默认序列化形式中省略掉
- 在决定将一个域做成非瞬时的前,请一定要确信它的值将是该对象逻辑状态的一部分
- 无论你是否使用默认的序列化形式,如果在读取整个对象的任何其他方法上强制任何同步,则也必须在对象序列化上强制这种同步
- 不管你选择了哪种序列化形式,都要为自己编写的每个可序列化的类声明一个显式的序列版本UID
- 不要修改序列版本UID,否则将会破坏类现有已被序列化实例的兼容性
- 保护性地编写readObject方法
- 当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,如果哪个域包含了这样的对象引用,就必须要做保护性拷贝,这是非常重要的
- 编写readObject方法的指导方针:
- 对于对象引用域必须保持为私有的类,要保护性地拷贝这些域中地每个对象。不可变类地可变组件就属于这一类别
- 对于任何约束条件,如果检查失败,则抛出一个InvalidObjectException异常。这些检查动作应该在所有地保护性拷贝之后
- 如果整个对象在被反序列化之后必须进行验证,就应该使用ObjectValidation接口
- 无论是直接方式还是间接方式,都不要调用类中任何可以被覆盖地方法。
- 对于实例控制,枚举类型优先于readResolve
- 如果依赖readResolve进行实例控制,带有对象引用类型的所有实例域则都必须声明为transient
- 考虑用序列化代理代替序列化实例
- 为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类的实例的逻辑状态。这个嵌套类被称作序列化代理,它应该有一个单独的构造器,其参数就是那个外围类。这个构造器只从它的参数中复制数据:它不需要进行任何一致性检查或者保护性拷贝。从设计的角度来看,序列化代理的默认序列化形式是外围类最好的序列化形式。外围类及其序列化代理都必须声明实现Serializable接口。
- 序列化代理模式的局限性:
- 不能与可以被客户端扩展的类相兼容
- 不能与对象图中包含循环的某些类想兼容
- 其他方法优先于Java序列化
effective java第三版 读后日志
最新推荐文章于 2025-05-05 11:41:32 发布