第四条:通过私有构造器强化不可实例化的能力。 1.有时候,你可能需要编写至包含静态方法和静态域的类。这些类的名声很不好,因为有些人在面向对象的语言中滥用这样的类来编写过程化的程序。 2.尽管如此,他们也确实有它们特有的好处: 1.利用这种类,以java.lang.Math或者java.util.Arrays的方式,把基本类型的值或者数组类型上的相关方法组织起来. 2.我们也可以通过java.util.Collections的方式,把实现特定接口的对象上的静态方法包括工厂方法组织起来。 3.利用这种类可以把final类上的方法组织起来,以取代扩展该类的做法。 3.这样的工具类Unility class不希望被实例化,实例对它没有任何意义。然而在缺少显示构造器的情况下,编译器会自动提供一个公有的,无参的缺省构造器default constructor.对于用户而言,这个构造器与其他的构造器没有任何区别。在已发行的API中常常可以看到一些被无意识地实例化的类。 4.企图通过将类做成抽象类来强制该类不可被实例化,这是行不通的。该类可以被子类化,并且该子类也可被实例化。这样做甚至会误导用户,以为这种类是专门为了继承而设计的。然而有一些简单的习惯用法可以确保类不可被实例化。 由于只有当类不包含显示的的构造器时,编译器才会生成缺省的构造器,因为我们只要让这个类包含私有的构造器,它就不能被实例化了: public class UtilityClass { private UtilityClass() { throw new AssertionError(); } //...others } 由于显示的构造器是私有的,所以不可以再该类的外部访问它。AssertionError不是必须的。但是它可以避免不小心在类的内部调用构造器。它保证该类在任何情况下都不会被实例化。 注:1.但是这种用法有点违背直觉,好像构造器就是专门设计成不能调用一样。因此明智的做法就是在代码中增加一条注释,如: //Supress default constructor for noninstantiability. 2.AssertionError:抛出该异常指示某个断言失败 5.这种惯用法的副作用:它使得一个类不能被子类化。所有的构造器都必须显示或隐式的调用超类构造器。在这种情形下子类就没有可访问的超类构造器可调用了。 第5条:避免创建不必要的对象 1.一般说来,最好能重用对象而不是在每次需要的时候就创建一个相同功能的新对象。重用方式即快速,又流行。如果对象是不可变的immutable,它就始终可以被重用。 极端反面的例子: String s = new String("landon");//Don't do this! 该语句每次执行的时候都会创建一个新的String实例,但是这些创建对象的动作全都是不必要的。传递给String构造器的参数("landon")本身就是一个String实例,功能方面等同于构造器创建的所有对象。 如果上述用法是在一个循环中,或者是在一个被频繁调用的方法中,就会创建爱你出成千上万不必要的String实例。改进:String s = "landon".该版本只用了一个String实例,而不是每次执行的时候都创建一个新的实例。而且其可以保证,对于所有在同一虚拟机中运行的代码,只要其包含相同的字符串字面常量,该对象就会被常用(注:String常量池,@link String#intern)。 对于同时提供了静态工厂方法和构造器的不可变类,通常可以使用静态工厂方法而不是构造器,以避免创建不必要的对象。如Boolean.valueOf(String)几乎总是优先于构造器Boolean(String).构造器在每次调用的时候都会创建一个新的对象,而静态工厂方法则从来不要求这样做,实际上也不会这样做. 2.除了重用不可变的对象外,也可以重用那些已知不会修改的可变对象。下面这个也是可以较常见的反面例子,其中涉及可变的Date对象,它们的值一旦计算出来之后就不再变化。 Person#isBabyBoomer(),检查这个人是否是生育高峰期出生的小孩,即1946至1964年间。 public boolean isBabyBoomer() { Calendar gmtCalendar = Calendar .getInstance(TimeZone.getTimeZone("GMT")); // 1946 gmtCalendar.set(1946, Calendar.JANUARY, 1, 0, 0, 0); Date boomStart = gmtCalendar.getTime(); gmtCalendar.set(1965, Calendar.JANUARY, 1, 0, 0, 0); Date boomEnd = gmtCalendar.getTime(); return birthDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) <= 0; } 该方法每次调用的时候都会创建一个Calendar,一个TimeZone和两个Date实例,这都是不必要的; 改进: static { Calendar gmtCalendar = Calendar .getInstance(TimeZone.getTimeZone("GMT")); // 1946 gmtCalendar.set(1946, Calendar.JANUARY, 1, 0, 0, 0); BOOM_START = gmtCalendar.getTime(); gmtCalendar.set(1965, Calendar.JANUARY, 1, 0, 0, 0); BOOM_END = gmtCalendar.getTime(); } <p>改进后的方法只在初始化的时候创建Calender,TimeZone和Date实例一次。如果该方法频繁调用,则会显著的提高性能。 <p>除了提高性能之外,代码的含义也更清晰了,BOOM_START和BOOM_END从局部变量改为final静态域,显然就应该作为常量对待。 从而使代码更易于理解。但是这种优化带来的效果不总是那么明显,因为Calendar实例的创建代价特别昂贵。 3.如果改进的Person类被初始化了,它的isBabyBoomer方法却永远不会被调用,那就没有必要初始化BOOM_START和BOOM_END域。通过延迟初始化lazy initializing,即把对这些域的初始化延迟到isBabyBoomer方法第一次被调用的时候进行,则有可能消除这些不必要的初始化工作。但是不建议这样做。正如延迟初始化中常见的情况一样,这样做会使方法的实现更加复杂,从而无法将性能显著提高到超过已经达到的水平。 4.前面的例子中,讨论到的对象均是能够被重用的,因为它们在被初始化之后不会再改变。考虑适配器adapter情形,有时也叫做视图view。适配器是指这样一个对象,它把功能委托一个后备对象backing object,从而为后备对象提供一个可以替代的接口。由于适配器除了后备对象之外,没有其他的状态信息,所以针对某个给定对象的特定适配器而言,它不需要创建多个适配器实例。 如:Map接口的keySet方法返回该Map对象的Set视图,其中包含该Map中所有的key.粗看起来,好友每次调用keySet都应该创建一个新的Set实例。但是对于一个给定的Map对象,实际上每次调用keySet都返回同样的Set实例。虽然被返回的Set实例一般是可改变的,但是所有返回的对象在功能上是等同的。当其中一个返回对象发生变化的时候,所有其他的返回对象也要发生变化。因为它们是由同一个Map实例支撑的。 虽然创建keySet视图对象的多个实例并无害处,却也是没有必要的。 5.在Java1.5发行版本中,有一种创建多余对象的新方法,称作自动装箱autoboxing。它允许程序员将基本类型和装箱类型Boxed primitive Type混用,暗需要自动装箱和拆箱。自动装箱使得基本类型和装箱基本类型的差别变的模糊起来,但是并没有完全消除。他们在语义上还有这微妙的区别,在性能上也有着比较明显的差别。 下面这段程序,计算所有int正值的总和,用long,因为int不够大: // 这里用的是Long,程序运行会非常慢,因为程序大约构造了2|31个多余的Long实例 //Long sum = 0L; long sum = 0L; for(long i = 0;i < Integer.MAX_VALUE;i++) { sum += i; } 将sum的声明从Long变为long,程序运行时间会减慢很多。结论很明显:要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。 6.不要错误的认为本条目所介绍的内容暗示着创建对象的代建非常昂贵,我们 应该尽可能避免的创建对象。相反,由于小对象的构造器只做少量的显示工作,所以小对象的创建和回收动作是非常廉价的,特别是在现代的jvm实现更是如此。通过创建附加的对象,提升程序的清晰性,简洁行和功能性,这通常是件好事。 7.反之通过维护自己的对象池object pool来避免创建对象并不是一种好的做法,除非池中的对象是非常重量级的。真正正确使用对象池的典型对象示例就是数据库连接池。建立数据库连接的代码是非常昂贵的,因此重用这些对象非常有意义。而且数据库的许可可能限制你只能使用一定数量的连接。但是一般而言,维护自己的对象池必定会把代码弄的很乱,同时增加内存使用footprint,并且还会损害性能。现代的JVM实现具有高度优化的垃圾回收器,其性能很容易就会超出轻量级对象池的性能。 8.与本条目对应的是保护性拷贝defensive copying的内容。本题目提及:当你应该重用现有对象的时候,请不要创建新对象。而39条则说,你应该创建新对象的时候,请不要重用现有对象。注意,在提倡保护性拷贝的时候,因重用对象而付出的代码要远远大于因创建重复对象而付出的代价。必要时如果没能实施保护性考虑,将会导致潜在的错误和安全漏洞,而不必要地创建对象则只会影响程序的风格和性能。
部分源码:
package com.book.chap2.privateConstructor;
/** *//**
*
*工具类
*<p>因为是工具类,所以不希望被实例化,而且实例对其没有任何意义。</p>
*<p>采用私有构造器来防止实例化(副作用,不可被子类化)</p>
*<p>可在私有构造器中抛出异常避免不小心在类的内部调用构造器</p>
*
*@author landon
*@since 1.6.0_35
*@version 1.0.0 2013-1-10
*
*/
public class UtilityClass
{
//Supress default constructor for noninstantiability
private UtilityClass()
{
throw new AssertionError();
}
}
package com.book.chap2.avoidCreateUnnecessaryObject;
/** *//**
*
*自动装箱问题
*
*@author landon
*@since 1.6.0_35
*@version 1.0.0 2013-1-24
*
*/
public class AutoBoxingProblem
{
public static void main(Stringargs)
{
// 这里用的是Long,程序运行会非常慢,因为程序大约构造了2|31个多余的Long实例
//Long sum = 0L;
long sum = 0L;
for(long i = 0;i < Integer.MAX_VALUE;i++)
{
sum += i;
}
System.out.println(sum);
}
}
package com.book.chap2.avoidCreateUnnecessaryObject;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
/** *//**
*
* 检查一个人是否出生于1946-1964的生育期高峰
*
* @author landon
* @since 1.6.0_35
* @version 1.0.0 2013-1-23
*
*/
public class Person
{
private final Date birthDate;
public Person(Date birth)
{
birthDate = birth;
}
/** *//**
*
* 是否出生在高峰期
* <p>
* 该方法每次调用的时候都会创建一个Calendar,一个TimeZone和两个Date实例,这都是不必要的
*
* @return
*/
public boolean isBabyBoomer()
{
Calendar gmtCalendar = Calendar
.getInstance(TimeZone.getTimeZone("GMT"));
// 1946
gmtCalendar.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
Date boomStart = gmtCalendar.getTime();
gmtCalendar.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
Date boomEnd = gmtCalendar.getTime();
return birthDate.compareTo(boomStart) >= 0
&& birthDate.compareTo(boomEnd) <= 0;
}
// 对于以上情况,采用静态初始化器,避免上面这种效率低下的情况
private static final Date BOOM_START;
private static final Date BOOM_END;
// 静态初始化器
static
{
Calendar gmtCalendar = Calendar
.getInstance(TimeZone.getTimeZone("GMT"));
// 1946
gmtCalendar.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_START = gmtCalendar.getTime();
gmtCalendar.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_END = gmtCalendar.getTime();
}
/** *//**
*
* 第二种方法判断是否生在高峰期
* <p>改进后的方法只在初始化的时候创建Calender,TimeZone和Date实例一次。如果该方法频繁调用,则会显著的提高性能。
* <p>除了提高性能之外,代码的含义也更清晰了,BOOM_START和BOOM_END从局部变量改为final静态域,显然就应该作为常 *
* 量对待。
* 从而使代码更易于理解。
@return
*/
public boolean isBabyBoomer2()
{
return birthDate.compareTo(BOOM_START) >= 0
&& birthDate.compareTo(BOOM_END) <= 0;
}
}
posted on 2013-03-15 16:10
landon 阅读(1822)
评论(0) 编辑 收藏 所属分类:
Program 、
Book