前言

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

  • 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结合起来使用,以确保及时终止

和我一起学Effective Java之创建和销毁对象的更多相关文章

  1. 《Effective Java》—— 创建与销毁对象

    本篇主要总结的是<Effecticve Java>中关于创建和销毁对象的内容. 比如: 何时以及如何创建对象 何时以及如何避免创建对象 如何确保及时销毁 如何管理对象销毁前的清理动作 考虑 ...

  2. Effective Java - [2. 创建与销毁对象]

    让对象的创建与销毁在掌控中. Item 1: 使用静态工厂方法而非使用构造函数 public static Boolean valueOf(boolean b) { return b ? Boolea ...

  3. 【读书笔记】《Effective Java》——创建和销毁对象

    Item 1. 考虑用静态工厂方法替代构造器 获得一个类的实例时我们都会采取一个共有的构造器.Foo x = new Foo(): 同时我们应该掌握另一种方法就是静态工厂方法(static facto ...

  4. 和我一起学Effective Java之类和接口

    类和接口 使类和成员的可访问性最小 信息隐藏(information hiding)/封装(encapsulation):隐藏模块内部数据和其他实现细节,通过API和其他模块通信,不知道其他模块的内部 ...

  5. Effective java笔记2--创建于销毁对象

    一.创建对象的两种方式 1.提供公有的构造器. 2.提供一个返回类实例的静态方法. 二.使用静态方法创建对象 优势: 1.静态工厂方法的一个好处是,与构造函数不同,静态工厂方法具有名字.产生的客户端代 ...

  6. 和我一起学Effective Java之泛型

    泛型 不要在新代码中使用原始类型 泛型(generic):声明中具有一个或多个类型参数 原始类型(raw type):不带任何实际类型参数的泛型名称 格式: 类或接口的名称 < 对应于泛型形式类 ...

  7. Java进阶 创建和销毁对象

    最近准备写点Javase的东西,希望可以帮助大家写出更好的代码. 1.给不可实例化的类提供私有构造器 比如:每个项目中都有很多工具类,提供了很多static类型的方法供大家使用,谁也不希望看到下面的代 ...

  8. Effective Java笔记一 创建和销毁对象

    Effective Java笔记一 创建和销毁对象 第1条 考虑用静态工厂方法代替构造器 第2条 遇到多个构造器参数时要考虑用构建器 第3条 用私有构造器或者枚举类型强化Singleton属性 第4条 ...

  9. effective java读书小记(一)创建和销毁对象

    序言 <effective java>可谓是java学习者心中的一本绝对不能不拜读的好书,她对于目标读者(有一点编程基础和开发经验)的人来说,由浅入深,言简意赅.每一章节都分为若干的条目, ...

随机推荐

  1. JAVA中验证邮箱是否有效

    String email = form.getEmail(); if(!email.matches("^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+((\\.[a-zA ...

  2. replace方法,替代敏感字符

    var val = $("#id").val(); var reg =/垃圾|你大爷|你妹的/g; if(val){   // false  : isNaN-- 0-- undef ...

  3. php get_magic_quotes_gpc()函数使用

    magic_quotes_gpc函数在php中的作用是判断解析用户提示的数据,如包括有:post.get.cookie过来的数据增加转义字符"\",以确保这些数据不会引起程序,特别 ...

  4. 20172319 2018.10.19《Java程序设计教程》第7周课堂实践(补写博客)

    20172319 2018.10.19 <Java程序设计教程>第7周课堂实践 课程:<程序设计与数据结构> 班级:1723 学生:唐才铭 学号:20172319 指导老师:王 ...

  5. __x__(24)0907第四天__ display 和 visibility

    <a>百度</a>    也是内联元素,无法设置width和height <img>可以设置width和height,但是不会占用一行,所以是典型的行内块元素inl ...

  6. iOS开发-UIView扩展CGRect

    关于UIView的位置都会遇到,一般需要改变UIView的位置,需要先获取原有的frame位置,然后在frame上面修改,有的时候如果只是改变了一下垂直方向的位置,宽度和高度的一种,这种写法很麻烦.下 ...

  7. ckeditor 上传图片解决跨域问题

    前后端分离ckeditor跨域问题处理 这个跨域问题很常见,特别是前后端分离的情况,IP地址不同导致了页面跨域,具体原因大多是因为前端ifame问题 分析 ckeditor插件里config.js需要 ...

  8. python新建txt文件,并逐行写入数据

    #coding=utf-8 txtName = "codingWord.txt"f=file(txtName, "a+")for i in range(1,10 ...

  9. Dubbo的使用入门

    一.包引入 1.父模块pom.xml中加入依赖: <!-- dubbo --> <dependency> <groupId>com.alibaba.boot< ...

  10. LeetCode 90:Subsets II

    Given a collection of integers that might contain duplicates, nums, return all possible subsets. Not ...