Guava学习之Joiner

本文是对 Guava 中 Joiner 的学习介绍。欢迎加入学习项目: LearningGuava

使用示例

以下参考:官方文档

开发过程中,用分隔符连接字符串序列可能是一个比较繁琐的过程,但本不应该如此。Joiner 可以简化这个操作。

如果序列中包含 null 值,那么可以使用 Joiner 跳过 null 值:

    // 跳过 null 值
    result = Joiner.on("; ").skipNulls().join("Harry", null, "Ron", "Hermione");
    Assert.assertEquals(result, "Harry; Ron; Hermione");

也可以通过 useForNull(String) 来将 null 值替换为指定的字符串。

    // 替换 null 值
    result = Joiner.on("; ").useForNull("null").join("Harry", null, "Ron", "Hermione");
    Assert.assertEquals(result, "Harry; null; Ron; Hermione");

同样可以在对象上使用 Joiner,最终会调用对象的 toString() 方法。

    // 使用在对象上,会调用对象的 toString() 函数
    result = Joiner.on(",").join(Arrays.asList(1, 5, 7));
    Assert.assertEquals(result, "1,5,7");

对于 Map ,可以使用这样的代码:

    // MapJoiner 的使用,将 map 转换为字符串
    Map map = ImmutableMap.of("k1", "v1", "k2", "v2");
    result = Joiner.on("; ").withKeyValueSeparator("=").join(map);
    Assert.assertEquals(result, "k1=v1; k2=v2");

源码分析

以下参考:Guava 是个风火轮之基础工具(1)

初始化方法

Joiner 的构造方法被设置成了私有,需要通过静态的 on(String separator) 或者 on(char separator) 函数初始化。

拼接基本函数

Joiner 了中最为核心的函数就是 <A extends Appendable> A appendTo(A appendable, Iterator<?> parts)。作为全功能函数,其它所有的字符串拼接最终都会调用这个函数。

  public <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 源码中传入的是实现该接口的 StringBuilder
  • 因为是公共方法,无法保证 appendable 值不为空,所以要先检查该值是否为空。
  • if ... while ... 的结构确保末尾不会添加多余的分隔符。
  • 通过本地 toString 方法,而不是直接调用对象的 toString 方法,这种做法提供了空指针保护。

不可能发生的异常

在源码中,有个地方的处理值得关注一下:

   public StringBuilder appendTo(StringBuilder builder, Iterator<? extends Entry<?, ?>> entries) {
      try {
        appendTo((Appendable) builder, entries);
      } catch (IOException impossible) {
        throw new AssertionError(impossible);
      }
      return builder;
    }

这里之所以 IOException 的变量名取名为 impossible 是因为:虽然 Appendable 接口的 append 方法会抛出 IOException,但是传入的 StringBuilder 在实现的时候并不会抛出改异常,所以为了适应这个接口,这里不得不捕捉异常。这样捕捉后的断言处理也就可以理解了。

巧妙的可变长参数转换

有一个添加的重载函数如下所示:

 public final <A extends Appendable> A appendTo(
      A appendable, @NullableDecl Object first, @NullableDecl Object second, Object... rest)
      throws IOException {
    return appendTo(appendable, iterable(first, second, rest));
  }

其中 iterable 函数将参数变为一个可以迭代的序列,该函数如下所示。

private static Iterable<Object> iterable(
      final Object first, final Object second, final Object[] rest) {
    checkNotNull(rest);
    return new AbstractList<Object>() {
      @Override
      public int size() {
        return rest.length + 2;
      }

      @Override
      public Object get(int index) {
        switch (index) {
          case 0:
            return first;
          case 1:
            return second;
          default:
            return rest[index - 2];
        }
      }
    };
  }

通过实现 AbstractList 的方式,巧妙地复用了编译器生成的数组,减少了对象创建的开销。这样的实现需要对迭代器有深入的了解,因为要确保实现能够满足迭代器接口各个函数的语义。

Joiner 二次创建

因为 Joiner 创建后就是不可更改的了,所以为了实现 useForNullskipNulls 等语义,源码会再次创建一个匿名类,并覆盖相应的方法。

useForNull 函数汇中为了防止重复调用 useForNullskipNulls,还特意覆盖了这两个方法,一旦调用就抛出运行时异常。为什么不能重复调用 useForNull ?因为覆盖了 toString 方法,而覆盖实现中需要调用覆盖前的 toString

  public Joiner useForNull(final String nullText) {
    checkNotNull(nullText);
    return new Joiner(this) {
      @Override
      CharSequence toString(@NullableDecl Object part) {
        return (part == null) ? nullText : Joiner.this.toString(part);
      }

      @Override
      public Joiner useForNull(String nullText) {
        throw new UnsupportedOperationException("already specified useForNull");
      }

      @Override
      public Joiner skipNulls() {
        throw new UnsupportedOperationException("already specified useForNull");
      }
    };
  }

skipNulls 函数实现如下所示。个人比较奇怪的是 skipNulls 中为什么不禁止重复调用 skipNulls 函数。

  public Joiner skipNulls() {
    return new Joiner(this) {
      @Override
      public <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;
      }

      @Override
      public Joiner useForNull(String nullText) {
        throw new UnsupportedOperationException("already specified skipNulls");
      }

      @Override
      public MapJoiner withKeyValueSeparator(String kvs) {
        throw new UnsupportedOperationException("can't use .skipNulls() with maps");
      }
    };
  }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • Scala与Java的关系 Scala与Java的关系是非常紧密的!! 因为Scala是基于Java虚拟机,也就是...
    灯火gg阅读 3,489评论 1 24
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,991评论 19 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,767评论 18 399
  • 面向对象主要针对面向过程。 面向过程的基本单元是函数。 什么是对象:EVERYTHING IS OBJECT(万物...
    sinpi阅读 1,096评论 0 4
  • 做个不纠结的人 受家里环境以及父母的教育影响,在买东西上面我一直是个比较纠结的人,我妈喜欢一个东西喜欢的不行,总是...
    桃紙是一種餅阅读 269评论 0 0