作者 | S.L

来源 | http://r6d.cn/abGzy

代码中的很多操作都是Eager的,比如在发生方法调用的时候,参数会立即被求值。总体而言,使用Eager方式让编码本身更加简单,然而使用Lazy的方式通常而言,即意味着更好的效率。

延迟初始化

一般有几种延迟初始化的场景:

  • 对于会消耗较多资源的对象:这不仅能够节省一些资源,同时也能够加快对象的创建速度,从而从整体上提升性能。

  • 某些数据在启动时无法获取:比如一些上下文信息可能在其他拦截器或处理中才能被设置,导致当前bean在加载的时候可能获取不到对应的变量的值,使用 延迟初始化可以在真正调用的时候去获取,通过延迟来保证数据的有效性。

在Java8中引入的lambda对于我们实现延迟操作提供很大的便捷性,如Stream、Supplier等,下面介绍几个例子。

Lambda

Supplier

通过调用get()方法来实现具体对象的计算和生成并返回,而不是在定义Supplier的时候计算,从而达到了延迟初始化的目的。但是在使用 中往往需要考虑并发的问题,即防止多次被实例化,就像Spring的@Lazy注解一样。

public class Holder {// 默认第一次调用heavy.get()时触发的同步方法private Supplier<Heavy> heavy = () -> createAndCacheHeavy(); public Holder() {System.out.println("Holder created");}public Heavy getHeavy() {// 第一次调用后heavy已经指向了新的instance,所以后续不再执行synchronizedreturn heavy.get(); }//...private synchronized Heavy createAndCacheHeavy() {// 方法内定义class,注意和类内的嵌套class在加载时的区别class HeavyFactory implements Supplier<Heavy> {// 饥渴初始化private final Heavy heavyInstance = new Heavy(); public Heavy get() {// 每次返回固定的值return heavyInstance; } }//第一次调用方法来会将heavy重定向到新的Supplier实例if(!HeavyFactory.class.isInstance(heavy)) {heavy = new HeavyFactory();}return heavy.get();}
}

当Holder的实例被创建时,其中的Heavy实例还没有被创建。下面我们假设有三个线程会调用getHeavy方法,其中前两个线程会同时调用,而第三个线程会在稍晚的时候调用。

当前两个线程调用该方法的时候,都会调用到createAndCacheHeavy方法,由于这个方法是同步的。因此第一个线程进入方法体,第二个线程开始等待。在方法体中会首先判断当前的heavy是否是HeavyInstance的一个实例。如果不是,就会将heavy对象替换成HeavyFactory类型的实例。显然,第一个线程执行判断的时候,heavy对象还只是一个Supplier的实例,所以heavy会被替换成为HeavyFactory的实例,此时heavy实例会被真正的实例化。等到第二个线程进入执行该方法时,heavy已经是HeavyFactory的一个实例了,所以会立即返回(即heavyInstance)。当第三个线程执行getHeavy方法时,由于此时的heavy对象已经是HeavyFactory的实例了,因此它会直接返回需要的实例(即heavyInstance),和同步方法createAndCacheHeavy没有任何关系了。

以上代码实际上实现了一个轻量级的虚拟代理模式(Virtual Proxy Pattern)。保证了懒加载在各种环境下的正确性。

还有一种基于delegate的实现方式更好理解一些(github):

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Supplier;public class MemoizeSupplier<T> implements Supplier<T> {final Supplier<T> delegate;ConcurrentMap<Class<?>, T> map = new ConcurrentHashMap<>(1);public MemoizeSupplier(Supplier<T> delegate) {this.delegate = delegate;}@Overridepublic T get() {// 利用computeIfAbsent方法的特性,保证只会在key不存在的时候调用一次实例化方法,进而实现单例return this.map.computeIfAbsent(MemoizeSupplier.class,k -> this.delegate.get());}public static <T> Supplier<T> of(Supplier<T> provider) {return new MemoizeSupplier<>(provider);}
}

以及一个更复杂但功能更多的CloseableSupplier:

public static class CloseableSupplier<T> implements Supplier<T>, Serializable {private static final long serialVersionUID = 0L;private final Supplier<T> delegate;private final boolean resetAfterClose;private volatile transient boolean initialized;private transient T value;private CloseableSupplier(Supplier<T> delegate, boolean resetAfterClose) {this.delegate = delegate;this.resetAfterClose = resetAfterClose;}public T get() {// 经典Singleton实现if (!(this.initialized)) { // 注意是volatile修饰的,保证happens-before,t一定实例化完全synchronized (this) {if (!(this.initialized)) { // Double Lock CheckT t = this.delegate.get();this.value = t;this.initialized = true;return t;}}}// 初始化后就直接读取值,不再同步抢锁return this.value;}public boolean isInitialized() {return initialized;}public <X extends Throwable> void ifPresent(ThrowableConsumer<T, X> consumer) throws X {synchronized (this) {if (initialized && this.value != null) {consumer.accept(this.value);}}}public <U> Optional<U> map(Function<? super T, ? extends U> mapper) {checkNotNull(mapper);synchronized (this) {if (initialized && this.value != null) {return ofNullable(mapper.apply(value));} else {return empty();}}}public void tryClose() {tryClose(i -> { });}public <X extends Throwable> void tryClose(ThrowableConsumer<T, X> close) throws X {synchronized (this) {if (initialized) {close.accept(value);if (resetAfterClose) {this.value = null;initialized = false;}}}}public String toString() {if (initialized) {return "MoreSuppliers.lazy(" + get() + ")";} else {return "MoreSuppliers.lazy(" + this.delegate + ")";}}}

Stream

Stream中的各种方法分为两类:

  • 中间方法(limit()/iterate()/filter()/map())

  • 结束方法(collect()/findFirst()/findAny()/count())

前者的调用并不会立即执行,只有结束方法被调用后才会依次从前往后触发整个调用链条。但是需要注意,对于集合来说,是每一个元素依次按照处理链条执行到尾,而不是每一个中间方法都将所有能处理的元素全部处理一遍才触发 下一个中间方法。比如:

List<String> names = Arrays.asList("Brad", "Kate", "Kim", "Jack", "Joe", "Mike");final String firstNameWith3Letters = names.stream().filter(name -> length(name) == 3).map(name -> toUpper(name)).findFirst().get();System.out.println(firstNameWith3Letters);

当触发findFirst()这一结束方法的时候才会触发整个Stream链条,每个元素依次经过filter()->map()->findFirst()后返回。所以filter()先处理第一个和第二个后不符合条件,继续处理第三个符合条件,再触发map()方法,最后将转换的结果返回给findFirst()。所以filter()触发了3次,map()触发了1次。

好,让我们来看一个实际问题,关于无限集合。

Stream类型的一个特点是:它们可以是无限的。这一点和集合类型不一样,在Java中的集合类型必须是有限的。Stream之所以可以是无限的也是源于Stream「懒」的这一特点。

Stream只会返回你需要的元素,而不会一次性地将整个无限集合返回给你。

Stream接口中有一个静态方法iterate(),这个方法能够为你创建一个无限的Stream对象。它需要接受两个参数:

public static Stream iterate(final T seed, final UnaryOperator f)

其中,seed表示的是这个无限序列的起点,而UnaryOperator则表示的是如何根据前一个元素来得到下一个元素,比如序列中的第二个元素可以这样决定:f.apply(seed)。

下面是一个计算从某个数字开始并依次返回后面count个素数的例子:

public class Primes {public static boolean isPrime(final int number) {return number > 1 &&// 依次从2到number的平方根判断number是否可以整除该值,即divisorIntStream.rangeClosed(2, (int) Math.sqrt(number)).noneMatch(divisor -> number % divisor == 0);}private static int primeAfter(final int number) {if(isPrime(number + 1)) // 如果当前的数的下一个数是素数,则直接返回该值return number + 1;else // 否则继续从下一个数据的后面继续找到第一个素数返回,递归return primeAfter(number + 1);}public static List<Integer> primes(final int fromNumber, final int count) {return Stream.iterate(primeAfter(fromNumber - 1), Primes::primeAfter).limit(count).collect(Collectors.<Integer>toList());}//...
}

对于iterate和limit,它们只是中间操作,得到的对象仍然是Stream类型。对于collect方法,它是一个结束操作,会触发中间操作来得到需要的结果。

如果用非Stream的方式需要面临两个问题:

  • 一是无法提前知晓fromNumber后count个素数的数值边界是什么

  • 二是无法使用有限的集合来表示计算范围,无法计算超大的数值

即不知道第一个素数的位置在哪儿,需要提前计算出来第一个素数,然后用while来处理count次查找后续的素数。可能primes方法的实现会拆成两部分,实现复杂。如果用Stream来实现,流式的处理,无限迭代,指定截止条件,内部的一套机制可以保证实现和执行都很优雅。

喜欢本文的朋友,欢迎点击下方卡片

关注我,订阅更多精彩内容

往期推荐

不容错过的灰度发布系统架构设计

还在封装各种 Util 工具类?这个神级框架帮你解决所有问题!

阿里开源台柱 Ant Design 源码仓库被删了...

GitHub最最最火的开源爬虫工具箱,一爬就取

明天即将开工,把今年的Flag加到头像上,时刻鞭策自己吧!

情人节微信红包数据公布,你离海王与海后有多远...

Java延迟加载的最佳实践应用示例!相关推荐

  1. 创建设计模式 - Singleton设计模式(最佳实践与示例)

    Java Singleton设计模式最佳实践与示例 Java Singleton Pattern是四种帮派设计模式之一,属于创建设计模式类别.从定义来看,它似乎是一个非常简单的设计模式,但是当涉及到实 ...

  2. 编写高性能Java代码的最佳实践

    编写高性能Java代码的最佳实践 摘要:本文首先介绍了负载测试.基于APM工具的应用程序和服务器监控,随后介绍了编写高性能Java代码的一些最佳实践.最后研究了JVM特定的调优技巧.数据库端的优化和架 ...

  3. 高性能Java代码的最佳实践

    高性能Java代码的最佳实践 前言 在这篇文章中,我们将讨论几个有助于提升Java应用程序性能的方法.我们首先将介绍如何定义可度量的性能指标,然后看看有哪些工具可以用来度量和监控应用程序性能,以及确定 ...

  4. Java Bean Validation 最佳实践

    <h1 class="postTitle"><a id="cb_post_title_url" class="postTitle2& ...

  5. java查询枚举_查找Java枚举的最佳实践

    查找Java枚举的最佳实践 我们有一个REST API,客户端可以在其中提供表示Java Enums中服务器上定义的值的参数. 因此,我们可以提供一个描述性错误,我们将此valueOf(..)方法添加 ...

  6. java replaceall正则表达式_编写高性能Java代码的最佳实践

    作者:Eugen Paraschiv 翻译:雁惊寒https://dzone.com 摘要:本文首先介绍了负载测试.基于APM工具的应用程序和服务器监控,随后介绍了编写高性能Java代码的一些最佳实践 ...

  7. singleton设计模式_Java Singleton设计模式最佳实践与示例

    singleton设计模式 Java Singleton Pattern is one of the Gangs of Four Design patterns and comes in the Cr ...

  8. Java日志管理最佳实践

    原文出处:http://www.ibm.com/developerworks/cn/java/j-lo-practicelog/. 感谢原作者,感谢ibm网站,里面有好多的精华帖. 日志记录是应用程序 ...

  9. 编写高性能 Java 代码的最佳实践

    摘要:本文首先介绍了负载测试.基于APM工具的应用程序和服务器监控,随后介绍了编写高性能Java代码的一些最佳实践.最后研究了JVM特定的调优技巧.数据库端的优化和架构方面的调整.以下是译文. 介绍 ...

最新文章

  1. mysql> select file,domain,alias,valid from tbl_check where file=‘ecloud_0824-0830.csv‘ into outfile
  2. android100 自定义内容提供者
  3. MySQL--4操作数据表中的记录小结
  4. Android --- 怎么设置 EditText 控件中光标默认位置,当 EditText 里有文字的时候,光标跑到了最前面
  5. MySQL之alter和upate
  6. Vue.js之组件及其易错点
  7. 按钮图片拉伸_图片墙有多香?高手都在用的PPT封面制作技巧!
  8. res.status === 200含义
  9. golang package 是什么意思?一份来自初学者的golang package体验指南
  10. 三层BP神经网络的python实现
  11. 中国移动游戏市场全球占比31.6% 掌趣科技入围竞争力企业前20
  12. libvirt Installation
  13. 解析器 java_Java高性能解析器实现思路及方法学习
  14. c语言以空格分割字符串_如何统计字符串中单词的个数?
  15. 修改Chrome默认搜索引擎
  16. 【渝粤教育】电大中专电子商务网站建设与维护 (13)作业 题库
  17. libcurl的封装,支持同步异步请求,支持多线程下载,支持https
  18. Hadoop 入门笔记
  19. python人流热力图_高德地图热力图插件实现人流量监控,如何实现人流数据实时刷新...
  20. 数仓(一)简介数仓,OLTP和OLAP

热门文章

  1. Silverlight 4正式版发布
  2. python flask 返回值 状态码 设置
  3. linux shell 逻辑判断 [] [[]] -n -z 用法区别
  4. Composer PHP依赖管理
  5. linux 查看磁盘空间 文件 文件夹 大小
  6. Java Servlet完全教程
  7. eclipse运行android项目出现The connection to adb is down, and a severe error has occured.的问题
  8. Linux下使用Eclipse开发C/C++程序
  9. Shell中的数据重定向--输入/输出重定向
  10. Linux C编程--进程间通信(IPC)2--信号处理函数