Effective Java学习笔记(二)

[size=large][b][color=blue]第二章:创建和销毁对象(条目1-7)[/color][/b][/size]
[size=medium][b][color=red]第1条:考虑用静态工厂方法代替构造器[/color][/b][/size]
[color=orange]静态工厂方法(static factory method)的几大优势:[/color]
1.有名称,能够根据名称知其意,比如:Boolean类的valueOf方法和BigInteger类的probablePrime方法,而使用构造器的话,在含多个构造器时,在不看注释情况下往往不知道该选用哪个。下面是两个例子:

public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}

public static BigInteger probablePrime(int bitLength, Random rnd) {
if (bitLength < 2)
throw new ArithmeticException("bitLength < 2");

// The cutoff of 95 was chosen empirically for best performance
return (bitLength < SMALL_PRIME_THRESHOLD ?
smallPrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd) :
largePrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd));
}

2.不必在每次调用时都创建一个新对象。
静态工厂方法能够为重复的调用返回相同对象,这样有助于类总能严格控制在某个时刻哪些实例应该存在。这种类被称为实例受控的类(instance-controlled)。实例受控使得类可以确保它是一个Singleton或者是不可实例化的。
3.可以返回原返回类型的任何子类型的对象,适用于基于接口的框架(interface-based framework)。下面EnumSet类的noneOf方法就是根据元素个数来返回不同的子类对象的:

public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
Enum[] universe = getUniverse(elementType);
if (universe == null)
throw new ClassCastException(elementType + " not an enum");

if (universe.length <= 64)
return new RegularEnumSet<E>(elementType, universe);
else
return new JumboEnumSet<E>(elementType, universe);
}

在基于接口的框架中,静态工厂方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不必存在。这种灵活的静态工厂方法构成了服务提供者框架(Service Provider Framework)的基础。服务提供者框架中有三个重要的组件:服务接口(Service Interface),这是提供者实现的;提供者注册API(Provider Registration API),这是系统用来注册实现,让客户端访问它们的;服务访问API(Service Access API),是客户端用来获取服务的实例的,第四个组件可选组件服务提供者接口(Service Provider Interface)。典型例子是JDBC API:Connnection是服务接口,DriverManager.registerDriver是提供者注册API,DriverManager.getConnection是服务访问API,Driver就是服务提供者接口。
4.在创建参数化类型实例时,使得代码更简洁。
HashMap中如果提供如下静态工厂,也就提供了类型推导(type inference),则会更好。

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

建议在自己的参数化类中(含泛型T),使用以上方法进行实例化。
[color=orange]静态工厂方法的缺点:[/color]
1.类如果不含public/protected的构造器,则不能被子类化。但这样也许会因祸得福,因为鼓励程序用组合(Composition),而不是继承(Inheritance)。
2.它们实际上和其他静态方法没有任何区别。在API文档中,它们没有像构造器那样在API文档中明确标识出来。所以需要在注释中关注静态工厂方法,添加注释,并遵守标准命名。下面是静态工厂方法的一些惯用名称:
valueOf:实际上是类型转换方法;
of:valueOf的简洁形式,在EnumSet中开始流行;
getInstance:返回的实例根据参数来描述的,对Singleton来说,该方法无参数并返回唯一实例;
newInstance:像getInstance一样,但newInstance能够确保返回的每个实例与其他实例不同;
getType:像getInstance一样,但是在工厂方法处于不同的类中的时候使用。Type表示工厂方法所返回的对象类型。
newType:像newInstance一样,但是在工厂方法处于不同的类中的时候使用。Type表示工厂方法所返回的对象类型。

[size=medium][b][color=red]第2条:遇到多个构造器参数时要考虑用构造器[/color][/b][/size]
静态工厂和构造器有个共同的局限性:它们都不能很好地扩展到大量的可选参数。程序员一向习惯用重叠构造器(telescoping constructor)模式。
重叠构造器模式可行,但是当有许多参数的时候,客户端代码会很难编写(有时会不小心颠倒两个参数顺序,但无编译错误),并且仍然比较难以阅读。
所以有了第二种:JavaBeans模式,这种模式弥补了重叠构造器的不足。
但JavaBeans模式自身有着很严重的缺点,因为构造过程被分到了几个调用中,在构造过程中JavaBean可能处于不一致的状态。另外一个缺点就是,JavaBeans模式组织了把类做成不可变的可能,这就需要程序员付出额外的努力来确保它的线程安全。
幸运的是,还有第三种模式,就是Builder模式,给出一个示例基本可以理解:

/**
* 学生类
* @author chasegalaxy
* @since 2012-08-01
*/
public class Student {
private final String code;
private final String name;
private final int age;
private final String email;

public static class Builder {
// Required parameters
private final String code;
private final String name;

// Optional parameters - initialized to default values
private int age = 0;
private String email = null;

public Builder(String code, String name) {
this.code = code;
this.name = name;
}
public Builder age(int val) {
age = val;
return this;
}
public Builder email(String val) {
email = val;
return this;
}
public Student build() {
return new Student(this);
}
}
private Student(Builder builder) {
code = builder.code;
name = builder.name;
age = builder.age;
email = builder.email;
}
// getter goes here...(no setter)
}

调用方式如下(其中code和name是必须参数,age和email可选,这里只填充了age):

Student obj = new Student.Builder("08109080", "张三").age(11).build();

设置了参数的builder生成了一个很好地抽象工厂(Abstract Factory),JDK1.5+可使用泛型,代码如下:

// A builder for objects of type T
public interface builder<T> {
public T build();
}

带有Builder实例的方法通常利用有限制的通配符类型(bounded wildcard type)来约束构建器的参数类型,比如下面就是构建每个节点的方法,它利用客户端提供的Builder实例来构建树:

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

Builder模式的不足是多了创建构建器的开销,但一般情况下不会非常注重性能。所以给出绝大多数情况都适用的结论:
如果类的构造器或静态工厂中具有多个参数时,Builder模式就是种不错的选择,特别是当大多数参数都是可选的时候。与传统的重叠构造器模式相比,Builder模式更容易编写和阅读,也比JavaBeans更加安全。

[size=medium][b][color=red]第3条:用似有构造器或者枚举类型强化Singleton属性[/color][/b][/size]
Singleton指仅仅被实例化一次的类,通常用来表示那些本质上唯一的组件。下面给出用似有构造器强化Singleton属性的例子:

/**
* 日志管理器
* @author chasegalaxy
* @since 2012-08-01
*/
public class LogManager {
/**
* 私有构造函数,避免通过构造函数初始化
*/
private LogManager() {
}

/**
* 日志管理器对象-单例
*/
private static LogManager logManager = null;

/**
* 获取日志管理器实例
* @return 日志管理器实例
*/
public static LogManager getInstance() {
if (null == logManager) {
logManager = new LogManager();
}
return logManager;
}

/**
* 添加日志-略
*/
public void addLog() {
// ...
}
}

JDK1.5起,实现Singleton还有第三种方法,只需编写一个包含单个元素的枚举类型:

public enum LogManager {
INSTANCE;

/**
* 添加日志-略
*/
public void addLog() {
// ...
}
}

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

[size=medium][b][color=red]第4条:通过私有构造器强化不可实例化的能力[/color][/b][/size]
有时候,你可能需要编写只包含静态方法和静态域的类,比如Math,Arrays和Collections。这样的工具类不希望被实例化,实例对它来说没有任何意义。企图通过将类做成抽象类来强制该类不可被实例化,这是行不通的,因为抽象类是专门为了继承而设计的。然而,有一些简单的习惯用法可以确保类不可被实例化:

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

这种习惯用法使得一个类不能被子类化,所以一般是用于不可子类化的工具类中。

[size=medium][b][color=red]第5条:避免创建不必要的对象[/color][/b][/size]
一般来说,最好能重用对象而不是在每次需要的时候就创建一个功能相同的新对象。如果对象是不可变的(immutable),它就始终可以被调用。下面这个例子中的代码编写方式是要避免的:

public class Person {
private final Date birthDate;

public Person(Date birthDate) {
this.birthDate = birthDate;
}

/**
* 是否是80后-Don't do this!
* @return 是否是80后
*/
public boolean is80s() {
// Unnecessary allocation of expensive object
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
cal.set(1980, Calendar.JANUARY, 1, 0, 0, 0);
Date begin = cal.getTime();
cal.set(1989, Calendar.JANUARY, 1, 0, 0, 0);
Date end = cal.getTime();

return birthDate.compareTo(begin) >= 0 && birthDate.compareTo(end) < 0;
}
}

使用静态的初始化器(initializer)改进后的代码如下:

public class PersonEx {
private final Date birthDate;

public PersonEx(Date birthDate) {
this.birthDate = birthDate;
}

private static final Date BEGIN;
private static final Date END;

static {
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
cal.set(1980, Calendar.JANUARY, 1, 0, 0, 0);
BEGIN = cal.getTime();
cal.set(1989, Calendar.JANUARY, 1, 0, 0, 0);
END = cal.getTime();
}

/**
* 是否是80后
* @return 是否是80后
*/
public boolean is80s() {
return birthDate.compareTo(BEGIN) >= 0 && birthDate.compareTo(END) < 0;
}
}

改进后的Person类只在初始化的时候创建Calendar,TimeZone和Date实例一次,而不是在每次调用is80s的时候都创建这些实例。如果is80s的方法被频繁调用,这种方法将会显著提高性能。
在JDK1.5以后,有一种创建多余对象的新方法,称作自动装箱(autoboxing),它允许程序员将基本类型和装箱基本类型(Boxed Primitive Type)混用,按需要自动装箱和拆箱。通过下面的一个例子,说明了要优先使用基本类型,而不是装箱基本类型,要当心无意识的自动装箱:

public class AvoidAutoBoxing {
public static void main(String[] args) {
Long sum = 0L; // 请使用:long sum = 0L; 性能会提高很多。
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
}
}

但是不要错误的认为“创建对象的代价非常昂贵,我们应该要尽可能地避免创建对象”,如果通过创建附加的对象,能使程序更清晰和简洁,这通常是好事。同理,通过维护对象池(object pool)来避免创建对象并不是一种好的做法,除非池中的对象是非常重量级的。真正正确使用对象池的典型示例就是数据库连接池。

[size=medium][b][color=red]第6条:消除过期的对象引用[/color][/b][/size]
Java中有GC,这很容易给程序员留下这样的印象:认为自己不再需要考虑内存管理的事情了,其实不然,请看下面的简单Stack实现的例子:

// Can you spot the "memory leak"?
public class Stack {
private static final int DEFAULT_INITIAL_CAPACITY = 16;
private Object[] elements;
private int size = 0;

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

以上程序中隐含了一个内存泄露bug:从Stack中弹出来的对象不会被当做垃圾回收,栈中会一直维护这这些对象的过期引用(obsolete reference),过期引用是永远不会被解除的引用。在极端情况下,会导致磁盘交换(Disk Paging),甚至导致程序失败(OutOfMemoryError)。
这类问题的修复方法很简单:一旦对象引用已经过期,只需清空这些引用即可。上面的例子修订代码如下:

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

但也不必这么做:对于每一个对象引用,一旦程序不用到它,就把它清空。其实这样做没必要,这样会让代码很乱。清空对象引用应该是一种例外,而不是一种规范行为。消除过期引用的最好办法是让包含该引用的变量结束其生命周期。如果你是在最紧凑的作用域范围内定义每一个变量,这种情形就会自然发生。
那么,上面的Stack类为什么需要清空引用呢?原因在于,Stack类自己管理内存(manage its own memory)。一般而言,只要类是自己管理内存,程序员就应该警惕内存泄露问题。
内存泄露另一个常见来源是缓存,关于使用缓存框架所要注意的问题不在这里研究。
内存泄露的第三个常见来源是监听器和其他回调。
内存泄露通常不会表现成明显的失败,所以它们可以在一个系统中存在很多年,只有仔细检查代码或者使用Heap剖析工具(Heap Profile)才能发现。所以最好能在编写代码时候,就避免该问题。

[size=medium][b][color=red]第7条:避免使用终结方法[/color][/b][/size]
终结方法(finalizer)通常是不可预测的,也是很危险地,一般情况下不推荐使用。不要把终结方法当做C++的析构器(destructors),在Java中,一般通过try和finally来完成资源释放的工作。
除非是作为安全网,或者是为了终止非关键的本地资源,否则请不要使用终结方法。在这些很少见的情况下,如果使用了终结方法,就要记住调用super.finalize。[b][color=blue](第二章完)[/color][/b]

[b][color=red]文章本人原创,转载请注明作者和出处,谢谢。[/color][/b]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值