Effective Java
2021-10-28 01:04:15 20 举报
Effective Java
作者其他创作
大纲/内容
前言-略
第一章 对象创建和销毁
1. 用静态工厂方法代替构造器<br>
优势
有名称
不必每次都创建新对象
可以返回原返回类型的任何子类<br>
所返回的对象可以随着调用参数的不同返回不同的类型
方法返回对象所属的类,在编写包含该静态方法的类时可以不存在
服务提供者框架
服务接口(Service Interface)
提供者注册API(Service Registration API)<br>
服务访问API(Service Access API)
服务提供者API(service provider interface)(可选)
对JDBC而言,Connection是服务接口的一部分。DriverManager.registerDriver是提供者注册API,DriverManger.getConnection是服务访问API,Driver是服务提供者接口。<br>
服务提供者框架模式有着无数的变体 。例如,服务访问API可以返回比提供者需要的更丰富的接口(桥接模式)。依赖注入框架可以被看做是强大的服务提供者。从Java6开始Java平台就提供了通用的服务提供者框架 java.util.ServiceLoader,因此一般不需要(也不应该)自行编写自行服务提供者框架。JDBC不使用该框架是因为其出现早于该框架。
缺点<br>
类如果没有公有public或受保护的protected 的构造器,就不能被子类化。
如不能继承Collections中的类(UnmodifiableSet等),鼓励程序员使用复合而不是继承
程序员很难发现他们,
静态方法不会像构造器一样在API中明确标识出来,因此对于提供了静态工厂方法而不是构造器的类来说,想要查明如何实例化一个类是比较困难的。
在需要在类注释中提及静态工厂,并遵守标准的命名习惯,可以弥补这一劣势。静态工厂方法惯用名称
from一一类型转换方法,单个参数,返回该类型对应的实例 Date d= Date.froom(instant) ; <br>
of一一聚合,带有多个参数,返回该类新的一个实例 Set<Rank> faceCa ds = EnumSet. of (JACK , QUEEN, KING];<br>
valueOf一一比from和to更烦琐的替代方法
instance和getInstance一一通过方法参数描述来返回实例
create和newInstance一一同上,保证每次都创建新的实例
getType一一工厂方法支持多个类时使用 Files.getFileStore<br>
newType一一工厂方法支持多个类时使用 Files.newBufferedRead
type一一getType和newType的简版 Collections.list<br>
简而言之,静态工厂方法和公有构造器各有用处,我们需要理解他们各自的好处。静态工厂方法经常更加合适,因此切忌第一反应就是提供公有构造器,而不先考虑静态工厂。
2.遇到多个构造器参数时要考虑使用构建器(Builder)
静态工厂和构造器有个共同的局限性:不能很好的扩展到大量的可选参数。涉及多个参数时一般有几种实现方式:
重叠构造器(telescoping contrustor)模式,创建多个构造器,分别包括全部的必要的参数和对应的可选参数
可行,参数过多时客户端的代码会很难编写,且难以阅读。
JavaBean模式。调用一个无参的构造器,再逐个调用setter设置属性值 Point p = new Point (); p.setA(a); p.setB(b);。
易读,易编写<br>
构建过程中JavaBean可能会处于状态不一致的状态。
这样做的话成员变量将失去设置为不可变(final)的可能性,程序员也就需要额外的努力来确保其线程安全(例如实现冻结特性,避免在构建完成前执行操作,笨拙且有可能会导致错误,很少使用)。
Builder模式,既保证重叠构造器的安全性,又有JavaBean模式的易读性。
构建一个Builder,使用setter方法设置其属性,再用builder方法生产目标对象。
Pojo p = new Pojo.Builder(2,4).a(2).addTag("a").addTag("B").build();<br>
子类方法声明返回超级类中声明的返回类型的子类型,被称为协变返回类型(covariant return type),它允许客户端无需类型转换就能使用这些构建器。
优势<br>
与构造器相比,builder的优势在于,可以有多个可变(varargs)参数,因为是用单独的方法来传入每个参数。builder还允许将多次调用某个方法而传入的参数集中到一个域中。如addTag。
Builder十分灵活,可以使用单个Builder创建多个对象。builder的参数可以在调用build方法来创建对象期间进行调整,也可以随不同的对象而改变。builder可以自动填充某些域,例如每次创建对象时自动增加序列号。
不足
为了创建对象,必须创建器构建器。
代码比重叠构造器更加冗长,因此只在有多个参数时使用<br>
如果考虑可能会新增参数,最好一开始就使用构建器,否则过时的工厂方法和构造器就会显得不协调
如果类的构造器和静态工厂中有多个参数,设计这种类时,Builder是不错的选择。尤其是大多数参数是可选或者类型相同的时候。与使用重叠构造器相比,使用Builder模式的客户端代码更易阅读和编写,也会比JavaBeans更加安全。
3.使用私有构造器或者枚举类型强化Singleton属性。
定义
singleton指仅被实例化一次的类,通常用于标识一个无状态的对象,如函数或本质上唯一的系统组件。
singleton会使他的客户端测试变得十分困难,因为不可能给Singleton替换模拟实现,除非实现一个充当起类型的接口。
实现
私有构造器,导出静态变量,以便客户端代码可以访问该类的唯一实例。
私有构造器仅被调用一次,用于实例化静态final变量 INSTANCE,
由于缺少公有或受保护的构造器,保证了实例的全局唯一性,
一旦实例化,将会只存在一个其实例,不多也不少,客户端的任何行为都不会改变这一点
存在的问题
拥有特权的客户端代码可以借助AccessibleObject.setAccessAble方法,通过反射机制调用私有构造器。要抵御此类攻击,可以修改构造器,让他在被第二次调用的时候抛出异常
4.通过私有化构造器强化不可实例化的能力<br>
5.优先考虑依赖注入来引入资源
6.避免创建不必要的对象
7.消除过期对象的引用
8.避免使用终结方法和清理方法
9.try-with-resources优于try-finally<br>
第二章 对于所有对象都通用的方法
10.覆盖equals时请遵守通用约定<br>
11.覆盖equals时总要覆盖hashCode
12.始终要覆盖toString<br>
13.谨慎覆盖clone
14.考虑实现Comparable接口
第三章 类和接口<br>
15.使类和成员的可访问性最小化
16.要在公开类的非公开域中使用访问方法
17.使可变性最小化
18.复合优于继承<br>
19.要么设计继承并提供文档说明,要么禁用继承
20.接口优于抽象类
21.为后代设计接口
22.接口只用于定义类型
23.类层次优于标签类<br>
24.静态成员类优于非静态成员类<br>
25.限制源文件为当个顶级类
第四章 泛型
26.不要使用原生态类型
27.消除非受检警告
28.列表优于数组
29.优先考虑泛型
30.优先考虑泛型方法
31.利用有限通配符提升API灵活性
32.谨慎并用泛型和可变参数
33.优先考虑类型安全的异构容器
第五章 枚举与注解
34.使用enum代替int常量
35.使用实例域代替序数(原生序数ordinal是不稳定的)
36.使用EnumSet代替位域
37.使用EnumMap代替序数索引
38.用接口模拟可扩展的枚举
39.注解优于命名模式
40.坚持使用Override注解
41.使用标记接口定义类型
第六章 Lambda和Stream
42.Lambda优于匿名类
43.方法引用优于Lambda
44.坚持使用标准的函数接口 <br>
45.谨慎使用Stream
滥用Stream会使程序变得更难读懂和维护
46.优先选择Stream中无副作用的函数
47.Stream优先使用Collection作为返回类型
48.谨慎使用Stream并行
不恰当的并行操作可能导致程序运行失败,或者造成性能灾难
不是要完全避免使用,使用时一定要确保代码正确,并在真实环境下认真进行性能测量<br>
第七章 方法
49.检查参数的有效性<br>
50.必要时进行保护性拷贝
有些对象是可变的,可能在通过检查之后发生变更
拷贝要在检查之前进行,检查的是拷贝的对象
51.谨慎设计方法签名
谨慎选择方法名称
不要过于追求提供便利的方法
如果不能确定,不要提供快捷方式
避免过长的参数列表
参数类型,优先使用接口而不是类
对于boolean类型,优先使用两个元素的枚举类型
52.慎用重载
对于重载方法的选择是静态的,而对于被覆盖的方法选择则是动态的
保守的策略:永远不导出两个相同参数数目的重载方法
不要在相同的地方的调用带有不同函数接口的方法<br>
能够重载方法不意味着应该重载方法
53.慎用可变参数
54.返回零长度的数组或者集合而不是null
使得API难以使用,也容易出错,且没有任何性能优势
55.谨慎返回Optional
如果发现自己在编写的方法始终无法返回值,且相信该方法的用户每次调用都要考虑这种可能性,就需要返回Optional
对于注重性能的方法,最好返回一个null,或者抛出异常。
尽量不要将Optional用作返回值以外的其他用途
56.为所有导出的API元素编写文档注释
给所有的类、接口、构造器、方法和域声明之前添加文档注释
方法的文档注释应该简洁地描述出它与客户端之间的约定。说明方法做了什么(而不是如何做),前置条件和后置条件
第八章 通用编程
57.将局部变量的作用域最小化
在第一次使用变量的地方区声明变量
每个局部变量都应该包含一个初始化表达式,如果没有足够的信息进行初始化,则变量声明应该推迟
58.for-each循环由于传统for循环
隐藏了迭代器和索引变量,避免了混乱和出错的可能性
59.了解并使用类库
通过使用标准类库,可以充分利用写标准类库的专家的知识,以及在你之前的其他人的经验
随机数生成器,在1.7之后大多使用ThreadLocalRandom,可以产生更高质量的随机数,而且速度更快
不必浪费时间为哪些与工作不太相关的问题提供特别的解决方案
标准类库的性能会随时间推移而增加新的功能<br>
使自己的代码融入主流,更易读、易维护、更易被大多数开发人员重用
总之不要重复造轮子;
如果要做的事是十分常见的,有可能已经有类库中的某个类完成了这样的工作。如果有的话就用现成的。如果不清楚有没有就去查一查。
一般而言,类库的代码可能会比你自己编写单独代码更好一些,并且会随时间推移不断改进。
不是怀疑你的能力,从经济角度分析表明:类库代码收到的关注远远超过大多数程序猿在相同功能上所能给与的投入。
60.如果需要精准的答案,请避免使用float和double
float和double不适合货币计算,1.03-0.42 = 0.6100000000000001<br>
61.基本类型优于装箱类型
62.如果其他类型更适合,应尽量避免使用字符串
字符串不适合代替其他的值类型(应根据数据意义选择合适的类型)<br>
字符串不适合代替枚举类型(枚举更精确,更适合标识常量)
字符串不适合代替聚合类型(不精确,想访问某个域需要解析该字符串)
字符串不适合代替能力表(重名会有冲突)
总之如果有、或是可以编写其他类型应避免使用字符串代表对象,字符串更笨拙、不灵活、速度更慢,也容易出错。
63.了解字符串连接的性能
不要使用字符串连接操作符来合并多个字符串,除非性能无关紧要。否则应该使用StringBuilder.append方法<br>
使用字符数组或是每次处理一个字符串,而不是将他们拼接起来。
64.通过接口引用对象
用接口作为类型,程序将更加灵活。(实现变更不影响客户端使用)
但如果原来的实现提供了特定的功能,(功能不是接口通用约定所要求的,如LinkedHashSet的顺序迭代),二客户端有对该功能有依赖,新的实现也应该提供相同的功能
没有合适的接口,可以考虑必要功能最小的具体类引用对象,例如OutputStream
如果没有合适的接口,完全可以使用类而不是接口引用对象,如String和BigInteger<br>
65.接口优于反射机制
核心反射机制(core reflectiono facility)java.util.reflect包提供了“通过程序访问任意类”的能力。
给定Class对象可以返回Contrustor构造器、Method方法、Field域。这些对象提供了通过程序访问类的成员名称、域类型和方法签名等信息的能力
Method.invoke是使你(在遵从常规安全限制的前提下),访问任何类的任何对象的任何方法。
反射机制允许一个类访问另一个类,即使前者编译时后者还根本不存在。<br>
这种能力也要付出代价:<br>
损失了编译时类型检查的优势
执行反射访问所需的代码非常笨拙和冗长
性能损失
66.谨慎使用本地方法
JNI(java native interface)允许java程序调用本地方法(native method),所谓本地方法是指用本地编程语言(c或者c++)编写的方法。他们提供了“访问特定于平台的机制的能力”,如访问注册表(registry)。他们还提供了访问本地遗留代码库的能力,从而可以访问遗留数据(legacy data)。最后本地方法可以通过本地语言,编写应用程序中注重性能的部分,以提高系统性能。
使用本地方法来访问特定于平台的机制是合法的,但是没必要。(java9新增了进程API提供了访问操作系统进程的能力)
使用本地方法提升性能的做法不值得提倡(早期版本jvm可能是有必要的)
使用本地方法前三思
极少情况下才需要使用本地方法提高性能,
如果访问底层资源或遗留代码库,应尽量减少本地代码并进行全面测试。
本地代码只要有一个bug都可能破坏整个应用程序
67.谨慎使用优化
优化相关的三条格言
很多计算上的过失都被归咎于效率(没有达到必要的效率),而不是任何其他原因——甚至包括盲目地做傻事
不要去计较效率上的一些小小得失,在97%的情况下,不成熟的优化才是一切问题的根源
优化方面,我们应该遵守两条规则:1.不要进行优化 2.(仅针对专家):还是不要进行优化 ——也就是说,在你没有绝对清晰的未优化方案前,请不要进行优化<br>
三条格言的出现都比Java语言造了20年,他们讲述了关于优化的深刻真理
优化弊大于利,尤其是不成熟的优化。优化过程中产生的软件既不快速也不不正确,还不容易修正。
因此:<br>
不要为了性能优化而牺牲合理的结构。要努力编写好的程序而不是快的程序
努力避免限制性能的设计决策
考虑API设计决策的性能后果
一般而言,好的也会带来好的性能。
为获得好的性能而对API进行包装是非常不好的想法
(未完)
68.遵守普遍的命名惯例
第九章 异常
69.只针对异常的情况抛出异常
异常应该只用于异常的情况,永远不应该用于正常的控制流。
应该使用标准的容易理解的模式,而不是生成可以提供更好性能的、弄巧成拙的方法。
设计良好的程序不应该强迫其客户端为了正常的控制流而使用异常
总之异常是为异常情况而设计的,不要将他们用于正常的控制流,也不要编写迫使它们这么做的API
70.对可恢复的情况使用受检异常,对于编程错误使用运行时异常
java提供了三种可抛出结构(throwable):<br>
受检异常(checked exception):期望调用者适当的恢复
运行时异常(runtime exception):不确定定是否可恢复,或是编程错误
错误(error):不应该产生新的子类
71.避免不必要的使用受检异常
72.优先使用标准异常
IllegalArgumentException
IllegalStateException
IndexOutOfBoundsException<br>
UnsupportedException
ConcurrentModificationExceptiono
NullPointerException
ArithmeticExeption和NumberFormatException
73.抛出与抽象对应的异常<br>
高层实现应该捕获底层的异常,同时抛出可以按照高层抽象进行解释的异常,这种做法被称为异常转译(exception translation),相对于直接抛出底层异常有所改进,但也不应滥用。最好的方法是避免出现底层异常,当无法避免时,有高层悄悄处理这些异常,通过日志系统记录异常方便调查问题,并将客户端代码和最终用户与问题隔离开来。
74.每个方法抛出的所有异常都要建立文档
始终单独地声明受检异常,并利用@throws标签,准确记录抛出异常的条件
对于非受检异常也可以适当声明,但不必使用throws关键字在方法签名上添加描述
75.在细节消息中包含失败-捕获信息
为了捕获失败,异常的细节信息应该包含“对该异常有贡献”的所有参数和域的值
安全敏感!不要在细节信息中包含密码、秘钥和类似的信息
76.努力使失败保持原子性
失败的方法调用应该使对象保持在被调用之前的状态。具有这种属性的方法被称为具有失败原子性(failure atomic)
几种方式可以实现这种效果
最简单的方法-设计一个不可变对象。操作失败会阻止创建新对象,但不会使已有对象保持在不一致的状态中,因为对象创建之后他就处于一致的状态中,之后也不再发生变化。
对于可变对象,获取失败原子性最常见的方法是,在执行操作之前检查参数有效性。这可以使得在对象状态变更之前,先抛出适当的异常。
第三种获得失败原子性的方法是,在对象的临时拷贝上进行操作,在操作完成之后再用临时拷贝中的结果代替对象的内容
最后一种是写一段回复代码(recovery code),由它来拦截操作过程中发生的失败,以及使对象回滚到操作开始之前的状态上。这种方法主要用于永久性(基于磁盘的)数据结构
总而言之,作为方法规范的一部分,他产生的任何异常都应该让对象保持在调用该方法之前的状态。如果违反这条规则,API文档应该清楚地指明对象将会处于什么样的状态。遗憾的是,大量现有的API文档都未能做到这一点。
77.不要忽略异常
如果忽略异常,catch块中应该包含一条注释,说明为什么可以这么做,而且变量应该命名为ignored
第十章 并发
78.同步访问共享的可变数据
解决数据共享的问题,最佳方法是不共享可变的数据。要么共享不可变的数据,要么压根不共享。话句话说,将可变数据限制在单个线程中
总而言之,当多个线程共享可变数的时候,每个读或者写数据的线程都必须执行同步。
未能同步共享可变数据会造成程序的活性失败(liveness failure)和安全性失败(safety failure)<br>
79.避免过度同步<br>
为了避免活性失败和安全性失败,在一个被同步的方法或者代码块中,永远不要放弃对客户端的控制。
在一个被同步的区域内,不要调用设计成要被覆盖的方法,或者有客户端以函数对象方式提供的方法。从包含该同步区域的类的角度来看,这样的方法是外来的(alien)。这个类不知道该方法会做什么事情,也无法控制它。根据外来方法的作用,从同步区域中调用它会导致异常、死锁或数据损坏。
为了避免死锁和数据破坏,千万不要在同步区域内部调用外来方法。更通俗的讲,要尽量将同步区域内部的工作量限制到最少。当你设计一个可变类的时候,要考虑一下他们是否应该自己完成同步操作。在这个多核的时代,这比永远不要过度同步来得更重要。
80.executor、task和stream优于线程
仍然是使用标准类库优于重新开发<br>
81.并发工具优于wait和notify
java.util.concurrent并发工具分三类:<br>
Executor Framework
并发集合(Concurrent Collection)<br>
并发集合为标准的集合接口(如 List Queue和Map)提供了 高性能的并发实现。为了提高并发性,这些实现在内部自己管理同步。因此同步集合不可能排斥并发活动;将它锁定没有什么作用,只会使程序的速度变慢。
因为无法排除并发集合的并发活动,这也意味着无法自动地在并发集合中组成方法调用。因此,有些并发集合已经通过依赖状态的修改操作(state-dependent modify operation)进行了扩展,它将几个基本操作合并到了单个源自操作中(Map.putIfAbsent)<br>
优先使用ConcurrentHashMap而不是Collections.synchronizedMap
同步器(Synchronizer)使线程能够等待另一个线程的对象
常用 CountDownLatch Semephore
不常用 CyclicBarrier Exchanger
功能最强大 Phaser <br>
对于间歇计时,最好使用nanoTime而不是currentTImeMillis更精确。微基准测试很困难最好有专门的框架协助进行(JMH)<br>
wait/notify注意事项<br>
有并发集合和同步框架,<br>
82.线程安全性的文档化
当一个类并并发使用的时候,这个类的行为如何,是该类与其客户端建立的约定的重要组成部分。
如果没有在类中描述其行为的并发性情况,使用这个类的程序员将不得不做出某些假设。
如果假设是错误的,所得到的程序就可能缺少足够的同步,或者过度同步,这两种情况都有可能导致严重的错误
synchronized修饰符是实现细节,而不是api的一部分。如果仅靠该关键字识别是否同步的话,也就是认为同步支持是全部支持或是全部不支持。事实上同步支持是分级别的
不可变的(immutable)——类实例是不便的,所以不需要外部同步。String Long BigInteger<br>
无条件的线程安全(unconditionally thread-safe)——该类实例是可变的,但这个类有足够的内部同步,所以其实例可以被并发使用,无需任何同步。ConcurrentHashMap AtomiLong<br>
有条件的线程安全(conditionally thread-safe)——除了有些方法进行安全的并发使用而需要外部同步以外,这种线程安全级别和无条件的线程安全相同。Collections.synchronized包装返回的集合。他们的迭代器要求外部同步
非线程安全(not thread-safe)该类的实例可变。为了并发使用,客户端必须利用自己选择的外部同步保卫每个方法调用(或者调用序列)。ArrayList HashMap<br>
线程对立(thread-hostile)这种类不能安全地被多个线程并发使用,即使所有外部方法调用都被外部方法保卫。线程对立的根源通常在于,没有同步地修改静态数据。没有人可以编写线程对立的类;这种类是因为没有考虑到并发性而产生的后果。如果类是线程对立的,就应该被修复,或是禁用。
83.慎用延迟初始化
延迟初始化是指延迟到需要域的值的时候采将它初始化
降低了初始化类或创建实例的开销,却增加了访问被延迟初始化域的开销。事实上降低了性能
跟大多数优化一样,除非绝对必要,否则就不要这么做。
当有多个线程时,延迟初始化是需要技巧的。
实例域:双重检查 double check<br>
静态域:使用lazy initialation holder class idiom<br>
对于可以接受重复初始化的实例域也可以使用单重检查 single check idiom<br>
绝大多数情况下,正常初始化要优于延迟初始化。
84.不要依赖于线程调度器
任何依赖线程调度器来达到正确性或者性能要求的程序,很有可能是不可移植的。
多个线程可以运行时,会由线程调度器(thread scheduler)决定哪个线程将会执行,以及运行多长时间。
任何一个合理的操作系统在做出决定时都会努力做到公正,但所采用的策略却大相径庭。因此编写良好的程序不应该依赖这种策略的细节
解决方法:
可运行线程数不明显多余处理器数量。那样线程调度器不会有太多选择。
让线程做些有意义的工作,然后等待更多有意义的工作。如果线程没有再做有意义的工作,就不应该运行。
合理确定线程池大小,任务保持适当的小,彼此独立。
任务不应该太小,否则分配的开销也会影响性能。
线程不应一直处于忙-等(busy-wait)的状态,即反复地检查一个共享对象,以等待某件事情的发生。除了使程序容易受到调度器变化的影响外,忙-等这种方法也会极大地增加处理器的负担,降低了同一台机器上其他进程可以完成的有用工作量。
不要试图使用Thread.yield来调整不同的程序,在不同JVM上表现可能会不一样,不可移植。
调整线程优先级(thread priority)线程优先级是Java平台上最不可移植的特征了。。
总之不要让程序的正确性依赖于线程调度器。否则得到的程序既不健壮,也不具有可移植性。同时,不要依赖Thread.yield或者线程优先级。这种机制只是影响线程调度器。线程优先级可以用来提高已经能够正常工作的程序的服务质量,但永远不应该用来“修正”一个原本并不能工作的程序。
第十一章 序列化<br>
85.其他方法优于Java序列化
正确性、性能、安全性、可维护性都有潜在的问题
序列化炸弹<br>
每当反序列化一个不信任的字节流是,自己就要试着去攻击它。<br>
避免序列化攻击的最佳方式是永远不要反序列化任何东西
WarGames中的名言“获胜的唯一方法就是压根不参与比赛”
在新编写的任何系统中都没理由使用Java序列化。
为了避免Java序列化的诸多风险,有很多其他机制完成对象和字节序列之间的转化,他们同时带来很多便利,如跨平台支持、高性能、大型工具生态系统,以及广阔的专家社区。这些机制统称为跨平台的结构化数据表示法(cross-platform structured-data representation)。他们的共同的特点是比Java序列化要简单,不支持任意对象图的自动序列化和反序列化。而是支持包含属性/值对的简单结构化数据对象。只支持一些基本类型和数组数据类型。事实证明,这个抽象虽然简单,却足以构建功能极其强大的分布式系统,同时有简单得足以避免Java序列化出现以来就一直造成困扰的那些重大问题。
JSON:为javascript设计,基于文本,易于阅读。
ProtoBuf:为C++设计,二进制、更高效。
永远不要反序列化不被信任的数据
尤其是不应该接受来自不信任资源的RMI通信
Java官方安全编码指导方针:对不信任数据的反序列化,从本质上来说是危险的,应该予以避免。
如果必须进行反序列化
利用Java9新增的对象反序列化过滤(Object Deserializetion Filtering)java.io.ObjectInputFilter。在进行序列化之前定义一个过滤器,可以操作类的粒度。默认接受类,同时拒绝黑名单;默认拒绝类,同时接收假定安全的白名单。白名单优于黑名单,因为黑名单只能抵御已知的攻击。有一个工具SWAT(Serial Whitelist Application Trainer),可以自动替应用准备好白名单。过滤设施 也能帮助你避免过多使用内存,并广泛深入对象图,但无法抵御上面提到的序列化炸弹。<br>
总之:序列化是很危险的,应该予以避免。如果是新设计的系统,一定要使用跨平台的结构化数据代替(JSON或ProtoBuf)不要反序列化不被信任的数据,如果必须这样做,使用对象的反序列化过滤,但它并不能阻挡所有的攻击。不要编写可序列化的类.如果必须这样做,一定要加倍小心地进行试验。
86.谨慎地使用Serializable接口
使用Serializable接口的最大代价,一旦一个类被发布,就大大降低了“改变这个类”的灵活性。
接受了默认的序列化形式,这个类中的私有域和包级私有的实例域都将编程API的一部分,这不符合最低限度的访问域的实践准则,从而也就失去了作为信息隐藏工具的有效性。
新旧版本不兼容会导致序列化失败。结构变更、序列化版本UID(流的唯一标识符)<br>
第二个代价,增加了出现Bug和安全漏洞的可能性
一般而言,对象是基于构造器创建的。<br>
序列化机制是一种语言之外的对象创建机制
第三个代价,随着类发型新版本,相关的测试负担也会增加
87.考虑自定义的序列化形式
88.保护性的编写readObject方法
89.对于实例控制,枚举类型优于readResolve
90.考虑用序列化代理代替序列化实例
0 条评论
下一页