【读书笔记】《Effective Java》——创建和销毁对象

Item 1. 考虑用静态工厂方法替代构造器

获得一个类的实例时我们都会采取一个公有的构造器。Foo x = new Foo();

同时我们应该掌握另一种方法就是静态工厂方法(static factory method)。

一句话总结,静态工厂方法其实就是一个返回类的实例的静态方法。

书中给出的例子是Boolean的valueOf方法:

通过valueOf方法将boolean基本类型转换成了一个Boolean类型,返回了一个新的对象引用。

valueOf外,像Java中的getInstancenewInstance等方法都为静态工厂方法。

静态工厂方法不同于设计模式中的工厂方法。

那么为什么要使用静态工厂方法呢?下面是它的几大优势:

它们有名字

给构造器起名字,增强了代码的可读性。

如果一个构造器的参数并不能确切描述它返回的对象,这时候可以考虑静态工厂方法。

或者你的多个构造器只是在参数列表中的参数顺序上有所不同,那么除非你提供了详尽的文档说明,否则你下次使用时就会一脸懵逼,这几个构造器到底要选哪个🤔?

例如下面这个例子,一个RandomIntGenerator类,从类名可以看出这是个用来产生整型随机数的类。

public class RandomIntGenerator {
    private final int min;
    private final int max;
    
    public int next(){...}
}

随机数的大小介于minmax两个参数之间,我们需要构造器去对它们进行初始化。

public RandomIntGenerator(int min, int max) {
  this.min = min;
  this.max = max;
}

很好,现在我们又想提供一个新的功能,用户只需要指定一个最小值即可,生成的随机数会介于指定的最小值和整型默认的最大值之间。

所以,我们可能会添加一个新的构造器:

public RandomIntGenerator(int min) {
  this.min = min;
  this.max = Integer.MAX_VALUE;
}

到这里事情进展很顺利,但是有指定最小值的功能,相对的我们还要添加一个指定最大值的方法。

public RandomIntGenerator(int max) {
  this.min = Integer.MIN_VALUE;
  this.max = max;
}

但是创建完之后你会得到一个编译错误,原因是两个构造器有相同的方法签名(方法名和参数类型)。

这时静态工厂方法就派上用场了,重新构造如下:

public class RandomIntGenerator {
  private final int min;
  private final int max;

  private RandomIntGenerator(int min, int max) {
    this.min = min;
    this.max = max;
  }

  public static RandomIntGenerator between(int max, int min) {
    return new RandomIntGenerator(min, max);
  }

  public static RandomIntGenerator biggerThan(int min) {
    return new RandomIntGenerator(min, Integer.MAX_VALUE);
  }

  public static RandomIntGenerator smallerThan(int max) {
    return new RandomIntGenerator(Integer.MIN_VALUE, max);
  }

  public int next() {...}
}

不仅没有了之前的错误,而且它们有着不同的名字,很清晰地描述了方法的功能。

总之,由于静态工厂方法有名称,所以他们不受那些限制。

当你有多个签名相同的构造器时,用几个名字有区分度的静态工厂方法代替可能是更好的解决办法。

不必在每次调用它们的时候创建一个新对象

每次调用构造器都会创建一个新对象,而静态工厂方法则不会。

这使得不可变类可以使用预先定义好的实例,或者将构建好的实例缓存起来,进行重复利用,避免创建不必要的重复对象。

public class BooleanGenerator {
    public static void main(String[] args) {
        Boolean b1 = Boolean.valueOf(true);
        Boolean b2 = Boolean.valueOf(true);

        Boolean b3 = new Boolean(true);
        Boolean b4 = new Boolean(true);

        System.out.println(b1 == b2);
        System.out.println(b3 == b4);
    }
}
//output:
//true
//false

可以看到使用valueOf并不会创建新的对象,对于一些经常创建相同对象的程序,并且创建对象的代价很高,静态工厂方法可以极大地提升性能。

可以返回原返回类型的任何子类型的对象

在选择返回对象的类时有了更大的灵活性。

API可以返回对象,同时不会使对象的类变成公有的既可以是非公有类,这样做的目的可以隐藏实现类。

公有的静态工厂方法所返回的对象的类不仅可以是非公有的,而且该类还可以随着每次调用发生变化,这取决于静态工厂方法的参数值。

参考java.util.EnumSet中的noneOf方法,根据不同的参数类型选择返回的是RegularEnumSet还是JumboEnumSet

接下来,书中通过服务提供者框架(Service Provider Framework)来说明了静态工厂方法的另一个用法。

利用的是静态工厂方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不必存在。

看起来有点绕,下面来通过代码来看一下。

//服务接口
public interface Service(){
 ...//具体的服务方法
}

//服务提供者接口
public interface Provider{
    Service newService();
}

//不可实例化的类,用于服务注册和访问
public class Services {
    private Services{};//防止实例化

    //将服务的名字映射到具体服务
    private static final Map<String,Provider> providers = new ConcurrentHashMap<String, Provider>();
    public static final String DEFAULT_PROVIDER_NAME = "<def>";

    //服务提供者注册API
    //默认的注册方法
    public static void registerDefaultProvider(Provider p){
        registerProvider(DEFAULT_PROVIDER_NAME,p);
    }
    //真正的注册方法
    public void registerProvider(String name, Provider p) {
        providers.put(name, p);
    }

    //服务访问API
    public static Service newInstance() {
        return newInstance(DEFAULT_PROVIDER_NAME);
    }

    //真正的实例化方法
    public static Service newInstance(String name) {
        Provider p = providers.get(name);
        if(p == null) {
            throw new IllegalArgumentException("No provider registered with name:" + name);
        }
        return p.newService();//返回服务实例
    }
}       

服务提供者框架是指这样一个系统:多个服务提供者实现一个服务,系统为服务提供者的客户端提供多个实现,并把他们从实现中解耦出来。

这块有点难理解,先来看一下UML图:

JDBC就是利用的服务提供者框架,当我们创建数据库连接时,需要先加载对应的驱动,然后获取连接。

Class.forName(jdbcDriver);
conn = DriverManager.getConnection(jdbcUrl, jdbcUser, jdbcPasswd);

对于JDBC来说Connection就是它的服务接口,里面的方法,不同的数据库需要自己实现。

DriverManager就是Services类,其中包含的registerDriver和getConnection方法对应的就是注册和访问。

Driver是一个服务提供者接口

最后返回的服务实际上是通过服务提供者接口,实现了解耦。

在创建参数化类型实例的时候,使得代码变得更加简洁

如果你用的是JDK1.7之前的版本在定义一个HashMap,那你不得不这么写:

Map<String, String> map = new HashMap<String, String>();

在使用构造器时必须再写一遍类型参数,因为不支持类型推到,每次都要干重复性的工作。

假设HashMap提供了静态工厂方法,事情就变得简单:

public static <K,V> HashMap<K,V> newInstance(){
    return new HashMap<K,V>();    
} 

你就可以通过下面的代码代替上面繁琐的声明:

Map<String,String> map = HashMap.newInstance();

显然作者在写这本书时已经考虑到了这个问题(那时候JDK的版本是1.6),JDK1.7之后的版本有了类型推导。

当然凡事都有两面性,除了上述的优点,静态工厂方法同样存在不足。

类如果不含共有的或者受保护的构造器,就不能被子类化

如果没有公有构造器,当然这个类就不能被子类继承。

这也许是一个优点,因为鼓励程序使用组合而不是继承。

他们与其他的静态方法实际上没有任何区别

在API文档中它们没有像构造器那样被明确标识出来,因此对于一个使用静态工厂方法而不是构造器的类来说,要想弄明白如何实例化,就需要费点事了。

你可以使用注释或者如下的命名规则让用户知道这是一个静态工厂方法:

  • valueOf——返回的实例与它的参数具有相同的值,被用来做类型转换。e.g. String.valueOf()。
  • of——valueOf的一种更加简洁的替代,在EnumSet中使用并流行起来。
  • getInstance——通过方法的参数来描述返回实例。
  • newInstance——和getInstance一样,每次返回新的实例。
  • getTypenewType——和上面两个方法类似,在工厂方法处于不同的类中时使用,Type表示工厂方法返回的对象类型。

总之,静态工厂方法和构造器各有优势,使用时需要衡量那种方法更好。

Item 2. 遇到多个构造器参数时要考虑用构建器

上一节介绍了静态工厂方法,虽然相对构造器来说有一定的优势,但是两者都有一个局限,就是存在大量可选参数时表现不是很好。

重叠构造器

当面对大量可选参数时,一些人可能会选择重叠构造器(telescoping constructor)。

文中举了一个食品营养成分表的例子,表中有些参数是必选的,有些参数是可选的。

对于重叠构造器来说,第一个构造器只包含必选参数,第二个构造器有一个可选参数,第三个构造器有两个,以此类推,直到最后一个构造器包含所有参数。

// Telescoping constructor pattern - does not scale well!
public class NutritionFacts {
    private final int servingSize;  // (mL)            required
    private final int servings;     // (per container) required
    private final int calories;     //                 optional
    private final int fat;          // (g)             optional
    private final int sodium;       // (mg)            optional
    private final int carbohydrate; // (g)             optional

    public NutritionFacts(int servingSize, int servings) {
        this(servingSize, servings, 0);
    }

    public NutritionFacts(int servingSize, int servings,
            int calories) {
        this(servingSize, servings, calories, 0);
    }

    public NutritionFacts(int servingSize, int servings,
            int calories, int fat) {
        this(servingSize, servings, calories, fat, 0);
    }

    public NutritionFacts(int servingSize, int servings,
            int calories, int fat, int sodium) {
        this(servingSize, servings, calories, fat, sodium, 0);
    }

    public NutritionFacts(int servingSize, int servings,
           int calories, int fat, int sodium, int carbohydrate) {
        this.servingSize  = servingSize;
        this.servings     = servings;
        this.calories     = calories;
        this.fat          = fat;
        this.sodium       = sodium;
        this.carbohydrate = carbohydrate;
    }
}

重叠构造器像套圈一样,对参数进行赋值。

你必须很小心地将值和参数的位置一一对应,随着参数数量的增加,你肯定不会记得第六个参数是什么。

并且如果两个类型相同参数的顺序发生了调换,可能编译期不会提示错误,但在运行时会报错。

重叠构造器模式可行,但是当有许多参数时,客户端代码会很难编写,并且可读性很差。

JavaBeans模式

另一种解决办法是JavaBeans模式,这种模式简单并且灵活,也是我们最经常使用的,通过setter方法来设置参数。

// JavaBeans Pattern - allows inconsistency, mandates mutability
public class NutritionFacts {
    // Parameters initialized to default values (if any)
    private int servingSize  = -1; // Required; no default value
    private int servings     = -1;  //     "     "      "      "
    private int calories     = 0;
    private int fat          = 0;
    private int sodium       = 0;
    private int carbohydrate = 0;

    public NutritionFacts() { }
    // Setters
    public void setServingSize(int val)  { servingSize = val; }
    public void setServings(int val)     { servings = val; }
    public void setCalories(int val)     { calories = val; }
    public void setFat(int val)          { fat = val; }
    public void setSodium(int val)       { sodium = val; }
    public void setCarbohydrate(int val) { carbohydrate = val; }
}

JavaBeans模式弥补了重叠构造器的不足,有着良好的可实现性和可读性。

但是其也存在着不足:

JavaBeans是可变的,意思是在被创建之后它们的状态可以通过setter方法随之更改。

它们的域不能声明为final,这也使它们不能成为不可变对象,不能保证线程安全。

Builder模式

Builder模式作为一种更好的方法,既能保证安全性,还有着良好的可读性。

通过Builder类来返回一个builder对象,然后在客户端调用Builder中的方法来设置参数,最后调用builder()方法来完成创建一个不可变对象。

Builder类是一个静态的内部类,其中的方法和setter类似,并且可以实现链式调用,易于使用和阅读。

/**
 * @ClassName: NutritionFacts
 * @Description: 构建器
 * @author LJH
 * @date 2017年6月26日 下午8:57:03
 */
public class NutritionFacts {

    // required
    private final int servingSize;
    private final int servings;

    // optional
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbo;

    public static class Builder {

        private final int servingSize;
        private final int servings;

        private int calories = 0;
        private int fat = 0;
        private int sodium = 0;
        private int carbo = 0;

        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }

        public Builder calories(int val) {
            calories = val;
            return this;
        }

        public Builder fat(int val) {
            fat = val;
            return this;
        }

        public Builder sodium(int val) {
            sodium = val;
            return this;
        }

        public Builder carbo(int val) {
            carbo = val;
            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;
        carbo = builder.carbo;
    }

    public int getServingSize() {
        return servingSize;
    }

    public int getServings() {
        return servings;
    }

    public int getCalories() {
        return calories;
    }

    public int getFat() {
        return fat;
    }

    public int getSodium() {
        return sodium;
    }

    public int getCarbo() {
        return carbo;
    }

    public static void main(String[] args) {
        NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbo(27).build();
        System.out.println("The nutritionfacts of cocaCola \nServing Size: " + cocaCola.servingSize + " ml");
        System.out.println("Servings: " + cocaCola.servings + " per container");
        System.out.println("Calories: " + cocaCola.calories);
        System.out.println("Fat: " + cocaCola.fat + " g");
        System.out.println("Sodium: " + cocaCola.sodium + " mg");
        System.out.println("Carbo: " + cocaCola.carbo + " g");
    }
}
View Code

使用Builder模式的好处如下:

  • 构建器能通过builder方法和setter方法对其参数强加约束条件并且检验,如果不满足条件可以抛出异常;
  • 使用了构建器模式的类可以是不可变的;
  • builder可以有多个可变参数(varargs); 
  • 构建器模式十分灵活,可以利用单个builder构建多个对象,参数可以改变,也可以自动填充;
  • 利用带有泛型的builder可以生成一个抽象工厂。

Builder模式的一个缺点就是,你必须自己编写代码创建。

总之,如果类的构造器或者静态工厂方法中含有多个参数,优先选择Builder模式。

Item 3. 用私有构造器或者枚举类型强化Singleton属性

Singleton既单例模式,作为设计模式中的一种,和其他模式一样,如果没有在项目中使用过真的很难理解。

单例模式虽然结构很简单,但一开始看的时候我就懵了😵,不知道为什么要这么做。

看了几篇博客之后,把干巴巴的代码和实际应用结合起来后就变得容易理解。

其中一篇博客中举了一个例子:

假设有这样一个应用,其中需要读取配置文件的内容。许多应用都会有自己的配置文件,开发人员可以对应用中的一些参数进行自定义,然后写入配置文件。

在实际项目中通常会使用xml或者properties格式的文件作为配置文件,现在假设我们通过一个叫Config的类来实现读取配置文件的功能。客户端可以通过new一个Config实例来获得操作配置文件内容的对象。

如果在程序运行时,有很多模块都需要加载配置文件,那么每使用一次都需要创建一个Config对象。这样做肯定会产生问题,在程序运行时会存在多个Config对象,而这些对象中的内容都是相同的,浪费了内存资源。

那么怎样能减少这种浪费,每次用到Config类时,都返回同一个对象呢?答案就是单例模式。

实现单例模式的方法有很多种,详细可以看这里

最常用的是饿汉式方法,优点是线程安全,创建简单。

但是由于没有实现懒加载,无论有没有用到对象都会创建,浪费了一定的空间。

public class Singleton {
    private static Singleton INSTANCE = new Singleton();
    private Singleton() {
        System.out.println("Singleton is created");
    }
    public static Singleton getInstance() {
        return INSTANCE;
    }
    public static void printStr() {
        System.out.println("Singleton");
    }
}

java.lang.Runtime使用的就是该方法实现单例模式。

另一种是通过枚举创建,这种方法是作者推荐的,利用了JDK1.5之后加入的Enum类。

public enum EnumSingleton {
    INSTANCE;
    public void printStr() {
        System.out.println("Singleton");
    }
    public static void main(String[] args) {
        EnumSingleton.INSTANCE.printStr();
    }
}

可以看到这个方法非常简洁明了,而且利用了枚举类的特性,提供了序列化机制,防止多次实例化。

作者认为单元素的枚举类型已经成为了实现单例模式的最佳方法。

不过感觉这种方法可读性不是很好,一般情况还是会选择饿汉式的创建方法。

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

我们经常会重复使用一些类,调用它们中的方法,这种情况下我们一般会把它们设计成一个工具类,这个类中包含一些静态方法,我们可以直接通过类名调用。

例如:java.lang.Mathjava.util.Arraysjava.util.Collections

这些工具类被设计成不可实例化的类,因为实例化对对它们来说没有意义。

然而在缺少显示构造器的情况下,编译期会自动提供一个公共的、无餐的默认构造器

你可能会试图通过抽象类的方式来使得这个类不能被实例化,但是抽象类可以被继承,它的子类仍然可以被实例化。

并且这样做会让用户以为设计成抽象类的目的是为了继承。

那么怎样才能使一个类具有不可实例化的能力?

因为只有提供一个显示的构造器,编译期才不会自动生成默认构造器,所以我们只需要将构造器设为私有的(private)即可。

这样由于外部的类和它的子类不能调用一个私有构造方法,这个类也就不能被实例化。

// Noninstantiable utility class
public class UtilityClass {
// Suppress default constructor for noninstantiability
private UtilityClass() {
     throw new AssertionError();
    }
}

为了防止在这个类的内部调用构造器,可以使用一个断言AssertionError()来避免这种情况的发生。

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

一般来说,最好通过重用对象来代替每次都创建一个功能相同的新对象。

重用方式快速,并且简单。如果一个对象是不可变的,那么它就可以一直被重用。

创建字符串时,你可能会选择这么做:

String s = new String(“stringette”); // 不要这么做!

用这种方式代替会更好:

String s = “stringette”;

初次使用字面量创建字符串也会在堆中创建对象,不过之后使用相同字符串时,都会利用字符串常量池中的引用,而不会创建新的对象。

(关于两种方法创建字符串更详尽的介绍可以看这两篇Java字符串常量池和intern()方法Java中的字符串字面量

除了重用不可变对象,也可以重用那些已知不会被修改可变对象

例如书中的例子,计算一个人是否是在“baby boomer”时期出生的。下面是一个反例:

public class Person {
    private final Date birthDate;
    // Other fields, methods, and constructor omitted
    // DON’T DO THIS!
    public boolean isBabyBoomer() {
        // Unnecessary allocation of expensive object
        Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
        Date boomStart = gmtCal.getTime();
        gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
        Date boomEnd = gmtCal.getTime();
        return birthDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) < 0;
    }
}

可以看到在isBabyBoomer方法中,创建了CalendarTimeZoneDate几个不会被修改的对象,如果每次调用方法都创建几个不必要的对象就会造成内存资源的浪费。

替代方法是:

class Person {
    private final Date birthDate;
    // Other fields, methods, and constructor omitted
    /**
     * The starting and ending dates of the baby boom.
     */
    private static final Date BOOM_START;
    private static final Date BOOM_END;
    static {
        Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
        BOOM_START = gmtCal.getTime();
        gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
        BOOM_END = gmtCal.getTime();
    }

    public boolean isBabyBoomer() {
        return birthDate.compareTo(BOOM_START) >= 0 && birthDate.compareTo(BOOM_END) < 0;
    }
}

改进后的Person类只在初始化的时候创建CalendarTimeZoneDate这个几个对象,之后再调用isBabyBoomer方法就可以一劳永逸了。

除了上面提到的两点,尽量做到以下来避免创建不必要的对象:

使用静态工厂方法。

例如Boolean.valueOf方法,不会重复创建对象。

优先使用基本类型,而不是装箱基本类型。

不同于基本类型,有时你可能会没有意识到程序会自动装箱,装箱就意味着创建对象。(虽然Character、Byte、Short、Integer和Long实现了常量池技术,但是范围只有[-127,128])

使用对象池,除非池中的对象是非常重量级的。

例如数据库连接池,将数据库连接对象保存在对象池中来重用。

Item 6. 消除过期的对象引用

虽然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); } }

pop方法中,被弹出的元素的引用依然存在于数组中,这个元素实际上已经是一个过期引用——它永远也不会再被访问,但Java的垃圾回收无法知道这一点,除非该引用被覆盖。

即使Stack对象不再需要这个元素,但是数组中的引用仍然可以让它继续存在。

在支持垃圾回收的语言中,内存泄漏的存在非常隐蔽。

为了解决这个问题,我们只需要做到:一旦对象的引用已经过期,就清空这些引用。

修改上面的例子:

public Object pop() {
    if (size == 0)
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null; // Eliminate obsolete reference
    return result;
}

那么何时清空引用呢?

  • 一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空。
  • 当你把引用放在缓存中,它就可能会被遗忘,导致过了很久之后虽然已经没用了,但还是残留在缓存中。这种情况我们应该偶尔去清理没有用的项。
  • 使用监听器和其他回调时,我们应该显式地注销。最好的方法是只保存它们的弱引用,然后储存在WeakHashMap中。
  • 使用分析工具(Heap Profiler)来发现内存泄漏。

转载请注明原文链接:http://www.cnblogs.com/justcooooode/p/7956048.html

参考资料

《Effective Java》第二章——创建和销毁对象 

https://jlordiales.me/2012/12/26/static-factory-methods-vs-traditional-constructors/

http://vojtechruzicka.com/avoid-telescoping-constructor-pattern/

https://www.ibm.com/developerworks/cn/java/j-lo-Singleton/index.html

https://medium.com/@biratkirat/learning-effective-java-item-4-4bc457fc5674

posted @ 2017-12-06 21:41  没课割绿地  阅读(1056)  评论(2编辑  收藏  举报