前言
Effective Java 作为 Java 四大名著之一,聚焦于 Java 语言习惯和高效的用法。EJ 告诉读者如何更好地构建代码,以便代码能够更好地工作;也便于其他人能够理解这些代码,便于修改和改善;程序也会因此变得更加令人愉快,更加优雅。
全书共90条,接下来笔者将逐条进行总结。
第1条:用静态工厂方法代替构造期
五大优势
- 有名称
- 不必每次创建一个新对象
- 可以返回原返回类型的任何自类型的对象
- 所返回的对象的类可以随方法的参数值而发生变化
- 所返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在
主要缺点
- 类如果不含
public
或者protected
的构造器,就不能被子类化 - 程序员很难发现它们
第2条:遇到多个构造器参数时要考虑使用构建器
- Builder 模式
第3条:用私有构造器或者枚举类型强化 Singleton 属性
实现 SIngleton 的三种方法
- 构造器私有,实例
public static final
- 构造器私有,实例
private static final
,公有的成员是个静态工厂方法 - 单元素的枚举类型
第4条:通过私有构造器强化不可实例化的能力
- 企图用抽象类阻止实例化不可行,因为该类可以被子类化
- 副作用:使一个类不能被子类化,因为子类所有的构造器都必须调用超类的构造器
第5条:优先考虑依赖注入来引用资源
- 将资源传到构造器中,在构造器中初始化成员变量
第6条:避免创建不必要的对象
- 重用不可变对象
- 优先使用基本类型而不是装箱基本类型,避免不必要的自动装箱
第7条:消除过期的对象引用
- 警惕内存泄漏
第8条:避免使用终结方法和清除方法
- 它们不可预测,危险,一般情况下也不必要
第9条:try-with-resources 优先于 try-finally
- 在使用必须关闭的资源时
第10条:覆盖 equals 时请遵守通用约定
5个要求
- 自反性
- 对称性
- 传递性
- 一致性
- 非空性
第11条:覆盖 equals 时总要覆盖 hashCode
- 相等的对象必须具有相等的散列码
第12条:始终要覆盖 toString
toString
方法应该返回对象中的所有值得关注的信息- 在文档中表明意图
第13条:谨慎地覆盖 clone
- 要确保不会伤害到原始的对象
- 实现了 Cloneble 接口的类都应该覆盖
clone
方法,先调用super.clone
方法,再拷贝任何内部“深层结构”的可变对象 - 更好的办法是提供一个拷贝构造器或拷贝工厂
第14条:考虑实现 Comparable 接口
- 以便其实例可以被比较、排序、分类、搜索
- 比较时避免使用 < 和 > 操作符,而是使用装箱基本类型的静态
compare
方法,或者在 Comparator 接口中使用比较器的构造方法
第15条:使类和成员的可访问性最小化
- 封装
- 解耦
- 规则很简单:尽可能地使每个类或者成员不被外界访问
- 公有类的实例域决不能是公有的
public static final
域所引用的对象要不可变
第16条:要在公有类中使用访问方法而非公有域
- 使用私有域和公有访问方法
- 对于可变的类,使用公有 setter 方法设置
第17条:使可变性最小化
不可变的类要遵循5条规则
- 不要提供任何会修改对象状态的方法
- 保证类不会被扩展
- 声明所有的域都是
final
的 - 声明所有的域都是
private
的 - 确保对于任何可变组建的互斥访问。如果有指向可变对象的域,必须确保客户端无法获得指向这些对象的的引用,并且不要用客户端提供的对象引用初始化这样的域
- 不可变对象是线程安全的,不需要同步,可以被自由的共享(不需要进行保护性拷贝)
- 如果类不能被做成不可变的,应该尽可能地限制它的可变性
- 构造器应该创建完全初始化的对象,并建立起所有的约束关系
第18条:复合优先于继承
- 复合:在一个新的类中增加一个私有域,引用现有类的一个实例。新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法(这被称为转发)
- 继承的问题:打破了封装性
第19条:要么设计继承并提供文档说明,要么禁止继承
- 如果方法调用到了可覆盖的方法,文档注释的末尾应该包含关于这些调用的描述信息
- 构造器决不能调用可被覆盖的方法
禁止子类化的2种办法
- 把类声明为
final
- 所有构造器变为私有或者包级私有的,提供公有静态工厂替代构造器
第20条:接口优于抽象类
- 现有的类很容易被更新,以实现新的接口
- 接口是定义 mixin(混合类型)的理想选择
- 接口允许构造非层次结构的类型框架
- 但是,通过对接口提供一个抽象的骨架实现类,可以结合接口和抽象类的优点——模板方法模式
接口负责定义类型,骨架实现类负责剩下的非基本类型接口方法
第21条:为后代设计接口
- Java 8 中接口增加了缺省方法构造,可以给现有接口添加新方法而不破坏现有的实现,但是建议尽量避免
- 在发布前以不同的方式实现接口,进行测试
第22条:接口只用于定义类型
- 不应该使用常量接口。一个类在内部使用某些常量是实现细节,常量接口会把这样的实现细节泄露到该类的导出 API 中
第23条:类层次优于标签类
- 标签类过于冗长、容易出错,并且效率低下,应该用类层次来代替
第24条:静态成员类优于非静态成员类
- 如果嵌套类的实例可以在它的外围类的实例之外独立存在,这个嵌套类就必须是静态成员类
- 如果成员类的实例需要一个指向其外围实例的指针,就把成员类做成非静态的
第25条:限制源文件为单个顶级类
- 确保编译时一个类不会有多个定义
第26条:请不要使用原生态类型
- 使用原生态类型失掉了范型在安全性和描述性方面的所有优势
第27条:消除非受检的警告
- 如果无法消除警告,同时可以证明引起警告的代码是类型安全的,可以使用
@SuppressWarnings("unchecked")
注解 - 应该在尽可能小的范围内使用
@SuppressWarnings("unchecked")
注解。不要在整个类上使用这个注解,因为这么做可能会掩盖重要的信息 - 使用该注解要添加一条注释,说明为什么这么做是安全的
第28条:列表优于数组
- 数组和范型有着截然不同的类型规则:数组是协变且可以具体化的;范型是不可变的且可以被擦除的
- 范型数组是非法的,因为它不是类型安全的
第29条:优先考虑范型
- 使用范型比使用需要在客户端代码中进行转换的类型更加安全和容易
- 只要时间允许,就把现有的类型都范型化
第30条:优先考虑范型方法
- 类型参数的声明,处在方法的修饰符和返回值之间
第31条:利用有限制通配符来提升 API 的灵活性
- 例如:
<? extends E>
- PECS 表示 producer-extends,consumption-super
- 所有的 comparable 和 comparator 都是消费者
第32条:谨慎并用范型和可变参数
- 可变参数是构建在顶级数组之上的一个技术露底,范型可变参数不是类型安全的
- 但是范型可变参数是合法的,在确保类型安全后,加上
@SafeVarargs
注解
第33条:优先考虑类型安全的异构容器
- 类型令牌:一个类的字面被用在方法中,来传达编译时和运行时的类型信息
- 集合 API 限制每个容器只能有固定数目的类型参数,可以通过将类型参数放在键上而不是容器上避开这一限制
- 例如:
private Map<Class<?>, Object> map = new HashMap<>():
第34条:用 enum 代替 int 常量
- 枚举可读性更好,更加安全,功能更加强大
- 枚举中的属性与常量关联,方法的行为受该属性的影响
第35条:用实例域代替序数
- 序数:枚举的
ordinal
方法返回每个枚举常量在类型中的数字位置,可以从序数中得到关联的 int 值。但是这样难以维护:例如要添加一个与已经用过的 int 值关联的枚举常量。 - 永远不要根据枚举的序数导出与它关联的值,而是将它保存在一个实例域中
- Enum 规范谈及
ordinal
方法:大多数程序员都不需要这个方法,它是用于像 EnumSet 和 EnumMap 这种基于枚举的通用数据结构的
第36条:用 EnumSet 代替位域
位域表示法的缺点
- 以数字形式打印时,翻译困难
- 遍历困难
- 编写 API 时需要先预测最多需要多少位,以选择对应的类型(不能超出位宽度)
EnumSet 的优点
- 位域的简洁和性能优势 + 枚举类型的优点(第33条)
EnumSet 的缺点
- 截止 Java 9,无法创建不可变的 EnumSet。但是这一点很可能在将来的版本中得到修正;现在可以用
Collections.unmodifiableSet
将 EnumSet 封装起来
第37条:用 EnumMap 代替序数索引
- EnumMap 是专门用于枚举键的映射
- 如果关系是多维的,使用
EnumMap<..., EnumMap<...>>
第38条:用接口模拟可扩展的枚举
- 枚举类型不是可扩展的,接口类型是可扩展的
第39条:注解优先于命名模式
- 命名模式:用于表明有些程序元素需要通过某种工具或者框架进行特殊处理
命名模式的缺点
- 文字拼写错误导致失败
- 无法确保它们只用于相应的程序元素上
- 没有提供将参数值与程序元素关联起来的好方法
- 大多数情况下不必自己定义注解类型,应该使用 Java 平台所提供的预定义的注解类型
第40条:坚持使用 Override 注解
原因
- 防止没能覆盖,而是重载了的情况
- 防止无意识的覆盖
第41条:用标记接口定义类型
- 标记接口:不包含方法声明的接口,它只是指明一个类实现了某种属性的接口
标记接口与标记注解
- 标记接口的优点:编译时的错误侦测;可以被更加精确地进行锁定;接口可以作为相关方法的类型参数
- 标记注解的优点:标记注解是更大的注解机制的一部分
何时使用标记注解、何时使用标记接口
- 如果标记是应用于任何程序元素而不是类或接口,就必须使用注解,因为只有类和接口可以实现或拓展接口
- 如果标记是广泛使用注解的框架的一个组成部分,使用标记注解
- 如果标记只用于类或接口,优先使用标记接口
第42条:Lambda 优点于匿名类
- Lambda 的优势在于简洁
- 从 Java 8 开始,Lambda 成为表示小函数对象的最佳方式,打开了函数式编程的大门
- 删除所有 Lambda 参数的类型(除非能够使程序变得更加清晰)
- Lambda 没有名称和文档,如果一个计算本身不是自描述的,或者超出了几行(对于 Lambda 而言,一行最理想,三行是合理的最大极限),就不要把它放在 Lambda 中
- 尽可能不要序列化 Lambda
第43条:方法引用优先于 Lambda
- 方法引用能够得到更加简短、清晰的代码
- 如果 Lambda 太长或者过于复杂,可以从 Lambda 中提取代码放到一个新的方法中,并用该方法的引用代替 Lambda
- Lambda 和方法引用,哪个更加简洁就用哪个
第44条:坚持使用标准的函数接口
- java.util.function 提供了大量标准的函数接口
- 函数接口用
@FunctionalInterface
注解标注
第45条:谨慎使用 Stream
- 滥用 Stream 会使程序代码更难以读懂和维护
- 避免使用 Stream 处理 char 值
- 使用 Stream pipeline,一旦将一个值映射到某个其他值,原来的值就丢失了
第46条:优先选择 Stream 中无副作用的函数
- forEach 操作应该只用于报告 Stream 计算的结果,而不是执行计算
- 静态导入 Collectors 的所有成员,可以提升可读性
counting
方法返回的收集器仅用作下游收集器。不应使用collect(counting())
,因为Stream 的count
方法有相同的功能。
第47条:Stream 要优先用 Collection 作为返回类型
- 对于公共的、返回序列的方法,Collection 或者适当的子类型通常是最佳的返回类型
- 如果无法返回集合,就返回 Stream 或者 Iterable
第48条:谨慎使用 Stream 并行
- 在 Stream 上获得的性能,最好是通过 ArrayList、HashMap、HashSet 和 ConcurrentHashMap 实例,数组,int 范围和 long 范围等。这些数据结构的共性是:都可以被精确、轻松地分成任意大小的子范围,使并行线程中的分工变得更加轻松;在进行顺序处理时,提供了优异的引用局部性(序列化的元素引用一起保存在内存中)
- Stream pipeline 的终止操作会影响并发执行的效率。并行的最佳终止操作是做减法,用一个 Stream 的
reduce
方法,将所有从 pipeline 产生的元素都合并在一起,或者预先打包像min
、max
、count
和sum
这类方法 - 如果是自己编写 Stream、Iterable 或者 Collection 实现,并且想要得到适当的并行性能,就必须覆盖
spliterator
方法,并广泛地测试结果 Stream 的并行性能 - 并行 Stream 不仅可能降低性能,包括活性失败、还可能导致结果出错,以及难以预计的行为(如安全性失败)
- 在适当的条件下,给 Stream pipeline 添加 parallel 调用,确实可以在多处理器核的情况下实现近乎线性的倍增
第49条:检查参数的有效性
- 在方法体的开头出检查参数
- 对于方法本身没有用到,却被保存起来供以后使用的参数,检验它们的有效性尤为重要(例如构造器的参数)
第50条:必要时进行保护性拷贝
- 假设类的客户端会近期所能来破坏这个类的约束条件,因此必须保护性地设计程序,进行保护性拷贝
- 保护性拷贝是在检查参数有效性之前进行的,并且有效性检查是针对拷贝之后的对象,这样做可以避免在“危险阶段”期间从一个线程改变类的参数
- 对于参数类型可以被不信任方子类化的参数,不要使用
clone
方法进行保护性拷贝 - 只要有可能都应该使用不可变的对象作为对象内部的组件,这样就不必保护性拷贝了
第51条:谨慎设计方法签名
- 方法的名称遵循标准的命名习惯
- 不要过于追求提供便利的方法,只有当一项操作被经常用到的时候,才考虑为它提供快捷方式
- 避免国产规定参数列表,尤其是相同类型的长参数序列
缩短过长的参数列表的技巧
- 把一个方法分解成多个方法
- 创建辅助类,用来保存参数的分组,辅助类一般为静态成员类
- 从对象构建到方法调用都采用
Builder
模式
- 对于参数类型,要优先使用接口而不是类
- 对于 boolean 参数,要优先使用两个元素的枚举类型
第52条:慎用重载
- 重载不一定执行“最为具体的方法”,调用的是哪个重载方法在编译时作出决定
- 可以用单个方法替换多个重载方法,并在这个方法中做一个显示的
instanceof
- 安全而保守的策略是,永远不要导出两个具有相同参数数目的重载方法
- 可以给方法起不同的名称,而不使用重载机制
- 不要在相同的参数位置调用带有不同函数接口的方法
第53条:慎用可变参数
- 可变参数方法接受0个或者多个指定类型的方法
- 在使用可变参数之前,要先包含所有必要的参数
- 性能问题:每次调用可变参数方法都会导致一次数组的分配和初始化
第54条:返回零长度的数组或者集合,而不是 null
- 如果返回 null,每次用到时都需要判断是否为 null,这样 API 更难以使用,也更容易出错
- 通过重复返回同一个不可变的零长度集合避免分配的执行,防止损害程序的性能
第55条:谨慎返回 optional
- Optional 本质上与受检异常相类似,因为它们强迫 API 用户面对没有返回值的现实
Optional#isPresent()
方法,当 optional 中包含一个值时返回 true,当 optional 为空时,返回 false- 容器类型不应该被包装在 optional 中
- 使用 OptionalInt、OptionalLong 和 OptionalDouble,而不是基本包装类型的 optional
- optional 不适合作为键、值,或者集合或数组中的元素
- 不要将 optional 用作返回值以外的任何其他用途
第56条:为所有导出的 API 元素编写文档注释
- 在每个被导出的类、接口、构造器、方法和域声明之前增加一个文档注释
- 简洁地描述出它和客户端之间的约定
- 文档注释在源代码和产生的文档中都应该是易于阅读的
- 同一个类或接口中的两个成员或者构造器,不应该具有同样的概要描述
- 为泛型或者方法编写文档时,要说明所有的参数类型
- 为枚举类型编写文档时,要说明常量
- 为注解类型编写文档时,要说明所有成员
- 在文档中说明线程安全级别
- Javadoc 具有“继承”方法注释的能力。如果一个 API 元素没有文档注释,Javadoc 会搜索最为适用的文档注释
- 文档注释中允许 HTML 标签,但是 HTML 元字符需要转义
第57条:将局部变量的作用域最小化
- 在第一次要使用它的地方进行声明
- 每一个局部变量的声明都应该包含一个初始化表达式(例外:一个变量的声明需要在 try 块内,但是变量在 try 块外要被用到,就要在 try 块之前声明,却不能被“有意义地初始化”)
- 如果在循环终止之后不再需要循环变量,for 循环优先于 while 循环(while 循环复制粘贴后可能会忘记改变迭代器)
- 使方法小而集中,将大方法拆分成多个小方法
第58条:for-each 循环优先于传统的 for 循环
- 迭代器和索引变量的使用可能会出错
无法使用 for-each 循环的情况
- 解构过滤——遍历时删除集合中的某些元素
- 转换——遍历列表或数组,并取代某些元素值
- 平行迭代——并行地遍历多个集合
第59条:了解和使用类库
- 类库的代码可能比你自己编写的代码更好一些,并且会随着时间的推移而不断改进
- 不要重复发明轮子!
第60条:如果需要精确的答案,请避免使用 float 和 double
- float 和 double 执行的是二进制浮点运算,这是为了在广泛的数值范围上提供较为精确的快速近似计算而精心设计的,但是它们没有提供完全精确的结果
- float 和 double 不能用于货币计算,应使用 BigDecimal、int 或者 long
- 使用 BigDecimal 的好处除了精确,还有可以控制舍入(8种舍入模式)
第61条:基本类型优先于装箱基本类型
- 对装箱基本类型使用
==
操作符几乎总是错误的 - 当一项操作中混合使用基本类型和装箱基本类型时,装箱基本类型就会自动拆箱。如果 null 被自动拆箱,就会抛出 NullPointerException 异常
何时使用装箱基本类型
- 作为集合中的元素
- 作为类型参数
- 反射的方法调用
第62条:如果其他类型更加合适,则尽量避免使用字符串
- 字符串不适合代替其他的值类型
- 字符串不适合代替枚举类型
- 字符串不适合代替聚合类型
- 字符串不适合代替能力表
第63条:了解字符串连接的性能
- 使用
+
连接 n 个字符串,需要 n 的平方级的时间 - 大量的字符串连接操作,应使用
StringBuilder#append
第64条:通过接口引用对象
- 用接口作为类型,决定更换实现时,只需要改变构造器中实现类的名称(或者使用一个不同的静态工厂),程序将会更加灵活
- 如果没有合适的接口,则用类层次结构中提供了必要功能的最小的具体类引用对象
第65条:接口优先于反射机制
反射机制的缺点
- 损失了编译时类型检查的优势
- 代码有些笨拙和冗长
- 性能损失
应该怎么做
- 仅仅使用反射机制实例化对象,用编译时已知的某个接口或者超类访问对象
第66条:谨慎地使用本地方法
- 本地方法提供了“访问特定与平台的机制”的能力”,但是随着 Java 平台的不断成熟,Java 提供了越来越多以前只有在宿主平台上才拥有的特性,因此使用本地方法几乎没有必要
- 使用本地方法来提高性能的做法不知的提倡
本地方法的严重缺陷
- 本地方法不是安全的
- 本地方法不是可自由移植的
- 使用本地方法的应用程序更难调试
- 编写不便,阅读困难
第67条:谨慎地进行优化
- 不要进行优化!
- 要努力编写好的程序而不是快的程序
- 在设计中就考虑到性能问题,努力避免限制性能的设计决策,而不是在系统完成后再改变某个基本方面,因为这样会破坏系统的结构
- 幸运的是,好的 API 设计也会带来好的性能——努力编写好的程序,速度自然会随之而来
- 试图做优化之前和之后,要对性能进行测量
第68条:遵守普遍接受的命名惯例
把标准的命名惯例当作一种内在的机制来看待,并把它们作为第二特性
- 包名为域名反写
- 类名大写驼峰
- 方法名、遍历名小写驼峰
- 常量全大写,下划线分隔
- 执行某个动作的方法用动词开头
- 返回 boolean 值的方法以 is 开头
- 返回被调用对象的一个非 boolean 属性的方法,用名词或者 getXxx 来命名
- 被废弃的 Java Beans 规范中:getXxx 和 setXxx 方法
- toType、asType、typeValue 方法
- 静态工厂的常用名称:from、of、valueOf、instance、getInstance、newInstance、getType、newType
第69条:只针对异常的情况才使用异常
- 异常不应该用于正常的控制流
第70条:对可恢复的情况使用受检异常,对编程错误使用运行是异常
三种可抛出结构(throwable)
- 受检异常(checked exception)
- 运行时异常(run-time exception)
- 错误(error)
- 如果希望适当恢复,应使用受检异常。通过抛出受检异常,强迫调用者在一个 catch 子句中处理该异常,或者将它传播出去
- 大多数运行时异常表示前提违例(API 的客户端没有遵守 API 规范建立的约定)
- 错误表明资源不足、约束失败,或者其他使程序无法继续执行的条件。不应该定义 Error 的子类,也不应该抛出 AsserionError 异常
- 永远不应该抛出一个不是 Exception、RuntimeException 或 Error 的子类,否则会困扰用户
第71条:避免不必要地使用受检异常
- 受检异常强迫程序员处理异常的条件(在 catch 块中处理或抛出去),这是一种负担
消除受检异常的方法
- 返回 optional
- 把抛出异常的方法,分为两个方法,第一个方法返回一个 boolean 值,使用 if-else 语句执行方法或处理异常
第72条:优先使用标准的异常
- 不要直接使用 Exception、RuntimeException、Throwable、Error,对待它们就像对待抽象类一样
- 按照具体场景选定抛出的异常
第73条:抛出与抽象对应的异常
- 异常转译:更高层的实现捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常
第74条:每个方法抛出的所有异常都要建立文档
- 始终要单独地声明受检异常,并且利用 Javadoc 的 @throws 标签记录抛出异常的条件(这也帮助区分受检异常和非受检异常)
- main 方法可以被安全地声明抛出 Exception,因为它只通过虚拟机调用
第75条:在细节消息中包含失败-捕获信息
- 异常的细节信息应该包含“对该异常有贡献”的所有参数或域的值
- 为了安全,不要在细节消息中包含密码等信息
- 可以在构造器中引入细节消息
第76条:努力使失败保持原子性
- 失败原子性:失败的方法调用应该使对象保持在被调用之前的状态
失败原子性实现方式
1, 设计不可变对象
2. 在执行操作之前检查参数的有效性
3. 调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前的发生
4. 在对象的一份临时拷贝上执行操作,操作完成后再代替原本的对象
5. 编写恢复代码
- 错误通常是不可恢复的,因此抛出 AssertionError 时不需要保持失败原子性
- 如果无法保证失败原子性,API 文档应该指明对象会处于什么状态
第77条:不要忽略异常
- 忽略异常:使用空的 catch 块
- 空的 catch 块会使异常达不到应有的目的
- 如果要忽略,catch 块中应该包含一条说明注释,并且异常变量命名为 ignored
第78条:同步访问共享的可变数据
- synchronized
- 读写都要被同步
- 最好使用不共享的可变的数据(将可变数据限制在单个线程中)
第79条:避免过度同步
- 为了避免死锁和数据破坏,不要再同步区域内部调用外来方法
- 在同步区域内做尽可能少的工作
- 过度同步会失去并行的机会
第80条:executor、task 和 stream 优先于线程
- 更加简单就能享受到性能优势
第81条:并发工具优先于 wait 和 notify
- 正确地使用 wait 和 notify 比较困难
- 始终应该使用 wait 循环模式来调用 wait 方法;永远不要在循环之外调用 wait 方法
- 一种合理而保守的建议:始终使用 notifyAll 方法,不使用 notify 方法
- 并发工具:并发集合、Synchronizer、CountDownLatch
第82条:线程安全性的文档化
说明类的线程安全性级别
- 不可变
- 无条件的线程安全
- 有条件的线程安全
- 非线程安全
- 线程对立的
第83条:慎用延迟初始化
- 如果利用延迟初始化来破坏初始化的循环,就要使用同步访问方法
- 双重检查(两次检查域的值是否不为 null)
第84条:不要依赖于线程调度器
- 依赖线程调度器可能会导致不可移植
- 确保可运行的线程的平均数量不明显多于处理器的数量,使线程调度器没有耕读欧的选择,只能运行这些可运行的线程,直到它们不再可运行为止
- 如果线程没有在做有意义的工作,就不应该运行
- 不能通过 Thread.yield 来修正不能工作的程序
第85条:其他方法优先于 Java 序列化
- 序列化的攻击面过于庞大,无法进行防护,并且还在不断扩大
- 避免序列化攻击的最佳方式是永远不要反序列化任何东西
- 永远不要反序列化不被信任的数据
第86条:谨慎地实现 Serializable 接口
实现 Serializable 接口的代价
- 一旦一个类被发布,就大大降低了“该变这个类的实现”的灵活性(每个可序列化的类都有一个唯一标识号与之关联)
- 增加了出现 Bug 和安全漏洞的可能性(序列化机制是一种语言之外的对象创建机制)
第87条:考虑使用自定义的序列化形式
- 理想的序列化形式应该只包含该对象所表示的逻辑数据,而逻辑数据和物理表示法应该是各自独立的
一个对象的物理表示法和逻辑数据内容有实质性区别时,使用默认序列化形式的缺点
- 使类的导出 API 永远束缚在该类的内部表示法上
- 消耗过多的空间
- 消耗过多的时间
- 会引起栈溢出
第88条:保护性地编写 readObject 方法
- readObject 方法可能会创建一个不可能的对象
- 保护性拷贝在有效性检查之前进行
第89条:对于实例控制,枚举类型优先于 readResolve
- 如果依赖 readResolve 进行实例控制,带有对象引用类型的所有实例域都必须声明为 transient
- 注意 readResolve 的可访问性
第90条:考虑用序列化代理代替序列化实例
序列化代理模式
为可序列化的类设计一个私有的静态嵌套类(序列化代理),精确地表示外围类的实例的逻辑状态。它有一个单独的构造器,参数类型就是外围类。构造器只从它的参数中复制数据:不需要进行任何一致性检查或者保护性拷贝。外围类和内部类都要实现 Serializable 接口
标签:总结,Java,Effective,对象,接口,参数,使用,类型,方法 From: https://blog.csdn.net/m0_56898000/article/details/137941679