前言

主要学习创建和销毁对象:

  • 1.何时以及如何创建对象
  • 2.何时以及如何避免创建对象
  • 3.如何确保它们能够适时地销毁
  • 4.如何管理对象销毁之前必须进行的清理动作

正文

一、用静态工厂方法代替构造器

获取类的实例的常用方法有:

  • 1.公有的构造器
  • 2.公有的静态工厂方法

下面通过Boolean类(基本类型boolean的包装类)的简单示例来学习:

//公有的构造器
public Boolean(String s) {
this(parseBoolean(s));
}
//公有的静态工厂方法
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}

静态工厂方法相对于构造器的优势:

1.有名称

具有适当名称的静态工厂方法更易使用,产生的代码更易阅读。可用名称来突出它们之间的区别。

如构造器BigInteger(int,int,Random)返回的BigInteger可能为素数,若用名为BigInteger.probablePrime的静态工厂方法来表示,显得更加清楚。

2.不必每次调用它们的时候都创建一个新对象

不可变类可以使用预先构建好的实例,或者是将构建好的实例缓存起来,进行重复利用,从而避免创建不必要的重复对象。

例如之前的Boolean的静态工厂方法就说明了这项技术,这种方法类似于FlyWeight模式。如果程序经常请求创建相同的对象,并且创建对象的代价很高,这项技术极大地提升了性能。

//缓存起来的Boolean实例
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false); public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}

静态工厂方法为重复的调用返回相同的对象。

3.可返回原返回类型的任何子类型的对象

4.创建参数化类型实例时,可使代码变得更加简洁

由于《Effective Java》这本书在编写的时候,JDK1.7还没有推出,因此在调用参数化类的构造器时,类型参数都必须要指明。而静态工厂方法能替我们实现类型推导的功能。

  //JDK7之前
Map<String,List<String>> map = new HashMap<String,List<String>>();
//JDK7
Map<String,List<String>> m = new HashMap<>();
//使用静态工厂方法
public class MyHashMap extends HashMap {
public static <K,V> HashMap<K,V> newInstance(){
return new HashMap<K,V>();
} //静态工厂方法实现类型推导
Map<String,List<String>> m = MyHashMap.newInstance();
}

静态工厂方法的缺点:

  • 1.类如果不含公有的或受保护的构造器,就不能被子类化。
  • 2.静态工厂方法和其他的静态方法实际上没有任何区别。
/*
*静态工厂方法的惯用名称 
*/
//valueOf:返回的实例与它的参数具有相同的值
//String.valueOf(int)方法
public static String valueOf(int value) {
return Integer.toString(value);
} //of:valueOf的简洁版
//EnumSet.of(E)方法
public static <E extends Enum<E>> EnumSet<E> of(E e) {
EnumSet<E> set = EnumSet.noneOf(e.getDeclaringClass());
set.add(e);
return set;
} //getInstance:返回的实例是通过方法的参数来描述的。
//newInstance:返回的每个实例与其他的所有实例不同。
//getType:Type表示工厂方法返回的对象类型
//newType:与newInstance类似

二、多个构造器参数考虑用构建器

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

对于有大量的参数的类的编写:

/**
*重叠构造器模式
*提供一个只有必要参数的构造器,第二个有一个可选参数,第二个有两个,依此类推。
*/ 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 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;
}
}

此方法虽可行,但如果有许多参数时,客户端代码很难编写,并且难以阅读

/**
*JavaBeans模式
*无参构造方法创建对象,使用setter方法设置必选或可选参数。
*/
public class NutritionFactsWithJavaBeans {
/**
* 必选元素
*/
private int servingSize;
private int servings;
/**
* 可选元素
*/
private int calories;
private int fat;
private int sodium;
private int carbohydrate; /**
* 无参构造方法
*/
public NutritionFactsWithJavaBeans(){ }
/**
* Setter方法
*/
public void setServingSize(int servingSize) {
this.servingSize = servingSize;
} public void setServings(int servings) {
this.servings = servings;
} public void setCalories(int calories) {
this.calories = calories;
} public void setFat(int fat) {
this.fat = fat;
} public void setSodium(int sodium) {
this.sodium = sodium;
} public void setCarbohydrate(int carbohydrate) {
this.carbohydrate = carbohydrate;
} public static void main(String[] args) {
NutritionFactsWithJavaBeans cocaCola =
new NutritionFactsWithJavaBeans(); cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
}
}

JavaBeans模式创建实例容易,代码易读。但有很严重的缺点

  • 1.构造过程中可能出现不一致的状态,调试困难。
  • 2.因为有set方法,使得在JavaBeans模式中,不能将类变为不可变的,需要额外的努力来确保它的线程安全。

重叠构造器的安全性+JavaBeans模式的可读性====>Builder模式

易写易读,模拟了具名的可选参数。build方法可检验约束条件。

public class NutritionFactsWithBuilder {
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 {
//必选元素
private final int servingSize;
private final int servings; //可选元素,设置默认值初始化
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0; public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
} public Builder calories(int calories) {
this.calories = calories;
return this;
} public Builder fat(int fat){
this.fat = fat;
return this;
} public Builder sodium(int sodium){
this.sodium = sodium;
return this;
} public Builder carbohydrate(int carbohydrate){
this.carbohydrate = carbohydrate;
return this;
}
public NutritionFactsWithBuilder build(){
return new NutritionFactsWithBuilder(this);
}
} private NutritionFactsWithBuilder(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) {
NutritionFactsWithBuilder nutritionFactsWithBuilder
= new NutritionFactsWithBuilder.Builder(20,30).calories(3).build();
} }

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

Singleton表示仅仅被实例化一次的类。

实现Singleton的方法:

  • 1.public static final域实现
public class SingletonClazz{
//public属性将本类唯一实例暴露出去
public static final SingletonClazz INSTANCE = new SingletonClazz();
//构造方法私有 保证全局唯一性
private SingletonClazz(){
}
public void test(){
System.out.println("test method");
}
}

缺点:利用反射机制可调用到私有构造方法

 try {
Constructor<SingletonClazz> constructor = SingletonClazz.class.getDeclaredConstructor();
constructor.setAccessible(true); SingletonClazz clazz = constructor.newInstance();
clazz.test();
} catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
e.printStackTrace();
}

如何防止调用私有构造方法,修改构造方法,在创建第二个实例的时候抛出异常即可。

 private SingletonClazz(){
if(INSTANCE!=null)
throw new RuntimeException("cannot create more than one instance of SingletonClazz");
}
  • 2.静态工厂方法实现
public class SingletonClazz {

    private static final SingletonClazz INSTANCE = new SingletonClazz();
private SingletonClazz(){ }
public static SingletonClazz getInstance(){
return INSTANCE;
} public void test(){
System.out.println("test method");
}
}

这个方法相比于之前的方法的优势是,灵活性,不需要更改API的前提下,可以改变该类是否应该为Singleton。

使Singleton类变成为可序列化的:

  • 实现Serializable接口
  • 需要声明所有实例为瞬时的(transient)
  • 提供一个readResolve()方法。

3.编写一个包含单个元素的枚举类型来实现

public enum SingletonClazz {
INSTANCE; public void test(){
System.out.println("test method");
}
}

优点:简洁,并提供了序列化机制,而且能保证不会被多次实例化(即使是面对复杂的序列化或是发射攻击的时候)----->实现Singleton的最佳方法

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

在缺少显式构造器的情况下,编译器会自动提供一个公有的,无参的缺省构造器

public class DefaultConstructor {
//没有显示构造器
public static void main(String[] args) {
//使用编译器提供的公有的,无参的缺省构造器
DefaultConstructor defaultConstructor = new DefaultConstructor();
}
}

通过将类做成抽象类来实现不可实例化的目的是不可取的。继承抽象类,子类也可以被实例化。同时也会误导用户,以为是为了继承而设计的。

public abstract class AbstractClazz {
//通过抽象类来实现不可实例化是不可取的
public static void main(String[] args) {
//仍旧可以通过继承抽象类,来达到实例化子类的目的
SubClazz subClazz = new SubClazz();
}
} class SubClazz extends AbstractClazz{ }

正确做法是:将构造器显式地声明为私有的。

public class UtilityClazz {
//防止类被实例化
private UtilityClazz(){
throw new AssertionError();
} }

避免创建不必要的对象

最好能重用对象,而不是在每次需要的时候创建一个相同功能的新对象。

重用不可变对象。

public class NoNeedObject {

    public static void main(String[] args) {
String s1 = new String("12345");//错误做法
String s2 = "12345";//正确做法
String s3 = "12345";
String s4 = new String("12345"); check(s1,s2);//不同
check(s2,s3);//相同 重用了对象
check(s1,s4);//不同 }
public static void check(String one,String two){
//==比较的是两个引用是否指向同一个对象
if(one == two)
System.out.println("same address");
else
System.out.println("different address");
}
}

重用那些已知不会被修改的可变对象。

class Person{
private final Date birthDate; public Person(Date birthDate){
this.birthDate = birthDate;
} public boolean isBabyBoomer(){
//没有必要的对象创建
//每次判断都会生成Calendar,Date,TimeZone实例
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&&birthDate.compareTo(boomEnd)<=0;
}
}

用一个静态的初始化器来避免上面那种效率低下的情况。

class Person{
private final Date birthDate; public Person(Date birthDate){
this.birthDate = birthDate;
} private static final Date BOOM_START;
private static final Date BOOM_END; static {
System.out.println("创建Calendar等对象");
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();
} public boolean isBabyBoomer(){
return birthDate.compareTo(BOOM_START)>=0&&birthDate.compareTo(BOOM_END)<=0;
}
}

自动装箱(JDK5引入):Java编译器能在基本类型和包装类之间自动转换。如intInteger,doubleDouble等等。

相关学习链接:

1.https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html

2.https://docs.oracle.com/javase/tutorial/java/data/autoboxing.html

 public static void main(String[] args) {
//使用包装类Long
long beforetime = System.currentTimeMillis();
Long sum = 0L;
for(long i = 0;i<Integer.MAX_VALUE;i++){
sum+=i;
}
long aftertime = System.currentTimeMillis(); System.out.println("Long--->sum="+sum);
System.out.println("time="+(aftertime-beforetime)); //使用基本类型long
beforetime = System.currentTimeMillis();
long sum2 = 0L;
for(long i = 0;i<Integer.MAX_VALUE;i++){
sum2+=i;
}
aftertime = System.currentTimeMillis();
System.out.println("long--->sum="+sum2);
System.out.println("time="+(aftertime-beforetime)); }

运行结果:

Long--->sum=2305843005992468481
time=9642
long--->sum=2305843005992468481
time=777

结论:优先使用基本类型而不是装箱基本类型,当心无意识的自动装箱。

注意: 很多规定只是建议,不要矫枉过正,犯了教条主义的错误,一定要与实际的开发情况结合,实事求是。

不要错误地认为“应该尽可能地避免创建对象”,应该是“避免创建不必要的对象”,注意是不必要的!

通过维护自己的对象池(object pool)来避免创建对象并不是一种好的做法,除非池中的对象非常重要。

消除过期的对象引用

手工管理内存的语言:C或C++

具有垃圾回收功能的语言:Java,简化程序员的工作

public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_SIZE = 16; public Stack(){
elements = new Object[DEFAULT_INITIAL_SIZE];
} public void push(Object e){
ensureCapacity();
elements[size++] = e;
} /**
* stack: push(1) push(2) push(3) push(4) push(5) pop() pop()
* 5
* 4
* 3 ---->栈顶
* 2
* 1
*
* 栈内部维护着过期的引用,也就是永远不会再被解除的引用,如4和5
* fixed:
* elements[size]=null;
*
*/
public Object pop(){
if(size==0)
throw new EmptyStackException();
return elements[--size];
} private void ensureCapacity() {
if(elements.length==size){
elements = Arrays.copyOf(elements,2*size+1);
}
}
}

上面这段代码存在内存泄露的问题,当栈先是增长,然后弹出,从栈中弹出的对象都不会被当做垃圾回收。

栈内部维护着这些对象的过期引用,也就是永远不会被解除的引用。栈数组中下标大于或等于size的元素的引用都是过期的引用。

在支持垃圾回收的语言中, 内存泄露(也就是无意识的对象保持)很隐蔽。

修复办法:一旦对象引用过期,就清空这些引用。

注意:清空对象引用应该是一种例外,而不是一种规范行为。

容易导致内存泄露的几种情形:

  • 类自己管理内存
  • 缓存
  • 监听器和其他回调

避免使用终结方法

终结方法(finalizer):不可预测,危险,一般情况下是不必要的。

终结方法的缺点:

  • 不能保证会被及时地执行,而且根本不会保证它们会被执行。(所以不应该依赖终结方法来更新重要的持久状态
  • 严重的性能损失

终止类的对象中封装的资源(文件或线程),只需提供一个显式的终止方法。不需要编写终结方法。

例子:

  • InputStream,OutputStream,java.sql.Connection的close方法
  • java.util.Timer的cancel方法

一般与try-catch结合起来使用,以确保及时终止

最新文章

  1. [锋利JQ]-图片提示效果
  2. UML - 类图
  3. 网络框架 &amp; 云端
  4. IT在线笔试总结(一)
  5. Maximal Square || LeetCode
  6. jdk环境变量配置方法
  7. 【CSS中width、height的默认值】
  8. lesson3:使用java代码的方式对不能识别的协议进行压力测试
  9. 实时预览的在线 Markdown 编辑器 - Markdoc
  10. 微信小程序基础之表单Form的使用
  11. Java实现简单的RPC框架
  12. Linux第八章:文件,文件系统的压缩,打包备份
  13. 详述socket编程之select()和poll()函数
  14. ActiveMQ新的Master/Slave存储共享机制Replicated LevelDB Store
  15. [转]本地 Windows 计算机密码登录 登录 腾讯云 Linux 实例
  16. 第16章 STM32中断应用概览
  17. MySQL索引背后的数据结构及算法原理 (转)
  18. java多线程整理
  19. javascript64位加密
  20. eventql操作脚本

热门文章

  1. python的条件与循环1
  2. netty同时做http和websocket(netty入门)
  3. no console to display at this time
  4. php 替换二维数组的 key
  5. Eclipse 安装Maven以及Eclipse配置Maven
  6. 喵哈哈村的魔法考试 Round #8 (Div.2) 题解
  7. Android笔记(五):广播接收者(Broadcast Receiver)
  8. Windows2003 内核级进程隐藏、侦测技术
  9. JAVA之Lamdba表达式使用摘要
  10. javascript中的数据结构