date: 2022-07-01
address: NC
author: 吴第广
目的:帮助读者更有效地使用 Java 及其类库(util、io、concurrent、function)等
本书大部分内容都不是讨论性能的,二十关心如何编写出清晰、正确、可用、健壮、灵活和可维护的程序来。
例子:
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
优点:
缺点:
静态功能和构造器有个共同的局限性:它们都不能很好地扩展到大量的可选参数。
对于参数较多时,程序员经常采用重叠构造器模式。提供的第一个构造器只有必要的参数,第二个构造器有一个可选参数,第三个构造器有两个可选参数,以此类推,最后一个构造器包含所有可选的参数。
简而言之,重叠构造器模式可行,但是当有许多参数的时候,客户端代码会很难编写,并且仍然难以阅读。
建造者模式
package com.wdg.effectivejava.chapter1;
import com.alibaba.fastjson.JSON;
import java.io.Serializable;
/**
* @author wudiguang
* @Description 建造者组装对象
* @Date 2022/7/1 17:55
*/
public class NutritionFacts implements Serializable {
// ignore getter&setter
private Integer servingSize;
private Integer servings;
private Integer calories;
private Integer fat;
private Integer sodium;
private Integer carbohydrate;
public static class Builder {
// 必填参数
private Integer servingSize;
private Integer servings;
//可选参数 -- 使用默认值初始化
private Integer calories = 0;
private Integer fat = 0;
private Integer sodium = 0;
private Integer carbohydrate = 0;
public Builder(Integer servingSize, Integer servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(Integer var) {
this.calories = var;
return this;
}
public Builder fat(Integer var) {
this.fat = var;
return this;
}
public Builder sodium(Integer var) {
this.sodium = var;
return this;
}
public Builder carbohydrate(Integer var) {
this.carbohydrate = var;
return this;
}
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
public static void main(String[] args) {
NutritionFacts nutritionFacts = new Builder(240, 8).calories(100).sodium(35).fat(0).carbohydrate(27).build();
System.out.println(JSON.toJSONString(nutritionFacts));
}
}
Builder 模式也适用于类层次结构。
与构造器相比,builder的微略优势在于,它可以有多个可变参数。因为builder是利用单独额方法来设置每一个参数。
Builder模式十分灵活,可以利用单个builder构建多个对象。builder的参数可以在调用build方法来创建对象期间进行调整,也可以随着不同的对象而改变,builder可以自动填充某些域,例如每次创建对象时自动增加序列号
Builder模式的确也有它自身的不足。为了创建对象,必须先创建它的构建器。虽然创建这个构建器的开销在实践中可能不那么明显,但在某些十分注重性能的情况下,可能就成问题了。
如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder模式就是一种不错的选择。
Singleton是指仅仅被实例化一次的类。Singleton通常被用来代表一个无状态的对象,或者那些本质上唯一的系统组件。使类成为Singleton会使它的客户端测试变得十分空难。
实现Singleton有两种常见的方法(都要保持构造器为私有的),并导出公有的静态成员,以便允许客户端能够访问该类的唯一实例、
public class Elvis{
public static final Elvis INSTANCE = new Elvis();
private Elvis(){}
public void leaveTheBuilding(){}
}
public class Elvis{
private static final Elvis INSTANCE = new Elvis();
private Elvis(){}
public static Elvis getInstance(){return INSTANCE}
public void leaveTheBuilding(){}
}
为了维护并保证Singleton,必须声明所有实例域都是瞬时的,并提供一个readResolve方法。否则,每次反序列化一个序列号的实例时,都会创建要给新的实例。
public enum Elvis{
INSTANCE;
public void leaveTheBuilding(){}
}
工具类不希望被实例化,因为实例化对它没有任何意义。然而,在缺少显式构造器的情况下,编译器会自动提供一个公有的、无参的缺省构造器。
企图通过将类做成抽象类来强制该类不可被实例化时行不通的。
让这个类包含一个私有构造器,他就不能被实例化
// Noninstantiable utility class
public class UtilityClass{
private UtilityClass(){
throw new AssertionError();
}
// Remainder omitted
}
有许多类会依赖一个或多个底层的资源。例如,拼写检查其需要依赖词典。因此,像下面这样把类实现为静态工具类的做法并不少见
public class SpellChecker{
private static final Lexicon dictionary = "";
private SpellChecker(){}
public static boolean isValid(String word){...}
public List<String> suggestions(String typo){}
}
public class SpellChecker{
private final Lexicon dictionary = "";
private SpellChecker(){}
public static SpellChecker INSTANCE = new SpellChecker();
public static boolean isValid(String word){...}
public List<String> suggestions(String typo){}
}
以上两种方法都不理想,因为它们都是假定只有一本词典可用。实际上,每一种语言都有自己的词典,特殊词汇还要使用特殊的词典。
静态工具和Singleton类不适合于需要引用底层资源的类。
这里需要的是能够支持类的多个实例,每个实例都是用客户端指定的资源。满足该需求的最简单的模式是,当创建一个新的实例时,就将该资源传到构造器中。这就是依赖注入的一种形式:词典时拼写检查其的一个依赖,在创建拼写检查器时就将词典注入其中
public class SpellChecker{
private Lexicon dictionary;
public SpellChecker(Lexicon dictionary){
this.dictionary = Objects.requireNonNull(dictionary);
}
public static boolean isValid(String word){...}
public List<String> suggestions(String typo){}
}
总而言之,不要用Singleton和静态工具来实现依赖一个或多个底层资源的类,且该资源的行为会影响到该类的行为;也不要直接用这个类来创建这些资源。而应该将这些资源或者工厂传给构造器(或者静态工厂,或者构建器),通过它们来创建类。这个实践就被称为依赖注入,它极大地提升了类的灵活性、可重用性和可测试性
一般来说,最好能重用单个对象,而不是在每次需要的时候就创建一个相同功能地新对象。重用方式既快速,又流行。如果对象是不可变的,它就始终可以被重用。
作为一个极端的反面例子,看看下面地语句
String s = new String("bikini"); // DO NOT DO THIS
该语句每次被执行的时候都会创建一个新的String实例,但是这些创建对象地动作全都是不必要的。传递给String构造器的参数“bikini”本身就是一个String实例,功能方面等同于构造器创建地所有对象。如果这种用法是在一个循环中,或者是在一个被频繁调用地方法中,就会创建出千万个不必要的String实例。
改进后的版本如下:
String s = "Bikini";
对于同时提供了静态工厂方法和构造器的不可变类,通常优先使用静态工厂方法而不是构造器,以避免创建不必要的对象。
静态工厂方法Boolean.valueOf(String) 几乎总是优先于构造器Boolean(String)
有些对象创建地成本比其他对象要高得多。如果重复地需要这类“昂贵的对象”,建议将它缓存下来重用。遗憾的是,在创建这种对象的时候,并非总是那么显而易见。
假设想要编写一个方法,确定一个字符串是否为一个有效的罗马数字。下面介绍一种最容易的方法,使用一个正则表达式:
static boolean isRomanNumeral(String s){
return s.matches("...");
}
这个实现的问题在于它依赖String.matches()。虽然String.matches方法最容易于查看一个字符串是否与正则表达式相匹配,但并不适合在注重性能的情形中重复使用。问题在于,它在内部为正则表达式创建了一个Pattern实例,却只用了一次,之后就可以进行垃圾回收了。创建Pattern实例的成本很高,因为需要将正则表达式编译成一个有限状态机
为了提升性能,应该显式地将正则表达式编译成一个Pattern实例(不可变),让它变成类初始化的一部分,并将它缓存起来,每当调用isRomanNumeral方法的时候就重用同一个实例:
public class RomanNumberals{
private static final Pattern ROMAN = Pattern.compile("...");
static boolean isRomanNumberal(String s){
return Roman.matcher(s).matches();
}
}
改进后的isRomanNumberal方法如果被频繁地使用,会显示出明显地性能优势。
要优先使用基本类型,而不是装箱基本类型,要当心无意识的自动装箱。
由于Java语言具有垃圾回收功能,使得程序员的工作变得更加容易,因为当你用完了对象之后,它们会被回收。
public class Stack{
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack(){
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e){
ensureCapacity();
elements[size++] = e;
}
public Object pop(){
if(size == 0){
throw new EmptyStackException();
}
return elements[--size];
}
private void ensureCapacity(){
if(elements.length == size){
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
这个程序隐藏着一个问题:随着垃圾回收器活动增加,或者由于内存占用的不断增加,程序性能地降低会逐渐表现出来。极端情况下,这种内存泄漏会导致磁盘交换,甚至导致程序失败(OutOfMemoryError错误),但是这种失败情形相对比较少见。
如果一个栈先是增长,然后再收缩,那么,从栈中弹出地对象将不会被当作垃圾回收,即使使用栈地程序不再引用这些对象,它们也不会被回收。这是因为栈内部维护着对这些对象地过期引用。所谓的过期引用,是指永远也不会再被解除地引用。
如果一个对象引用被无意识地保存起来了,那么垃圾回收机制不仅不会处理这个对象,而且也不会处理这个对象所引用地所有其他对象。
这类问题修复方法很简单:一旦对象引用已经过期,只需清除这些引用即可。
public Object pop(){
if(size == 0){
throw new EmptyStackException();
}
Object element = elements[--size];
elements[size] = null;
return element;
}
清空过期引用地另一个好处是,如果它们以后又被错误地解除引用,程序就会立即抛出NullPointerException,而不是悄悄地错误运行下去。
内存泄露的另一个常见来源是缓存。
一旦你把对象引用放到缓存中,它就很容易被遗忘,从而使得它不再有用之后很长一段时间仍然留在缓存中。
只要在缓存之外存在对某个项的键的引用,该项就有意义,那么就可以用WeakHashMao代表缓存。
内存泄漏的第三个常见来源是监听器和其他回调。
注册回调后却没有显式地取消注册
终结方法(finalizer)通常是不可预测地,也是很危险地,一般情况下是不必要的。
在Java中,当一个对象变得不可达时,垃圾回收器会回收与该对象相关联地存储空间,并不需要程序员做专门的工作。
永远不应该依赖终结方法或者清除方法来更新重要的持久状态。
使用终结方法和清除方法有一个非常重要的性能损失
终结方法有一个严重的安全问题
Java类库中包括许多必须通过调用 close 方法来手工关闭的资源。如InputStream、OutputStream和java.sql.Connection等。客户端经常会忽略资源的关闭,造成严重的性能后果也就可想而知了
根据经验,try-finally语句是确保资源会被适当关闭的最佳方法,就算发生异常或者返回也一样,但是如果再添加第二个资源,就会一团糟了。
static String firstLineOfFile(String path) throws IOException{
try(BufferedReader br = new BuffereReader(
new FileReader(path))){
return br.readLine();
}
}
使用try-with-resources 不仅使代码变得更简洁易懂,也更容易进行诊断。
结论:在处理必须关闭的资源时,始终要优先考虑使用try-with-resources,而不是try-finally
Object 非final 方法(equals、hashCode、toString、clone和finalize)都有明确的约定,因为它们设计成是要被覆盖的。
如果不能做到这一点,其他依赖于这些约定的类(HashMap、HashSet)就无法结合该类一起正常运作
如果类具有自己特有的“逻辑相等”概念,而且超类还没有覆盖equals。如Integer,String等