创建和销毁对象
1.用静态工厂方法代替构造器
-
使用静态工厂方法代替构造器的好处:
- 可以更清楚的描述返回的对象
- 可以不必每次调用都创建并返回一个新的对象,这样有助于类总能严格控制在某个时刻哪些实例应该存在
- 这种类被称为实例受控的类,实例受控的类可以确保它是一个Singleton或者是不可实例化的
- 可以返回原返回类型的任何子类型的对象
- 根据静态工厂方法的参数值,返回的对象的类可以随着每次调用而产生变化
- 方法返回的对象所属的类,在编写包含该静态方法的类时可以不存在
- 意为当返回你可以在定义静态工厂方法时,返回一个实现某个接口的对象,而不需要提前创建该对象所属的具体类。
- 这种灵活的静态工厂方法构成了服务提供者框架的基础
- 服务提供者框架:将服务的定义与实现相分离,在这种架构中,服务提供者(即实现服务的类)通过提供一个或多个接口,来将服务注册到框架中。而服务消费者可以根据需要从框架中获取相应的服务实现,而无需关注具体的实现类。
-
使用静态工厂方法代替构造器的缺点
- 类如果没有公有(public)或受保护(protected)的构造方法,就不能被子类化
- 但这也许会因祸得福,它会鼓励我们使用复合而不是继承,这正是不可变类型需要的
- 程序员或许很难发现静态构造方法
- 类如果没有公有(public)或受保护(protected)的构造方法,就不能被子类化
-
一些常用的静态工厂方法命名
- form 返回该类型一个相对应的实例
- of 聚合多个参数,返回该类型的一个实例
- valueof
- instance/getInstance 返回的实例是通过方法的参数描述的
- create/newInstance 如上,确保每次调用都返回一个新的实例
- getType Type为返回的对象类型
- newType Type为返回的对象类型,确保每次调用都返回一个新的实例
- type
2.遇到多个构造器参数时要考虑使用构造器
当我们遇到多参数的构造器时,我们经常采取以下措施
-
采取重叠构造器: 这个方法比较常用,但是我们有时依然无法避免在调用时传入本来不想设置的参数,并且当参数的数目增加时,客户端代码会变得很难编写并且难以阅读。
-
使用JavaBeans模式,这种方式创建实例很容易,并且易于阅读,但是这有一个严重的缺点:因为构造过程被分到了几个调用中,在构造过程中JavaBeans可能处于不一致的状态,同时JavaBeans模式与不可变类之间存在冲突,这就需要程序员付出额外的努力来确保它的线程安全。
-
有一种方法,它既能保证像重叠构造器的安全性,也能保证JavaBeans的可读性,它就是建造者模式:
public class OuterClass { private final int requireValue1; private final int requireValue2; private final int simpleValue1; private final int simpleValue2; public static class InnerClass { /* 必须指定的参数 */ private final int requireValue1; private final int requireValue2; /* 可选参数 */ private int simpleValue1; private int simpleValue2; public InnerClass(int requireValue1, int requireValue2) { this.requireValue1 = requireValue1; this.requireValue2 = requireValue2; } InnerClass simpleValue1(int simpleValue1) { this.simpleValue1 = simpleValue1; return this; } InnerClass simpleValue2(int simpleValue2) { this.simpleValue2 = simpleValue2; return this; } public OuterClass build() { return new OuterClass(this); } } private OuterClass(InnerClass builder) { this.requireValue1 = builder.requireValue1; this.requireValue2 = builder.requireValue2; this.simpleValue1 = builder.simpleValue1; this.simpleValue2 = builder.simpleValue2; } @Override public String toString() { return "OuterClass{" + "requireValue1=" + requireValue1 + ", requireValue2=" + requireValue2 + ", simpleValue1=" + simpleValue1 + ", simpleValue2=" + simpleValue2 + '}'; } }
调用:
public static void main(String[] args) { System.out.println( new OuterClass.InnerClass(1,2).simpleValue1(3).simpleValue2(4).build() ); }
Builder模式十分灵活,可以利用单个Builder构建多个对象。简而言之,如果类的构造器或静态工厂中具有多个参数,设计这种类时,Builder模式就是一个不错的选择。
3.用私有构造器或者枚举类型强化Silngleton属性
-
使类成为Singleton会使它的客户端测试变得非常困难
-
难以进行模拟测试:Singleton类的实例在整个应用程序中是唯一的,客户端无法直接创建多个实例。这使得在测试过程中很难对该类进行模拟,尤其是在需要创建多个独立实例的情况下。测试通常需要使用依赖注入或模拟框架来模拟Singleton实例的行为,以确保测试的可重复性和独立性。
-
全局状态的影响:Singleton类的实例在整个应用程序中共享状态。如果测试过程中的一个测试用例修改了Singleton实例的状态,这个修改可能会影响到其他测试用例的结果,导致测试结果的不确定性。测试用例之间的依赖关系变得更加复杂,需要更加谨慎地管理和重置状态,以确保每个测试用例的独立性。
-
难以隔离测试:由于Singleton类的实例在整个应用程序中是全局可访问的,测试一个使用Singleton的类时,很难将其与Singleton实例的其他使用场景隔离开来。这种全局可访问性可能会导致测试用例之间的相互依赖,使得测试的隔离性变差。测试失败时,难以确定是由于被测试类的问题还是Singleton实例的问题。
-
难以重置状态:在某些测试情景下,可能需要在每个测试用例之间重置Singleton实例的状态,以确保每个测试用例都从一个干净的状态开始。然而,由于Singleton实例的全局性质,重置其状态可能会对其他正在进行的测试产生不良影响。
-
实现Singleton有两种常见的方法,这两种方法都要保持构造器为私有的
-
使用公有域方法
public class SingletonTest { public final static SingletonTest INSTANCE = new SingletonTest(); private SingletonTest() { } public void doSomething() { System.out.println("doSomethings"); } public static void main(String[] args) { SingletonTest.INSTANCE.doSomething(); } }
公有域方法的优势在于可以清楚的理解这个类是一个Singleton:共有的静态域是静态的
-
使用静态工厂方法
public class SingleStaticFactoryTest { private final static SingleStaticFactoryTest INSTANCE = new SingleStaticFactoryTest(); private SingleStaticFactoryTest(){} public static SingleStaticFactoryTest getInstance() { return INSTANCE; } public void doSomethings() { System.out.println("doSomethings"); } public static void main(String[] args) { final SingleStaticFactoryTest instance = SingleStaticFactoryTest.getInstance(); instance.doSomethings(); } }
静态工厂方法的优势在于
-
它提供了灵活性,在不改变其API的前提下,我们可以改变该类是否应该为Singleton的想法。
-
可以编写一个泛型Singleton工厂
public class GenericSingletonFactory<T> { private T instance; private Class<T> instanceClass; private GenericSingletonFactory(Class<T> instanceClass){ this.instanceClass = instanceClass; } public T createInstance() { try { final Constructor<T> declaredConstructor = instanceClass.getDeclaredConstructor(); declaredConstructor.setAccessible(true); return declaredConstructor.newInstance(); }catch (Exception e) { e.printStackTrace(); } return null; } public static void main(String[] args) { GenericSingletonFactory<TestEntity> singletonFactory = new GenericSingletonFactory<>(TestEntity.class); final TestEntity instance = singletonFactory.createInstance(); instance.run(); } }
以上代码为chatGpt协助下编写,具体关于泛型Singleton的内容详见第30条
-
可以通过方法引用作为提供者:
final Supplier<SingleStaticFactoryTest> instance = SingleStaticFactoryTest::getInstance; final SingleStaticFactoryTest singleStaticFactoryTest = instance.get(); singleStaticFactoryTest.doSomethings();
-
除非满足以上任何一个优势,还是优先使用公有域方法
-
-
-
为了维护并保证Singleton,必须要声明所有实例域都是瞬时的,并提供一个readResolve方法。否则,每次反序列化一个序列化的实例时,都会创建一个新的实例。
- 关于序列化:将实例域设置为瞬时的(transient ),可以使其不被序列化,当反序列化的时候这些字段将会被初始化为默认值,而不是保留序列化时的值。
- 关于readResolve(): 反序列化时默认执行的方法。为了确保在反序列化时返回同一个实例,Singleton类必须提供一个readResolve方法。在readResolve方法中,可以指定在反序列化时返回的实例。通常情况下,readResolve方法直接返回Singleton类的实例,从而确保反序列化后得到的对象与序列化前的对象是同一个。
-
实现Singleton的第三种方法是声明一个包含一个元素的枚举类型
public enum SingletonEnum { INSTANCE; public void run() { System.out.println("run"); } public static void main(String[] args) { final SingletonEnum instance = SingletonEnum.INSTANCE; instance.run(); } }
这种方式不仅简洁,并且无偿地提供了序列化机制,绝对防止多次实例化。原因如下:
- 枚举常量是静态和最终的:枚举常量在枚举类型中被声明为静态和最终的(
static final
)。这意味着它们在类加载时被初始化,并且无法被修改或重新赋值。 - 枚举类型的构造函数是私有的:枚举类型的构造函数被声明为私有的,因此无法从外部创建枚举类型的实例。
- 编译器生成单例实例:在编译时,Java编译器会自动为枚举类型生成一个唯一的实例。这个实例在枚举类被加载时被创建,且只会被创建一次。
- 枚举常量是静态和最终的:枚举常量在枚举类型中被声明为静态和最终的(
-
4.通过私有构造器强化不可实例化的能力
对于一些工具类,它只包含一些静态方法和静态域,我们不需要也不希望它被实例化,我们可以让这个类包含一个私有构造器,它就不能被实例化。
这种方法的副作用就是它使得一个类不能被子类化。
5.优先考虑使用依赖注入来引用资源
-
首先让我们看一下依赖注入的示例
public class DependencyInjectionTest { private final Object object; public DependencyInjectionTest(Object object) { this.object = Objects.requireNonNull(object); } }
可以看到依赖注入是非常简单的
-
不要用Singleton和静态工具类来实现依赖一个或多个底层资源的类,且该资源的行为会影响到该类的行为。
- 当我们创建一个依赖于一个或多个底层资源的类时,如果我们使用Singleton,会导致:
- 多个地方同时获取 Singleton 实例可能会导致资源状态的不一致性。如果某个地方关闭了资源,其他地方仍然使用的是无效的资源。
- 难以管理和控制底层资源的生命周期。在某些情况下,需要手动释放或重置资源,但 Singleton 实例的生命周期由系统控制,无法灵活地管理资源的状态。
- 如果使用静态工具类,会导致:
- 静态工具类通常是无状态的,无法正确处理底层资源的状态变化。如果底层资源状态发生变化,静态工具类内部的方法无法察觉到这些变化,导致不一致或意外的结果。
- 静态工具类对底层资源的管理能力有限。无法有效地控制资源的生命周期、获取新的资源实例或释放资源。
- 当我们创建一个依赖于一个或多个底层资源的类时,如果我们使用Singleton,会导致:
-
因此,在这种情况下,更好的实现方式是使用依赖注入。
6.避免创建不必要的对象
1. 当你应该重用现有对象的时候,请不要创建新的对象
1. 本条目与第50条“保护性拷贝”相对应
7.消除过期的对象引用
-
**清空对象引用应该是一种例外,而并不是一种规范行为。**消除过期引用的最好办法是让包含该引用的变量结束其生命周期。
-
只要是类自己管理内存,程序员就应该警惕内存泄漏问题。
-
内存泄漏的常见原因
-
无意识的对象保持:一旦对象引用已经过期,只需清空这些引用即可
-
缓存:一旦将对象引用放到缓存中,它就很容易被遗忘,使得它不再有用之后很长一段时间仍然留在缓存中。
-
如果你需要这么一个缓存:只要在缓存之外存在对某个键的引用,该项就有意义,那么就可以使用WeakHashMap代表缓存,当缓存中的键过期之后,它们就会被自动删除。
public static void main(String[] args) throws InterruptedException { final WeakHashMap<String, String> map = new WeakHashMap<>(); String a = new String("111"); String b = "222"; map.put(a,b); System.out.println(map.size()); a = null; System.gc(); TimeUnit.SECONDS.sleep(1); System.out.println(map.size()); }
这里如果你将String a = new String(“111”);替换为String a = “111”,会发现map里的元素不会被清理掉,这是因为通过"111"创建的是字符串常量,因为字符串常量池中的字符串常量是被常量池引用的,而不会被垃圾回收器回收。通过使用
new String("111")
创建的字符串对象不会被添加到字符串常量池中,而是存放在堆内存中。 -
更常见的情况是,我们并不确定“缓存项的生命周期是否有意义”,缓存中的项会随着时间的推移变得越来越没有价值,在这种情况下,缓冲应该时不时清除掉没用的项。这个工作可以交给一个后台线程来完成,也可以通过在给缓存增加新条目的时候顺便清理。对于更加复杂的类,必须直接使用java.lang.ref.
-
-
监听器和其他回调:
- 如果有一个API,客户端在其中注册了监听器,却没有显式的取消注册,他们就会不断堆积(因为常规方法下HashMap的键值是保存的强引用,如果没有显式取消,则对应的键值对就不会被GC回收,从而导致内存泄漏)。
- 因此,我们可以使用WeakHashMap保存键值的弱引用,这样就可以保证当没有键值的强引用时,对应的键值对就会被GC回收。
-
8.避免使用终结方法和清除方法
- 不确定的调用时机:终结方法和清除方法的调用时机是由垃圾回收器决定的,而垃圾回收器的行为是不确定的。这意味着无法精确控制或预测终结方法和清除方法的执行时间。这可能导致在需要及时释放资源或执行特定操作的情况下,无法满足预期的行为。
- 不可靠的执行保证:终结方法的执行不受程序控制,甚至不能保证一定会执行。当对象处于无法访问或被垃圾回收期间,终结方法可能不会被调用。这可能导致资源泄漏和未完成的清理操作。
- 性能影响:终结方法和清除方法的调用可能会导致不必要的延迟和资源消耗。垃圾回收器需要额外的开销来管理和处理这些方法。此外,如果对象的终结方法链较长,可能会导致垃圾回收器的效率下降。
- 安全性问题:终结方法的执行发生在垃圾回收过程中,而垃圾回收器可能在任意时间执行。这导致了安全性问题,因为在终结方法中执行的代码可能与其他并发操作产生竞态条件或不一致的状态。
9.try-with-resources优先于try-finally
- 在处理必须关闭的资源时,始终要优先考虑用try-with-resources,而不是try-finally
- 使用try-finally时,如果在try和finally块中都发生了异常,在这种情况下,finally块中的异常完全抹除了try块中的异常,在异常堆栈中完全没有第一个异常的记录。这使调试变得非常困难。
- 使用try-with-resources的条件
- 必须先实现AutoCloseable接口,其中包含了单个返回void的close方法。
- 使用示例:
try(BufferedReader bufferedReader = new BufferedReader(new FileReader("path"))) {
// 执行相关代码
} catch (IOException e) {
e.printStackTrace();
}