《Effective Java》读书笔记(一)之创建和销毁对象

最近在研读《Effective Java》一书,读书不做点笔记,感觉很容易就忘掉,于是用本篇博客来记录阅读此书的笔记。
郑重声明: 由于是《Effective Java》一书的笔记,所以大部分内容基本来自此书,还有一小部分是自己的理解。

第1条 考虑用静态工厂方法代替构造器

一般来说,获取一个类的实例,大家想到的都是通过该类的公有构造函数来new 一个实例,而第一条推荐我们可以考虑使用静态工厂方法来创建实例,这里的工厂方法并不直接对应于设计模式中的工厂方法模式。

静态工厂方法与构造器不同的第一大优势在于:它们有名称
使用静态工厂方法可以通过方法的名称较好的描述返回的对象是什么类型,例如BigInteger的构造方法,BigInteger(int, int, Random)可能返回素数,用BigInteger
.probablePrime(int bitLength, Random rnd) 就让人很容易通过方法名,知道返回值是什么类型。

静态工厂方法与构造器不同的第二大优势在于:不必再每次调用它们的时候都创建一个新对象
如果一个类是不可变类,那在获取它的实例时,就不必每次都通过构造方法去new,而是通过静态工厂方法将之前已经创建好,并缓存的实例返回即可。这样可以在单进程中保持一直都只有一个实例,最典型的例子莫过于单例模式了。

静态工厂方法与构造器不同的第三大优势在于,它们可以返回原返回类型的任何子类型的对象
它的灵活性在于,API可以返回对象,同时又不会使对象变为公有的。典型就是Collections几乎都是通过静态工厂方法在一个不可实例化的类中导出。

静态工厂方法返回对象所属的类,在编写包含该静态工厂方法的类时可以不必存在,这种灵活的方式构成了服务提供者框架

服务提供者框架包含三个组件:

服务接口 提供者实现
提供者注册API 系统用来注册实现,让客户端访问
服务访问API 客户端用来获取服务的实例
服务提供者接口(可选) 负责创建其服务实现的实例

示例代码

public interface Service
{
    public void service();
}
public interface Provider
{
    Service newService();
}
public class Services
{
    private Services(){}
    private static final Map< String, Provider > = new ConcurrentHashMap< String, Provider >();
    public static final String DEFAULT_PROVIFER_NAME = "";

    public static void registerDefaultProvider( Provider p )
    {
        registerProvider( DEFAULT_PROVIDER_NAME, p );
    }
    public static void registerProvider( String name, Provider p )
    {
        providers.put( name, p );
    }

    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.newServices();
    }
}

静态工厂方法的第四大优势在于,在创建参数化类型实例的时候,它们使代码变得更加简洁

优化前:

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

静态工厂方法优化

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

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

静态工厂方法的主要缺点在于,类如果不含公有的或者受保护的构造器,就不能被子类化

静态工厂方法的第二个缺点在于,它们与其他的静态方法实际上没有任何区别

静态工厂方法惯用名称:

valueOf 返回的实例和其参数具有相同的值,实际上是类型转换方法
of valueOf的替代
getInstance 根据方法的参数返回相应的实例,对于Singleton,无参数,返回唯一的实例
newInstance 和getInstance功能类似,但确保返回的每个实例是新创建的
getType 和getInstance功能类似,在工厂方法处于不同的类中的时候使用,Type表示工厂方法所返回的对象类型
newType 和newInstance功能类似,在工厂方法处于不同的类中的时候使用

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

静态工厂和构造器的局限:不能很好地扩展到大量的可选参数

对于这种情况,一般有以下解决方案:
1.重载构造器模式

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;
    }

    public static void main(String[] args) {
        NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
    }
}

缺点:当有许多参数的时候,客户端代码会很难编写,且难以阅读

2.javaBean模式
调用一个无参构造器来创建对象,然后调用setter方法来设置每个必要的参数,以及每个相关的可选参数

缺点:构造过程中javaBean可能出于不一致的状态,类无法仅仅通过检验构造器参数的有效性来保证一致性

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;
    }

    public static void main(String[] args) {
        NutritionFacts cocaCola = new NutritionFacts();
        cocaCola.setServingSize(240);
        cocaCola.setServings(8);
        cocaCola.setCalories(100);
        cocaCola.setSodium(35);
        cocaCola.setCarbohydrate(27);
    }
}

3.Builder模式
既能保证像重叠构造器模式那样的安全性,也能保证像javaBean模式那么好的可读性。它不直接生成想要的对象,而让客户端利用所有必要的参数调用构造器(或者静态工厂),得到一个builder对象。然后客户端在builder对象上调用类似于setter的方法,来设置每个相关的可选参数。最后,客户端调用无参的build方法来生成不可变的对象。这个builder是它构建的类的静态成员类。另外,一个明显的优势是,builder利用单独的方法来设置每个参数,你想要多少个可变参数,它们就可以有多少个,直到每个setter方法都有一个可变参数

public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;

    public static class Builder {
        // Required parameters
        private final int servingSize;
        private final int servings;

        // Optional parameters - initialized to default values
        private int calories = 0;
        private int fat = 0;
        private int carbohydrate = 0;
        private int sodium = 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 carbohydrate(int val) {
            carbohydrate = val;
            return this;
        }

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

    public static void main(String[] args) {
        NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
                .calories(100).sodium(35).carbohydrate(27).build();
    }
}

builder模式十分灵活,可以利用单个builder构建多个对象。builder的参数可以再创建对象期间进行调整,也可以随着不同的对象而改变,builder可以自动填充某些域,例如每次创建对象时自动增加序列号。

简而言之,如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder模式就是中不错的选择,特别是当大多数参数都是可选参数的时候。与使用传统的重叠构造器模式相比,使用builder模式的客户端代码将更易于阅读和编写,构建器也比javaBean更加安全。

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

使类成为Singleton会使它的客户端测试变的十分困难,因为无法给Singleton替换模拟实现,除非它实现一个充当其类型的接口。

Singleton方法一般有两种

public class Elvis {
    public static final Elvis INSTANCE = new Elvis();

    private Elvis() {
    }

    public void leaveTheBuilding() {
        System.out.println("Whoa baby, I'm outta here!");
    }

    // This code would normally appear outside the class!
    public static void main(String[] args) {
        Elvis elvis = Elvis.INSTANCE;
        elvis.leaveTheBuilding();
    }
}

私有构造器仅被调用一次,客户端的任何行为都不会改变这一点,但要提醒一点:享有特权的客户端可以借助AccessObject.setAccessible方法,通过反射机制调用私有构造器。如果需要抵御这种攻击,可以修改该构造器,让它在被要求创建第二个实例的时候抛出异常。

第二种方法

public class Elvis {
    private static final Elvis INSTANCE = new Elvis();

    private Elvis() {
    }

    public static Elvis getInstance() {
        return INSTANCE;
    }

    public void leaveTheBuilding() {
        System.out.println("Whoa baby, I'm outta here!");
    }

    // This code would normally appear outside the class!
    public static void main(String[] args) {
        Elvis elvis = Elvis.getInstance();
        elvis.leaveTheBuilding();
    }
}

需要注意的是,为了实现Singleton类可序列化(Serializable),仅仅在声明中加上“implements Serializable”是不够的,为了维护并保证Singleton,必须声明所有实例域都是瞬时(transient)的,并提供一个readResolve方法。否则,每次反序列化一个序列化实例时,都会创建一个新的实例。实现如下:

public class Elvis {
    public static final Elvis INSTANCE = new Elvis();

    private Elvis() {
    }

    public void leaveTheBuilding() {
        System.out.println("Whoa baby, I'm outta here!");
    }

    private Object readResolve() {
        // Return the one true Elvis and let the garbage collector
        // take care of the Elvis impersonator.
        return INSTANCE;
    }

    // This code would normally appear outside the class!
    public static void main(String[] args) {
        Elvis elvis = Elvis.INSTANCE;
        elvis.leaveTheBuilding();
    }
}

从Java1.5发行版本起,实现Singleton还有第三种方法。只需编写一个包含单个元素的枚举类型。

public enum Elvis {
    INSTANCE;

    public void leaveTheBuilding() {
        System.out.println("Whoa baby, I'm outta here!");
    }

    // This code would normally appear outside the class!
    public static void main(String[] args) {
        Elvis elvis = Elvis.INSTANCE;
        elvis.leaveTheBuilding();
    }
}

使用单个元素的枚举类型来保证Singleton的方法,更加简洁,且无偿地提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是它已经成为实现Singleton的最佳方法。

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

有时候,可能需要编写只包含静态方法和静态域的类,如:Java.lang.Math或者java.util.Arrays,这样的工具类,不希望被实例化,实例对它没有任何意义。然而,在缺少显式构造器的情况下,编译器会自动提供一个公有的、无参的缺省构造器(default constructor)。对于用户而言,这个构造器与其他的构造器没有任何区别。

企图通过将类做成抽象类来强化该类不可被实例化是行不通的,由于只有当类不包含显式的构造器时,编译器才会生成缺省的构造器,因此我们只要让这个类包含私有构造器,它就不被实例化了:

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

其中,AssertionError不是必须的,但是它可以避免不小心在类的内部调用构造器。它保证该类在任何情况下都不会被实例化。这种习惯用法有点违背直觉,好像构造器就是专门设计成不能被调用一样,因此,明智的做法就是在代码中增加一条注释,如上代码所示。

第5条 避免创建不必要的对象

一般来说,最好能重用对象而不是在每次需要的时候就创建一个相同功能的新对象
最极端的例子如下:

String s = new String( "aaa" );//Donot do this

上面的代码,每次被执行都会创建一个新的String 实例,但这些创建对象的动作都是不必要的。

改进如下:

String s = "aaa";

这个版本创建了一个String 实例

对于同时提供了静态工厂方法和构造器的不可变类,通常优先使用静态工厂方法,避免创建不必要的对象

public class Person
{
    private final Date birthDate;

    //Donot do this
    public boolean isBabyBoomer()
    {
        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
                && birthData.compareTo( boomEnd )<0;
    }
}

isBabyBoomer方法每次调用的时候,都会去创建boomStart 和boomEnd 两个值,然而这两个值都是固定的,没必要每次都创建,修改如下:

//better implements
public class Person
{
    private final Date birthDate;

    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();
    }

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

在Java1.5发行版本中,有一种创建“多余”对象的新方法,乘坐自动装箱(autoboxing),它允许程序员将基本类型和装箱基本类型(Boxed Primitive Type)混用,按需要自动装箱和拆箱。自动装箱使得基本类型和装箱基本类型之间的差别变的模糊起来,但是并没有完全消除。非常值得关注的一点,使用自动装箱的性能远低于使用基本类型的性能。要当无意识的自动装箱。

public class Sum {
    // Hideously slow program! Can you spot the object creation?
    public static void main(String[] args) {
        Long sum = 0L;
        for (long i = 0; i < Integer.MAX_VALUE; i++) {
            sum += i;
        }
        System.out.println(sum);
    }
}

上述sum是Long类型而不是long,就是这个Long,就会导致计算的耗时增加

通过维护自己的对象池来避免创建对象并不是一种好的做法,除非池中的对象是非常重量级的,真正正确使用对象池的典型对象示例就是数据库连接池
一般而言,维护自己的对象池会增加代码的复杂性,增加内存占用,还会损害性能

当应该重用现有对象的时候,不要创建新的对象
当该创建新对象的时候,不要重用现有的对象
在提倡使用保护性拷贝的时候,因重用对象而付出的代价要远远大于因创建重复对象而付出的代价
必要时如果没能实施保护性拷贝,将会导致潜在的错误和安全漏洞;不必要地创建对象则只会影响程序的风格和性能

第6条 消除过期的对象引用

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];
    }

    /**
     * Ensure space for at least one more element, roughly doubling the capacity
     * each time the array needs to grow.
     */
    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}

上面的代码存在内存泄漏的可能,如果一个栈先是增长,然后再收缩,那么,从栈弹出的对象将不会当作垃圾进行回收,即使是使用栈的程序不在引用这些对象,因为栈维护着过期引用
过期引用,指永远也不会再被解除的引用。

如果一个对象引用被无意识地保留起来了,那么垃圾回收机制不仅不会处理这个对象,而且也不会处理被这个对象所引用的所有其他对象

这类问题解决方式是:一旦对象的引用已经过期,只需要清空这些引用即可。如下:

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

清空对象引用是一种例外,而不是一种规范行为,只要类是自己管理内存,应该警惕内存泄漏问题,消除过期引用最好的方法让包含该引用的变量结束其生命周期。

内存泄漏的另外一个常见来源是缓存:只要在缓存之后存在对某个项的键的引用,该项就有意义,可以用WeakHashMap代表缓存;当缓存中的项过期之后,会自动被删除

只有当所要的缓存项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处

内存泄漏的第三个常见来源是监听器和其他回调:确保回调立即被当做垃圾回收的最佳方法是只保存它们的弱引用

第7条 避免使用终结方法

终结方法(finalize)通常是不可预测的,也是很危险的,一般情况下是不必要的

C++的程序员别告知”不要把终结方法当作是C++中析构器(destructors)的对应物“。在C++中,析构器是回收一个对象所占用资源的常规方法,是析构器所必需的对应物。
在java中,当一个对象变得不可到达的时候,垃圾回收器会回收与该对象相关联的存储空间,并不需要程序员做专门的工作。

C++的析构器也可以被用来回收其他的非内存资源,而在java中,一般用try-finally块来完成类似的工作。

终结方法的缺点是不能保证会被及时地执行,从一个对象变得不可到达开始,到它的终结方法被执行,所花费的时间是任意长的。

Java语言规范不仅不保证终结方法会被及时地执行,而且根本就不保证其被执行,不应该依赖终结方法来更新重要的持久状态

不要被System.gc和System.runFinalization这两个方法所诱惑,它们确实增加了终结方法被执行的机会,但是它们并不保证终结方法一定会被执行。唯一声称保证终结方法被执行的方法是System.runFinalizersOnExit,以及它臭名昭著的孪生兄弟Runtime.runFinalizersOnExit。这两个方法都有致命的缺陷,已经被废弃了。

使用终结方法有一个非常严重的性能损失,用终结方法创建和销毁对象的速度更加缓慢。

对于确实需要终止的方法,应提供一个显示的终止方法,并要求改类的客户端在每个实例不再有用的时候调用这个方法
显式的终止方法通常与try-finally结构结合起来使用,以确保及时终止

终结方法有两种合法用途

1.当对象所有者忘记调用显式终止方法时,终结方法充当“安全网”
2.与对象的本地对等体有关。本地对等体是一个本地对象,普通对象通过本地方法委托给一个本地对象,垃圾回收器无法感知本地对象的存在,当Java对等体被回收时,它不会被回收
“终结方法链”不会被自动执行,需要进行显式调用
另外一种可选方法是终结方法守卫者

你可能感兴趣的:(java)