《Effective Java》主要给了78条编码建议,指导,方便开发者开发出,高效,稳定,健壮,设计优良的程序。下面看一下这78条建议。
创建和销毁对象
1、考虑用静态工厂方法代替构造器
/*
为了让客户端获取他自身的一个实例,最常用的方法就是提供一个公有的构造器。
还有一种方法,类可以提供一个公有的静态工厂方法,它只是返回类的实例的静态方法。
*/
//一个简单的单例
public class Singleton {
private static Singleton singleton;
private Singleton() {
singleton=new Singleton();
}
public static Singleton getSingleton() {
return singleton;
}
}
2、遇到多个构造器参数时要考虑用 建造者模式
当一个类有多个构造器参数时,该怎么创建?
//重叠构造器
public class User {
private String name;
private String password;
private String id;
private String phonenumber;
private String desc;
private String emil;
public User(String name) {
this(name, null);
}
public User(String name, String password) {
this(name, password, name, null);
}
public User(String name, String password, String id) {
this(name, password, id, password, null);
}
public User(String name, String password, String id, String phonenumber) {
this(name, password, id, phonenumber, id,null);
}
public User(String name, String password, String id, String phonenumber, String desc) {
this(name, password, id, phonenumber, desc,null);
}
public User(String name, String password, String id, String phonenumber, String desc, String emil) {
this.name = name;
this.password = password;
this.id = id;
this.phonenumber = phonenumber;
this.desc = desc;
this.emil = emil;
}
}
//不易扩展 当有许多参数的时候,客户端代码会很难编写,很难阅读
//java beans
public class User {
private String name;
private String password;
private String id;
private String phonenumber;
private String desc;
private String emil;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getPhonenumber() {
return phonenumber;
}
public void setPhonenumber(String phonenumber) {
this.phonenumber = phonenumber;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public String getEmil() {
return emil;
}
public void setEmil(String emil) {
this.emil = emil;
}
}
//多线程情况下要考虑线程安全
//建造者模式
public class User {
private String name;
private String password;
private String id;
private String phonenumber;
private String desc;
private String emil;
public static class Builder() {
private String name;
private String password;
private String id;
private String phonenumber;
private String desc;
private String emil;
public Builder name(String m) {
this.name=m;
}
public Builder password(String m) {
this.password=m;
}
public Builder id(String m) {
this.id=m;
}
public Builder phonenumber(String m) {
this.phonenumber=m;
}
public Builder desc(String m) {
this.desc=m;
}
public Builder emil(String m) {
this.emil=m;
}
public User build() {
return new User(this);
}
}
private User(Builder builder) {
name=builder.name;
password=builder.password;
id=builder.id;
phonenumber=builder.phonenumber;
desc=builder.desc;
emil=builder.emil;
}
}
//如果类的构造器中有多个参数时 建议使用建造者模式 容易扩展 也不存在线程安全问题
3、用私有构造器或者枚举类型强化 单例 属性
//简单的单例
public class Singleton {
private static Singleton singleton;
private Singleton() {
singleton=new Singleton();
}
public static Singleton getSingleton() {
return singleton;
}
}
不考虑线程安全,懒加载等问题,这样存在两个风险
- 可以通过反射调用单例类的构造方法创建实例
- 可以通过序列化,反序列化 来获得 单例的实例
使用枚举类型实现单例,可以有效避免这两个问题
推荐使用下面这种写法
public enum Singleton {
INSTANCE;
public void doSomething() {
}
}
//可以有效避免 反射和序列化反序列 创建对象 问题
4、通过私有构造器强化不可实例化的能力
有些类(例如工具类)不希望被实例化,实例对他没有任何意义。
可以通过如下方法避免类被实例化
public class Utils {
private Utils() {
throw new AssertionError();
}
}
5、避免创建不必要的对象
一般来说,最好能重用对象而不是在每次需要的时候就创建一个相同功能的新对象。
6、消除过期的对象引用
消除过期的对象引用,避免程序出现内存泄露,进而导致OOM。
7、避免使用终结方法
终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的。
终结方法的缺点不能保证会被及时执行。从一个对象变得不可达开始,到它的终结方法被执行,所花费的这段时间是任意长的。
对于所有对象都通用的方法
8、覆盖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。
9、覆盖equals是总要覆盖hashcode
相等的对象必须有相同的散列码
3点约定:
1)在java应用执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一对象调用多次hashCode方法都必须始终如一地同一个整数。在同一个应用程序的多次执行过程中,每次执行该方法返回的整数可以不一致。
2)如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。
3)如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法没必要产生不同的整数结果。但是程序猿应该知道,给不同的对象产生截然不同的整数结果,有可能提高散列表(hash table)的性能。
因此,覆盖equals时总是要覆盖hashCode是一种通用的约定,而不是必须的,如果和基于散列的集合(HashMap、HashSet、HashTable)一起工作时,特别是将该对象作为key值的时候,一定要覆盖hashCode,否则会出现错误。那么既然是一种规范,那么作为程序猿的我们就有必要必须执行,以免出现问题。
10、始终要覆盖tostring
需要覆盖 toString方法,返回对象中包含的所有值得关注的信息。 程序员以这种方式来产生真短消息。
建议用 编译器自动生成的tostring, 如果其他要求,可以自己定制。
11、谨慎的覆盖clone
12、考虑实现Comparable接口
compareTo方法:
将这个方法与指定的对象进行比较,当对象小于,等于或大于指定对象的时候,分别返回一个负整数,零,或者正整数。如果由于指定对象的类型二无法与该对象进行比较,则抛出
ClassCastException.
举个例子
/*
大整数排序
题目描述
对N个长度最长可达到1000的数进行排序。
输入描述:
输入第一行为一个整数N,(1<=N<=100)。
接下来的N行每行有一个数,数的长度范围为1<=len<=1000。
每个数都是一个正数,并且保证不包含前缀零。
输出描述:
可能有多组测试数据,对于每组数据,将给出的N个数从小到大进行排序,输出排序后的结果,每个数占一行。
示例
输入:
3
11111111111111111111111111111
2222222222222222222222222222222222
33333333
输出:
33333333
11111111111111111111111111111
2222222222222222222222222222222222
*/
import java.util.*;
public class Main{
public static void main(String[] args) {
Scanner scanner=new Scanner(System.in);
int n=scanner.nextInt();
List<String> list=new ArrayList<>();
int index=0;
while (scanner.hasNext()) {
list.add(scanner.next());
}
Collections.sort(list,new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
//先比较长度,长度长的数大
//如果大,返回1,小返回-1
if(o1.length()-o2.length()>0) return 1;
if(o1.length()-o2.length()<0) return -1;
else {
if (Integer.valueOf(o1.charAt(0))>Integer.valueOf(o2.charAt(0))) {
return 1;
}
if (Integer.valueOf(o1.charAt(0))<Integer.valueOf(o2.charAt(0))) {
return -1;
}
else {
//如果长度相等,从第一位开始比较,若想等 依次向后比较
int i=1;
while (i<o1.length()) {
if (Integer.valueOf(o1.charAt(i))>Integer.valueOf(o2.charAt(i))) {
return 1;
}
if (Integer.valueOf(o1.charAt(i))<Integer.valueOf(o2.charAt(i))) {
return -1;
}
else {
i++;
}
}
}
}
return 0;
}
});
for (int i = 0; i <list.size(); i++) {
System.out.println(list.get(i));
}
}
}
类和接口
13、使类和成员的可访问性最小化
尽可能地使每个类或者成员不被外界访问。
可以有效地降低耦合,使得各模块可以独立 开发,测试,修改,优化和理解。
14、在公有类中使用访问方法而非公有域
//公有域不应该直接暴露数据域
public class Point {
public int x;
public int y;
}
//应该使用私有域和公有设值(setter) 方法
public class Point {
private int x;
private int y;
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
}
15、使可变性最小化
不可变的类比可变的类更加易于设计,实现和使用。他们不容易出错,而且更加安全。
为了使类成为不可变,要遵循下面五条规则。
1、不要提供任何会修改对象状态的方法。
2、保证类不会被扩展。
3、使所有域都是final的。
4、使所有域都成为私有的。
5、确保对于任何可变组件的互斥访问。
16、复合优先于继承
当只有子类和父类确实存在子类型关系时。使用继承才是最恰当的。
如果子类和父类处在不同的包,并且父类并不是为继承而设计,那么继承会导致脆弱性。
继承打破了 封装性,子类依赖其父类中特定功能的实现细节。 父类的实现可能会随着发行版本的不同而有所变化,如果真的发生了变化,子类可能会遭到破坏,即使他的代码完全没有改变。
17、要么为继承而设计,并提供文档说明,要么就禁止继承
该类的文档必须精确的描述覆盖每个方法所带来的影响。
要么设计成final类 禁止继承。
18、接口优先于抽象类
Java 单继承多实现。
接口通常是定义允许多个实现的类型的最佳途径。
19、接口只用于定义类型
接口应该只被用来定义类型,他们不应该被用来导出常量。
20、类层次优先于标签类
标签类过于冗长,容易出错,并且效率低下。
21、用函数对象对象表示策略
22、优先考虑静态成员类
当必须使用静态内部类时,建议使用静态内部类。
某些情况还是要多考虑。
泛型
23、请不要再新代码中使用原生态类型
如果使用原生态类型,就失掉了泛型在安全性和表述性方面的所有优势。
屏蔽掉了编译错误,可能会出现运行时错误,抛出 ClassCastException。
24、消除非受检警告
要尽可能消除每一条非受检警告。
如果无法消除非受检警告,同时可以证明引起警告的代码是类型安全的,就可以在尽可能小的范围中,用@SuppressWarnings("unchecked")注解禁止该警告。要用注释把禁止改警告色原因记录下来。
25、列表优先于数组
建议优先使用列表泛型,可以在编译期间检查代码参数合法性。避免运行时错误。
26、优先考虑泛型
使用泛型比使用类型转换更加安全,也更加易读。
27、优先考虑泛型方法
泛型方法像泛型一样,使用方法比要求客户端转换输入参数并返回值的方法来得更加安全,也更加容易。
28、利用有限制通配符来提升 API 的灵活性
29、优先考虑类型安全的异构容器
枚举和注解
30、用enum代替常量
public enum Color {
RED,YELLOW,BLACK,GREEN
}
public enum Color {
//数值是瞎编的 a ~
RED(0,0,0,0),
YELLOW(1,1,1,1),
BLACK(1,0,1,0),
GREEN(1,0,1,1);
private int a;
private int r;
private int g;
private int b;
private Color(int a, int r, int g, int b) {
this.a = a;
this.r = r;
this.g = g;
this.b = b;
}
public void doSomething() {
}
}
相比int常量,枚举易读,更安全,也更强大。
31、用实力域代替序数
32、用EnumSet代替位域
EnumSet 简洁 ,性能高
33、用EnumMap 代替序数索引
最好不要用序数索引数组,而要使用 EnumMap
34、用接口模拟可伸缩的枚举
虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的枚举类型。对它进行模拟。
35、注解要优先于命名模式
命名模式 JDK1.5之前的东西,现在使用的基本都是注解。
36、坚持是有override注解
如果想要在每个方法声明中使用 override 注解覆盖父类声明,编译器会替你防止大量的错误。
37、用标记接口定义类型
标记接口 是没有包含方法生命的接口,只是指明 一个类实现了具有某种属性的接口,例如Serializable
如果想要定义类型,一定要使用接口。
方法
38、检查参数有效性
每当编写方法或者构造器时,应该考虑它的参数有哪些限制,应该把这些限制写到文档中,并且在每个方法体的开头处,通过显示的检查实施这些限制。
39、必要时进行保护性拷贝
40、谨慎的设计方法签名
谨慎的选择方法的名称
不要过于追求便利的方法
避免过长的参数列表
41、慎用重载
一般情况下,对于多个具有相同参数数目的方法来说,应该尽量避免重载方法。
42、慎用可变参数
//main函数
public static void main(String...args) {
}
public static int sum(int... args) {
int sum=0;
for (int i = 0; i < args.length; i++) {
sum=sum+args[i];
}
return sum;
}
//可变参数的设计初衷就是为了 方便 printf和 反射机制的使用
//在定义参数数目不定的方法时,可变参数是一种很方便的方式,但是他们不应该被过度滥用。如果使用不当,会产生混乱的结果。
43、返回零长度的数组或者集合,而不是null
private List<String> mTitles=Arrays.asList("帖子","好友","消息");
public List<String> getTitles(List<String> list){
if(list.size() == 0) {
return null;
}else {
return list;
}
}
//应该返回零长度的数组或者集合
private List<String> mTitles=Arrays.asList("帖子","好友","消息");
public List<String> getTitles(List<String> list){
if(list.size() == 0) {
return new ArrayList<>();
}else {
return list;
}
}
// 返回数据或集合的方法 不应该返回null 而应该返回一个零长度的数组或集合
//返回null容易出错 因为编写客户端程序的程序员可能会忘记写这种专门的代码来处理null返回值
44、为所有导出的API元素编写文档注释
要为API编写文档,文档注释是最好,最有效的途径。
通用程序设计
45、将局部变量的作用域最小化
将局部变量的作用最小化,可以增强代码的可读性和可维护性,并降低出错的可能性。
46、for-each 循环优先于传统的for循环
for-each 循环在简洁性和预防bug方面有着传统的for循环无法比拟的优势,而且没有性能损失,应该尽可能的使用 for-each 循环。
47、了解和使用类库
总而言之,不要重复造轮子,如果你要做的事情是看起来十分常见的,有可能类库中某个类已经完成了这样的操作,如果确实是这样,就使用现成的,如果不清楚是否存在这样的类,就去查一查。
48、如果需要精确的答案,请避免使用float和double
如果需要精确答案,请使用 BigDecimal
49、基本类型优先于装箱基本类型
当可以选择的时候,基本类型要优先于装箱基本类型。基本类型更加简单,也更加迅速。
50、如果其他类型更合适,则尽量避免使用字符串
如果可以使用更加合适的数据类型,或者可以编写更加适当的数据类型,就应该避免用字符串来表示对象。
若使用不当,字符串会比其他的类型更加笨拙,更不灵活,速度更慢,也更容易出错。
51、当心字符串链接的性能
为连接n个字符串而重复使用字符串链接操作符,需要n的平方级时间。
不要使用字符串连接操作符来合并多个字符串,除非性能无关紧要。
相反,应该使用 StringBuilder的 append方法。
52、通过接口引用对象
53、接口优先于反射
54、谨慎的使用本地方法
使用本地方法提高性能的做法是不值得提倡的。在这些年间,VM已经变得很快了。
是用本地方法有一些严重的缺点,因为本地语言是不安全的,是用本地方法的应用程序也不再能面授内存毁坏错误的影响
55、谨慎的进行优化
没有绝对清晰的优化方案之前,请不要进行优化。
在每次视图做优化之前和之后,要对性能进行测量。
试着使用性能剖析工具
检查所选择的的算法,再多的底层优化也无法弥补算法的选择不当。
56、遵守普遍接受的命名惯例
团队应该有统一的编码规范,每个成员都需要遵守。方便后续团队开发。
异常
57、只针对异常的情况下才使用异常
因为异常机制的 设计初衷是用于不正常的情形,所以很少会有jvm实现试图进行优化。
把代码 放在 try-catch 块中反而阻止了现代jvm实现本来可能要执行的某些特定优化。
实际上,现代jvm实现上,基于异常的模式比标准模式要慢的多。
总而言之,异常是为了在异常情况下使用而设计的,不要将它们用于普通的控制流,也不要编写迫使他们这么做的API。
58、对可恢复的情况使用受检异常,对编程错误使用运行时异常
如果期望调用者能够适当的进行恢复,对于这种情况,应该使用受检异常。
通过抛出该受检的异常,强迫调用者在一个catch字句中处理该异常。
59、避免不必要的使用受检异常
过分使用受检异常会使API使用起来非常不方便。
如果一个方法抛出一个或多个受检异常,调用该方法的代码就必须在一个或者多个catch快中处理这些异常,或者它必须声明它抛出这些异常,并让他们传播出去,无论那种方法,都给程序员增添了不可忽视的负担。
60、优先使用标准的异常
优先使用标准的异常,有如下好处:
1、它使你的API更加容易学习和使用,因为它与程序员 已经熟悉的习惯用法是一致的。
2、对于用到这些API的程序来说,它的可读性更好,因为他不会出现很多程序员不熟悉的异常。
3、异常类越少,意味着 内存印迹(footprint)就越小,装载这些类的时间开销也越少。
61、抛出与抽象相对应的异常
62、每个方法抛出的异常都要有文档
如果没有为抛出的异常建立相应的文档,其他人很难或者根本不可能使用你的类和接口。
63、在细节信息中包含 能捕获失败的类型
当程序由于未被捕获的异常而失败的时候,系统会自动打印出该异常的堆栈轨迹,在堆栈轨迹中包含该字符串的堆栈表示法,即它的 tostring 方法的调用结果,他通常包含异常的类名,紧随其后的是细节信息。
因此,异常类型的toString 方法应该尽可能多的返回有关失败原因的信息。
64、努力使异常保持原子性
65、不要忽略异常
不管异常代表了可预见的异常条件,还是编程错误,用空的catch快忽略他,将会导致程序在遇到错误的情况下悄然的执行下去,然后,有可能在将来的某个点上,当程序不能容忍与错误源明显相关的问题时,它就会失败。
正确的处理异常可以彻底挽回失败,只要将异常传播给外界,至少会导致程序会迅速失败,从而保留了有助于调式该失败条件的信息。
并发
66、同步访问共享的可变数据
当某个数据需要被多个线程共享访问时,需要进行同步。
如果没有同步,就无法保证一个线程所做的修改可以被另一个线程获知。 即内存可见性。
67、避免过度同步
要限制同步区域的工作量 ,不要多层同步。避免过度同步提高程序性能。
68、executor和task 优先于线程
当项目中线程的数量很多时,推荐使用线程池。
因为手动创建和销毁线程耗费性能,不便于管理。
69、并发工具优先于 wait和notify
当遇到并发问题时,优先使用 java并发工具 。java.util.concurrent
java.util.concurrent 主要有三类:
Executor Framework 线程池
Concurrent Collection 并发集合
Synchronizer 同步器
70、线程安全的文档化
每个类应该有说明或者线程安全注解,说明他的线程安全属性。
71、慎用延迟初始化
大多数域应该正常地进行初始化,而不是延迟初始化。
为了达到性能的目标,可以使用相应的延迟初始化方法。
72、不要依赖于线程调度器
73、避免使用线程组(Thread Group)
线程组 设计初衷主要是 为隔离 applet, 在其他地方很少见到。
如果你正在设计的一个类需要处理线程的逻辑组,或许应该使用线程池executor。
序列化
74、谨慎的实现Seriaizable 接口
75、考虑使用自定义的序列化形式
76、保护性地编写 readObject方法
readObject 方法实际上相当于另一个公有的构造器,如同其他构造器一样,它也要求注意同样的所有注意事项。狗在其必须检查其参数的有效性,并且在必要的时候对参数进行保护性拷贝。
77、对于实际控制,枚举类型优先于 readResolve
78、考虑用序列化代理代替序列化实例