Joiner

我们经常需要将几个字符串,或者字符串数组、列表之类的东西,拼接成一个以指定符号分隔各个元素的字符串,比如把 [1, 2, 3] 拼接成 "1 2 3"。

在 Python 中我只需要简单的调用 str.join 函数,就可以了,就像这样。

  1. ' '.join(map(str, [1, 2, 3]))

到了 Java 中,如果你不知道 Guava 的存在,基本上就得手写循环去实现这个功能,代码瞬间变得丑陋起来。

Guava 为我们提供了一套优雅的 API,让我们能够轻而易举的完成字符串拼接这一简单任务。还是上面的例子,借助 Guava 的 Joiner 类,代码瞬间变得优雅起来。

  1. Joiner.on(' ').join(1, 2, 3);

被拼接的对象集,可以是硬编码的少数几个对象,可以是实现了 Iterable 接口的集合,也可以是迭代器对象。

除了返回一个拼接过的字符串,Joiner 还可以在实现了 Appendable 接口的对象所维护的内容的末尾,追加字符串拼接的结果。

  1. StringBuilder sb = new StringBuilder("result:");
  2. Joiner.on(" ").appendTo(sb, 1, 2, 3);
  3. System.out.println(sb);//result:1 2 3

Guava 对空指针有着严格的限制,如果传入的对象中包含空指针,Joiner 会直接抛出 NPE。与此同时,Joiner 提供了两个方法,让我们能够优雅的处理待拼接集合中的空指针。

如果我们希望忽略空指针,那么可以调用 skipNulls 方法,得到一个会跳过空指针的 Joiner 实例。如果希望将空指针变为某个指定的值,那么可以调用 useForNull 方法,指定用来替换空指针的字符串。

  1. Joiner.on(' ').skipNulls().join(1, null, 3);//1 3
  2. Joiner.on(' ').useForNull("None").join(1, null, 3);//1 None 3

需要注意的是,Joiner 实例是不可变的,skipNulls 和 useForNull 都不是在原实例上修改某个成员变量,而是生成一个新的 Joiner 实例。

Joiner.MapJoiner

MapJoiner 是 Joiner 的内部静态类,用于帮助将 Map 对象拼接成字符串。

  1. Joiner.on("#").withKeyValueSeparator("=").join(ImmutableMap.of(1, 2, 3, 4));//1=2#3=4

withKeyValueSeparator 方法指定了键与值的分隔符,同时返回一个 MapJoiner 实例。有些家伙会往 Map 里插入键或值为空指针的键值对,如果我们要拼接这种 Map,千万记得要用 useForNull 对 MapJoiner 做保护,不然 NPE 妥妥的。

源码分析

源码来自 Guava 18.0。Joiner 类的源码约 450 行,其中大部分是注释、函数重载,常用手法是先实现一个包含完整功能的函数,然后通过各种封装,把不常用的功能隐藏起来,提供优雅简介的接口。这样子的好处显而易见,用户可以使用简单接口解决 80% 的问题,那些罕见而复杂的需求,交给全功能函数去支持。

初始化方法

由于构造函数被设置成了私有,Joiner 只能通过 Joiner#on 函数来初始化。最基础的 Joiner#on 接受一个字符串入参作为分隔符,而接受字符入参的 Joiner#on 方法是前者的重载,内部使用 String#valueOf 函数将字符变成字符串后调用前者完成初始化。或许这是一个利于字符串内存回收的优化。

追加拼接结果

整个 Joiner 类最核心的函数莫过于 <A extends Appendable> Joiner#appendTo(A, Iterator<?>),一切的字符串拼接操作,最后都会调用到这个函数。这就是所谓的全功能函数,其他的一切 appendTo 只不过是它的重载,一切的 join 不过是它和它的重载的封装。

  1. public <A extends Appendable> A appendTo(A appendable, Iterator<?> parts) throws IOException {
  2. checkNotNull(appendable);
  3. if (parts.hasNext()) {
  4. appendable.append(toString(parts.next()));
  5. while (parts.hasNext()) {
  6. appendable.append(separator);
  7. appendable.append(toString(parts.next()));
  8. }
  9. }
  10. return appendable;
  11. }

这段代码的第一个技巧是使用 if 和 while 来实现了比较优雅的分隔符拼接,避免了在末尾插入分隔符的尴尬;第二个技巧是使用了自定义的 toString 方法而不是 Object#toString 来将对象序列化成字符串,为后续的各种空指针保护开了方便之门。

注意到一个比较有意思的 appendTo 重载。

  1. public final StringBuilder appendTo(StringBuilder builder, Iterator<?> parts) {
  2. try {
  3. appendTo((Appendable) builder, parts);
  4. } catch (IOException impossible) {
  5. throw new AssertionError(impossible);
  6. }
  7. return builder;
  8. }

在 Appendable 接口中,append 方法是会抛出 IOException 的。然而 StringBuilder 虽然实现了 Appendable,但是它覆盖实现的 append 方法却是不抛出 IOException 的。于是就出现了明知不可能抛异常,却又不得不去捕获异常的尴尬。

这里的异常处理手法十分机智,异常变量命名为 impossible,我们一看就明白这里是不会抛出 IOException 的。但是如果 catch 块里面什么都不做又好像不合适,于是抛出一个 AssertionError,表示对于这里不抛异常的断言失败了。

另一个比较有意思的 appendTo 重载是关于可变长参数。

  1. public final <A extends Appendable> A appendTo(
  2. A appendable, @Nullable Object first, @Nullable Object second, Object... rest)
  3. throws IOException {
  4. return appendTo(appendable, iterable(first, second, rest));
  5. }

注意到这里的 iterable 方法,它把两个变量和一个数组变成了一个实现了 Iterable 接口的集合,手法精妙!

  1. private static Iterable<Object> iterable(
  2. final Object first, final Object second, final Object[] rest) {
  3. checkNotNull(rest);
  4. return new AbstractList<Object>() {
  5. @Override public int size() {
  6. return rest.length + 2;
  7. }
  8. @Override public Object get(int index) {
  9. switch (index) {
  10. case 0:
  11. return first;
  12. case 1:
  13. return second;
  14. default:
  15. return rest[index - 2];
  16. }
  17. }
  18. };
  19. }

如果是我来实现,可能是简单粗暴的创建一个 ArrayList 的实例,然后把这两个变量一个数组的全部元素放到 ArrayList 里面然后返回。这样子代码虽然短了,但是代价却不小:为了一个小小的重载调用而产生了 O(n) 的时空复杂度。

看看人家 G 社的做法。要想写出这样的代码,需要熟悉顺序表迭代器的实现。迭代器内部维护着一个游标,cursor。迭代器的两大关键操作,hasNext 判断是否还有没遍历的元素,next 获取下一个元素,它们的实现是这样的。

  1. public boolean hasNext() {
  2. return cursor != size();
  3. }
  4. public E next() {
  5. checkForComodification();
  6. try {
  7. int i = cursor;
  8. E next = get(i);
  9. lastRet = i;
  10. cursor = i + 1;
  11. return next;
  12. } catch (IndexOutOfBoundsException e) {
  13. checkForComodification();
  14. throw new NoSuchElementException();
  15. }
  16. }

hasNext 中关键的函数调用是 size,获取集合的大小。next 方法中关键的函数调用是 get,获取第 i 个元素。Guava 的实现返回了一个被覆盖了 size 和 get 方法的 AbstractList,巧妙的复用了由编译器生成的数组,避免了新建列表和增加元素的开销。

空指针处理

当待拼接列表中可能包含空指针时,我们用 useForNull 将空指针替换为我们指定的字符串。它是通过返回一个覆盖了方法的 Joiner 实例来实现的。

  1. public Joiner useForNull(final String nullText) {
  2. checkNotNull(nullText);
  3. return new Joiner(this) {
  4. @Override CharSequence toString(@Nullable Object part) {
  5. return (part == null) ? nullText : Joiner.this.toString(part);
  6. }
  7. @Override public Joiner useForNull(String nullText) {
  8. throw new UnsupportedOperationException("already specified useForNull");
  9. }
  10. @Override public Joiner skipNulls() {
  11. throw new UnsupportedOperationException("already specified useForNull");
  12. }
  13. };
  14. }

首先是使用复制构造函数保留先前初始化时候设置的分隔符,然后覆盖了之前提到的 toString 方法。为了防止重复调用 useForNull 和 skipNulls,还特意覆盖了这两个方法,一旦调用就抛出运行时异常。为什么不能重复调用 useForNull ?因为覆盖了 toString 方法,而覆盖实现中需要调用覆盖前的 toString。

在不支持的操作中抛出 UnsupportedOperationException 是 Guava 的常见做法,可以在第一时间纠正不科学的调用方式。

skipNulls 的实现就相对要复杂一些,覆盖了原先全功能 appendTo 中使用 if 和 while 的优雅实现,变成了 2 个 while 先后执行。第一个 while 找到 第一个不为空指针的元素,起到之前的 if 的功能,第二个 while 功能和之前的一致。

  1. public Joiner skipNulls() {
  2. return new Joiner(this) {
  3. @Override public <A extends Appendable> A appendTo(A appendable, Iterator<?> parts)
  4. throws IOException {
  5. checkNotNull(appendable, "appendable");
  6. checkNotNull(parts, "parts");
  7. while (parts.hasNext()) {
  8. Object part = parts.next();
  9. if (part != null) {
  10. appendable.append(Joiner.this.toString(part));
  11. break;
  12. }
  13. }
  14. while (parts.hasNext()) {
  15. Object part = parts.next();
  16. if (part != null) {
  17. appendable.append(separator);
  18. appendable.append(Joiner.this.toString(part));
  19. }
  20. }
  21. return appendable;
  22. }
  23. @Override public Joiner useForNull(String nullText) {
  24. throw new UnsupportedOperationException("already specified skipNulls");
  25. }
  26. @Override public MapJoiner withKeyValueSeparator(String kvs) {
  27. throw new UnsupportedOperationException("can't use .skipNulls() with maps");
  28. }
  29. };
  30. }

拼接键值对

MapJoiner 实现为 Joiner 的一个静态内部类,它的构造函数和 Joiner 一样也是私有,只能通过 Joiner#withKeyValueSeparator 来生成实例。类似地,MapJoiner 也实现了 appendTo 方法和一系列的重载,还用 join 方法对 appendTo 做了封装。MapJoiner 整个实现和 Joiner 大同小异,在实现中大量使用 Joiner 的 toString 方法来保证空指针保护行为和初始化时的语义一致。

MapJoiner 也实现了一个 useForNull 方法,这样的好处是,在获取 MapJoiner 之后再去设置空指针保护,和获取 MapJoiner 之前就设置空指针保护,是等价的,用户无需去关心顺序问题。

Joiner的更多相关文章

  1. guava之Joiner 和 Splitter

    最近在给客户准备一个Guava的分享,所以会陆续的更新关于Guava更多的细节分享.本文将记录Guava中得字符串处理Joiner(连接)和Splitter(分割)处理. Joiner 首先我们来看看 ...

  2. Mariadb 10.1 joiner节点加入报错WSREP: Failed to prepare for incremental state transfer

    Mariadb 10.1 galera cluster 集群joiner 节点加入集群 会出现这种报错,导致mysql一直点点点,这里我贴出报错.2016年04月19日13:34:58 2016-04 ...

  3. Google Guava学习笔记——基础工具类Joiner的使用

    Guava 中有一些基础的工具类,如下所列: 1,Joiner 类:根据给定的分隔符把字符串连接到一起.MapJoiner 执行相同的操作,但是针对 Map 的 key 和 value. 2,Spli ...

  4. guava之Joiner 和 Splitter(转)

    最近在给客户准备一个Guava的分享,所以会陆续的更新关于Guava更多的细节分享.本文将记录Guava中得字符串处理Joiner(连接)和Splitter(分割)处理. Joiner 首先我们来看看 ...

  5. 优化之Joiner组件

    Joiner组件在运行时需要额外的内存空间处理中间结果,因此会影响性能 可通过查看Joiner performance计数器来决定Joiner组件是否需要优化 通过如下方式优化Joiner组件 将Ma ...

  6. Guava学习笔记(二):基础(Joiner,Objects,Splitter及Strings)

    添加Maven依赖 JoinerTest import com.google.common.base.Joiner; import org.junit.Assert; import org.junit ...

  7. Joiner的用法

    Google Guava提供了Joiner类专门用来连接String. 譬如说有个String数组,里面有"a","b","c",我们可以通 ...

  8. Guava Joiner 拼接字符串

    Joiner Guava 是Google 对Java的内置类型进行增强和扩展的工具. Joiner.on(", ").join(Iterator<> iter) Joi ...

  9. <正则吃饺子> :关于Guava中 Joiner 和 Splitter 的简单使用

    在现在项目中经常看到 这两个类的使用,开始时候不明白具体是做的什么事情,就单独拿出来学习下了,参照了网上的博文,这里主要是简单的讲讲用法. 具体对这两个类,不做过多介绍,有个在线文档,需要的可以自己去 ...

随机推荐

  1. webAPP开发的问题(总结)

    自Iphone和Android这两个牛逼的手机操作系统公布以来,在互联网界从此就多了一个新的名词-WebApp(意为基于WEB形式的应用程序.执行在高端的移动终端设备). 开发人员们都知道在高端智能手 ...

  2. svn Couldn't open rep-cache database

    出现这样的错误原因是因为 问题一 开始出现错误:The database disk image is malformed 解决方案:http://www.cnblogs.com/chen110xi/p ...

  3. 也谈隐藏盘符等windows 的管理员的策略实现

    网上的文章都知道在HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer 文件夹下有控制隐藏驱动器 ...

  4. linux挂载远程windows服务器上的ISO,给内网的服务器安装软件

    原文: http://blog.csdn.net/chagaostu/article/details/45195817 给内网的服务器安装软件 直接用yum install XXX的话,会告知找不到源 ...

  5. 大数据(6) - MapReduce简易介绍入门

    一 MapReduce入门 MapReduce定义(简单来说就是hadoop的数据分析核心,理解其中的原理,则可以分析聚合一切需求) Mapreduce是一个分布式运算程序的编程框架,是用户开发“基于 ...

  6. go的sync.Map

    sync.Map这个数据结构是线程安全的(基本类型Map结构体在并发读写时会panic严重错误),它填补了Map线程不安全的缺陷,不过最好只在需要的情况下使用.它一般用于并发模型中对同一类map结构体 ...

  7. php基本语法之逻辑运算符

    百度经验 | 百度知道 | 百度首页 | 登录 | 注册 新闻 网页 贴吧 知道 经验 音乐 图片 视频 地图 百科 文库 帮助 首页 分类 杂志 任务 签到 回享计划 商城 知道 百度经验 > ...

  8. servelet 直接输出内容

    package helloworld; import java.io.IOException; import javax.servlet.ServletException; import javax. ...

  9. codeforces(559C)--C. Gerald and Giant Chess(组合数学)

    C. Gerald and Giant Chess time limit per test 2 seconds memory limit per test 256 megabytes input st ...

  10. redhat6.5安装postgresql8.4数据库

    Redhat6.5安装postgresql8.4数据库 step1 先移除原有的postgresql数据库(如果有),否则直接跳过 rpm -qa | grep postgresql* rpm -ev ...