Effective Java通俗理解(持续更新)
Posted CoderBuff
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Effective Java通俗理解(持续更新)相关的知识,希望对你有一定的参考价值。
这篇博客是Java经典书籍《Effective Java(第二版)》的读书笔记,此书共有78条关于编写高质量Java代码的建议,我会试着逐一对其进行更为通俗易懂地讲解,故此篇博客的更新大约会持续1个月左右。
第1条:考虑用静态工厂方法代替构造器
通常情况下我们会利用类的构造器对其进行实例化,这似乎毫无疑问。但“静态工厂方法”也需要引起我们的高度注意。
什么是“静态工厂方法”?这不同于设计模式中的工厂方法,我们可以理解它为“在一个类中用一个静态方法来返回这个类的实例”,例如:
public static People getInstance() { return new People(); }
它是一个“方法”,那么它不同于构造器,它可以随意修改方法名,这就带来第一个优点——有名称。有时一个类的构造器往往不止一个,而它们的名称都是相同的,不同的仅仅是参数,如果不同参数带来不同的含义这样对于调用方来说除了注释很难理解它们有什么不同的含义。例如BigInteger(int, int, Random)返回一个素数,但调用者很难理解API设计者所要想表达的意思,如果此时有BigInteger.probablePrime静态工厂方法,则能一目了然的清楚API设计者所要想表达的含义。举一个JDK的例子:Executors类,在这个类中有newFixedThread、newSingleThreadExecutor、newCachedThreadPool等静态方法,因为它们有“名字”,所有就较为清晰的明白API的含义。
《Effective Java》中所提到的静态工厂方法第二个优点在于不用重复创建一个对象,实际上也就是勤加载或者称为饿汉式的单例模式。例如:
public class Instance() { private static Instance instance = new Instance(); private Instance(){} public static Instance getInstance() { return instance; } }
静态工厂方法的第三个优点,可以返回原返回类型的任何子类型的。这句话初看不好理解,举个JDK中的例子:Collections类。
List list = Collections.synchronizedList(new ArrayList())
这个例子就说明了可以返回原返回类型的任何子类型的对象。
关于静态工厂方法的第四个优点,在创建参数化类型实例的时候,它们使代码变得更加简洁,书中举了一个例子:
Map<String, List<String>> m = new HashMap<String, List<String>>(); //这会显得很繁琐
给集合类提供静态工厂方法后:
public static <K, V> HashMap<K, V> newInstance() { return new HashMap<K, V>(); }
但是实际上从JDK7(包括JDK7)之后的集合类可以用以下简洁的代码代替:
Map<String, List<String>> m = new HashMap<>();
静态工厂方法也有两个缺点:一是公有的静态方法所返回的非公有类不能被实例化,也就是说Collections.synchronizedList返回的SynchronizedList不能被实例化;二是查找API比较麻烦,它们不像普通的类有构造器在API中标识出来,而是和其他普通静态方法一样,鉴于此,书中提到了几种惯用名称:
valueOf
of
getInstance
newInstance
getType
newType
第2条:遇到多个构造器参数时要考虑用构建器
你是否写过下面类似的代码:
public void Student() { /*必填*/ private String name; private int age; /*选填*/ private String sex; private String grade; public Student(String name, String sex) { this(name, sex, 0); } public Student(String name, String sex, int age) { this(name, sex, age, “”); } public Student(String name, String sex, int age, String grade) { this.name = name; this.sex = sex; this.age = age; this.grade = grade; } }
当我想实例化一个名字叫“kevin”,性别男,但是不写年龄,只有年级“1年级”,这个时候代码就:不得不要为年龄这个参数传递值。如果新增一个只含年级的构造方法,那又将多出许多代码,更严重的是,如果没有一份详尽的文档或者注释,看到如此多的构造方法将无从下手,这就是非常常见的重叠构造器。
Student student = new Student(“Kevin”, “男”, “0”, “1年级”);
当然还有另外一种方法,只有一个必填项的构造方法,而其他选填项利用setter方法传递。例如:
Student student = new Student(“kevin”, “男”); student.setGrade(“1年级”);
这实际上导致了在构造过程中JavaBean可能处于不一致的状态,也就是说实例化对象本该是一气呵成,但现在却分割成了两大步,这会导致它线程不安全,进一步引发不可预知的后果。
书中提到较为“完美”的解决方案就是利用“Builder模式(建造者模式)”,有关此设计模式可以查看《建造者模式》。这种解决方案属建造者模式的一种形式,其核心就是不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或者静态工厂),得到一个builder对象,再调用类似setter的方法设置相关可选参数。构建器模式如下所示:
/** * 构建器模式 * Created by yulinfeng on 2017/8/3. */ public class Student { /*必填*/ private String name; private int age; /*选填*/ private String sex; private String grade; public static class Builder { private String name; private int age; private String sex = ""; private String grade = ""; public Builder(String name, int age) { this.name = name; this.age = age; } public Builder sex(String sex) { this.sex = sex; return this; } public Builder grade(String grade) { this.grade = grade; return this; } public Student build() { return new Student(this); } } private Student(Builder builder) { this.name = builder.name; this.age = builder.age; this.sex = builder.sex; this.grade = builder.grade; } }
客户端代码:
Student student = new Student.Builder("kevin", 23).grade("1年级").build();
这样的客户端代码很容易边写,并且便于阅读。对于不了解的可能来说利用构建器模式编写Student类不算易事,甚至代码比重叠构造器的代码更多。所以当可选参数在很多时,谨慎使用重叠构造器,而是使用构建器模式。
2017-08-03
第3条:用私有构造器或者枚举类型强化Singleton属性
书中的此条目下,我总结出来认为最有价值的是“能被序列化的线程安全的类,被序列化后是否还会是单例”。单例模式几乎人人会写,例如上面提到的勤加载(饿汉式)的单例模式:
public class Instance { private static final Instance obj = new Instance(); private Instance() { } public static Instance getInstance() { return obj; } }
毫无疑问这个单例是线程安全的,如果我们希望一个类能被序列化,那么这个类直接实现Serializable就可以了,但是这样能否保证被反序列化过后还是单例呢?
1 import java.io.Serializable; 2 3 /** 4 * 序列化单例对象 5 * Created by 余林丰 on 2017/8/4/0004. 6 */ 7 public class Instance implements Serializable { 8 private static final Instance obj = new Instance(); 9 private Instance() { 10 11 } 12 public static Instance getInstance() { 13 return obj; 14 } 15 }
测试代码:
1 import java.io.FileInputStream; 2 import java.io.FileOutputStream; 3 import java.io.ObjectInputStream; 4 import java.io.ObjectOutputStream; 5 6 /** 7 * 序列化与反序列化单例对象 8 * Created by 余林丰 on 2017/8/4/0004. 9 */ 10 public class Main { 11 public static void main(String[] args) throws Exception{ 12 ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\\\objFile.obj")); 13 Instance instance = Instance.getInstance(); 14 out.writeObject(instance); 15 out.close(); 16 17 ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\\\\objFile.obj")); 18 Instance instance1 = (Instance) in.readObject(); 19 in = new ObjectInputStream(new FileInputStream("D:\\\\objFile.obj")); 20 Instance instance2 = (Instance) in.readObject(); 21 System.out.println("obj1 hashcode:" + instance1.hashCode()); 22 System.out.println("obj2 hashcode:" + instance2.hashCode()); 23 in.close(); 24 } 25 }
比较两个实例对象的hash值,可以看到执行结果为:
这就说明被反序列化过后便不再是单例。要保证单例还必须在单例类中实现readResolve的方法:
1 import java.io.Serializable; 2 3 /** 4 * 序列化单例对象 5 * Created by 余林丰 on 2017/8/4/0004. 6 */ 7 public class Instance implements Serializable { 8 private static final Instance obj = new Instance(); 9 private Instance() { 10 11 } 12 public static Instance getInstance() { 13 return obj; 14 } 15 private Object readResolve(){ 16 return obj; 17 } 18 }
如此一来的执行结果为:
显然此时被序列化过后还是单例,至于为什么要实现readResolve,这个方法并不是Serializable接口的方法,在此我并没有深究,究其原因估计是在反序列化的时候会调用这个奇怪的方法。
另外书中还提到了另外高档的一种单例模式,此方法既能保证线程安全,也能保证被反序列化后还是单例。
1 /** 2 * 枚举类型单例 3 * Created by 余林丰 on 2017/8/4/0004. 4 */ 5 public enum Instance { 6 INSTANCE //同样可以像普通类一样定义普通的方法变量等 7 }
将测试代码第13行稍作修改:
1 import java.io.FileInputStream; 2 import java.io.FileOutputStream; 3 import java.io.ObjectInputStream; 4 import java.io.ObjectOutputStream; 5 6 /** 7 * 序列化与反序列化单例对象 8 * Created by 余林丰 on 2017/8/4/0004. 9 */ 10 public class Main { 11 public static void main(String[] args) throws Exception{ 12 ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\\\objFile.obj")); 13 Instance instance = Instance.INSTANCE; 14 out.writeObject(instance); 15 out.close(); 16 17 ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\\\\objFile.obj")); 18 Instance instance1 = (Instance) in.readObject(); 19 in = new ObjectInputStream(new FileInputStream("D:\\\\objFile.obj")); 20 Instance instance2 = (Instance) in.readObject(); 21 System.out.println("obj1 hashcode:" + instance1.hashCode()); 22 System.out.println("obj2 hashcode:" + instance2.hashCode()); 23 in.close(); 24 } 25 }
运行结果为:
可以看到它自带序列化机制,且防止了多次实例化,非常高档的一个单例实现。
第4条:通过私有构造器强化不可实例的能力
在此条书中建议有的工具类例如Arrays等,对它们进行实例化并没有意义,所以应该在它们的构造方法上应该使用private修饰。
2017-08-04
第5条:避免创建不必要的对象
此条目下书中提到“当你应该重用现有对象的时候,请不要创建新的对象”。
最为突出的例子莫过于字符串常量的创建,众所周知String字符串有两种创建方式
String str = “hello”; String str = new String(“hello”);
第一种String字符串的创建是在方法区(JDK7后改到了堆内存)中的常量池中创建一个”hello”常量,将来若再有一个字符串变量为“hello”时将直接指向常量池中的“hello”变量而不用重新创建;第二种则是会在堆变量中创建一个新的String实例,将来若再以此方式创建一个字符串变量为“hello”时还是会重新创建一个String实例。显然第二种方式创建了一个“不必要”的实例,相比较而言第一种方式更加高效。
另外一个例子则是将”true”变量转换为Boolean类型也有以下两种转换方式:
Boolean b = new Boolean(“true”); Boolean b = Boolean.valueOf(“true”);
第一种转换方式也是每次都会在堆内存中创建一个新的Boolean实例;第二种查看其源代码便知不会每次返回一个新的实例,返回的是一个在编译器就已经确定了的static final Boolean型变量:
public static final Boolean TRUE = new Boolean(true); public static final Boolean FALSE = new Boolean(false); public static Boolean valueOf(String s) { return toBoolean(s) ? TRUE : FALSE; } private static boolean toBoolean(String name) { return ((name != null) && name.equalsIgnoreCase("true")); }
书中举了一个例子是否是在1946年至1964年出生来说明,这个例子在现实当中也很常见:
1 import java.util.Calendar; 2 import java.util.Date; 3 import java.util.TimeZone; 4 5 /** 6 * “是否在1946-1965年出生” 7 * 这在现实中应该是比较常见的一种写法 8 * Created by yulinfeng on 8/5/17. 9 */ 10 public class Person { 11 private final Date birthDate; 12 13 public Person(Date birthDate) { 14 this.birthDate = birthDate; 15 } 16 17 public boolean isBabyBoomer() { 18 Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); //新创建Calendar实例 19 gmtCal.set(1949, Calendar.JANUARY, 1, 0, 0, 0); 20 Date boomStart = gmtCal.getTime(); //新创建Date实例 21 gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0); 22 Date boomEnd = gmtCal.getTime(); //新创建Date实例 23 return birthDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) < 0; 24 } 25 }
这样的代码我相信人人都写过类似的,书中提到这种代码的写法每次都创建新的实例对象,而实际上是不必要的,而给出了一种比较高效的代码示例:
1 import java.util.Calendar; 2 import java.util.Date; 3 import java.util.TimeZone; 4 5 /** 6 * “是否在1946-1965年出生” 7 * 这在现实中应该是比较常见的一种写法 8 * Created by yulinfeng on 8/5/17. 9 */ 10 public class Person { 11 private final Date birthDate; 12 private static final Date BOOM_START; 13 private static final Date BOOM_END; 14 static { 15 Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); //新创建Calendar实例 16 gmtCal.set(1949, Calendar.JANUARY, 1, 0, 0, 0); 17 BOOM_START = gmtCal.getTime(); //新创建Date实例 18 gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0); 19 BOOM_END = gmtCal.getTime(); //新创建Date实例 20 } 21 public Person(Date birthDate) { 22 this.birthDate = birthDate; 23 } 24 25 public boolean isBabyBoomer() { 26 return birthDate.compareTo(BOOM_START) >= 0 && birthDate.compareTo(BOOM_END) < 0; 27 } 28 }
利用静态代码块在类Person初始化的时候创建对象,而在之后调用方法判断时不再每次重新创建新的实例对象,这种写法有点“烧脑”,确实有点“不符合”编码习惯,大概是因为这是需要思考才能写出来的原因吧。
第6条:消除过期的对象引用
此条目较为容易理解,之所以要消除过期的对象引用其目的就在于尽量避免内存泄露的问题,书中举了一个“栈”的例子,其中在弹出一个元素时代码如下:
public Object pop() { if (size == 0) { throw new EmptyStackException(); } return elements[--size]; }
可以看到弹出元素时仅仅是将元素弹出后在将数组的索引-1,实际上数组维护的那个元素引用还在,也就是说那个被弹出的元素并不会被GC,如此一来最后就很有可能导致内存泄露的问题。解决此类的办法就是,当不再使用这个元素时,将其置为null。
public Object pop() { if (size == 0) { throw new EmptyStackException(); } Object result = elements[--size]; elements[size] = null; return result; }
实际上当你写出上面的代码时一定注意这并不是在任意条件下都成立,你应该仔细思考此时的引用是否为过期引用,“清空对象引用应该是一种例外,而不是一种规范行为”。
第7条:避免使用终结方法
此处所谓的终结方法指的就是finalize()方法,这个方法可能对于从C++转向Java的新手感到混淆,因为在C++中有一个“析构函数”,析构函数所代表的意义就是在这个对象垃圾回收前所做的一些动作例如资源的关闭等。对于Java来说垃圾回收是自动的,或者称之为不可预知或不可控,尽管finalize方法所代表的也是在垃圾回收前所做的一些动作,但对于GC的时间你不能掌握,也就是说不能保证finalize方法会被及时执行,这是很危险的,一般情况下这个方法不会被用到。
终结方法既然存在那它就并不是毫无用处,第一种用途就是充当一个“安全网”,终结方法“本该”是在GC前做一些清理动作,但GC的时间未知,也就是终结方法执行时间未知,对于FileInputStream类我们都知道应该在try-finally中对其调用close方法,但也许我们会忘记编写此方法,在FileInputStream源代码中就实现了终结方法目的就在于如果忘记了close方法,至少还有终结方法,虽然可能不能得到及时执行,但晚执行总比不执行好吧。第二个用途可能使用的场景就可能比较少,JVM只回收普通对象,对于本地对象(也就是不是Java实现的对象),JVM并不会对它进行回收,此时我们就可以在终结方法中对本地对象进行一些清理操作,但一定记住一定要是“不拥有关键资源的前提”,且在子类中重写了终结方法一定要现实调用super.finalize(),否则父类的终结方法不会被调用。
综上,对于终结方法,一般代码中并不会使用,如果要使用一定要考虑上面两种用途是否值得去做,万万不应该依赖终结方法来更新重要的持久状态。
2017-08-06
第8条:覆盖equals时请遵守通用约定
对于equals方法,在编码中最常用的可能就是比较两个字符串是否是值相等的。需要自己重写equals方法的场景可能并不是人人都能有幸碰到,而如果碰到了该怎么办,本条目下书中说明了重写equals方法时需要遵守的一些通用约定,如果不遵守这些约定可能导致无法和其他类配合使用。
equals方法来自于Object类:
public boolean equals(Object obj) { return (this == obj); }
可以看到在Object类中equals比较的两个实例是否是引用相等的,换句话说,在不考虑“值相等”的情况下,每个实例都是独一无二的,每个实例都只与它自身相等。何时需要重写equals方法呢?顶级类只提供了引用是否相等,如果你需要自己实现一个逻辑是否相等,此时则需要重写equals方法,例如String类,但当在重写equals方法时,应该遵守以下约定:
自反性:对于任何非null的引用值x,x.equals(x)必须返回true。也就是说一个类的实例一定是与它本身相等的,不管你怎么实现它的逻辑判断,但它的“本”不能忘。
对称性:对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。这条比较好理解,x=1,y=1,你不能y=x而x!=y吧。
传递性:对于任何非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)必须返回null。
一定要反复检查测试自己重写的equals方法是否遵守以上约定,否则程序可能会变得不正常,因为许多类,包括所有的集合类都依赖于是否遵守了equals约定。书中举了详细的例子来说明上述约定,这里不再叙述。
我们来分析下String类中重写的equals方法:
//String.equals public boolean equals(Object anObject) { if (this == anObject) { //是否等于自身 return true; } if (anObject instanceof String) { //类型是否相等 String anotherString = (String) anObject; //转换类型 int n = value.length; if (n == anotherString.value.length) { //先判断长度是否相等 char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { //一个一个字符判断值是否相等 if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
String中equals方法的实现实际上就是书中给我们的重写equals的一些诀窍:
1、使用==操作检查“对象是否为这个对象的引用”,这不是必须的,只是作为一种性能优化,例如Integer类中并无此项判断。
2、使用instanceof操作符检查“参数是否为正确的类型”。
3、把参数转换成正确的类型。
4、对于该类中的每个“关键”域,检查参数中的域是否与该对象中对应的域想匹配。
第9条:覆盖equals时总要覆盖hashCode
如果这个类仅仅是重写了equals方法而没有重写hashCode,那么这个类和基于散列的集合类一起工作时就会出现问题。
首先明确一个概念,两个对象使用equals返回true,则它们的hashCode也一定相等;如果两个对象的hashCode相等,则它们的equals则不一定相等。这个概念和散列函数相关,在《哈希》这篇博客里我曾谈到过有关散列(哈希)相关的知识。
如何实现hashCode,当然你可以使hashCode返回一个固定的数值,任何对象的hashCode都是一个固定的数值,这没有问题。但当它与基于散列的集合类一起工作时,这些元素将具有相同的散列码,进而使得所有对象都被映射到统一散列桶中,使得散列表退化为链表。散列函数应该如何编写在《哈希》 一文中有提到常用的散列算法,这里不再叙述。
第10条:始终要覆盖toString
这条建议我在实际当中遇到过,因为当时几乎并没有人去重写toString方法,使得我不得不在后来去将几乎所有的POJO类的toString方法都重写了。原因在于在有的场景下会打印一条日志,日志的内容就是POJO类的属性字段值,这个时候toString的意义很明显的就体现出来了,好在eclipse能按照一定的格式自动生成toString方法。有的类是自己已经重新实现了toString方法例如集合类。
第11条:谨慎地覆盖clone
按照书中的话来讲,能不重写clone就不要去重写,因为它带来的问题太多了。我们暂且不讨论这里面的陷阱有多少,只从对Java基础知识的掌握程度来说明什么是clone,以及什么是“深拷贝”和“浅拷贝”。
首先观察以下代码,并思考对象在内存中的分配以及引用的变化:
1 /** 2 * Created by 余林丰 on 2017/8/7. 3 */ 4 public class Student { 5 private String name; 6 private int age; 7 8 public Student(String name, int age) { 9 this.name = name; 10 this.age = age; 11 } 12 13 public String getName() { 14 return name; 15 } 16 17 public void setName(String name) { 18 this.name = name; 19 } 20 21 public int getAge() { 22 return age; 23 } 24 25 public void setAge(int age) { 26 this.age = age; 27 } 28 }
1 /** 2 * Created by 余林丰 on 2017/8/7/0004. 3 */ 4 public class Main { 5 public static void main(String[] args) throws Exception{ 6 Student stu = new Student("kevin", 23); 7 Student stu2 = stu; 8 stu2.setAge(0); 9 System.out.println(stu.getAge()); 10 } 11 }
这是一段很简单的代码,Student对象实例stu、stu2在内存中的分配及引用分别如下图所示:
所以代码中出现修改stu2实例的age字段时,stu中的age字段也被修改了,原因很简单因为它们的引用指向的都是同一个对象实例。
那如果我们想在实例化一个name=”kevin”,age=23的Student实例怎么办呢?当然可以再写一段Student stu2 = new Student(“kevin”, 23);如果再重新构造一个对象实例很复杂,能不能直接复制呢?显然,使Student实现Cloneable接口并重写clone方法即可,注意此时的重写clone方法在里面仅有一句代码即是即调用父类的clone方法,而不是自定义实现:
1 /** 2 * Created by 余林丰 on 2017/8/7/0007. 3 */ 4 public class Student implements Cloneable{ 5 private String name; 6 private int age; 7 8 public Student(String name, int age) { 9 this.name = name; 10 this.age = age; 11 } 12 13 public String getName() { 14 return name; 15 } 16 17 public void setName(String name) { 18 以上是关于Effective Java通俗理解(持续更新)的主要内容,如果未能解决你的问题,请参考以下文章Effective Java -- 重写equals方法的通用约定