二、创建和销毁对象
- 何时以及如何创建对象,
- 何时以及如何避免创建对象,
- 如何确保它们能够适时地销毁,
- 如何管理对象销毁之前必须进行的各种清理动作
1.用静态工厂方法代替构造器
优势:
- 它们有名称
- 不必在每次调用它们的时候都创 建一个新对象。
- 它们可以返回原返回类型的任何子类 型的对象。
- 所返回的对象的类可以随着每次调用而发生变化,这取 决于静态工厂方法的参数值。
- 方法返回的对象所属的类,在编写包含该静态工厂方 法的类时可以不存在。
缺点:
- 类如果不含公有的或者受保护的构造器,就不能被子 类化。
- 程序员很难发现它们
例:
下面是静态工厂方法的一些惯用名称。 这里只列出了其中的一小部分:
from一一类型转换方法,它只有单个参数,返回该类型的一个相对应的实例
Date d = Date.from(instance);
public static Date from(Instant instant) {
try {
return new Date(instant.toEpochMilli());
} catch (ArithmeticException ex) {
throw new IllegalArgumentException(ex);
}
}
valueOf一一比 from 和 of 更烦琐的一种替代方法
BigInteger bigInteger = BigInteger.valueOf(Integer.MAX_VALUE);
public static BigInteger valueOf(long val) {
// If -MAX_CONSTANT < val < MAX_CONSTANT, return stashed constant
if (val == 0)
return ZERO;
if (val > 0 && val <= MAX_CONSTANT)
return posConst[(int) val];
else if (val < 0 && val >= -MAX_CONSTANT)
return negConst[(int) -val];
return new BigInteger(val);
}
等等。。。
2.遇到多个构造器参数时要考虑使用构建器
方法一:重叠构造器
- 提供的第一个构造器只有必要的参数,第二个构造器有一个可选参数,第三个构造器有两个可选参数,最后包含所有。
- 缺点:当有许多参数时,难编写
eg
public class NutritionFacts {
private final int servingSize;//required
private final int servings; //required
private final int calories; //optional
private final int fat; //optional
private final int sodium; //optional
private final int carbohydrate;//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;
}
}
NutritionFacts nutritionFacts = new NutritionFacts(240, 8, 100, 0, 35, 27);
方法二:javaBeans
- 先调用一个无参构造器来创建对象,再调用setter方法设置每个必要的参数
- 缺点:构造过程被分到几个调用中,在构造过程中,JavaBean可能处于不一致状态,JavaBean模式使得把类做成不可变的可能性不复存在。额外确保它的线程安全
方法三:建造者模式
例:
public class NutritionFacts {
private final int servingSize;//required
private final int servings; //required
private final int calories; //optional
private final int fat; //optional
private final int sodium; //optional
private final int carbohydrate;//optional
public static class Builder{
//required parameters
private final int servingSize;
private final int servings;
//可选参数
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohudrate = 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){
fat = val;
return this;
}
public Builder carbohyrate(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;
sodium = builder.sodium;
carbohydrate = builder.carbohudrate;
}
}
NutritionFacts nutritionFacts = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohyrate(27).build();
Builder模式也适用于类层次结构
Pizza.Builder的类型是泛型,带有一个递归类型参数,它和抽象的self方法一样,允许在子类中适当地进行方法链接,不需要转换类型,这个针对java缺乏self类型的解决方案,被称作模拟的self类型。
public abstract class Pizza {
public enum Topping{
HAM, MUSHROOM, ONION, PEPPER, SAUSAGE
}
final Set<Topping> toppings;
abstract static class Builder<T extends Builder<T>>{
EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
public T addTopping(Topping topping){
toppings.add(Objects.requireNonNull(topping));
return self();
}
abstract Pizza build();
protected abstract T self();
}
Pizza(Builder<?> builder){
toppings = builder.toppings.clone();
}
}
两个具体的pizza类。
package leetcode.shuzu;
import java.util.Objects;
public class NyPizza extends Pizza {
public enum Size{SMALL, MEDIUM, LARGE}
private final Size size;
public static class Builder extends Pizza.Builder<Builder>{
private final Size size;
public Builder(Size size){
this.size = Objects.requireNonNull(size);
}
@Override
public NyPizza build(){
return new NyPizza(this);
}
@Override
protected Builder self(){
return this;
}
}
private NyPizza(Builder builder){
super(builder);
size = builder.size;
}
}
public class Calzone extends Pizza {
private final boolean sauceInside;
public static class Builder extends Pizza.Builder<Builder>{
private boolean sauceInside = false;
public Builder sauceInside(){
sauceInside = true;
return this;
}
@Override
public Calzone build(){
return new Calzone(this);
}
@Override
protected Builder self(){
return this;
}
}
private Calzone(Builder builder){
super(builder);
sauceInside = builder.sauceInside;
}
}
NyPizza pizza = new NyPizza.Builder(SMALL).addTopping(SAUSAGE).addTopping(ONION).build();
Calzone calzone = new Calzone.Builder().addTopping(HAM).sauceInside().build();
如果类的构造器或者静态工厂中具有多个参数,设计这种类时, Builder 模式就是一种不错的选择,
3.用私有构造器或者枚举类型强化 Singleton 属性
方法一:在第一种方法中,公有静态成员是个 final 域
public class Elvis{
public static final Elvis INSTANCE = new Elvis();
private Elvis{...}
public void leaveTheBuilding(){...}
}
私有构造器仅被调用一次,用来实例化公有的静态 fina l 域 Elvis . INSTANCE 保证了 Elvis 的全局唯一性 , 一旦 Elvis 类被实例化,将只会存在一个 Elvis 实例 ,不多也不少 。
享有特权的客户端可以借助 Access 工bleObject.setAccessible 方法,通过反射机制调用私有构造器 。 如果需要抵御这种攻击,可以修改构造器,
让它在被要求创建第二个实例的时候抛出异常 。
方法二:公有的成员是个静态工厂方法
public class Elvis{
private static final Elvis INSTANCE = new Elvis();
private Elvis(){...}
public static Elvis getInstance(){return INSTANCE;}
public void leaveTheBuilding(){...}
}
会导致假冒的Elvis
将上述方法实现的singleton类变成是可序列化的,仅仅在声明中加上implements serializable是不够的,为了维护并保证singleton必须声明所有实例都是瞬时的(transient),并提供一个readResolve方法。
private Object readResolve(){
return INSTANCE;
}
方法三:声明一个包含单个元素的枚举类型
public enum Elvis{
INSTANCE;
public void leaveTheBuilding(){...}
}
单元素的枚举类型经常成为实现 Singleton 的最佳方法 。
注意,如果 Singleton必须扩展一个超类,而不是扩展 Enum 的时候,则不宜使用这个方法(虽然可以声明枚举去实现接口) 。
4.通过私有构造器强化不可实例化的能力
5.优先考虑依赖注人来引用资源
静态工具类和 Singleton 类不适合于需要引用底层资源的类 。
静态工具类和 singleton 实现
public class SpellChecker {
//singlton
// private final Lexicon dictionary = ...;
// private SpellChecker() {
// }
// public static INSTANCE = new SpellChecker();
//静态工具类
// private static final Lexicon dictionary = ...;
//
// private SpellChecker() {
// }
public static boolean isValid(String word){...};
public static List<String> suggestions(String type){...}
}
这里需要支持类的多个实例,每个实例使用客户端指定的资源(这里词典),当创建一个新的实例时 , 就将该资源传到构造器中 。 这是依赖注入( dependency injection )的一种形式:词典( dictionary )是拼写检查器的一个依赖( dependency ),在创建拼写检查器时就将词典注入( injected )其中 。
public class SpellChecker {
private final Lexicon dictionary;
public SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public static boolean isValid(String word){...};
public static List<String> suggestions(String type){...}
}
6.避免创建不必要的对象
eg:
static boolean isRomanNumeral(String s){
return s.matches("^(?=.)M*()C[MD]|D?C{0,3})"
+ "(X[CL] | L?X{0,3})(I[XV]|V?I{0,3})$");
}
不适合在注重性能的情形中重复使用,修改为
public class RomanNumerals{
private static final Pattern ROMAN = Pattern.compile("^(?=.)M*()C[MD]|D?C{0,3})"
+ "(X[CL] | L?X{0,3})(I[XV]|V?I{0,3})$");
static boolean isRomanNumeral(String s){
return ROMAN.matcher(s).matches();
}
}
情形二:自动装箱使得基本类型和装箱基本类型之间的差别变得模糊起来, 但是并没有完全消除 。
private static long sum(){
Long sum = 0L;
for(long i = 0; i <= Integer.MAX_VALUE; i++){
sum += i;
}
return sum;
}
分析:答案正确,但比实际情况慢,sum被声明成了Long, 程序需要构造大约2的31次方个多余的Long实例, 每次往Long sum中增加long时构造一个实例。
因此:要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。
7.消除过期的对象引用
过期引用 :永远也不会在被解除的引用
eg:栈先增长,再收缩,从栈中弹出来的对象不会被当作垃圾回收。因为栈内部维护着对这些对象的过期引用。
解决:一旦对象引用已经过期,只需清空这些引用即可 。 对于上述例子中的 Stack 类而言,只要一个单元被弹出拢,指向它的引用就过期了
public Object pop(){
if(size == 0) throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}
- 只要类是自己管理内存,程序员就应该警惕内存泄漏问题
- 内存泄漏的另一个常见来源是缓存。 只要在缓存之外存在对某个项的键的引用,该项就有意义,那么就可以用 WeakHashMap 代表缓存;当缓存中的项过期之后,它们就会自动被删除 。 记住只有当所要的缓存项的生命周期是由该键的外部引用而不是由值决定时, WeakHashMap 才有用处 .
- 内存泄漏的第三个常见来源是监昕器和其他回调
8.避免使用终结方法和清除方法
终结方法和清除方法的缺点在于不能保证会被及时执行
注重时间的任务不应该由终结方法或者清除方法来完成 。
永远不应该依赖终结方法或者清除方法来更新重要的持久状态。
9.try-with-resources 优先于 try-finally
在处理必须关闭的资源时,始终要优先考虑用 try-with-resources ,而不是用 try-finally 。 这样得到的代码将更加简洁、清晰,产生的异常也更有价值。 有了 trywith -resources 语句,在使用必须关闭的资源时,就能更轻松地正确编写代码了 .
static void copy(String src, String dst) throws IOException{
try(InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst)){
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0){
out.write(buf, 0, n);
}
}
}
带catch
static String firstLineOfFile(String path, String defaultVal){
try(BufferedReader br = new BufferedReader(new FileReader(path))){
return br.readLine();
}catch (IOException e){
return defaultVal;
}
}