JAVA必读经典系列: Effective JAVA 好记性不如烂笔头,引以为记
序列化
谨慎的实现Serializable接口
- 实现Serializable最大的代价,一旦这个类被发布就大大降低了改变这个类实现的灵活性,这个类中所有私有实例域都将变成导出API的一部分,不符合最低限度访问域的实践原则
- UID流的唯一标识符,如果没有就会自动产生,受类名称接口名称等影响而变化,如果没有显示声明新版本的类反序列化旧版本会InvaildClassException.
- 实现序列化增加了出现bug的安全漏洞的可能性,增加了兼容性测试负担.
- 一般来说值类应当实现Serializable,活动实体则不用,为继承而设计的类更应当谨慎.Throwable,Component,HttpServlet
- readObjectNoData用于初始化反序列化对象时,发生一些情况导致反序列化对象无法获取数据eg:类的某些实例域默认值违反了约束条件时使用
- 一些专门为了继承的类不是可序列化的,就不可能编写出可序列化的子类,如果超类没有提供可访问的无参构造器,子类也不可能被序列化
- 最好在所有约束关系已经创建的情况下载创建类-创建者模式
- 内部类不应该实现serializable,除了静态成员类
- 简而言之,千万不要以为实现Serializable接口会很容易,除非一个类在用了一段时间后就会被抛弃,否则,实现Serializable接口就是个严肃的承若,必须认真对待.如果一个类是为了继承而设计,则更加需要小心
- 提供一个无参的构造器,允许但不要求子类实现Serializable
考虑使用自定义的序列化形式
- 不要贸然接收默认的序列化形式,除非一个对象的物理表示法等同于它的逻辑视图,通常还必须提供一个ReadObject方法来保证约束关系和安全性
- 默认序列化的缺点:束缚永远,空间消耗,时间消耗,栈溢出等
- @serial @seialData文档标签 transient瞬时,writeObject,readObject,不调用defaultWriteObject/readObject允许但不推荐
- 无论你是否使用默认的序列化形式,当defaultWriteObject被调用时,每个非transient都会被序列化,决定非transient时确保是逻辑状态的一部分.
- transient反序列化时初始化为默认值,否则提供readObject,在调用defaultReadObject后,恢复成可接受的值/或者延迟初始化.
- 如果在读取整个对象状态的任何其他方法上强制任何同步,序列化也必须同步
- 无论选用那种序列化形式,都要生成一个显示的UID/如果不想兼容,则只需要修改UID即可
保护性的编写readObject方法
- readObject方法相当于一个公有的构造器.
- 许多类的安全性依赖于String的不可变性.
- 当一个对象被反序列化时,对于客户端不应该拥有的对象引用,如果过那个域包含了这样的对象应用,就必须要做保护性拷贝,保护性拷贝是在有效性检查之前做的.对于final域保护性拷贝是不可能的
- 不要使用writeUnshared/readUnshired,更快但是不安全
- readObject和构造器一样,不可以调用可被覆盖的方法,无论是直接调用还是间接调用都不可以
- 告诫:
- 私有对象引用域,要保护性拷贝这些域中的每个对象,eg:不可变类中的可变组件
- 检查在保护性拷贝之后,抛出InvalidObjectException
- 如果对象图在反序列化之后必须验证,则使用ObjectInputValidation接口
- 无论是直接还是间接,都不应该使用任何可被覆盖的方法
对于实例控制,枚举类型优先于readResolve
- 序列化会破坏单例特性,而readResolve允许你用readObject创建的实例代替另一个实例,反序列化后新建对象上的readResolve方法就会被调用,该方法返回的对象引用将会取代新建的对象.
- 如果依赖readResolve进行实例控制,带有对象引用类型的所有实例域都必须声明为transient的.否则就会带来安全性问题.
- 将实例受控的类编写成枚举,JVM对此提供了保障.用readResolve进行实例控制并不过时
考虑使用序列化代理代替序列化实例
- 序列化代理,一个私有的静态嵌套类精确地表示外围类的实例的逻辑状态,它应该有个单独的构造器,并以外围实例为参数并从中复制数据.然后用writeReplace方法在序列化之前将外围类的实例变成了序列化代理,并在外围类的ReadObject方法中抛出异常,防止伪造.最后在ReadResolve方法中构造外围类的实例,这个readResolve方法仅利用公有API创建外围类实例,最大程度上消除了序列化机制中语言本身之外的特征.
- 局限:不能与可被客户端扩展的类兼容,不能与对象图中循环的某些类兼容,序列化开销会增加
并发
同步访问共享的可变数据
- 同步的含义:正确的同步可以保证没有方法会看到对象处于不一致的状态,此外保证每个进入同步代码块的线程都可以看到由同一个锁保护的之前所有的修改,不仅互斥,还保证可见性.
- java规范保证读–或者–写一个变量是原子的,除了long和double,及时没有同步的情况下并发修改也是如此
- 你可能听说过为了提高性能,在读写原子数据时应该避免使用同步,这种说法不仅错误而且危险,原子数据并不保证一个线程的写入值对于另一个线程是可见的.
- Thread.stop很久之前就已经不提倡使用,其本质上是不安全的,会导致数据遭到破坏,要防止一个线程妨碍另一个线程建议使用轮询一个boolean域
- volatile保证可见性,每次读取之前都会同步主内存
- 增量操作符++不是原子的
- AutomicLong所做的工作正是你想要的
- 线程封闭,把可变数据限制在单个线程内
- 安全的发布对象:保存在静态域中,作为类初始化的一部分,可以保存在volitile,final或者正常访问锁定的域中,或者放到并发的集合中
- 当多个线程共享可变数据时,每个读或者写的线程都必须执行同步,否则就会造成活性失败和安全性失败
避免过度同步
- 为了避免活性失败和安全性失败,在一个被同步的方法或者代码块中,永远不要放弃对客户端的控制,在被同步的区域内不要设计成要被覆盖的方法,或者客户端以函数对象的形式提供的方法
- 锁是可重入的,但是会将火活性失败转换为安全性失败
- 快照CopyOnWriteArrayList,不需要锁定,但是大量使用性能会大受影响
- 在同步区域之外被调用的外来方法被称作”开放调用”,除了避免死锁外,可以极大的增加并发性
- 通常你应当在同步区域内做尽量少的事情,如果必须要执行某个很耗时的操作,应当设法把这个动作移到同步区域外
- 过度同步的实际成本不是指获取锁所花费的时间,而是指失去了并行的机会,以及需要确保每个核都有一个一致的内存视图而导致的延迟,此外还会限制JVM优化的能力.
- 如果内部同步你可以获得明显比外部锁定整个对象更好的并发性,否则就不要使用内部同步,让客户在必要的时候从外部同步,eg:StringBuilder>StringBuffer
- 分拆锁,分离锁,非阻塞并发控制
- 为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法,尽量限制同步区域内的工作量.
- 当你在设计一个可变类时要考虑它们是否应该自己完成同步操作,当你有足够理由的时候在使用内部同步,并文档说明
executor和task优先于线程
- java.util.concurrent,executor service
- 等待任何线程invokeAny或者所有线程invokeAll完成
- 优雅的完成终止awaitTermination
- 任务完成时逐个的获取这些任务的结果ExecutorCompletionService
- 线程池Executors提供了大多数executor,也可以直接使用ThreadPoolExecutor,Executor.newCachedThreadPool任意大,newFixedThreadPool固定大小
- 尽量不要使用Thread,它既是工作单元又是执行机制,工作单元用Runable/Callable,执行用executor service
- 用SchedulerThreadPoolExecutor代替Timer,支持多个线程并可以优雅的从抛出未受检异常的任务中恢复
并发工具优先于wait和notify
- 没有理由使用wait和notify,notifyAll,在同步代码块内执行,等待/唤醒操作
- 一般使用更高级的工具:Executor Framework/Concurrent Collection/SynChronizer
- ConcurrentMap.pubIfAbsent,优先使用ConCurrentHashMap,而不是Collections.synchronizedMap或者HashTable
- ———————————————————–
- 扩展阻塞操作如BolckingQueue扩展Queue接口,提供了take,put等方法:
- add 增加一个元索 如果队列已满,则抛出一个IIIegaISlabEepeplian异常
- remove 移除并返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常
- offer 添加一个元素并返回true 如果队列已满,则返回false
- poll 移除并返问队列头部的元素 如果队列为空,则返回null
- put 添加一个元素 如果队列满,则阻塞
- take 移除并返回队列头部的元素 如果队列为空,则阻塞
- element 返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常
- peek 返回队列头部的元素 如果队列为空,则返回null
- ———————————————————–
- 大多数ExecutorService实现都是用BlockingQueue,生产者/消费之
- 同步器Sunchronizer最常使用的是CountDownLatch栅栏和Semaphore(计数信号量),不常用的是CynlicBarrizer屏障/Exchanger进程间协调,同步点交换数据 countdown.await();
- 线程饥饿死锁
- 如果工作线程捕捉到InterruptedException,就要习惯用Thread.currentThread().interrupt(),重新断言中断,并从它的run方法中返回
- 对于间歇式的定时任务,始终应当优先使用System.nanoTime,而不是System.currentTimeMills,更精确,不受系统时钟影响
- wait方法被用来使线程等待某个条件,他必须在同步区域内被调用,这个同步区域将对象锁定在了调用wait方法的对象上.应该使用while(…){obj.wait()}的wait循环模式来调用wait方法:永远不要再循环之外调用wait
- notify唤醒的是某个正在等待的线程,notifyAll等待是所有正在等待的线程,总应该使用notifyAll,除非每次只有一个线程允许被唤醒.
- 没有理由在新的代码中使用notify/wait,如果必须使用那么就用wait循环模式,并优先使用notifyAll
线程安全的文档化
- 不可变类:immutable, String Long BigInteger
- 无条件的线程安全: unconditionally thread-safe,Random ConcurrentHashMap
- 有条件的线程安全:conditionally thread-safe,Collections.synchronize包装返回的集合,其迭代器需要额外的外部同步
- 非线程安全:not thread-safe, Arraylist HashMap
- 线程对立:thread-hostile
- 线程安全注解:Immutable ThreadSafe NotThreadSafe
- Collections.synchronizedMap,当遍历任何返回的Map视图时,用户必须手工对它们(map而不是ketset)进行同步
- 没有必要说明枚举的不可变性,静态工厂必须说明被返回对象线程安全性
- 当一个类承诺了使用一个公有可访问的锁对象,意味着允许客户端以原子的方式执行一个方法调用序列-客户端可以发起拒绝服务攻击,方案:使用一个私有锁对象代替同步方法.私有锁对象模式只能用在无条件的线程安全类上.
- 私有锁对象模式特别适用于为继承而设计的类,防止子类无意中修改基类的操作,反之亦然
- 有条件的线程安全类必须在文档中指明那个方法调用序列需要外部同步以及在执行这些序列的时候获得那把锁
慎用延迟初始化
- 除非绝对必要,否则就不要这么做,大多数情况下,正常的初始化要优先于延迟初始化,如果利用延迟优化开破坏初始化的循环,就要使用同步访问方法
- 如果出于性能考虑而需要对静态域使用延迟初始化,就使用lazy initiization holder class 模式-使用私有静态内部类holder来持有对象,只有在第一次调用的时候改内部类才会被初始化,从而达到延迟初始化的目的,不会增加任何访问成本
- 如果出于性能考虑而需要对实例域使用延迟初始化,使用双重检查锁,volatile+使用局部变量复制检查(因为局部变量没有锁,所以性能会好很多);
- 如果可以接受重复初始化,就是用单重检查模式
不要依赖于线程调度器
- 线程调度器决定那些线程将会运行,任何依赖与线程调度器来达到正确性或者性能要求的程序,很有可能是不可移植的,最好的办法是保证可运行线程平均数量不明显多于处理器数量
- 保持可运行线程数量尽可能少的主要方法是让每个线程做有意义的事情,然后等待更有意义的事情,如果线程没有在做有意义的事情,就不应该运行.适当规定线程池的大小,并且使任务保持合适的小,但是不能太小,否则分配的开销也会影响到性能,线程不应一直处于忙-等的状态,即反复的检查一个共享对象以等待某些事情发生
- 如果某一个程序不能工作.不要企图使用Thread.yield来修正程序(暂停当前执行的线程对象,并执行其他线程),不可移植,没有可供测试的语义.更好的解决方法是重构,减少可并发运行的程序.
- 线程优先级是JAVA平台上最不可移植的特征了,并非不合理,但是确实不必要的.
- yield的唯一用途是在测试的时候增加并发性,他并不做实际工作,只是将控制权返回给调用他的程序,应该使用Thread.sleep(1)代替yield来进行并发测试
- 总之,不要让程序的正确性依赖于线程调度器,不要依赖于Thread.yield或者线程优先级
避免使用线程组
- thread Group允许你把Thead的某些基本功能应用到一组线程上,因为线程组已经过时了,所以实际上根本没有必要修正
- 在java1.5之前,当线程抛出未被捕获的异常时,ThreadGroup的uncaughtException是获得控制权的唯一方法.但是1.5之后Thread.setUncaughtExceptionHandeler<指定异常处理方法>方法提供了同样的功能
- 使用线线程池代替
创建和销毁对象
考虑用静态工厂方法代替构造器
- 优点:
- 有名称
- 不用每次调用都创建实例
- 可以返回任何子类型
- 代码更加简洁
- 缺点:不可继承,与其他静态方法没有区别
- 常用名称:valueOf/Of/getInstance/newInstance/getType/newType
遇到多个构造器参数时请考虑用构建器
- JavaBean模式的缺点:状态不一致,非不可变
- Builder模式(建造者模式):利用所有参数得到builder对象,然后来设置每个参数,最后build来生成不可变的对象.
- 典型eg :new bulider().setA().setB().builder();
- 和抽象工厂配合使用,特别适合具有多个参数并且大部分参数都是可选的情况
用私有构造器或者枚举类型强化singleton属性
- AccessibleObject.setAccessible方法通过反射访问私有方法
- 公有静态成员
- 双重检查锁
- 静态内部类
- 单元素枚举<最佳>
- public enum T{
A;
int do(){
};
}
通过私有构造器强化不可实例化的能力
- 甚至在私有构造器内抛出异常
- 静态工厂方法通常优于构造器,
- “”>new String(“”)
- Boolean.valueOf>Boolean()
- 使用一个无状态的适配器对象>多个
- 优先使用基本类型而不是装箱类型,当心无意识的自动装箱
- 小对象的创建和销毁是非常廉价的,通常提供清晰和简洁性是有好处的
- 通常除了连接池自己维护对象池不是一种好的做法.
避免创建不必要的对象
重用而不是创建对象
消除过期的对象引用
- 清空过期引用,如果又被错误的解除引用立即会抛出异常,但应该只是一种意外而不是规范
- 常见内存泄漏:
- 只要自己管理内存,就应该警惕内存泄漏问题
- 缓存 ->WeakHashMap(弱引用,会被回收)
- 监听器和其他回调->弱引用
避免使用终结方法
- Object.finalizer,不可预测一般情况下也不必要.该方法不能保证被及时执行甚至不能保证被执行,不应该依赖终结方法来更新重要的状态,如果有异常则使得对象处于不确定的状态,连警告都不会有,严重的性能损失.eg:关闭打开的文件
- 推荐方案:
- 提供一个显示的终结方法<eg:close.cancle,flush..>,并在私有域里面记录下不可用状态,检查该域并抛出illegalStateException 异常.通常与try finally结合使用
- finallizer好处:充当安全网,避免忘记,本地对等体?
- 注意override finallizer子类并不会主动调用父类的finalizer方法必须手动调用.super.finallize();
- 终结方法守卫者:在私有实例域里面使用一个匿名内部类对象,该对象的唯一用途就是终结它的外围实例.当守卫者被终结的时候外围实例也被终结,不会影响父类的终结方法执行.
对于所有对象都通用的方法
覆盖equals时请遵守通用的约定
- 通常针对于值类,满足
- 自反性(x.equal(x))
- 对称性(x.equal(y),y.equal(x))
- 传递性(x->y->z)
- 一致性(多次调用行为一致).
- 非空性(x.equals(null)==false)
- 无法在扩展可实例化类的的同时,既增加新的值组件,同时又保留equals约定,除非愿意放弃面向对象所带来的优势.
- 永远不要使equals方法依赖于不可靠的资源.
- 高质量的equals:
- 使用==判断
- 使用instanceof检查类型(不需要单独的null检查)
- 转换成正确的类型
- 检查关键域
- 基本整形可以用==,Float使用Float.compare, Double使用Double.copare.因为存在Flota.NaN.
- 不要企图让equals过于智能.
- 误:不要把equals的Object对象替换成其他类型,Override
覆盖equals时总要覆盖hashCode
- 只要equals比较所用到的信息没有被修改,那么调用多次都必须始终如一的返回同一个整数.
- 只要两个对象equals相等,那么hashCode必须产生同样的整数.
- 如果不相等,那么不一定要产生不同的整数,但是不相同的整数有利于提高散列表的性能
始终要覆盖toString()
谨慎的覆盖clone
- 实现Cloneable接口,否则会抛出异常.
- Clone的约定:
- 1.x.cone()!=x
- 2.x.clone().getClass()==Class
- 3.x.clone.equals(x)
- 如果类的域中包含了可变对象,必须确保不会伤害到原始对象,数组类型必须单独的拷贝每个对象,并确保正确的创建被克隆对象中的约束条件.
- clone架构与引用可变对象的final域的正常使用是不兼容的
- 克隆复杂对象:
- 先调用super.clone()
- 将对象的所有域置成空白状态
- 然后调用高层的方法重新产生对象的状态.
- 建议:提供拷贝构造器或者拷贝工厂来替代clone.很多专家级别的程序员从来也不覆盖或者干脆不使用clone(),除非拷贝数组
考虑实现Comparable
- 小于 <0 -1
- 等于==0; 0
- 大于 >0 1
- Collections或者Arrays内部的排序逻辑
- 满足特性:自反/对称/传递
- 强烈建议 x.compareTo(y)==0 == x.equals(y),但是并非必要
- 浮点域使用Float.compare或者Double.compare
类和接口
使类和成员的可访问性最小化
- default访问级别:包访问级别
- 子类不允许低于父类的访问级别
- 实例域绝不能是公有的,包含该实例域的类并不是线程安全的
- 类的公有静态final数组域或者返回这个域的访问方法几乎总是错误的:替代方法
- 1私有.Collections.unmodifiableList…
- 2.array.clone
在公有类中使用访问方法而非公有域
- 公有类永远不应该暴漏可变的域,虽然还有问题但是公有类暴漏不可变类的危害比较小
使可变性最小化
- 不可变类:不要提供任何会修改对象状态的方法,保证类不会被扩展,使所有域都是final,使所有域都是私有的,确保对于任何可变组件的互斥访问.
- 保持不可变性,简单线程安全可被自由共享,唯一的缺点:对于不同的值都需要一个单独的对象.
- final类的一个替代方案:私有构造器+静态工厂方法.
- String–>StringBuilder(StringBuffer基本废弃线程安全)
- BIgInteger-BigSet.
- 不要为每一个get方法编写一个相应的set方法,除非有很好的理由要让类成为可变的类否则就应该是不可变的.
- 只有你确定需要更好的性能时才提供公有的可变配套类.
复合优于继承
- 继承打破了封装性,使用组合的方式转发方法,常见的装饰模式.继承会传播设计缺陷,只有当两者是is-a关系时才使用继承.
- 继承的重点,不会创建父类实例,所有父类方法覆盖,成员变量和静态方法隐藏,而普通方法完全覆盖掉.
要么为继承而设计并提供文档说明,要么就禁止继承
- 文档来说明可覆盖方法的自用性.
- 为了允许继承,构造器决不能调用可被覆盖的方法,无论是直接调用还是间接调用.否则很可能导致程序失败.超类的构造器在子类构造器之前运行.
- 无论是clone还是readObject,都不可以调用可以被覆盖的方法无论是直接还是间接的.
- 为了继承而设计类会有一些实质性的限制,需要消除自用特性:case将每个可覆盖方法的代码移到一个私有的辅助方法中.
接口优于抽象类
- 优点:容易更新实现新的接口,定义mixxin混合类型,构造非层次结构的类型框架.
- 常用的实现方式:接口+抽象的骨架实现,把接口和抽象类的优点结合起来AbstractInterface,提供了实现上的帮助,而又不受到类型定义的严格限制.
- 模拟多重继承:实现接口的类把对于接口方法的调用转发到一个内部私有类的实例上,这个类扩展了骨架实现类.
- 接口一旦被公开发行并被广泛实现,在想改变几乎是不可能的,而抽献给的演变比接口容易得多.
- 接口通常是定义类型的最佳途径,但是如果演变的容易性比灵活性更重要的时候应该使用抽象类,如果你导出了一个重要的接口,那么应该坚决考虑提供骨架实现.
接口应该只用于类型定义
- 常量接口模式是对接口的不良使用,应该考虑使用枚举或者不可实例化的工具类.
- 静态导入…
类层次优于标签类
- 标签类是对层次类的一种简单效仿但是充斥着模版代码,要将标签类转变成层次类需要将标签类中的每个方法都定义一个包含抽象方法的抽象类,而每个方法的行为都依赖于标签值.
- 类层次的好处,可以final,反映类型之间本质上的层次关系,有助于增强灵活性,并进行更好的编译时类型检查
用函数对象表示策略
- 函数指针的主要作用就是用来实现策略模式,为了在java中使用需要声明一个接口来表示该策略,并且为每个具体策略声明一个实现该接口的类,当一个具体策略只被使用一次时通常使用匿名类.当一个策略是设计来重复使用的时候就要被实现为私有的静态成员类,并通过公有的静态final域被导出.
优先考虑静态成员类
- 嵌套类:静态成员类,非静态成员类,匿名类,局部类.
- 静态成员类常作为公有的辅助类,或者用来代表外围类所代表对象的组件.
- 非静态成员类隐含了外围类的一个引用,如果声明成员不要求外围引用,就要始终把static放在声明中.
- 如果一个嵌套类需要在方法之外可见的或者太长了不适合放在方法内部就应该使用成员类:
- 如果成员类的每个实例都需要一个指向外围实例的引用,那么就使用非静
- 否则就做成静态的.
- 假设这个嵌套类属于一个方法的内部,如果你只需要在一个地方创建实例并且已经有了一个预置的类型可以说明这个类的特征就要把它做成匿名类
- 否则就应该做成局部类(很少使用)
泛型
请不要再新代码中使用原生态类型
- 参数化类型list<E>和原生态类型list(表示任意一个对象的集合)和无限制通配符list<?>(表示某种未知对象的集合):通配符类型是安全的因为不能将任何元素除了null放到collection<?>中,除了collection<? super E>可以放入元素.
- PECS原则:生产者<出>用extend,消费者<入>用super
- 例外:1.在类文字中必须使用原生态类型,list<String>.class和list<?>.class不合法,string[].class和list.class,int.class合法.
- 2.在参数化类型而非无限制通配符上使用insranceof是非法的,使用无限制图片那个佩服代理原生态类型对instanceof不会产生任何影响,但是多余了.(是指在instanceof右边)
消除非受检警告
- 在尽可能小的范围内使用@supressWarnings,永远不要再类上使用.(技巧:使用局部变量来表示)
列表优先于数组
- 数组是协变的,类有继承层次,但集合是不可变的,没有子类型或者父类型的差别.
- 利用列表可以在编译时发现错误,数组是具体化的,只有在运行时才会检查元素类型约束,泛型是通过擦除来实现,在编译时强化类型信息,并在运行时丢弃元素类型,创建list<E>[],list<String>[],new E[]是非法的.但是list<?>[]是合法的.
- 泛型是不可具体化的,指的是运行时比编译时有更少的类型信息,所有不可具体化的类型均不能用于数组.
- 缺点:损失性能和简洁性
- 优先考虑泛型
- 名称通常为E,使用泛型会使类型转换更加安全,也更加容易
优先考虑泛型方法
- 优点:类型推导,无需指定参数类型
- extend:递归类型限制<E extend Comparable<E>>
利用有限制通配符来提升API的灵活性
- 有限制通配符<extend super >,无限制通配符?
- PECS:为了获得最大程度上的灵活性梦瑶在生产者或者消费者的输入参数上使用通配符类型
- 不要用通配符作为返回类型,这样会强制用户在客户端代码中使用通配符类型.
- 类型推导的规则相当复杂,有时候需要显式的类型参数类似Union.<Number>union
- 类型参数和通配符类型具有双重性,许多方法都可以利用其中得有一个或者另一个进行声明,如果类型参数只在方法声明中出现一次,那么就可以用通配符类型替换
- extend:list<?>不可以放入任何非null元素,技巧:使用一个私有辅助泛型方法private static <E> put(E e,list<E>)
优先考虑类型安全的异构容器
- 类型安全的异构容器:Collections.checkedSet,checkedMap,checkList,通过将类型参数放在键上,而不是容器上来规避这一限制实际上就是Map<type,E>,可以用class作为健称为类型令牌
- extend:class.cast类型转换,class.asSubclass转换成超类型
枚举和注解
用枚举代替int常量/String常量
- Java枚举的本质是int final 实例受控 类型安全
- 优点:允许添加任意的方法和域,实现任意接口,以及序列化支持.
- 如果枚举具有普遍适用性,那它就应该成为一个顶层类,否则是被用在一个特定的顶层类中,就应该成为顶层类的一个成员类.
- 将不同的行为和实例关联起来,使用抽象abstract方法.造成模版代码,解决方案:策略枚举,内部枚举策略模式代用.
- 枚举构造器不可以访问枚举的静态域,除了编译时静态域,因为枚举也是静态常量在初始化的时候,静态域并没有初始化;ex:初始化顺序,父类静态域>子类静态域>父类非静态公共域>子类非静态公共域>父类构造器>子类构造器,同一层级和书写顺序有关
- 总结:枚举易读安全功能强大,1.每个常量与属性关联,1.提供行为受这个属性影响的方法,3.策略枚举
用实例域代替序数
- 永远不要根据枚举的序数导出与它关联的值,而是保存在一个实例域中,大多数程序员都不需要这个方法original
用enumSet代替位域
- 性能比得上位的性能 EnumSet
用enumMap代替序数索引
- new EnumMap<Hed.TYPE,Set<Hed>>(Hed.class),不要用序数索引数组,使用EnumMap代替.
用接口模拟可伸缩的枚举
- 枚举的可伸缩性最后证明都不是什么好主意.方法:实现接口
- 虽然无法编写可扩展的枚举类型,但是可以通过实现接口来模拟.
- ex<T extend Enum<T> & IOperator> &表示且是的关系
注解优先于命名模式
- @interface类型
- @Retent(RetentionPolicy.RUNTIME)称作元注解,用于运行时
- @Target(ElementType.METHOD)只有在方法中才是合法的
- 注解永远不会改变被注解元素的语义,但是可以通过工具(反射)进行特殊处理.
- class.isAnnotationPresent(Test.class)是否有Test注解
- class.getAnnotation(Test.class).value()
- 如果是一个数组的话需要用{}括起来.
- 大多数程序员都不需要编写注解
坚持使用Override注解
- 除了抽象方法,虽然写了也没什么坏处
用标记接口定义类型
- 标记接口指的是没有任何方法的接口.
- 标记接口胜过注解的两大原因:编译时检查>运行时检查,可以更加精确地锁定
- 注解胜过标记接口:可以添加一个或者多个注解元素,和更多信息,属于更大的注解框架的部分.
- 如果想要定义一个任何新方法都不会与之关联的类型,标记接口就是最好的选择.
- 如果标记程序元素而非类或者接口,考虑未来可能给标记添加更多信息,或者标记要适合于已经广泛使用了注解元素的框架,那么标记注解就是正确的选择
方法
检查参数的有效性
- 应该在发生错误之后尽快检测出错误 ex:IllegalAugumentException
- 非公有的方法应当使用Assert来检查参数,但是线上一般会关闭掉
- 检查构造器的参数是非常重要的
- 也有例外,有些情况下有效性检查非常昂贵或者根本不切实际或者在隐含的计算中完成
- 应当把这些限制写到文档中,养成这样的习惯非常重要
必要时进行保护性拷贝
- warning:TOC/TOU攻击,保护性拷贝是在在检查参数有效性之前进行,并且有效性检查是针对拷贝后的对象
- 对于参数类型可以是不可信任方子类的参数,请不要使用clone方法进行拷贝.
- 总要进行保护性拷贝,返回数组的不可变视图.
- 有经验的程序员使用Date.getTime来返回long类型,因为其实不可变的.
- 如果拷贝的成本收到限制,并且类信任他的客户端不会不恰当是修改组件,就应当在文档中指明.
谨慎的设计方法签名
- 谨慎的选择方法的名称:遵循标准的命名习惯
- 不要追求提供便利的方法:方法太多会使类难以学习
- 避免过长的参数列表:目标是4个或者更少,相同类型的长参数序列格外有害.解决:把方法分解成多个方法了;创建辅助类;使用Builder模式
- 对于参数类型优先使用接口而不是类
- 对于boolean类型要优先使用两个元素的枚举类型
慎用重载
- 重载的依据是编译时类型,也就是字面类型,对其的选择是静态的,而覆盖的依据是被调用方法所在对象的运行时类型,是动态选择的.
- 替代方案,对于易混淆的重载使用命名模式,对于构造器重载使用静态工厂模式
- 对于每一个重载方法,至少有一个对应的参数在两个重载方法中具有根本不同的类型
- 让更具体化的重载方法把调用转发给更一般的重载方法.
- 如果无法避免,就应该保证当传递同样的参数时,所有重载方法的行为必须一致
- ex:set.remove(E)和list.remove(i),自动装箱与拆箱和泛型成为JAVA一部分后….
慎用可变参数
- Array.asList(…),不能是基本参数,否则会被当成单一元素(version>=1.5),如果需要打印的化使用Arrays.toString();
- 不必改造具有final数组参数的每个方法;只当确实是在数量不定的值上执行调用时才使用可变参数.
- 在重视性能的情况下,使用可变参数机制要特别小心,会导致一次数组的分配和初始化
- 可变参数不应该被滥用
返回零长度的数组或者集合,而不是null
为所有导出的API元素编写文档注释
- javadoc:{@literal}不会被转义 和 {@code}代码格式展示 @throws @param @return
- 包级私有的文档注释放在package-info.java的文件中
- 继承能力{@inheritDoc}
通用程序设计
将局部变量的作用域最小化
- 在第一次使用的地方声明局部变量,在使用之前进行声明只会造成混乱,过早的声明变量会使其作用域过早的扩展
- 几乎每个局部变量的声明都应该包含一个初始化表达式,否则就应该推迟声明
- for循环优先于while循环for(iterator=…;iterator.hasNext;)最优 for(int i=0,eee=l.size;i<eee;i++)最优
- 要使方法小而集中
for-each循环优先于传统的for循环
- 遍历集合/数组/以及Iterable
- 无法使用,remove/转换/平行迭代
了解和使用类库
- 随机数Random.nextInt(N)
- java.lang,java.util,java.io,Collection Framework/concurrent
- 不要重新发明轮子
- 如果需要精确地答案,请避免使用float和double
- float和double尤其不适合于货币计算,int/long或者使用BigDecimal代替缺点在于很慢且不方便
- 数字<=9位数字使用int,<=18使用long,>18必须使用BigDecimal
基本类型优先于装箱基本类型
- 基本类型只有值,没有null,节省空间和时间,装箱基本类型有同一性
- 当在一项操作中混合使用基本类型和装箱基本类型时.就会自动拆箱
- 只有在集合元素,键值等参数化类型时才使用装箱基本类型,否则会导致高开销和不必要的对象创建
如果其他类型更合适,则尽量避免使用字符串
- 字符串不适合代替枚举类型/聚集类型(eg:dfuyah|fdj)/能力表
当心字符串连接的性能
- 连接n个字符而重复使用+时,时间复杂度n2
- 用StringBuilder代替String,StringBuffer已经过时,而且最好预设字符长度
通过接口引用对象
- 如果有可能对于参数/返回值/变量和域,都应该使用接口来声明,否则应当使用最基础的类
- ThreadLocal使用IdentifyHashMap来实现
接口优先于反射机制
- 反射使用Constructor/Method/Field,Class.newInsatnce
- 丧失了编译时检查的好处/非常笨拙和冗长/性能损失50倍?
- 如果运行时必须依赖其他包的多个版本,那么反射可能就非常有用
- 如果有可能就应该仅仅使用反射机制来实例化对象,而访问对象方法时使用编译时已知的某个接口或者类
谨慎的使用本地方法
- 1JNI,用本地方法来提高性能的做法不值得提倡,不是安全的/不可移植/更难调试/固定开销,请务必三思
谨慎的进行优化
- 优化的弊大于利,特别是不成熟的优化
- 不要为性能牺牲合理的机构/要努力编写好的程序而不是快的程序,但是要努力避免哪些限制性能的设计,再多底层优化也无法弥补算法的选择不当
- 要获得更好的性能而对API进行包装是一种很不好的想法.
- JDK带了简单的性能剖析工具.现在的IDE也提供相关功能
遵守普遍接受的命名惯例
- 包:层次状,使用句号分割每个部分,每个部分使用小写字母/数字,使用你的组织的internet域名开头,并且把顶级域名放在前面,标准类库和一些可选类库是以java/javax开头,其他用户绝对不可以使用java/javax开头,每个部分通常不应当超过8个字符,鼓励使用有意义的缩写或者首字母缩写
- 类和接口/枚举/注解:每个单次首字母大写,应尽量避免缩写,对于首字母缩写强烈建议采用仅有首字母大写的格式
- 方法/域:首字母小写,除了常量域,大写用下划线隔开
- 类型参数:T表示任意类型.E表示集合元素.K/V表示键值对,X表示异常,任何类型的序列可以是T/U/V,或者T1/T2/T3
- 接口的命名-able或者ible结尾或者I开头
- boolean类型的值往往以is开头,很少使用has,返回属性通常以get开头,bean必须以get开头
- 转换对象类型通常使用toType,返回视图则为asType,返回基本类型通常为typeValue.静态工厂使用valueOf,of,getInstance,newInstance,getType,newType
异常
只针对异常的情况才使用异常
- 反面例子:企图使用JAVA的错误判断机制来提高性能,这样反而阻止了JVM本来可能的优化,尤其是现代的JVM上
- 异常应该只用于异常的情况下,他们永远不应该用于正常的控制流
- 正面:提供状态测试方法或者返回一个可以识别的值如null,如果对象将在缺少外部同步的情况下被并发访问那么返回可识别的值是必要的,从性能的角度考虑可识别>状态测试,其余情况应当使用状态测试
对于可恢复的情况使用受检异常,对编程错误使用运行时异常
- 受检异常 checked:期望会恢复
- 运行时异常 runtime:不需要抛出也不应该被捕获,来表示编程错误
- 错误 error:JVM保留
- 异常也是一个对象,字符串表示法非常脆弱,提供一些辅助的方法非常必要
避免不必要的使用受检的异常
- 抛出-处理=负担
- 把受检的异常变成非受检的一种方法:把抛出异常的方法分为两个方法,第一个返回boolean表示是否应该抛出异常
优先使用标准的异常
- IllegalArgumentException参数值不正确
- IllegalStateException对象状态不合适
- NullPointerException空指针
- IndexOutOfBoundsException 越界
- ConCurrentModificationException 禁止并发修改并检测到修改
- UnsupportedOperationException 不支持的方法
抛出与抽象相对应的异常
- 异常转译/异常链Throwable.getCause
- 尽管异常转译有改进们也不应当滥用,最好在传递给底层参数之前检查,避免在底层抛出异常
- 如果无法避免那么在高层绕开,从而将问题隔离
每个方法抛出的异常都要有文档
- @throws
- 不要为未受检的异常提供 throws子句,在文档中记录非受检的异常是满足前提条件的最佳做法
- 永远不要声明 throws Exception/Throwable,为每个受检异常提供单独的throws子句.
在细节消息中包含能捕获失败的信息
- 大量的描述信息没有意义,一个推荐的做法,在异常的构造器中而不是字符串细节中引入这些消息
努力使失败保持原子性
- 1,在操作之前检查参数的有效性
- 调整计算顺序使得任何可能失败的计算在对象修改之前发生
- 编写一段恢复代码
- 使用拷贝
不要忽略异常
- 可以忽略的一条,在关闭FileInputStream时,因为信息已经装载,而还没有改变文件状态所以没必要恢复
- 最少也应该在catch中包含一条说明,解释为什么可以忽略这个异常
超哥:
最近研读JUC相关的知识,并查看了相关线程池的源码。
executor和task优先于线程,这一个小标题中第五项提到:
线程池Executors提供了大多数executor,也可以直接使用ThreadPoolExecutor,Executor.newCachedThreadPool任意大,newFixedThreadPool固定大小
查看对应的源码,newFixedThreadPool其中的阻塞队列使用的是无界队列,其中newCachedThreadPool中用到的最大线程数是Integer.MAX_VALUE,在复杂生产环境中,这两种原生API其实都是不建议使用的。两种都会因为不同的原因可能造成对应的OOM。
commons-lang3包和com.google.guava包这两种线程池API根据业务配置对应的线程池会好的很多。
厉害了
👍👍👍👍