我们在码砖的过程中,经常会遇到List转字符串、字符串转List这类需求,当然这不仅仅是单纯的转字符串,而是加入了一个连接符。比如:将一个list转换成以","分隔的字符传,这个时候仅仅使用list.toString()是做不到的。初级的猩猩会想到循环list,然后用StringBuilder来拼装字符串,这样最后一般会多一个字符,再切分。大概代码如下:

     List<String> list = new ArrayList<String>();list.add("a");list.add("b");list.add("c");String separator = ",";StringBuilder stringBuilder = new StringBuilder();list.forEach(str -> {if (str != null)stringBuilder.append(str).append(separator);});stringBuilder.setLength(stringBuilder.length() - delimiter.length());System.out.println(stringBuilder.toString());

说句题外话,面试当中也问过很多人,就上面这段代码来说,如果你还在用substring或者干脆用String+String。那只能说你写代码写的真是太随意(Low)了!

上面代码实现了list转string的功能,并且按照逗号分隔,但是大家也看到了,实现起来代码量还是很多的,这对于工程来说并不友好,所以中级的猩猩一般会使用Guava工具中的Joiner来帮助我们实现这一功能。具体代码如下:

     List<String> list = new ArrayList<String>();list.add("a");list.add("b");list.add("c");String separator = ",";String result = Joiner.on(separator).join(list);System.out.println(result);

使用Joiner,我们可以实现输出a,b,c。功能已经实现,现在进入本篇文章的核心,这段代码其实没有任何问题,但是这么写是有一个隐藏的bug,而且你怎么测试一般都测不出来。高级的猩猩,经验丰富,写代码的时候会很自然的绕过去,但是初级和中级的可能就会留下坑。

上述代码在实际应用中肯定不会是这样写,大多数情况是传进来一个list变量或者调用方法返回一个list,数据来源可能是数据库或其他,list的内容、长度其实对我们来说并不那么直观。假如,数据库中存在一个null值,这个null值最终被传入list中,那么再执行这段代码会怎样呢?

     list.add(null);

再次执行上面的代码会发现,程序报错了:

Exception in thread "main" java.lang.NullPointerExceptionat com.google.common.base.Preconditions.checkNotNull(Preconditions.java:770)at com.google.common.base.Joiner.toString(Joiner.java:454)at com.google.common.base.Joiner.appendTo(Joiner.java:109)at com.google.common.base.Joiner.appendTo(Joiner.java:154)at com.google.common.base.Joiner.join(Joiner.java:197)at com.google.common.base.Joiner.join(Joiner.java:187)at com.wf.test.main(test.java:62)

这个报错是checkNotNull判断空指针异常,为什么会null指针呢,就是因为我插入了一个null值,是这个null导致的嘛?答案是的,请注意我文章开头的那段代码,循环list的中是有一个非null判断的,所以,我们是要屏蔽掉null值得,但是我一个初级猩猩都能想到得问题Google那帮大神难道会忽略掉这个情况?那你可能是想多了。但是为什么没有屏蔽掉null值,反而报错了呢?

下面我们开始分析Joiner源码

首先我们调用Joiner.on(delimiter).join(list);on方法是返回一个初始化得Joiner

  public static Joiner on(String separator) {return new Joiner(separator);}

并且初始化Joiner得时候将分隔符传参进去,注意这里得checkNotNull判断,判断的是传入得分隔符,而不是list

  private Joiner(String separator) {this.separator = checkNotNull(separator);}

回到第一步,join方法,传入list,并使用迭代器

  public final String join(Iterable<?> parts) {return join(parts.iterator());}

进入join方法,初始化一个StringBuilder

  public final String join(Iterator<?> parts) {return appendTo(new StringBuilder(), parts).toString();}

进入appendTo方法,开始进行转换。好吧,还没开始转换

  @CanIgnoreReturnValuepublic final StringBuilder appendTo(StringBuilder builder, Iterator<?> parts) {try {appendTo((Appendable) builder, parts);} catch (IOException impossible) {throw new AssertionError(impossible);}return builder;}

再次进入appendTo方法,这次开始转换了

  @CanIgnoreReturnValuepublic <A extends Appendable> A appendTo(A appendable, Iterator<?> parts) throws IOException {checkNotNull(appendable);if (parts.hasNext()) {appendable.append(toString(parts.next()));while (parts.hasNext()) {appendable.append(separator);appendable.append(toString(parts.next()));}}return appendable;}

看上去任然没有任何毛病,根据上面得报错,我们可以发现是appendable.append(toString(parts.next()));这行报错,并且是toString这个方法

  CharSequence toString(Object part) {checkNotNull(part); // checkNotNull for GWT (do not optimize).return (part instanceof CharSequence) ? (CharSequence) part : part.toString();}

好的,终于发现你了,就是checkNotNull(part); 这里报出了null指针,从上面代码我们可以看出,这里判断得part就是list中的一个元素,因为我们插入了一个null值,所以这里报错了。但是有的猩猩就说了,我明明判断while (parts.hasNext()),既然是null值为什么还是会进入循环,关于这个问题这里就不详细说了,百度一堆一堆的。

从上面的源码可以看出,对于list中的null元素问题,Joiner是没有进行过判断的,就是说如果我们的list中有null值,使用Joiner就会出现这个异常。但是我也说了,Google的大神未必多牛逼但肯定不会比你菜,他们肯定考虑到这个问题,所以如果我们享用Joiner并且避免这种情况,我们应该这样使用:

Joiner.on(separator).skipNulls().join(list);

加一个skipNulls()方法来跳过null值,我们来看skipNulls方法:

  public Joiner skipNulls() {return new Joiner(this) {@Overridepublic <A extends Appendable> A appendTo(A appendable, Iterator<?> parts) throws IOException {checkNotNull(appendable, "appendable");checkNotNull(parts, "parts");while (parts.hasNext()) {Object part = parts.next();if (part != null) {appendable.append(Joiner.this.toString(part));break;}}while (parts.hasNext()) {Object part = parts.next();if (part != null) {appendable.append(separator);appendable.append(Joiner.this.toString(part));}}return appendable;}@Overridepublic Joiner useForNull(String nullText) {throw new UnsupportedOperationException("already specified skipNulls");}@Overridepublic MapJoiner withKeyValueSeparator(String kvs) {throw new UnsupportedOperationException("can't use .skipNulls() with maps");}};}

这里我们可以看到又初始化了一个Joiner,并且重写了appendTo方法,在这个appendTo方法中,加入了part != null的判断,这样就完美的解决null值问题了。

总结:为什么说这个问题是一个bug呢。因为我们不使用skipNulls,一样可以实现我们的功能,甚至测试过程中也不会出现问题,因为我们的数据中有没有null值谁也不清楚,如果碰巧测试数据没有空值,那这个问题就不会被发现,并且,这个bug一旦拿到线上,查找起来也是要消耗一定功夫的。我相信大部分的人都知道使用Joiner,但是知道使用skipNulls的应该不多,非常繁忙之中写下这篇博客希望能够帮到大家。

更多文章关注公众号

教你如何写Bug:Google Guava源码分析之——Joiner相关推荐

  1. google protobuf源码分析1

    突然来了兴趣,相分析它的源码,找最简单的开始读: 里面有actomicops.h里面写了几个线程安全的交换函数,发现只在一个地方用了: void GoogleOnceInitImpl(Protobuf ...

  2. Guava 源码分析(Cache 原理)

    作者:crossoverJie's Blog 来源:https://crossoverjie.top/2018/06/13/guava/guava-cache/ 前言 Google 出的 Guava  ...

  3. Guava源码分析——Immutable Collections(4)

    Immutable的集合体系,还有中很重要的集合没有介绍,就是ImmutableMap,通过UML图,可以看出ImmutableMap的结构体系. 首先来看一下ImmutableBiMap,因为普通I ...

  4. Spring IOC 容器源码分析 - 获取单例 bean

    1. 简介 为了写 Spring IOC 容器源码分析系列的文章,我特地写了一篇 Spring IOC 容器的导读文章.在导读一文中,我介绍了 Spring 的一些特性以及阅读 Spring 源码的一 ...

  5. java channel源码_Netty 4.0 源码分析(三):Channel和ChannelPipeline

    Client和server通过Channel连接,然后通过ByteBuf进行传输.每个Channel有自己的Pipeline,Pipeline上面可以添加和定义Handler和Event. Chann ...

  6. Spring Core Container 源码分析七:注册 Bean Definitions

    前言 原本以为,Spring 通过解析 bean 的配置,生成并注册 bean defintions 的过程不太复杂,比较简单,不用单独开辟一篇博文来讲述:但是当在分析前面两个章节有关 @Autowi ...

  7. 一文给你解决linux内存源码分析- SLUB分配器概述(超详细)

    SLUB和SLAB的区别 首先为什么要说slub分配器,内核里小内存分配一共有三种,SLAB/SLUB/SLOB,slub分配器是slab分配器的进化版,而slob是一种精简的小内存分配算法,主要用于 ...

  8. 云客Drupal源码分析之数据库系统及其使用

    在开始本主题前请允许一点点题外话: 在我写这个博客的时候(2016年10月28日),<Begining Drupal 8>这本书已经翻译完成并做成了PDF格式供给大家免费下载,这是一本引导 ...

  9. 云客Drupal源码分析之前言

    Drupal是一个非常优秀的网站系统,可以说她是一个网站应用开发框架,也可以说是一个cms,她在世界范围内被广泛使用,最为人所知的是美国白宫.联合国等知名机构的官方网站使用了她,随着Drupal8的来 ...

  10. vue3源码分析——看看complier是怎么来解析的

    引言 <<往期回顾>> vue3源码分析--手写diff算法 vue3源码分析--实现组件更新 vue3源码分析--解密nextTick的实现 想知道vue3-complier ...

最新文章

  1. Linux——POSIX有名信号量
  2. Keepalived+HAProxy基于读写分离方式实现discuz论坛
  3. [Spring5]IOC容器_Bean管理XML方式_注入集合类型属性
  4. 【渝粤教育】国家开放大学2018年春季 0004-21T有机合成单元反应 参考试题
  5. SX1268与SX1278、SX1276对比分析以及选型南
  6. 常见的linux命令及其翻译
  7. linux一步一脚印---more、less、head、tail
  8. OneNote怎样显示或者隐藏网格线
  9. HAproxy编译安装
  10. 那个一年发四篇Cell的研究生,后来怎么样了?
  11. AWS RDS强制升级的应对之道——版本升级的最佳实践
  12. 图解Visual Studio 2010中的UML建模功能
  13. NSString删除换行符号
  14. vivado中的OOC技术
  15. (秒杀项目) 4.2 用户登录和注册
  16. Atitit 存储引擎核心技术 总结目录1. 表的存储有三个文件:结构+数据+索引 12. 页式管理
  17. XRD的检出限是多少?如何检测含量极低的物质?
  18. 室温金刚石共聚焦平台
  19. Mstar数据集的获取和使用
  20. threejs道路贴图动画

热门文章

  1. 指数族分布(2):矩母函数、累积量生成函数
  2. 嵌入式数据结构以及算法(数据结构篇)
  3. 统计假设检验之显著性检验(significance test)
  4. 元转万元单位换算_元要以万元为单位要怎么换算
  5. 【推荐】实现跟随鼠标移动的浮动提示框、气泡框、Tip效果
  6. 服务器上系统使用排行,服务器操作系统使用排行榜
  7. 经济机器是如何运行的
  8. 汽车电子学习笔记---CAN网络(二)
  9. Guass消元 poj 1830
  10. java ppt转图片,怎么用POI将PPT的内容转换为图片