Guava学习之Splitter

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

使用示例

以下参考:官方文档

Splitter

概述

Java 中关于分词的工具类会有一些古怪的行为。例如:String.split 函数会悄悄地丢弃尾部分割符,而 StringTokenizer 处理5个空格字符串,结果将会什么都没有。

问题:",a,,b,".split(",") 的结果是什么?

  1. "", "a", "", "b", ""
  2. null, "a", null, "b", null
  3. "a", null, "b"
  4. "a", "b"
  5. 以上都不是

正确答案是:5 以上都不是,应该是 "", "a", "", "b"。只有尾随的空字符串被跳过。这样的结果很令人费解。

Splitter 可以让你使用一种非常简单流畅的模式来控制这些令人困惑的行为。

Splitter.on(',')
    .trimResults()
    .omitEmptyStrings()
    .split("foo,bar,,   qux");

以上代码将会返回 Iterable<String> ,包含 "foo"、 "bar"、 "qux"。一个 Splitter 可以通过这些来进行划分:PatterncharStringCharMatcher

如果你希望返回的是 List 的话,可以使用这样的代码 Lists.newArrayList(splitter.split(string))

工厂函数

方法 描述 例子
Splitter.on(char) 基于特定字符划分 Splitter.on(';')
Splitter.on(CharMatcher) 基于某些类别划分 Splitter.on(';')
Splitter.on(String) 基于字符串划分 Splitter.on(CharMatcher.BREAKING_WHITESPACE)
Splitter.on(CharMatcher.anyOf(";,."))
Splitter.on(Pattern)
Splitter.onPattern(String)
基于正则表达式划分 Splitter.on(", ")
Splitter.fixedLength(int) 按指定长度划分,最后部分可以小于指定长度但不能为空 Splitter.fixedLength(3)

修改器

方法 描述 例子
omitEmptyStrings() 移去结果中的空字符串 Splitter.on(',').omitEmptyStrings().split("a,,c,d") 返回 "a", "c", "d"
trimResults() 将结果中的空格删除,等价于trimResults(CharMatcher.WHITESPACE) Splitter.on(',').trimResults().split("a, b, c, d") 返回 "a", "b", "c", "d"
trimResults(CharMatcher) 移除匹配字符 Splitter.on(',').trimResults(CharMatcher.is('_')).split("_a ,_b_ ,c__") 返回 "a ", "b_ ", "c"
limit(int) 达到指定数目后停止字符串的划分 Splitter.on(',').limit(3).split("a,b,c,d") 返回 "a", "b", "c,d"

Splitter.MapSplitter

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

通过 SplitterwithKeyValueSeparator 方法可以获得 Joiner.MapJoiner 对象。

MapSpliter 只有一个公共方法,如下所示。可以看到返回的对象是 Map<String, String>

public Map<String, String> split(CharSequence sequence)

以下代码将返回这样的 Map: {"1":"2", "3":"4"}

Splitter.on("#").withKeyValueSeparator(":").split("1:2#3:4");

需要注意的是,MapSplitter 对键值对格式有着严格的校验,下例会抛出 java.lang.IllegalArgumentException 异常。

Splitter.on("#").withKeyValueSeparator(":").split("1:2#3:4:5"); 

因此,如果希望使用 MapSplitter 来拆分 KV 结构的字符串,需要保证键-值分隔符和键值对之间的分隔符不会称为键或值的一部分。也许是出于类似方面的考虑,MapSplitter 被加上了 @Beta 注解(未来不保证兼容,甚至可能会移除)。所以一般推荐使用 JSON 而不是 MapJoiner + MapSplitter

源码分析

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

Splitter 的实现中有十分明显的策略模式和模板模式,有各种神乎其技的方法覆盖,还有 Guava 久负盛名的迭代技巧和惰性计算。

成员变量

Splitter 类有 4 个成员变量:

  • CharMatcher trimmer:用于描述删除拆分结果的前后指定字符的策略。
  • boolean omitEmptyStrings:用于控制是否删除拆分结果中的空字符串。
  • Strategy strategy:用于帮助实现策略模式。
  • int limit:用于控制拆分的结果个数。

策略模式

Splitter 可以根据字符、字符串、正则、长度还有 Guava 自己的字符匹配器 CharMatcher 来拆分字符串,基本上每种匹配模式的查找方法都不太一样,但是字符拆分的基本框架又是不变的,所以策略模式正好合用。

策略接口的定义很简单,就是传入一个 Splitter 和一个待拆分的字符串,返回一个迭代器。

  private interface Strategy {
    Iterator<String> iterator(Splitter splitter, CharSequence toSplit);
  }

每个工厂函数创建最后都需要去调用基本的私有构造函数。这个创建过程中,主要是提供一个可以创建 Iterator<String>Strategy

  private Splitter(Strategy strategy, boolean omitEmptyStrings, CharMatcher trimmer, int limit);

Splitter on(final CharMatcher separatorMatcher) 创建函数为例,这里返回的是 SplittingIterator (它是个抽象类,继承了 AbstractIterator,而 AbstractIterator 继承了 Iterator)。

  public static Splitter on(final CharMatcher separatorMatcher) {
    checkNotNull(separatorMatcher);
    return new Splitter(
        new Strategy() {
          @Override
          public SplittingIterator iterator(Splitter splitter, final CharSequence toSplit) {
            return new SplittingIterator(splitter, toSplit) {
              @Override
              int separatorStart(int start) {
                return separatorMatcher.indexIn(toSplit, start);
              }

              @Override
              int separatorEnd(int separatorPosition) {
                return separatorPosition + 1;
              }
            };
          }
        });
  }

SplittingIterator 需要覆盖实现 separatorStartseparatorEnd 两个方法才能实例化。这两个方法也是 SplittingIterator 用到的模板模式的重要组成。

惰性迭代器与模板模式

惰性计算目的是要最小化计算机要做的工作,即把计算推迟到不得不算的时候进行。Java中的惰性计算可以参考《你应该更新的 Java 知识之惰性求值:Supplier 和 Guava》

Guava 中的迭代器使用了惰性计算的技巧,它不是一开始就算好结果放在列表或集合中,而是在调用 hasNext 方法判断迭代是否结束时才去计算下一个元素。

AbstractIterator

为了看懂 Guava 的惰性迭代器实现,我们要从 AbstractIterator 开始。

AbstractIterator 使用私有的枚举变量 state 来记录当前的迭代进度,比如是否找到了下一个元素,迭代是否结束等。AbstractIterator 有一个抽象方法 computeNext,负责计算下一个元素。由于 state 是私有变量,而迭代是否结束只有在调用 computeNext 的过程中才知道,于是提供了一个保护的 endOfData 方法,允许子类将 state 设置为 State.DONE

  private enum State {
    READY,
    NOT_READY,
    DONE,
    FAILED,
  }

AbstractIterator 实现了迭代器最重要的两个方法,hasNextnext

hasNext 很容易理解,一上来先判断迭代器当前状态,如果已经结束,就返回 false;如果已经找到下一个元素,就返回 true,不然就试着找找下一个元素。

  @Override
  public final boolean hasNext() {
    checkState(state != State.FAILED);
    switch (state) {
      case READY:
        return true;
      case DONE:
        return false;
      default:
    }
    return tryToComputeNext();
  }

next 则是先判断是否还有下一个元素,属于防御式编程,先对自己做保护;然后把状态复原到还没找到下一个元素,然后返回结果。至于为什么要把 next 置为 null,可能是帮助 JVM 回收对象。

   @Override
    public final T next() {
      if (!hasNext()) {
        throw new NoSuchElementException();
      }
      state = State.NOT_READY;
      T result = next;
      next = null;
      return result;
    }

tryToComputeNext 可以认为是对模板方法 computeNext 的包装调用,首先把状态置为失败,然后才调用 computeNext。这样一来,如果计算下一个元素的过程中发生 RuntimeException,整个迭代器的状态就是 State.FAILED,一旦收到任何调用都会抛出异常。

private boolean tryToComputeNext() {
    state = State.FAILED; // 暂时悲观
    next = computeNext();
    if (state != State.DONE) {
      state = State.READY;
      return true;
    }
    return false;
  }

AbstractIterator 的代码就这些,我们现在知道了它的子类需要覆盖实现 computeNext 方法,然后在迭代结束时调用 endOfData。接下来看看 SplittingIterator 的实现。

SplittingIterator

SplittingIterator 还是一个抽象类,虽然实现了 computeNext 方法,但是它又定义了两个虚函数:

  • separatorStart: 返回分隔符在指定下标之后第一次出现的下标
  • separatorEnd: 返回分隔符在指定下标后面第一个不包含分隔符的下标。

之前的策略模式中我们可以看到,这两个函数在不同的策略中有各自不同的覆盖实现,在 SplittingIterator 中,这两个函数就是模板函数。

接下来看看 SplittingIterator 的核心函数 computeNext,这个函数一直在维护的两个内部全局变量: offsetlimit

  @Override
    protected String computeNext() {
      // 返回的字符串介于上一个分隔符和下一个分隔符之间。
      // nextStart 是返回子串的起始位置,offset 是下次开启寻找分隔符的地方。 
      int nextStart = offset;
      while (offset != -1) {
        int start = nextStart;
        int end;

        // 找 offset 之后第一个分隔符出现的位置
        int separatorPosition = separatorStart(offset);
        if (separatorPosition == -1) {
          // 处理没找到的情况
          end = toSplit.length();
          offset = -1;
        } else {
          // 处理找到的情况
          end = separatorPosition;
          offset = separatorEnd(separatorPosition);
        }
        
        // 处理的是第一个字符就是分隔符的特殊情况
        if (offset == nextStart) {
          // 发生情况:空字符串 或者 整个字符串都没有匹配。
          // offset 需要增加来寻找这个位置之后的分隔符,
          // 但是没有改变接下来返回字符串的 start 的位置,
          // 所以此时它们二者相同。
          offset++;
          if (offset > toSplit.length()) {
            offset = -1;
          }
          continue;
        }

        // 根据 trimmer 来对找到的元素做前处理,比如去除空白符之类的。
        while (start < end && trimmer.matches(toSplit.charAt(start))) {
          start++;
        }
        // 根据 trimmer 来对找到的元素做后处理,比如去除空白符之类的。
        while (end > start && trimmer.matches(toSplit.charAt(end - 1))) {
          end--;
        }
        // 根据需要去除那些是空字符串的元素,trim完之后变成空字符串的也会被去除。
        if (omitEmptyStrings && start == end) {
          // Don't include the (unused) separator in next split string.
          nextStart = offset;
          continue;
        }

        // 判断 limit,
        if (limit == 1) {
          // The limit has been reached, return the rest of the string as the
          // final item. This is tested after empty string removal so that
          // empty Strings do not count towards the limit.
          end = toSplit.length();
          // 调整 end 指针的位置标记 offset 为 -1,下一次再调用 computeNext 
          // 的时候就发现 offset 已经是 -1 了,然后就返回 endOfData 表示迭代结束。
          offset = -1;
          // Since we may have changed the end, we need to trim it again.
          while (end > start && trimmer.matches(toSplit.charAt(end - 1))) {
            end--;
          }
        } else {
          // 还没到 limit 的极限,就让 limit 自减
          limit--;
        }

        return toSplit.subSequence(start, end).toString();
      }
      return endOfData();
    }
  }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容

  • 一:官方教程 网址:http://blog.csdn.net/axi295309066/article/detai...
    漫步_2310阅读 1,334评论 0 1
  • Java8 in action 没有共享的可变数据,将方法和函数即代码传递给其他方法的能力就是我们平常所说的函数式...
    铁牛很铁阅读 1,227评论 1 2
  • 晚上,儿子回来给我说:妈妈,我们班好多同学都谈朋友了。我问他:你有吗?儿子说:没人看上我。儿子有点沮丧,说:我学习...
    微笑的石子妈妈阅读 127评论 0 0
  • 走着走着 就散了 回忆都淡了; 看着看着 就累了 星光也暗了; 听着听着 就醒了 开始埋怨了; 回头发现 你不见了...
    我遇见一棵树阅读 268评论 3 7
  • 每次都有很多的话想说 可是到了嘴边就忘的干净了 感觉活到了高中的那个时候了 很累 不论学习 还是 感情 但是没有那...
    怪了喵的咪阅读 116评论 0 0