第一章-创建和销毁对象
| 版本 | 内容 | 时间 |
|---|---|---|
| V1 | 新建 | 2023年07月13日23:49:21 |
第一章:创建和销毁对象,主要包含下面 9 条。
- 用静态工厂方法替代构造器;
- 遇到多个构造器参数时考虑使用 builder 模式;
- 用私有构造器或者枚举类型强化 Singleton 属性;
- 通过私有构造器强化不可实例化的能力;
- 优先考虑依赖注入来引用资源;
- 避免创建不必要的对象;
- 消除过期的对象引用;
- 避免使用终结方法和清除方法;
- try-with-resources 优先于 try-finally;
用静态工厂方法替代构造器
一个类除了可以提供构造器之外,还可以提供一些见名知意的静态工厂方法去返回该类的实例。
静态工厂方法的优点
- ,而构造器的方法名只能有一种。虽然构造器可以重载,但是如果重载的构造器过多的话,会对增加使用的难度,调用者还要斟酌用哪个构造器;
- (eg.单例模式和享元模式);
- **,**这样可以屏蔽一些不是 public 的子类的实现,减少调用者的使用难度。例如在 JDK 的集合框架中有这样一个类 java.util.Collections,这个类中提供了不可修改的集合、 同步集合的实现,这样可以通过调用 API 直接返回带有指定特性(不可修改、同步等)的集合,这样我们的 API 调用者就不用去关系其内部的子类是如何实现这些特性的;
- ;
- ;这个有点典型的案例有 SPI 机制(Service Provider Interface),SPI 是一种用于实现松耦合、可扩展插件系统的机制,它允许应用程序在运行时发现和加载某个特定服务接口的提供者。JDK 自带的 ServiceLoader 类实现了 SPI 机制,还有其他开源框架中有大量的 SPI 的案例(JDBC、日志门面、motan rpc、dubbo、spring 等);
静态工厂方法的缺点以及弥补方式
- ,也就是说不能继承这个类。不过这样也有个好处就是我们可以多多使用组合,而不是继承;
- 。 Java 自带的 API 文档会把构造器明确的标注出来,但是我们写的静态工厂方法并不容易被发现;
我们可以通过一些合适的命名来弥补静态工厂方法不容易被发现的劣势:
:类型转换的方法,只有单个参数,返回该类型的一个实例。例如 java.util.Date#from 方法
public static Date from(Instant instant) { try { return new Date(instant.toEpochMilli()); } catch (ArithmeticException ex) { throw new IllegalArgumentException(ex); } }:聚合方法,带有多个参数,返回该类型的一个实例,把它们合并起来。例如 java.util.List#of(E, E, E) 系列方法返回一个不可变的 List 实例(JDK 9 之后)。
static <E> List<E> of(E e1, E e2, E e3) { return ImmutableCollections.listFromTrustedArray(e1, e2, e3); }:例如 java.lang.Integer#valueOf(java.lang.String) 方法
public static Integer valueOf(String s) throws NumberFormatException { return Integer.valueOf(parseInt(s, 10)); }:获取一个实例,静态工厂方法可以有参数,也可以没有参数,没有参数的可以视为默认的实现。例如:java.util.Calendar#getInstance()
public static Calendar getInstance() { Locale aLocale = Locale.getDefault(Locale.Category.FORMAT); return createCalendar(defaultTimeZone(aLocale), aLocale); }:获取一个实例,这个命名强调的是每次都返回一个新的实例。例如:java.lang.reflect.Array#newInstance(java.lang.Class<?>, int)
public static Object newInstance(Class<?> componentType, int length) throws NegativeArraySizeException { return newArray(componentType, length); }:这里的 type 表示静态工厂方法需要返回的对象的类型的名字,主要是工厂方法处于不同的类中的时候使用。例如:java.lang.Class#getConstructor
public Constructor<T> getConstructor(Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException { checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true); return getConstructor0(parameterTypes, Member.PUBLIC); }:这里的 type 表示静态工厂方法需要返回的对象的类型的名字,主要是工厂方法处于不同的类中的时候使用。例如:java.lang.Class#newInstance
public T newInstance() throws InstantiationException, IllegalAccessException { // ...... 省略具体实现 ...... }
遇到多个构造器参数时考虑使用 builder 模式
静态工厂和构造器的缺点
静态工厂和构造器有一个共同的缺点,它们在多个参数的情况下的扩展性并不好,例如有些参数可能是必填的,有些参数可能是可选的。先说两种并不是最优的处理:① 使用构造函数重载的方式去处理多个参数;② 创建好 JavaBean 后,调用对应的 set 方法;
- 使用构造函数重载的方案的缺点是客户端调用起来不方便,而且比较难阅读;
- 创建 JavaBean 后调用 set 方法去设置参数,缺点是在创建对象然后设置参数是多个步骤,也就是说在实例构造的过程中 JavaBean 可能处于不一致的状态,导致不可预知的错误;
建造者 builder 模式
上面两种解决方案并不是最优的,Effective Java 作者推荐使用 builder 模式,也就是建造者模式来处理多个参数的情况。看一个案例:
public class Person {
private String name; // 必填
private String hobby; // 可选
private String career; // 可选
public static final class PersonBuilder {
private String name;
private String hobby;
private String career;
private PersonBuilder(String name) {
this.name = name;
}
public static PersonBuilder personBuilder(String name) {
return new PersonBuilder(name);
}
public PersonBuilder withHobby(String hobby) {
this.hobby = hobby;
return this;
}
public PersonBuilder withCareer(String career) {
this.career = career;
return this;
}
public Person build() {
Person person = new Person();
person.career = this.career;
person.name = this.name;
person.hobby = this.hobby;
return person;
}
}
}其中 name 是必填的属性,所以在创建 PersonBuilder 的时候必填,其余的属性在各自的 with 开头的方法里面去构建。调用方的构建代码如下:
public static void main(String[] args) {
Person person = PersonBuilder.personBuilder("千珏")
.withCareer("ADC")
.withHobby("印记")
.build();
System.out.println(person);
}类层次的建造者模式
抽象类有抽象的 builder,具体类有具体的 builder。
先看一个抽象的 Phone 类,Phone 类内部有一个抽象的 Builder,需要子类实现的方法有两个:
- 一个是 build 方法,构建一个 Phone 对象出来;
- 一个是 self 方法,该方法需要子类实现返回 “this”,用来组成链式调用;
public abstract class Phone {
private String brand; // 品牌
private Double size; // 尺寸
public Phone(Builder builder) {
this.brand = builder.brand;
this.size = builder.size;
}
abstract static class Builder<T extends Builder<T>> {
private String brand; // 品牌
private Double size; // 尺寸
public T withBrand(String brand) {
this.brand = brand;
return this.self();
}
public T withSize(Double size) {
this.size = size;
return this.self();
}
/* 构建一个实例 */
abstract Phone build();
/* 子类必须重载这个方法,返回 'this' */
protected abstract T self();
}
}看一个子类 SmartPhone 的实现
class SmartPhone extends Phone {
private String games3D;
public SmartPhone(SmartPhoneBuilder builder) {
super(builder);
this.games3D = builder.games3D;
}
public static class SmartPhoneBuilder extends Phone.Builder<SmartPhoneBuilder> {
private String games3D;
public SmartPhoneBuilder withGames3D(String games3D) {
this.games3D = games3D;
return this;
}
@Override
public Phone build() {
return new SmartPhone(this);
}
@Override
protected SmartPhoneBuilder self() {
return this;
}
}
}测试类
class Test {
public static void main(String[] args) {
Phone phone = new SmartPhone.SmartPhoneBuilder()
.withBrand("小米")
.withSize(7.2)
.withGames3D("小米枪战")
.build();
System.out.println(phone);
}
}子类的构建器中的 build 方法,都声明返回正确的子类。例如在子类 SmartPhone 中的 build 方法就是返回的 SmartPhone 对象实例。
builder 模式应用场景
如果类的构造器或者静态工厂方法中有多个参数,设计这种类的时候,可以考虑 Builder 模式。
builder 模式的优缺点
优点:;
缺点:
- 。虽然创建这个构建器的开销并不是很大,如果是某些十分注重性能的情况下,可能就有问题了;
- builder 模式比构造函数重载还要冗长,所以它只有在多个参数的时候才使用,比如 4 个或者更多个参数;
如果某个类的参数比较多,建议一开始就使用 builder 模式。如果一开始使用的是构造函数或者静态工厂方法,后面参数变多后又改为 builder 模式的话,这时候之前那些过时的构造函数和静态工厂方法就会显得不协调了。
用私有构造器或者枚举类型强化 Singleton 属性
其实就是单例模式的一些写法,但是本次并不是分析单例模式有多少次写法,这里主要阐述的是作者在文中的观点
实现单例的两种写法
第一种就是直接使用一个 public 的静态的 final 域,私有化构造函数。为了防止反射创建实例,在构造函数中做了判空处理。
public class Person {
public static final Person INSTANCE = new Person();
private Person() {
if (INSTANCE != null) {
throw new RuntimeException("请不要重复构造!!!");
}
}
public void sayHello() {
System.out.println("你好...");
}
public static void main(String[] args) {
Person.INSTANCE.sayHello();
}
}第二种和上面差不多,只不过是使用的静态工厂方法返回单例对象。也是需要私有化构造函数,如下:
public class Person {
private static final Person INSTANCE = new Person();
private Person() {
if (INSTANCE != null) {
throw new RuntimeException("请不要重复构造!!!");
}
}
public static Person getInstance() {
return INSTANCE;
}
public void sayHello() {
System.out.println("你好...");
}
public static void main(String[] args) {
Person.getInstance().sayHello();
}
}防止反序列化破坏单例
为了防止反序列化创建一个新的实例,可以在类中增加一个 readResolve() 方法,并返回单例对象。这样就可以防止反序列化破坏单例。具体的原理是反序列化的时候会判断当前类中是否有 readResolve() 方法,如果有的话就会直接拿 readResolve() 方法的返回值作为反序列化后得到的实例。
具体的源码可以去看 java.io.ObjectInputStream#readOrdinaryObject 的实现。
public class Person implements Serializable {
private static final Person INSTANCE = new Person("古拉加斯大肚子");
private String name;
private Person(String name) {
if (INSTANCE != null) {
throw new RuntimeException("请不要重复构造!!!");
}
this.name = name;
}
public static Person getInstance() {
return INSTANCE;
}
private Object readResolve() {
System.out.println("调用了 readResolve() 方法");
return INSTANCE;
}
public void sayHello() {
System.out.println(this.name + " 说你好...");
}使用枚举实现单例模式
public enum Person {
JIU_TONG; // 酒桶
public void sayHello() {
System.out.println("你好...");
}
public static void main(String[] args) {
Person.JIU_TONG.sayHello();
}
}用枚举实现单例是一个最简单的方式,Java 对枚举的处理已经保证单个枚举值是单例的了,而且无法通过反射和序列化去破坏单例。可以去反编译查看枚举的源码,就可以得到答案。
通过私有构造器强化不可实例化的能力
有些工具类提供了一些静态方法,其实我们并不需要实例化工具类,因为实例化它们是没有意义的,我们可以在工具类中提供一个私有的构造函数来防止它被实例化。
例如 JDK 的 java.lang.Math 类:
public final class Math {
/**
* Don't let anyone instantiate this class.
*/
private Math() {}
// ...... 省略其他 ......
}又比如 JDK 的 java.util.Arrays
public class Arrays {
// ...... 省略其他 ......
// Suppresses default constructor, ensuring non-instantiability.
private Arrays() {}
// ...... 省略其他 ......
}私有构造函数带来的问题就是无法子类化该类,因为子类无法调用父类的 private 的构造函数。
优先考虑依赖注入来引用资源
这个意思其实就是说,假如某个类依赖了另外一个类的功能,优先考虑依赖注入的方式。看个案例:
public class ClusterApi {
private HaService haService;
private LoadBalance loadBalance;
public ClusterApi(HaService haService, LoadBalance loadBalance) {
this.haService = haService;
this.loadBalance = loadBalance;
}
}
class HaService {
}
class LoadBalance {
}假如 ClusterApi 是集群相关的 API 类,内部需要使用 HA 高可用和负载均衡的服务的功能,那么可以通过依赖注入的方式注入到 ClusterApi 中。
这样看起来好像我们平时开发一直在用依赖注入啊,其实这些都是 Spring 帮我们做好了,我们自然而然的就用了。
避免创建不必要的对象
一般来说,最好重用单个对象,而不是每次需要的时候就创建一个相同功能的新对象。如果对象是不可变(immutable)的对象,它就始终可以被重用。
看几个案例:
关于 String 对象的创建,如果使用
String s = new String("nihao")创建字符串,入参nihao其实就是一个 String 实例,这样就会多创建 String 实例。建议直接使用String s = "nihao",因为在 JVM 中有个字符串常量池,会重用字符串;。例如在 java.lang.Integer#valueOf 系列方法中实现了缓存机制,如果没有命中缓存才会去创建 Integer 实例,可以避免创建多个实例;
public static Integer valueOf(int i) { // 假如 i 的大小命中了缓存,直接返回缓存中的对象 if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; // 未命中缓存则新建一个 Integer 对象 return new Integer(i); }。需要注意的是对于那些创建和回收代价很小的小对象,就不适合使用对象池来重用这些对象了;
。举个例子吧,比如 HashMap 的 java.util.HashMap#keySet 方法,每次返回 Map 对象的 Set 视图。这里很明显看到每次返回都是返回相同的 keySet 实例。
public Set<K> keySet() { Set<K> ks = keySet; if (ks == null) { ks = new KeySet(); keySet = ks; } return ks; }。
Long boxSum = 0L; long boxStart = System.currentTimeMillis(); for (long i = 0; i < Integer.MAX_VALUE; i++) { boxSum += i; } System.out.println("box waste time, " + (System.currentTimeMillis() - boxStart) + "ms."); long unboxSum = 0L; long unboxStart = System.currentTimeMillis(); for (long i = 0; i < Integer.MAX_VALUE; i++) { unboxSum += i; } System.out.println("unbox waste time, " + (System.currentTimeMillis() - unboxStart) + "ms.");控制台打印如下,可以看到没有自动装箱的操作花费的时间大大减少了。
box waste time, 4415ms. unbox waste time, 862ms
消除过期的对象引用
隐蔽的内存泄漏问题及解决方案
在支持垃圾回收的语言中,内存泄漏是十分隐蔽的,作者称这类内存泄漏为 “无意识的对象保持” (unintentional object retention)。
修复方案非常简单:。
注意点
。
关于内存泄漏的来源有下面几种:
- 只要是类自己管理内存,程序员就应该警惕内存泄漏的问题;常见的如数组的元素,后面再举一个 RocketMQ 的例子;
- 缓存也是内存泄漏的一个来源(这里的缓存是本地缓存的概念),因为”缓存项的生命周期是否有意义“并不容器被确定,所以应该时不时的清除调没有用的数据。例如可以通过 java.util.LinkedHashMap#removeEldestEntry 来实现一个 LRU;
- 内存泄漏的第三个常见来源是监听器和其他回调。 如果你实现了一个 API,客户端在这个 API 中注册回调,却没有显式地取消注册,那么除非你采取某些动作,否则它们就会不断地堆积起来 。
简易栈的问题以及解决方案
先看下作者给的案例,一个简易栈的实现:
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
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);
}
}上面的 pop 方法会出现比较隐蔽的内存泄漏,关键点就是在 elements[--size] 这里,我们只是将 size 减 1 了,这样之前的元素只是我们访问不到了,并没有被删除,它和它引用的那些对象都不会被垃圾回收器回收,这就出现了内存泄漏了。解决方案如下:
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}其实就是显示的将用不到的对象置空就行了,help gc。
RocketMQ 中的案例
RocketMQ 中的消费者需要去 broker 拉取消息去消费,其中 broker 的返回对象如下:
public class PullResultExt extends PullResult {
private final long suggestWhichBrokerId;
// 从网络中读取消息列表中的属性
private byte[] messageBinary;
// ...... 省略其他 ......
}上面的 messageBinary 是 broker 返回的消息的字节数组,消费者这边需要进行解码,解码代码如下:
public PullResult processPullResult(final MessageQueue mq, final PullResult pullResult,
final SubscriptionData subscriptionData) {
PullResultExt pullResultExt = (PullResultExt) pullResult;
// ...... 省略其他处理 ......
if (PullStatus.FOUND == pullResult.getPullStatus()) {
// 消息转换成 ByteBUffer
ByteBuffer byteBuffer = ByteBuffer.wrap(pullResultExt.getMessageBinary());
// 解码
List<MessageExt> msgList = MessageDecoder.decodes(byteBuffer);
// ...... 省略过滤处理 ......
// 将再次过滤后的消息 list,保存到 pullResult 中
pullResultExt.setMsgFoundList(msgListFilterAgain);
}
// help gc
pullResultExt.setMessageBinary(null);
return pullResult;
}关键点就是将 byte 数组转码成消费者识别的对象后,保存到新的字段中去了,最后会将字节数组字段置空,help gc,就是 pullResultExt.setMessageBinary(null)
避免使用终结方法和清除方法
终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的。使用终结方法会导致行为不稳定、性能降低,以及可移植性的问题。一般需要避免使用终结方法。在 Java9 中用清除方法(cleaner)代替了终结方法。清除方法没有终结方法那么危险,但仍然是不可预测、运行缓慢的,一般情况也是不建议使用的。
终结方法和清除方法都有的问题:
- 终结方法和清除方法有非常严重的性能损失;
- 终结方法和清除方法不保证会被及时执行,当一个对象被视为不可达时,才会去调用它们的终结方法或清除方法,这段时间是无法控制的,这个现象在不同的 JVM 中的效果也是不一样的;
终结方法独有的问题:
- 调用终结方法的线程是一个低优先级的线程,所以终结方法的执行效率在某些情况下可能并不高;
- 终结方法还有一个问题是,如果忽略在终结过程中被抛出来的未被捕获的异常,该对象的终结过程也会被终止。如果异常发生在终结方法中,并不会打印出栈轨迹,甚至连警告都不会打印出来;
。
try-with-resources 优先于 try-finally
Java 类库中有许多需要通过 close 方法来手动关闭的资源。例如 InputStream、OutputStream 和 java.sql.Connection。但是客户端往往会忘记关闭资源。
try-finally 语句是确保资源关闭的最佳方式,就算发送异常或者返回也会调用 finally 后的语句。但是 try-finally 对关闭多个资源并不友好,如下:
private static final int BUFFER_SIZE = 8 * 1024;
// try-finally is ugly when used with more than one resource! (Page 34)
static void copy(String src, String dst) throws IOException {
InputStream in = new FileInputStream(src);
try {
OutputStream out = new FileOutputStream(dst);
try {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
} finally {
out.close();
}
} finally {
in.close();
}
}Java7 引入了 try-with-resource 的语句。使用这个语句的前提是,要被 close 的资源必须实现 AutoCloseable 接口,其中包含了单个返回 void 的 close 方法。Java 类库和其他第三方类库中,现在都实现或者扩展了 AutoCloseable 接口。如果编写了一个类,它代表的是必须关闭的资源,那么这个类也应该实现 AutoCloseable 接口。上面的案例改造如下:
private static final int BUFFER_SIZE = 8 * 1024;
// try-with-resources on multiple resources - short and sweet (Page 35)
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);
}
}