effective java

本文提炼了Effective Java的关键设计原则,包括静态工厂方法、构建者模式、Singleton模式、避免创建不必要的对象、优化equals和hashCode方法、谨慎使用clone、使类和成员的可访问性最小化、复合优于继承、接口优于继承、泛型使用技巧、类型安全的异构容器、枚举的最佳实践、注解使用、异常处理策略、线程安全、延迟初始化、线程调度和序列化注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

发现自己水得一比,决定把effective java的设计原则记一下笔记

 

1.静态工厂方法代替构造器

好处:

a.有名称 比如BigIntger.probablePrime可能产生一个素数

b.不会产生新的对象

c.可以返回子类型的对象,比如Enumset的noneOf

d.使代码变得简洁 Map<String, String>  map = new Map<String, String>(); 和 Map<String, String>  map = Maps.newHashMap();

坏处:

a.没有私有构造器,不能子类化

b.和其他static类一样,不容易被认出来

 

2.有多个构造器参数,用builder类

a.使用构造器,参数传入顺序容易出错,参数多了也很难编写和阅读

b.使用javaBean模式去set每个参数,线程不安全,难以调试

使用builder能避免上述情况,适用于多个参数可选。

如果builder用于构造多个类,可以声明为接口:

public interface Builder<T> {

    public T build();

}

 

3.Singleton属性

单例的三种实现方法:

a.私有构造器 + 公有静态域

b.私有构造器 + 私有静态域 + 公有静态工厂方法

c.创建只包含单个元素的枚举类型(最佳方法),优势在于:简洁,并且安全(面对复杂序列化和反射攻击时)

因为序列化会泄漏类的私有域,为反射攻击提供了可能,而enum里面根本没有私有域

 

4.通过私有构造器强化不可实例化的能力

这样的类有一个缺点,就是不能被子类化,因为所有子类都必须显式或者隐式调用父类构造器方法

 

5.避免创建不必要的对象

a.静态工厂方法优于构造器

b.创建不必要的对象只会影响程序风格和性能,但是没能实现保护性拷贝将会导致潜在的错误和安全漏洞

 

6.消除过期的对象引用

"清空对象引用应该是一种例外,而不是一种规范行为"

"只要程序员自己管理内存,就应该提防内存泄漏(该被回收的引用没有被回收,原因比如是没有释放,即没有被置为null)"

"内存泄漏的另一个常见原因是缓存",处理办法:

1.WeakHashMap 有需要注意的一点(哪一点)?

2.LinkedHashMap的removeEledstEntry

“内存泄漏的第三个常见来源是监听器和其他回调”  见https://cloud.tencent.com/developer/ask/119839

 

7.避免使用终结方法(finalize)

因为不保证会及时地执行,甚至不保证其会被执行,异常发生在终结方法中时,不会打印异常信息,而且终结方法还造成严重性能问题

为了避免终结方法,必须显示终止方法,比如java.sql.Connection的close,java.util.Timer的cancel,这些终止方法一般放在finally块里面

终结方法在哪种场景下使用:

1.充当安全网,在终止方法没有被执行的情况下,再调用终结方法

2.在本地对等体并不拥有关键资源的请况下,调用终结方法

tips: 子类的终结方法必须手动调用超类的终结方法(在finally里面调用),finallyGuardian没看,等会看

 

8.覆盖equals遵守通用约定

什么时候不需要覆盖equals?

a. 每个类本来就互不相同,是唯一的

b.不关心是否"逻辑相等"(就是本来两个本质上不同的实例,逻辑上可以认为是相等的)

c.超类已经覆盖了equals,而子类继承equals也是合适的

必须要覆盖equals的情况: 类是私有的或者包级私有的,那么类的equals永远不应该被调用(此时要抛出异常) 

值类对象也需要覆盖equals,以达到逻辑相等,除了"每个值最多只对应一个对象"的值类,比如Enum类型

equals必须满足的规范: 自反性,对称性,传递性,一致性

 

9.覆盖equals总要覆盖hashCode

散列码的计算必须把equals中没有用到的域都排除在外,否则相同的对象可能有不同的hashCode

散列过程中 result = result * 31 + c;

选31,是因为31是奇素数,移位不会丢失信息,而且可以用移位和减法来代替乘法,从而性能可以更好

 

对于不可变的,计算散列码开销比较大的类,最好对散列码作一个缓存

散列码的计算也可以延迟初始化,其实对于开销比较大,后面被用到的可能性比较小的结果,可以等到真正用到的时候才计算,也即延迟初始化

 

10.始终覆盖toString
toString应该包含所有有利于调试的信息

toSring可以指定格式和不指定格式,如果指定了格式,以后就不方便变动,因为有些代码可能依赖于这种格式;如果不指定格式,就可以保留灵活性,方便未来改动

toString内的字段,需要这样对外提供可访问的接口,而不是从toString解析(易出错,而且不稳定)

11.谨慎地覆盖clone


13.使类和成员的可访问性最小化类
a.顶层类可以是包级私有和公有的,能是包级私有的就做成包级私有的(即默认的,没有用private等修饰符修饰的),如果你把类做成公有的,那你就不得不永远支持它

b.包级私有的顶层类,可以变成一个类的嵌套类,但有个前提条件(什么条件?)

c.(是否是对公有类,才有其)实例域绝对不能是公有的(实例域就是类内的非static字段,静态域则相反),静态域也最好不要是公有的,除非该静态域表示的是常量,但public static final things[] values = {}
这样的总是错误的,因为values内的内容是可变的

d.public static final things[] {return values.clone() }这种方法,应该用clone(),避免内部的values传出去被改变    

类的私有成员和包级私有成员可能会因为类implements Serializable而被泄露到导出的API

最好除了API都是private的,可访问性逐渐增加

14.公有类中使用访问方法而非公有域

如果类是包级私有的或者是私有嵌套类,暴露数据域并没有太大的问题(而且有时候还是必要的),因为这些域的作用范围只在该包内,或者该外围类以内

public final int a; 是可以的,可以在初始化(构造)的时候再赋值


15.使可变性最小化

不可变类有:String,Integer,BigInteger(这个类比较特殊,不是final的) 不可变类的优点:易于设计,不容易出错,安全

函数的做法返回一个函数的结果,不改变操作数;而过程的或者命令式的做法会改变操作数

因为不可变,所以便于重用,对于常用到的值,最好提供公有的静态final常量,或者提供一些静态工厂方法

而且不可变类无需保护性拷贝,而且可以成为集合的元素或者map的键

问题: BigInteger的可变的配套类到底是啥???? BitSet算一个,但BigInteger的包级私有配套类是它吗?还是另有其人


让一个类不可变,除了用final关键字,还有一种方法就是: 私有构造器 + 公有静态工厂方法

对于不可变类,其计算结果可以缓存起来,因为其是不可变的,这样可以节省计算开销

除非有很好的理由让类成为可变的类,否则就应该是不可变的,此时也不要为每个get方法编写对应的set方法

不要在构造器和静态工厂方法之外再提供公有的初始化方法,而且也不要提供重新初始化方法

不可变类在序列化是要额外注意,readObject方法

16.复合优于继承

包的内部(甚至顶层类的内部?)使用继承是非常安全的

其实整节就是一句话,复合优于继承(装配者模式给原来的对象加新功能),没其他了

17. 不好意思。。。我看不下去,,,


18.接口优于继承
说来说去,就是单继承比较坑,所以接口牛逼

有一个抽象类优于接口的点,就是抽象类适合演变,而接口一旦确定了,增加其他方法就很麻烦

还有一个比较精彩的点是,通过把自身请求转发(同16节的forwardSet)到"实现了骨架实现类的私有内部类"模拟多继承

接口的经典使用方法是:
1.声明一个接口
2.声明一个抽象类(即骨架实现类),继承该接口,并实现了该接口的大部分方法
3.声明一个具体类,继承该抽象类,具体类被用作实际使用

总而言之,上面的方法是最经典的方法。。。18节基本就说了以上这些东西

19.接口只用于定义类型
整节就说了一个东西:
常量接口(接口里都是常量)不该被使用,想要导出常量可以用不可被实例化的工具类

20.类层次优于标签类
标签类:我感觉就是枚举加一堆方法,本节就是说,要把这种类改成抽象类,及其子类的关系。其他的没啥了

代码整洁之道说过,枚举加一堆方法,是优于优一堆整形常量的类的

21.函数对象表示策略
a.声明一个接口
b.使用匿名类或者私有静态成员类实现该接口,那么实现类就成了一个策略
这节没啥东西,就是个Comparator

22.优先考虑静态成员类
也即静态嵌套类,静态成员类不依赖于实例
interface里面还可以声明inteface,活久见!
transient关键字(map源码里面看见的)标记的成员变量不参与序列化过程
非静态成员类,常见用于提供视图,比如Map.EntrySet, Map.KeySet,至于为什么这些类是final的,因为要使类的可变性最小(15条)
私有静态成员类,常见用于代表外围类所代表的对象的组件,比如Map.Entry,可以看成是一个Map的组成部分(一个小键值对)
局部嵌套类,基本不用看,这一节就这么多东西,其他的没啥了    

23.不要使用原生态类型
就是要用List<E>而不是用List,有利于提前发现错误
使用List只有两种情况,List.class和instanceof
本节就这点东西,没有其他

24.消除非受检警告
出现非受检警告的情况即:出现了类型不安全的代码,比如Set<String> str = new HashSet();类型不安全的代码,运行时可能会出现ClassCastException
@SuppressWarings("unchecked")注解可以用来消除非受检警告,@SuppressWarings可以放在任何粒度,从类到方法到变量,但最好让它的作用范围尽量小(类似于第13条使类和成员的访问性最小化)

25.列表优先于数组
这一节,基本就一句话:当列表和数组混用,得到警告或者错误时,第一反应就该是用列表代替数组

26.优先考虑泛型

private E[] elements = new E[16]是错误的,不能创建不可具体化的类型的数组,修正的方法有两种:
1.elements = (E[]) new Object[16];//需要加@SuppressWarings("unchecked")

2.改成private Object[] elements = new Object[],但在处理单个E元素时,要改成E result = (E) elements[--size];//也需要加@SuppressWarings("unchecked")

总之就是,新类型(的类)能做成泛型的,就做成泛型的

27.优先考虑泛型方法
能做成泛型的就做成泛型的??(是的,不仅容易、简洁而且安全)
得益于类型推导,泛型方法不需要明确指定类型参数的值
静态工具方法尤其适合泛型化,比如Collections.sort()
本节就这么点东西

28.利用通配符来提升api灵活性
a.api的参数多用? extends E, ? super E,但是返回类型不要用通配符(书上说的)
b.PECS原则,producer extends consumer super,consumer可以理解为消耗某种类型参数E,producer可以理解为获得某种类型参数E
c.所有的comparable和comparator都是消费者
d.你不能把null之外的任何值放进List<?>中,可以编写辅助方法来捕捉通配符类型,比如对于swap(List<?> list, int i, int j),可以有<E> swapHelper(List<E> list, int i, int j)

29.优先考虑类型安全的异构容器
这一节对目前的我没啥用,异构就是指Map里面的键可以是不同类型,比如普通的Map<String, Integer>,异构的Map<Class<?>, Object>

30.用enum代替枚举
int和String的枚举很脆弱,容易出错
enum是final的,是单例的泛型化 
看不下了。。。不好意思

31.用实例域代替序数
标题中的序数是指枚举中的ordinal()方法,本章只讲了这些:我们要用 SOLO(1)这种枚举,来代替SOLO + ordinal(),因为ordinal()非常难维护,绝大多数情况下都要完全避免ordinal方法

32.用EnumSet代替位域
让你用or位运算将几个常量合并到一个集合中,称作位域。位域也允许位操作执行交集等集合操作。
用text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC))来代替text.applyStyles(STYLE_BOLD | STYLE_ITALIC)

33.用EnumMap代替序数索引
序数就是指ordinal方法返回的值,这一章其实就是说枚举中,当我们用ordinal返回值作为数组的索引时,我们可以考虑用EnumMap来代替数组(一维数组或者多维的都可以,比如二维对应EnumMap<...,
EnumMap<...>>),总之要极力避免ordinal

34.用接口模拟可伸缩的枚举
虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型,对它进行模拟
比如我们可以编写
public interface Operation() {
double apply(double x, double y);
}
然后编写基本枚举类型BasicOperation implements Operation,实现+-*/操作,但如果我们还要扩展实现求幂和取余操作,我们只需另外编写一个枚举类型ExtendOperation implements Operation, 实现对应操作
就可以了

35.注解优先于命名模式
a.命名模式容易出错
b.本节用的是反射,来完成Test注解使用,具体内容可以看书,目前对自己没啥需要看的
c.总之有了注解,完全没必要使用命名模式了
本节就这么点内容

36.坚持使用Override注解
感觉就是一句话:使用注解,覆盖不容易出错

37.用标记接口定义类型
本章有点啰嗦,我看了一遍以后总结就是:如果想要定义类型,一定要使用接口,例子就是Serializable接口

38.检查参数的有效性
在方法或者构造器的开头检查参数("应该在错误发生之后尽快检测出错误"),除非检查的开销非常大,或者参数检查在方法执行的时候也执行了

39.必要时进行保护性拷贝
总结:类内部的成员最好是不可变的,如果是可变的,则要考虑可变可能带来的影响,以及是否要进行保护性拷贝

40.谨慎使用方法签名
a.不要有太多的方法,否则会使程序变复杂
b.创建辅助类(通常是静态成员类)来缩短长参数列表

41.慎用重载
对于重载(overloaded)方法的选择是静态的(编译时确定),对于被覆盖(override)的方法是动态的(运行时确定)
安全保守的策略是永远不要导出两个具有相同参数数目的重载方法,比如可以用wirteInt,writeString,来代替write(xx)
如果方法使用可变参数,保守的策略是根本不要重载它
如果实在有多个重载方法有相同的参数,那么最好避免"同一组参数只需经过类型转换就可以被传递给不同的重载方法",如果仍无法避免,则最好保证这些重载方法的行为(或者说功能)必须一致

42.慎用可变参数
可变参数适用于printf和反射机制
如果方法要使用可变参数,但该方法95%的调用会有3个或者更少的参数时,就该这么声明
public void foo();
public void foo(int a);
public void foo(int a, int b);
public void foo(int a, int b, int c);
public void foo(int a, int b, int c, int... d);
总之不要过度滥用就可以,也不要太注意

43.返回零长度的数组或者集合,而不是null
整章如标题,其他的没有了

44.为所有导出的API元素编写文档注释
虽然这章我没看,但我觉得标题说的很有道理

45.将局部变量的作用域最小化
要使局部变量的作用域最小化,最有力的方法是在第一次使用它的地方声明
几乎每个局部变量的声明都应该包含一个初始化表达式,除非初始化会抛出异常(这种情况,你碰见了自然会正确的初始化,也不用死记)
for循环通常要优于while循环
方法要小而集中,比如一个方法完成只一个操作,若几个操作均在同一个方法,操作a的变量可能会传到操作b里面去

46.for-each循环优于传统的for循环
for-each循环即为for(E e: Es)
for-each支持循环遍历任意实现Iterator接口(不难实现)的对象,当你编写的类型是一组元素,最好实现Iterator接口
for-each对bug有很好的预防,而且简洁
过滤(比如删除),转换(比如更新、设值),平行迭代(比如i和j同时自增)

47.了解和使用类库
使用Random.nextInt(int)要优于自己去实现一个random生成器,然后类库的方法会不断更新迭代,并被大家广泛使用,自己的代码也可以融入主流。
总之不要重新发明轮子,java.lang,java.util(特别是collections framework和concurrent),某种程度的java.io,应该是每个程序员基本工具箱的一部分

48.如果需要精确的答案,请避免使用float和double
使用BigDecimal会返回精确值,并且有8种舍入方式,但是它使用不方便,而且慢
如果要求性能好,涉及数值又不会太大,那么可以使用int或者long

49.基本类型优于装箱基本类型
装箱后的==,是比较内存地址,所以基本上这种比较总是错误的
而且装箱拆箱,很影响性能
tips: 一项操作中混合使用基本类型和装箱基本类型时,装箱基本类型就会自动拆箱。
装箱类型适用范围:a.map,set b.表示类型,比如ThreadLocal<Integer> c.反射


50.如果其他类型更适合,则尽量避免使用字符串
其实就是一句话:不要随便用字符串代替其他类型(基本类型、枚举类型和聚集类型(String key = className + "#" + i.next()))的值,否则会有不好的影响
tips: 想保证类型安全,可以考虑泛型化


51.当心字符串连接的性能
就是不要用String的+=,而要用StringBuilder或者字符数组

52.通过接口引用对象
要用List<String> a = new Vertor<String>(); 而不是Vector<String> a = new Vertor<String>();有利于提升灵活性
如果没有合适的接口存在,完全可以用类而不是接口来引用对象
之前sms项目的例子,要用抽象类名BaseSmsService而不是具体实现类名来引用对象
若类实现了接口不存在的方法(比如LingkedHashMap),那么只能用类来引用了

53.接口优于反射机制
题目的意思应该是:访问对象时,使用接口去访问更好,而不是用反射去访问
普通程序在运行时不应该以反射方式访问对象
反射的代价:
a.丧失编译时类型检查的好处(所以容易产生运行时错误)
b.代码冗长且笨拙
c.性能损失
某些类在编译时无法获取,但存在超类或者接口,此时"就可以以反射方式创建实例,然后通过它们的接口或者超类,以正常的方式访问这些实例"
反射机制最好仅用来实例化对象,而访问对象时则使用编译时已知的某个接口或者超类

54.谨慎使用本地方法
使用本地方法来提高性能的做法不值得提倡
本地语言不是安全的
总之要极力避免本地方法,即便要用也要少用,并且全面测试

55.谨慎地进行优化
两个原则:
a.不要进行优化
b.还是不要进行优化,除非你有绝对清晰的优化方案
要努力编写好的而不是快的程序,好的程序只提供api,而不暴露内部信息给调用者
努力避免那些限制性能的设计决策,有可能限制性能的组件有API、线路层协议、永久数据格式(其实我不太懂这句话说的是啥)
要考虑API设计决策的性能后果:
a.公有的类成为可变的,可能会导致不必要的保护性拷贝
b.API中使用实现类型而不是接口,那么将来有更快的实现你也无法使用
好的API一般会带来好的性能
可用性能剖析工具来帮助你找到最需要优化的部分,首要检查的是所选择的算法

56.遵循普遍接受的命名惯例
a.包名最好不超过8个字符,util而不是utilities
b.一般变量,仅首字母大写,HttpUrl而不是HTTPURL
c.常量域大写并用下划线隔开,MAX_VALUE
d.执行某个动作的方法,用动词命名,如append;boolean值方法命名,is + 名词,如isDigit; 返回非boolean,用名词或者get,如hashCode,getTime


57.只针对异常的情况才使用异常
设计良好的API不应该强迫它的客户端为了正常的控制流而使用异常(基于异常的模式比正常模式执行起来可能更慢),本章主要就是这句话

 

 

58.可恢复的情况使用受检异常,编程错误使用运行时异常
三种throwable:受检异常,运行时异常,错误
受检=可恢复=必须抛出
未受检异常:运行时异常和错误
错误:默认为JVM保留,OutOfMemoryError,所以自定义的非受检异常必须是Runtime Exception的子类
异常也是对象,可以在异常定义一些方法,便于把异常信息和处理办法暴露给外部

常见的受检异常:IOException, SQLException,NoClassFoundException,NoSuchMethodException

 

59.避免不必要的使用受检异常
使用受检的必要条件:
1.正常使用API也会产生的异常
2.产生异常以后,程序会合理的处理

方法抛出的受检异常很麻烦,最好避免使用

把受检异常变成未受检异常: try + action + catch改成actionPermitted + action(可能有同步问题),或者直接 action

60.优先使用标准的异常
多使用可重用的异常
如果想暴露更多异常信息,则将现有的异常子类化

61.
底层异常应该通过转译,变成高层异常,必要的时候可以把底层的异常原因(super(cause))传递给上层

同时,在传递参数到底层之前,最好在高层先作参数校验(觉得和校验放在API最前面做有点类似)

62.没必要看。。

63.中心思想就是:异常信息要完备,方便程序员排查

64.失败原子性: 对象在失败的方法调用前后都一样
如何维持原子性:1.使用不可变对象
2.调用前先检验参数有效性
3.编写恢复代码,达到回滚,不常用也不用记
4.先创建拷贝,再用拷贝对象代替原对象

65.就一句有用的:catch块至少包含一条语句,解释为什么可以忽略这个异常

66.同步访问共享的可变数据
同步 = 互斥 + 每个线程看到的同步结果都是一样的(通信效果)
除了long和double,Java读写其他类型都是原子的
i++这个操作不是原子的
使用AtomicLong代替long
总之:多个线程共享可变数据时,每个读或者写数据的线程都必须执行同步

67.避免过度同步
在一个同步区域,不要调用外部方法(运行时间任意长),也不要调用会被覆盖的方法(你不知道里面会发生什么),这两种方法都无法被控制

可以使用并发集合来代替(显式的)同步,copyOnWriteList

你应该在同步区域做尽可能少的工作

内部同步通常来说比外部同步有更高的并发性(因为一个是同步一部分,一个是同步整体)。
67节总体就这么点东西,没必要看太细

 

68.executor和task优于线程
一般使用Executor的静态工厂来生成某种类型的线程池(小程序,低负载,用CacheThreadPool,高负载用FixedThreadPool)就可以了,否则可以直接使用ThreadPoolExecutor来控制线程池各个方面
还有就是要implements Runnable或者Callable,而不要extends Thread,本节就这些

69、70暂时不看,先把主要的做一下总结

71.慎用延迟初始化
"除非绝对必要,否则就不要"延迟初始化
如果初始化开销很大,延迟初始化是可行的
实例域的延迟初始化要注意单重和双重检查模式(性能较好),双重模式适用于只初始化一次,如果要满足重复初始化,那么就用单重检查模式
静态域的延迟初始化用lazy holder模式
"破坏有害的初始化循环",这句话看不懂。。

72.不要依赖于线程调度器(根据优先级来调度的类)
健壮程序的要素: 可运行线程的平均数量(不是线程的总数量)不明显多于处理器的数量
如何减少可运行线程数量: 让线程做有意义的工作
不要让线程一直处于忙等状态
不要让程序的正确性依赖于调度器
不要依赖Thread.yield和线程优先级,优先级能提高正常工作的程序的服务质量,但不能用来"修正"原本不能工作的程序

73.避免使用线程组
不要用thread group,可以直接忽略它,而要用线程池取代


74.谨慎地实现serializable接口
代价(了解即可):灵活性降低,增加bug和安全漏洞,测试难度增加

本章重点:
1.为了继承而设计的类要尽可能少地去实现serializable接口,用户的接口也应该尽可能少地继承serializable接口(除非所有属于同一框架的其他类都继承了该接口)
2.如果要允许子类可以序列化,那么父类必须提供一个子类可访问的无参构造器,否则子类无法序列化
3.内部类不应该实现seli,但是静态成员类却可以实现seli接口


75.考虑使用自定义的序列化形式
1.散列表的序列化和反序列化,很容易产生bug
2.默认的序列化有蛮多缺点,具体看书
3.要为每个可序列化的类声明一个显示的serialVersionUID(对于新类,serialVersionUID可以是任意的,对于已经有的类,新版本UID要和旧的一样,否则不兼容)

76-78 序列化代理模式之类的,感觉没必要看,以后用到再说

 

延迟初始化的条件???

 

必须throw的异常,为受检异常

除了RuntimeException,均为受检异常

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值