Effective.Java第1-11条
1. 考虑使用静态工厂方法替代构造方法
一个类可以提供一个公共静态工厂方法,它只是返回类实例的静态方法。例如JDK的Boolean的valueOf方法:
public final class Boolean implements java.io.Serializable, Comparable<Boolean> { public static final Boolean TRUE = new Boolean(true); public static final Boolean FALSE = new Boolean(false); public Boolean(boolean value) { this.value = value; } public static Boolean valueOf(boolean b) { return (b ? TRUE : FALSE); } ... }
静态工厂方法与设计模式中的工厂方法模式不同。
优点:
(1)静态工厂方法不像构造方法,它们有名字,语义清晰
(2)静态工厂方法不需要每次调用时都创建对象。例如返回静态对象,避免不必要的创建对象。
(3)静态工厂可以返回其返回类型的任何子类型的对象。
(4)其返回类型可以根据参数的不同而不同,比如EnumSet类的下面方法:
public static <E extends Enum<E>> EnumSet<E> copyOf(Collection<E> c) { if (c instanceof EnumSet) { return ((EnumSet<E>)c).clone(); } else { if (c.isEmpty()) throw new IllegalArgumentException("Collection is empty"); Iterator<E> i = c.iterator(); E first = i.next(); EnumSet<E> result = EnumSet.of(first); while (i.hasNext()) result.add(i.next()); return result; } }
(5)在编写包含该方法的类时,返回的对象的类不需要存在。这种灵活的静态工厂方法构成了服务提供者框架的基础。比如Java数据库连接JDBC。由对应的厂商提供具体的实现类。
缺点:
(1)如果类不含public或protected的构造方法,将不能被继承。
(2)与其它普通静态方法没有区别,没有明确的标识一个静态方法用于实例化类。也就是API不是非常全。所以,一般一个静态工厂方法需要有详细的注释,遵守标准的命名。如下:
from——A 类型转换方法,它接受单个参数并返回此类型的相应实例,例如:Date d = Date.from(instant);
of——一个聚合方法,接受多个参数并返回该类型的实例,并把他们合并在一起,例如:Set faceCards = EnumSet.of(JACK, QUEEN, KING);
valueOf——from 和 to 更为详细的替代 方式,例如:BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
instance 或 getinstance——返回一个由其参数 (如果有的话) 描述的实例,但不能说它具有相同的值,例如:StackWalker luke = StackWalker.getInstance(options);
create 或 newInstance——与 instance 或 getInstance 类似,除了该方法保证每个调用返回一个新的实例,例如:Object newArray = Array.newInstance(classObject, arrayLen);
getType——与 getInstance 类似,但是如果在工厂方法中不同的类中使用。Type 是工厂方法返回的对象类型,例如:FileStore fs = Files.getFileStore(path);
newType——与 newInstance 类似,但是如果在工厂方法中不同的类中使用。Type 是工厂方法返回的对象类型,例如:BuweredReader br = Files.newBuweredReader(path);
type—— getType 和 newType 简洁的替代方式,例如:List litany = Collections.list(legacyLitany);
2. 当构造方法参数过多时考虑构造模式
比如一个User,有好多属性,但是只有ID是必须有的,其他属性可有可无。如下:
package cn.qlq.builder; public class User { // 必须字段 private int id; private String name; private String sex; private String job; private String health; private String BMI; private int height; private int weight; public User() { super(); } public User(int id, String name, String sex, String job, String health, String bMI, int height, int weight) { super(); this.id = id; this.name = name; this.sex = sex; this.job = job; this.health = health; BMI = bMI; this.height = height; this.weight = weight; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } public String getJob() { return job; } public void setJob(String job) { this.job = job; } public String getHealth() { return health; } public void setHealth(String health) { this.health = health; } public String getBMI() { return BMI; } public void setBMI(String bMI) { BMI = bMI; } public int getHeight() { return height; } public void setHeight(int height) { this.height = height; } public int getWeight() { return weight; } public void setWeight(int weight) { this.weight = weight; } @Override public String toString() { return "User [id=" + id + ", name=" + name + ", sex=" + sex + ", job=" + job + ", health=" + health + ", BMI=" + BMI + ", height=" + height + ", weight=" + weight + "]"; } }
当我们设置几个属性的时候可以通过构造方法进行创建,但是比如我们只想设置一些属性,其他属性没用,我们可能会写成空值,这样的代码阅读起来很难懂,属性更多的时候更加难以阅读:
User user = new User(1, "张三", "", "", "", "", 0, 0);
也有可能通过setter进行设值,如下:(属性更多的时候需要更多的setter)
User user = new User(); user.setId(1); user.setName("xxx"); user.setBMI("XXX");
...
解决办法:采用建造模式 + 流式写法
由于是用Builder模式来创建某个对象,因此就没有必要再定义一个Builder接口,直接提供一个具体的建造者类就可以了。
对于创建一个复杂的对象,可能会有很多种不同的选择和步骤,干脆去掉“导演者”,把导演者的功能和Client的功能合并起来,也就是说,Client这个时候就相当于导演者,它来指导构建器类去构建需要的复杂对象。
package cn.qlq.builder; public class UserBuilder { private User user = new User(); /** * 构造方法确保ID必有 * * @param id */ public UserBuilder(int id) { user.setId(id); } UserBuilder name(String name) { user.setName(name); return this; } UserBuilder sex(String sex) { user.setSex(sex); return this; } UserBuilder job(String job) { user.setJob(job); return this; } UserBuilder health(String health) { user.setHealth(health); return this; } UserBuilder BMI(String BMI) { user.setBMI(BMI); return this; } UserBuilder height(int height) { user.setHeight(height); return this; } UserBuilder weight(int weight) { user.setWeight(weight); return this; } public User build() { if (user.getId() == 0) { throw new RuntimeException("id必须设置"); } return user; } }
客户端代码:
package cn.qlq.builder; public class MainClass { public static void main(String[] args) { UserBuilder userBuilder = new UserBuilder(2); User user = userBuilder.name("张三").BMI("xxx").health("健康").build(); System.out.println(user); } }
这样的代码读起来也舒服,语义也更好理解。
3.使用私有构造方法或枚举类实现Singleton属性
(1)使用静态工厂方法实现
public class Runtime { private static Runtime currentRuntime = new Runtime(); public static Runtime getRuntime() { return currentRuntime; } private Runtime() {} 。。。 }
如果需要防御通过反射创建对象,可以在构造方法中判断,当currentRuntime != null 的时候禁止创建(抛出一个RuntimeException).
(2)枚举类实现
package cn.qlq.thread.fifteen; /** * 枚举实现单例模式 * * @author Administrator * */ public enum Singleton_6 { instance; private Singleton_6() { System.out.println("调用构造方法"); } public Singleton_6 getInstance() { return instance; } public static void main(String[] args) { System.out.println(Singleton_6.instance.getInstance()); } }
4.使用私有构造方法执行非实例化
我们经常会使用一些只包含静态方法和静态属性的类。这样的类获得了不好的名声,因为有些人滥用这些类而避免以面向对象的方式思考,但是它们确实有着特殊的用途。比如说工具类或者说常量工具类。
JDK8开始,接口也允许有默认的静态方法和default方法。
这样的类不允许被实例化:一个实例是没有意义的。然而,在没有显示构造方法的情况下,编译器提供了一个公共的、无参的默认构造方法。因为当类不包含显示构造方法的时候,才会生成一个默认的构造方法,因此可以通过包含一个私有构造方法来实现类的非实例化:
public class DocUtils { // 静止实例化 private DocUtils() { throw new RuntimeException(); } }
5. 依赖注入优于硬链接资源
许多类依赖一个或者多个底层资源。如:拼写检查类依赖于一个字典类.
常见一:将此类实现为静态实用工具类
public class SpellChecker { private static final Lexicon dictionary = ...; private SpellChecker() {} // Noninstantiable public static boolean isValid(String word) { ... } public static List suggestions(String typo) { ... } } }
常见二:实现为单例
public class SpellChecker { private final Lexicon dictionary = ...; private SpellChecker(...) {} public static INSTANCE = new SpellChecker(...); public boolean isValid(String word) { ... } public List<String> suggestions(String typo) { ... } }
上面两种做法都令人不满意,因为它们假设只有一本字典。
最高效的做法是使 dictionary 属性设置为非final,并且通过一个方法改变此属性,以实现支持多个字典。
依赖项注入(dependency injection)的一种形式:字典是拼写检查器的一个依赖项,当它创建时被注入到拼写检查器中。
// Dependency injection provides flexibility and testability public class SpellChecker { private final Lexicon dictionary; public SpellChecker(Lexicon dictionary) { this.dictionary = Objects.requireNonNull(dictionary); } public boolean isValid(String word) { ... } public List<String> suggestions(String typo) { ... } }
6. 避免创建不必要的对象
每次需要时重用一个对象而不是创建一个新的功能相同的对象。常用的如静态工厂方法模式,如:
public static Boolean valueOf(boolean b) { return (b ? TRUE : FALSE); }
例如:
String s = new String("str");
每次执行都会创建一个String对象,而这些对象并不是必须的,建议改为:
String s = "str";
优先使用基本类型而不是装箱的基本类型,也要注意无意识地装箱。
7. 消除过期的对象引用
简单的理解为:一旦对象引用过期,将其设为null。一个好处就是如果之后被错误的引用会抛出NPE。
8. 避免使用Finalizer和Cleaner机制
Java9中用Cleaner代替了Finalizer,但是Finalizer仍被保留使用。
注意不能把Java中的Finalizer和Cleaner当成c++的析构函数。在c++中,析构函数是正确的回收对象相关资源方式,是与构造方法对应的。在Java中,当一个对象不可达时,垃圾收集器回收与对象相关联的存储空间,不需要开发人员做额外工作。C++析构函数也被用来回收其他内存资源。Java中,try-finally或者try-with-resources用于此目的。
Finalizer和Cleaner的一个缺点是不能保证他们能够及时地执行。也就是说当对象引用不可达时,这两个方法的执行时间是不固定的,所以不应该做任何业务相关代码。
不能相信System.gc() 和 System.runFinalization() 等方法,他们也只是简单的通知进行GC,并不会马上GC。
9. 使用 try-with-resources语句替代 try-finally 语句
关闭流的方式有很多种方式,一般都是手动关闭,比如 InputStream、OutputStream和Java.sql.Connection。
package cn.qlq; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; public class MainClass { public static void main(String[] args) { File file = new File("/usr/test.txt"); InputStream inputStream = null; OutputStream outputStream = null; try { inputStream = new FileInputStream(file); outputStream = new FileOutputStream(file); // 使用流 } catch (FileNotFoundException e) { e.printStackTrace(); } finally { if (outputStream != null) { try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
再次升级变为如下方式,有点类似于IOUtils的方法:
package cn.qlq; import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; public class MainClass { public static void main(String[] args) { File file = new File("/usr/test.txt"); InputStream inputStream = null; OutputStream outputStream = null; try { inputStream = new FileInputStream(file); outputStream = new FileOutputStream(file); // 使用流 } catch (FileNotFoundException e) { e.printStackTrace(); } finally { closeQuietly(outputStream); closeQuietly(inputStream); } } private static void closeQuietly(Closeable closeable) { if (closeable != null) { try { closeable.close(); } catch (IOException e) { // ignored } } } }
上面代码比较冗余,而且不易阅读,Java7引入了 try-with-resources。代码阅读比较简单,而且提供了更好的诊断。如下:(建议这种写法,当然catch代码块可以不写,一般用于记录一些信息)
public static String readValue(String filePath, String defaultValue) { File file = new File("/usr/test.txt"); try (InputStream inputStream = new FileInputStream(file); OutputStream outputStream = new FileOutputStream(file)) { // 读取文件返回值 return ""; } catch (Throwable e) { // 记录日志 return defaultValue; } }
10. 重写equals()方法时遵守通用约定
按照约定,equals方法要满足以下规则。
自反性: 对于任何非空引用x,x.equals(x) 一定是true
对称性: 对于非空引用x和y, x.equals(y) 和 y.equals(x)结果一致
传递性: a 和 b equals , b 和 c equals,那么 a 和 c也一定equals。
一致性: 对于任何非空引用x和y,如果在equals比较中使用的信息没有被修改,则x.equals(y) 的多次调用必须始终返回true或者false。
非空性: 对于任何非空引用x, x.equals(null) 一定是false
同时,也有一些提醒,比如:
(1)重写了euqals方法的对象必须同时重写hashCode()方法。
(2)在equals方法中,不要将参数的Object类型换成其他类型。
编写高质量equals()方法的建议:
(1)使用==运算符检查参数是否为该对象的引用。如果是,返回ttrue。这只是一种性能优化。
(2)使用 instanceof 来检查参数是否是正确的类型,如果不是返回false。
(3)参数转换为正确的类型。
(4)对于每个类的重要属性,在equals中进行比较。
对于类型为非float或double的基本类型,使用==运算符比较;对于对象引用属性,递归地调用equals方法;对于float基本类型的属性,使用Float.compare(float, float)方法;对于double基本类型的属性,使用Double.compare(double, double)。
在很多情况下,不要重写equals方法,从Object继承的完全是想要的。如果确实重写了equals()方法,那么一定要比较这个类的所有属性,并且遵守上面五条规则。
在比较两个对象是否是同一个对象的时候用equals,不用==。
11. 重写了euqals方法的对象必须同时重写hashCode()方法。
这个方法返回对象的散列码,返回值是int类型的散列码。对象的散列码是为了更好的支持基于哈希机制的Java集合类,例如 Hashtable, HashMap, HashSet 等。
等价的(调用equals返回true)对象必须产生相同的散列码。不等价的对象,不要求产生的散列码不相同。
假设只重写equals而不重写hashcode,那么类的hashcode方法就是Object默认的hashcode方法,由于默认的hashcode方法是根据对象的内存地址经哈希算法得来的,所以会导致equals相等的两个对象的hasCode()不一定相等,违反了上面的约定。
例如:使用commons-lang包自带的工具类编写equals和hasCode方法:
package cn.qlq; import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; public class User implements Cloneable { private int age; private String name, sex; @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (obj == this) { return true; } if (!(obj instanceof User)) { return false; } final User tmpUser = (User) obj; return new EqualsBuilder().appendSuper(super.equals(obj)).append(name, tmpUser.getName()).isEquals(); } @Override public int hashCode() { return new HashCodeBuilder().append(name).toHashCode(); } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } @Override public String toString() { return "User [age=" + age + ", name=" + name + ", sex=" + sex + "]"; } }