《Effective Java》读书笔记 - 2.创建和销毁对象

时间:2022-09-06 09:39:14

Chapter 2 Creating and Destroying Objects

item 1:Consider static factory methods instead of constructors

一个static factory method只不过是一个返回这个类的实例的static方法。与设计模式中的factory method是不同的概念。static factory methods有几点好处:

一.它们可以有自己的名字,而不像constructor只能与类名相同。一个好的名字可以增进可读性。

二.不像constructor那样,每次调用static factory methods的时候,并不一定都要创建一个新的对象,比如对immutable classes来说,可以返回那些已存在的对象。比如Boolean.valueOf(boolean)这个static factory method,就从不会创建一个新的对象。这种能多次调用,但都返回相同对象的能力 使得这个类可以对 什么时候有哪些自己的实例 有着严格的控制,我们把这种类叫做instance-controlled。Instance control可以让一个类保证它是一个singleton或者noninstantiable。或者,它可以让一个immutable class能保证自己的所有实例都是不同的,所以a.equals(b)当且仅当ab,那么就可以用ab来代替a.equals(b)来增强性能(Enum types就保证了这一点)。

三.你可以返回一个 是返回类型的子类 的对象。比如你的返回类型是一个interface,并且返回的对象的class都可以是非public的(注意是class,而不是method,class只能是public或者default的访问权,内部类例外)。这样的话,用户只关心interface中定义的API,而类的实现者可以随时更改此类,比如java.util.EnumSet,没有public constructor,只有static factories,如果只有小于等于64个enum types,那么它会返回一个RegularEnumSet实例,内部基于一个long实现;如果大于64个,那么就返回一个JumboEnumSet实例,内部基于一个long数组实现。而RegularEnumSet和JumboEnumSet这两个类对用户都是不可见的。

四.减少代码长度,比如一个static factory method:

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

然后就不必写两遍type parameters:

Map<String, List<String>> m = HashMap.newInstance();

而static factory methods不好的地方在于:

一.只有private的constructor的类不能被继承,package级别(简称default)的class或者private的内部class对于client来说也是不能被继承的。(我没看出来哪儿不好,作者也说这可能是因祸得福,因为这鼓励我们use composition instead of inheritance)。我做了个实验:private的内部类即使有个public的constructor,除了在它内部或者它的outer class中,都不能实例化一个这个内部类。

二.static factory methods不能很明显地区别于其他static methods。几种常见的static factory methods的命名:valueOf、getInstance、newInstance、getType(这里的Type应该是某个接口的名字,或者基类)、newType。

Item 2: Consider a builder when faced with many constructor parameters

当一个类的constructor需要很多参数,并且有些参数是可选并带有默认值的时候,最好的办法就是比如C#中的命名参数和默认参数。但Java里面没有怎么办?有一种古老的Telescoping constructor pattern,假设只有servingSize和servings是必选的:

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, int sodium, int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
}

默认值被分开写在不同的方法里,而且这个例子中的这个类只有四个状态变量,如果有十个那么就吃瘪了。还有一种叫JavaBeans pattern的方法,就是只有一个可以设置必选值的constructor,并把默认值用field initialization写在相关field的后面,然后每个field都有一个public setter。然后当你创建一个对象的时候,必须在调用完构造函数后,马上紧跟着set你需要设置的参数。这种方法的弊端在于:由于要分开调用多个方法来构造这个对象,这个对象可能在构造到一半的时候处于一种不一致的状态。我的理解就是:这样更有可能会使得 还没有被完全构造好的对象被使用,就会出很难找的bug。另外,这样的对象不可能是immutable的,所以要保证线程安全还得另下功夫。那么如何才能模拟其他语言中的命名参数和默认参数?请参考下面的Builder Pattern:

public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat; public static class Builder {
private final int servingSize;
private final int servings;
private int calories = 0;
private int fat = 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 NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
}
}

然后当你需要一个这个类的实例的时候可以:

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).fat(100).build();

我承认client用的时候确实比较爽,当implementor实现的时候也太tm麻烦了吧,我的理解就是:这个多出来的一个叫Builder的类基本就和本来的类一样,它的唯一作用貌似就是设好所有要设的instance fields,然后拷贝到本来的class中,注意这里是static inner class,所以它可以访问外部类的private的方法(但是只能访问static的,还记得constructor是隐式static的吗?)。关于参数验证应该放在哪里我没太看懂。你甚至可以用一个Builder来构造多个对象。另外,用这个Builder可以很好地诠释一个叫Abstract Factory的模式,就是让client传一个Builder给一个方法,让这个方法为client创建一个或多个对象,比如:首先定义一个接口:

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

这里的T就是这个Builder是为谁服务的,拿刚才的例子来说就是NutritionFacts.Builder可以声明成实现了Builder<NutritionFacts>。然后就可以写一个方法接受一个这个接口类型的参数,比如:

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

这个方法构造一个Tree对象给你,但是需要client提供的Builder,来构造树的每一个node。

这种Builder模式的缺点在于:你要构造一个对象前必须先构造一个这个对象的Builder;其次,这个模式的代码更加冗长,最好在你需要的参数确实很多的情况下使用。

Item 3: Enforce the singleton property with a private constructor or an enum type

singleton就是一个只被实例化一次的类。不可能让一个mock对象来取代一个singleton,所以难以测试,除非这个singleton的class实现了某个interface,然后client是通过这个interface来用这个singleton的。一种实现singleton的方法是用一个public的static final的filed:

public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {...}
}

注意构造函数是private的。还有一种是用一个public的factory method:

public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() {...}
public static Elvis getInstance() {
return INSTANCE;
}
}

这种方法的好处是更灵活,比如说如果你不想让这个类是个singleton了,你可以很轻松地把这个method修改成,比如 对每个线程返回一个实例。而用public field的好处是更简单直观,因为client可以看到这个field的声明,知道它是final的所以肯定是个singleton。如果想让一个singleton的class是Serializable的话,必须做一些额外工作,这个需要的时候再查阅其他资料吧。

还有一种用enum来实现singleton的方法:

public enum Elvis {
INSTANCE;
//这里可以定义其他方法
}

这种方法在功能上和刚才public field的方法一样(因为对client来说都是用Elvis.INSTANCE来得到这个singleton),但是更简洁,免费提供serialization功能,等。

Item 4: Enforce noninstantiability with a private constructor

经常会有一些utility classes,他们可能只包含static methods,这种class应该被弄成 不能被实例化的。方法如下:

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

如果你不指定任何constructor的话,就会默认生成一个public的,所以这里我们要显式地写一个private的constructor,只是为了让这个类不能被实例化。异常是防止在类的内部不小心被实例化。这种方法也可以防止这个类被继承,因为子类需要调用基类的构造函数。

Item 5: Avoid creating unnecessary objects

先举个反例:

String s = new String("stringette"); // DON'T DO THIS!

为什么这样很不好?因为每当这句statement被执行的时候,都会创建一个新的对象,而且传给String构造函数的"stringette"这玩意儿本身也是个String的实例。所以说正确做法应该是:

String s = "stringette";

以这种方式得到或创建的String对象会得到复用。

Item1中讲过,用static factory methods可以避免创建不必要的对象,比如Boolean.valueOf(String)。而构造函数Boolean(String)每次都会创建一个新的对象。书上还举了一个例子就是:一个方法里面有几个local对象,创建起来比较麻烦,每次你调用这个方法都会创建这几个对象,所以作者说更好的办法是把这几个local variable变成类的private field,然后在static initializer中初始化他们(创建对象),于是只需要创建这一次。后面书上说的adapter模式不明觉厉,例子是:Map.keySet只是返回一个view(也就是你修改原来Map中的key也会在这个view中看到改变,反之亦然),每次调用这个方法其实都是返回相同的Set实例。还有要小心autoboxing,比如:Long sum = 0L;sum +=1;这时候貌似就会进行autoboxing,把1提升为一个Long对象,这是没必要的,把sum声明成原始类型long就行了。这里的意思只是:prefer primitives to boxed primitives,但不是说创建对象是很昂贵的所以尽量要避免。现代的JVM都会帮你处理得很好,所以别瞎操心了。Item 39与本条item相反:“Don’t reuse an existing object when you should create a new one”(defensive copy)。

Item 6: Eliminate obsolete object references

一个叫Stack的类里面有下面这么一个方法:

public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}

如果不写有注释的那行,那么就可能导致“内存泄漏”。如果一个元素已经出栈了,那么这个元素对用户来说就是已经obsolete了,可以被回收了,但是由于Stack内部用一个数组实现,这个数组中的一部分元素可能是obsolete的,这时候就需要你手动null掉它们。万一这些引用没有被null掉,它们也许指向一些很大的对象,这些对象又指向更大的对象。还有一个好处就是万一不小心access了某个obsolete的数组元素,那么就会NullPointerException,而不会默默地不报错。那么什么时候需要像这样null掉引用?简单地说就是:当一个类会管理自己的内存的时候,那么你就要注意是不是要null掉引用了。啥意思?拿上面的Stack来说,它内部的数组中的一部分是被allocated,而剩余部分是free的,但GC无法得知这一点,所以你只能通过null掉引用来告诉它。

另一种内存泄漏是缓存,当你把一个引用放入一个缓存中后,可能你就忘了,这时候你可以用WeakHashMap,只有对key的某个外部引用还存活着,这个entry才活着。

还有一种内存泄漏是Listeners以及其他callbacks。矮油,我刚好昨天看了观察者的设计模式,也就是说如果一个对象要求Subject把自己注册到通知列表,但是这个对象之后比如go out of scope了,那么Subject还保持着它的引用,还在不断通知一个已经没用的东西,咋整?用WeakHashMap好了。

Item 7: Avoid finalizers

finalizers无法保证他们会及时地被执行(我记得是一个对象要被GC的时候,看看他有没有finalize方法,有的话就让他再活一轮)。所以说你根本不知道它什么时候会被执行,而且不同的JVM也不一样。给一个Class写一个Finalize方法会延迟它的对象被reclaim,所以可能造成OutOfMemoryError。甚至有可能这个Finalizer根本就不会被执行都有可能。System.gc和System.runFinalization方法只是增加了finalizer被执行的几率,但并不保证。而且在Finalize方法里面有个uncaught exception,那么会被直接忽略掉,连stacktrace都不会打印。而且有Finalize的对象性能会大大受损。所以说如果一个对象封装了某些需要被terminated的资源,就在它的类里面显式提供一个回收资源的方法,然后需要让client用try-finally来调用它。顺便一提,这个对象必须记录自己的资源是不是已经被terminate掉了,如果是,那么再调用自己的某些方法的话就要throw IllegalStateException。那么finalizer有什么用?一是可以试着看一下用户是不是忘记terminate资源了,better late than never,如果是,那么就terminate资源并log一个warning,告诉用户他有bug。二是有些不太critical的native object可以在Finalizer中被回收,如果是critical的就必须像上面说的那样让client显式处理一下,那么问题就来了,子类的Finalizer最后必须用finally手动调用父类的Finalizer,万一忘了咋办?书上提供了一个叫finalizerGuardian的方法,但我不写了,因为实际中估计根本不会用到Finalizer,应该避免用它。

《Effective Java》读书笔记 - 2.创建和销毁对象的更多相关文章

  1. 【Effective Java读书笔记】创建和销毁对象(一):考虑使用静态工厂方法代替构造器

    类可以提供一个静态方法,返回类的一个静态实例,如Boolean包装类的一个获取实例的静态方法 public static Boolean valueOf(boolean b) { return (b ...

  2. Effective Java 读书笔记之一 创建和销毁对象

    一.考虑用静态工厂方法代替构造器 这里的静态工厂方法是指类中使用public static 修饰的方法,和设计模式的工厂方法模式没有任何关系.相对于使用共有的构造器来创建对象,静态工厂方法有几大优势: ...

  3. Effective Java 学习笔记之创建和销毁对象

    一.考虑用静态工厂方法代替构造器 1.此处的静态工厂方法是指返回指为类的对象的静态方法,而不是设计模式中的静态工厂方法. 2.静态工厂方法的优势有: a.使用不同的方法名称可显著地表明两个静态工厂方法 ...

  4. Effective Java(1)-创建和销毁对象

    Effective Java(1)-创建和销毁对象

  5. effective java 第2章-创建和销毁对象 读书笔记

    背景 去年就把这本javaer必读书--effective java中文版第二版 读完了,第一遍感觉比较肤浅,今年打算开始第二遍,顺便做一下笔记,后续会持续更新. 1.考虑用静态工厂方法替代构造器 优 ...

  6. Effective Java 读书笔记之二 对于所有对象都通用的方法

    尽管Object是一个具体的类,但设计它主要是为了扩展.它的所有非final方法都有明确的通用约定.任何一个类在override时,必须遵守这些通用约定. 一.覆盖equals时请遵守通用的约定 1. ...

  7. Effective Java——(一)创建和销毁对象

    第一条 考虑用静态工厂方法代替构造器 什么叫静态工厂方法,就是通过在类中通过静态方法对对象初始化. 比如说 public class StaticFactory { private String na ...

  8. Effective Java(一)—— 创建和销毁对象

    在客户端(调用端)获取自身实例的方法: 公有的构造器: 类的静态工厂方法: 1. 使用静态工厂方法代替构造器 Boolean 是对基本类型 boolean 的包装类: public final cla ...

  9. Effective Java 读书笔记(二):对象通用方法

    1 重写equals方法时请遵守通用约定 (1)无需覆盖equals方法的情况 要求独一无二 不要求逻辑相等 超类已经覆盖equals方法,对其子类也适用 一个类是私有的或者是包私有(可以重写后抛出异 ...

随机推荐

  1. 一站式解决&comma;Android 拍照 图库的各种问题&period;

    在android开发中, 在一些编辑个人信息的时候,经常会有头像这么一个东西,就两个方面,调用系统相机拍照,调用系统图库获取图片.但是往往会遇到各种问题: 1.oom 2.图片方向不对 3.activ ...

  2. scrapy数据存入mongodb

    存入mongodb的pipelines文件是这样子写的 from openpyxl import Workbook from scrapy.conf import settings import py ...

  3. 第一个android应用程序

    首先打开Eclipse和一个AVD.在Eclipse中选择File→New→Project→Android→Android Application Project 点击Next,按照下图所示填写 注: ...

  4. Android发送请求到不同的Servlet&comma;但都是一个Servlet处理

    错误原因,在Servlet文件中 @WebServlet("/ServletForGETMethod") 与实际的ServletForQUERYMethod 文件名不符. @Web ...

  5. 批量扫描互联网无线路由设备telnet,并获取WIFI密码

    批量扫描互联网无线路由设备telnet,并获取WIFI密码 http://lcx.cc/?i=4513

  6. &lbrack;翻译&rsqb;利用C&num;获取终端服务&lpar;Terminal Services&rpar;会话的闲置时间

    [翻译]利用C#获取终端服务(Terminal Services)会话的闲置时间 作者:Tuuzed(土仔)   发表于:2008年2月29日版权声明:可以任意转载,转载时请务必以超链接形式标明文章原 ...

  7. Extjs中grid前端分页使用PagingMemoryProxy【二】

        在项目中遇到Grid前端分页,本人也是刚接触extjs没多久,为了实现效果,一直找了很久才实现出来,对于代码中的一些也不能详细的说明出来, 不知道能不能帮助到遇到同样问题的朋友,所以将例子代码 ...

  8. mysql常用基础操作语法(十二)~~常用数值函数【命令行模式】

    数值函数是常用函数之一,也是学习mysql必会的,常用的有如下一些: 1.ceil:返回大于某个数的最小整数值: 2.floor:和上一个相反,返回小于某个数的最大整数值: 3.round:返回某个数 ...

  9. 【第十课】Tomcat入门

    目录 1.Tomcat介绍 2.Tomcat安装部署和配置 (1)tomcat下载和解压 (2)jdk环境变量配置 (3)设置tomcat以普通用户启动 (4)查看tomcat的配置 (5)tomca ...

  10. vuejs、eggjs、mqtt

    vuejs.eggjs.mqtt全栈式开发设备管理系统 vuejs.eggjs.mqtt全栈式开发简单设备管理系统 业余时间用eggjs.vuejs开发了一个设备管理系统,通过mqtt协议上传设备数据 ...