《Effective Java》读书笔记 - 2.创建和销毁对象

java

item 1:Consider static factory methods instead of constructors

一个static factory method只不过是一个返回这个类的实例的static方法。与设计模式中的factory method是不同的概念。static factory methods有几点好处:
一.它们可以有自己的名字,而不像constructor只能与类名相同。一个好的名字可以增进可读性。
二.不像constructor那样,每次调用static factory methods的时候,并不一定都要创建一个新的对象,比如对immutable classes来说,可以返回那些已存在的对象。比如Boolean.valueOf(boolean)这个static factory method,就从不会创建一个新的对象。这种能多次调用,但都返回相同对象的能力 使得这个类可以对 什么时候有哪些自己的实例 有着严格的控制,我们把这种类叫做instance-controlled。Instance control可以让一个类保证它是一个singleton或者noninstantiable。或者,它可以让一个immutable class能保证自己的所有实例都是不同的,所以a.equals(b)当且仅当ab,那么就可以用ab来代替a.equals(b)来增强性能(Enum types就保证了这一点)。
三.你可以返回一个 是返回类型的子类 的对象。比如你的返回类型是一个interface,并且返回的对象的class都可以是非public的(注意是class,而不是method,class只能是public或者default的访问权,内部类例外)。这样的话,用户只关心interface中定义的API,而类的实现者可以随时更改此类,比如java.util.EnumSet,没有public constructor,只有static factories,如果只有小于等于64个enum types,那么它会返回一个RegularEnumSet实例,内部基于一个long实现;如果大于64个,那么就返回一个JumboEnumSet实例,内部基于一个long数组实现。而RegularEnumSet和JumboEnumSet这两个类对用户都是不可见的。
四.减少代码长度,比如一个static factory method:

public static <K, V> HashMap<K, V> newInstance() {

return new HashMap<K, V>();

}

然后就不必写两遍type parameters:

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

而static factory methods不好的地方在于:

一.只有private的constructor的类不能被继承,package级别(简称default)的class或者private的内部class对于client来说也是不能被继承的。(我没看出来哪儿不好,作者也说这可能是因祸得福,因为这鼓励我们use composition instead of inheritance)。我做了个实验:private的内部类即使有个public的constructor,除了在它内部或者它的outer class中,都不能实例化一个这个内部类。

二.static factory methods不能很明显地区别于其他static methods。几种常见的static factory methods的命名:valueOf、getInstance、newInstance、getType(这里的Type应该是某个接口的名字,或者基类)、newType。

Item 2: Consider a builder when faced with many constructor parameters

当一个类的constructor需要很多参数,并且有些参数是可选并带有默认值的时候,最好的办法就是比如C#中的命名参数和默认参数。但Java里面没有怎么办?有一种古老的Telescoping constructor pattern,假设只有servingSize和servings是必选的:

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, int sodium, int carbohydrate) {

this.servingSize = servingSize;

this.servings = servings;

this.calories = calories;

this.fat = fat;

}

默认值被分开写在不同的方法里,而且这个例子中的这个类只有四个状态变量,如果有十个那么就吃瘪了。还有一种叫JavaBeans pattern的方法,就是只有一个可以设置必选值的constructor,并把默认值用field initialization写在相关field的后面,然后每个field都有一个public setter。然后当你创建一个对象的时候,必须在调用完构造函数后,马上紧跟着set你需要设置的参数。这种方法的弊端在于:由于要分开调用多个方法来构造这个对象,这个对象可能在构造到一半的时候处于一种不一致的状态。我的理解就是:这样更有可能会使得 还没有被完全构造好的对象被使用,就会出很难找的bug。另外,这样的对象不可能是immutable的,所以要保证线程安全还得另下功夫。那么如何才能模拟其他语言中的命名参数和默认参数?请参考下面的Builder Pattern:

public class NutritionFacts {

private final int servingSize;

private final int servings;

private final int calories;

private final int fat;

public static class Builder {

private final int servingSize;

private final int servings;

private int calories = 0;

private int fat = 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 NutritionFacts build() {

return new NutritionFacts(this);

}

}

private NutritionFacts(Builder builder) {

servingSize = builder.servingSize;

servings = builder.servings;

calories = builder.calories;

fat = builder.fat;

}

}

然后当你需要一个这个类的实例的时候可以:

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).fat(100).build();

我承认client用的时候确实比较爽,当implementor实现的时候也太tm麻烦了吧,我的理解就是:这个多出来的一个叫Builder的类基本就和本来的类一样,它的唯一作用貌似就是设好所有要设的instance fields,然后拷贝到本来的class中,注意这里是static inner class,所以它可以访问外部类的private的方法(但是只能访问static的,还记得constructor是隐式static的吗?)。关于参数验证应该放在哪里我没太看懂。你甚至可以用一个Builder来构造多个对象。另外,用这个Builder可以很好地诠释一个叫Abstract Factory的模式,就是让client传一个Builder给一个方法,让这个方法为client创建一个或多个对象,比如:首先定义一个接口:

// A builder for objects of type T

public interface Builder<T> {

public T build();

}

这里的T就是这个Builder是为谁服务的,拿刚才的例子来说就是NutritionFacts.Builder可以声明成实现了Builder<NutritionFacts>。然后就可以写一个方法接受一个这个接口类型的参数,比如:

Tree buildTree(Builder<? extends Node> nodeBuilder) { ... }

这个方法构造一个Tree对象给你,但是需要client提供的Builder,来构造树的每一个node。

这种Builder模式的缺点在于:你要构造一个对象前必须先构造一个这个对象的Builder;其次,这个模式的代码更加冗长,最好在你需要的参数确实很多的情况下使用。

Item 3: Enforce the singleton property with a private constructor or an enum type

singleton就是一个只被实例化一次的类。不可能让一个mock对象来取代一个singleton,所以难以测试,除非这个singleton的class实现了某个interface,然后client是通过这个interface来用这个singleton的。一种实现singleton的方法是用一个public的static final的filed:

public class Elvis {

public static final Elvis INSTANCE = new Elvis();

private Elvis() {...}

}

注意构造函数是private的。还有一种是用一个public的factory method:

public class Elvis {

private static final Elvis INSTANCE = new Elvis();

private Elvis() {...}

public static Elvis getInstance() {

return INSTANCE;

}

}

这种方法的好处是更灵活,比如说如果你不想让这个类是个singleton了,你可以很轻松地把这个method修改成,比如 对每个线程返回一个实例。而用public field的好处是更简单直观,因为client可以看到这个field的声明,知道它是final的所以肯定是个singleton。如果想让一个singleton的class是Serializable的话,必须做一些额外工作,这个需要的时候再查阅其他资料吧。

还有一种用enum来实现singleton的方法:

public enum Elvis {

INSTANCE;

//这里可以定义其他方法

}

这种方法在功能上和刚才public field的方法一样(因为对client来说都是用Elvis.INSTANCE来得到这个singleton),但是更简洁,免费提供serialization功能,等。

Item 4: Enforce noninstantiability with a private constructor

经常会有一些utility classes,他们可能只包含static methods,这种class应该被弄成 不能被实例化的。方法如下:

public class UtilityClass {

// Suppress default constructor for noninstantiability

private UtilityClass() {

throw new AssertionError();

}

}

如果你不指定任何constructor的话,就会默认生成一个public的,所以这里我们要显式地写一个private的constructor,只是为了让这个类不能被实例化。异常是防止在类的内部不小心被实例化。这种方法也可以防止这个类被继承,因为子类需要调用基类的构造函数。

Item 5: Avoid creating unnecessary objects

先举个反例:

String s = new String("stringette"); // DON'T DO THIS!

为什么这样很不好?因为每当这句statement被执行的时候,都会创建一个新的对象,而且传给String构造函数的"stringette"这玩意儿本身也是个String的实例。所以说正确做法应该是:

String s = "stringette";

以这种方式得到或创建的String对象会得到复用。

Item1中讲过,用static factory methods可以避免创建不必要的对象,比如Boolean.valueOf(String)。而构造函数Boolean(String)每次都会创建一个新的对象。书上还举了一个例子就是:一个方法里面有几个local对象,创建起来比较麻烦,每次你调用这个方法都会创建这几个对象,所以作者说更好的办法是把这几个local variable变成类的private field,然后在static initializer中初始化他们(创建对象),于是只需要创建这一次。后面书上说的adapter模式不明觉厉,例子是:Map.keySet只是返回一个view(也就是你修改原来Map中的key也会在这个view中看到改变,反之亦然),每次调用这个方法其实都是返回相同的Set实例。还有要小心autoboxing,比如:Long sum = 0L;sum +=1;这时候貌似就会进行autoboxing,把1提升为一个Long对象,这是没必要的,把sum声明成原始类型long就行了。这里的意思只是:prefer primitives to boxed primitives,但不是说创建对象是很昂贵的所以尽量要避免。现代的JVM都会帮你处理得很好,所以别瞎操心了。Item 39与本条item相反:“Don’t reuse an existing object when you should create a new one”(defensive copy)。

Item 6: Eliminate obsolete object references

一个叫Stack的类里面有下面这么一个方法:

public Object pop() {

if (size == 0)

throw new EmptyStackException();

Object result = elements[--size];

elements[size] = null; // Eliminate obsolete reference

return result;

}

如果不写有注释的那行,那么就可能导致“内存泄漏”。如果一个元素已经出栈了,那么这个元素对用户来说就是已经obsolete了,可以被回收了,但是由于Stack内部用一个数组实现,这个数组中的一部分元素可能是obsolete的,这时候就需要你手动null掉它们。万一这些引用没有被null掉,它们也许指向一些很大的对象,这些对象又指向更大的对象。还有一个好处就是万一不小心access了某个obsolete的数组元素,那么就会NullPointerException,而不会默默地不报错。那么什么时候需要像这样null掉引用?简单地说就是:当一个类会管理自己的内存的时候,那么你就要注意是不是要null掉引用了。啥意思?拿上面的Stack来说,它内部的数组中的一部分是被allocated,而剩余部分是free的,但GC无法得知这一点,所以你只能通过null掉引用来告诉它。

另一种内存泄漏是缓存,当你把一个引用放入一个缓存中后,可能你就忘了,这时候你可以用WeakHashMap,只有对key的某个外部引用还存活着,这个entry才活着。

还有一种内存泄漏是Listeners以及其他callbacks。矮油,我刚好昨天看了观察者的设计模式,也就是说如果一个对象要求Subject把自己注册到通知列表,但是这个对象之后比如go out of scope了,那么Subject还保持着它的引用,还在不断通知一个已经没用的东西,咋整?用WeakHashMap好了。

Item 7: Avoid finalizers

finalizers无法保证他们会及时地被执行(我记得是一个对象要被GC的时候,看看他有没有finalize方法,有的话就让他再活一轮)。所以说你根本不知道它什么时候会被执行,而且不同的JVM也不一样。给一个Class写一个Finalize方法会延迟它的对象被reclaim,所以可能造成OutOfMemoryError。甚至有可能这个Finalizer根本就不会被执行都有可能。System.gc和System.runFinalization方法只是增加了finalizer被执行的几率,但并不保证。而且在Finalize方法里面有个uncaught exception,那么会被直接忽略掉,连stacktrace都不会打印。而且有Finalize的对象性能会大大受损。所以说如果一个对象封装了某些需要被terminated的资源,就在它的类里面显式提供一个回收资源的方法,然后需要让client用try-finally来调用它。顺便一提,这个对象必须记录自己的资源是不是已经被terminate掉了,如果是,那么再调用自己的某些方法的话就要throw IllegalStateException。那么finalizer有什么用?一是可以试着看一下用户是不是忘记terminate资源了,better late than never,如果是,那么就terminate资源并log一个warning,告诉用户他有bug。二是有些不太critical的native object可以在Finalizer中被回收,如果是critical的就必须像上面说的那样让client显式处理一下,那么问题就来了,子类的Finalizer最后必须用finally手动调用父类的Finalizer,万一忘了咋办?书上提供了一个叫finalizerGuardian的方法,但我不写了,因为实际中估计根本不会用到Finalizer,应该避免用它。

以上是 《Effective Java》读书笔记 - 2.创建和销毁对象 的全部内容, 来源链接: utcz.com/z/389763.html

回到顶部