Guava 是个风火轮之基础工具(2)

前言

Guava 是 Java 开发者的好朋友。虽然我在开发中使用 Guava 很长时间了,Guava API 的身影遍及我写的生产代码的每个角落,但是我用到的功能只是 Guava 的功能集中一个少的可怜的真子集,更别说我一直没有时间认真的去挖掘 Guava 的功能,没有时间去学习 Guava 的实现。直到最近,我开始阅读 Getting Started with Google Guava,感觉有必要将我学习和使用 Guava 的一些东西记录下来。

Splitter

Guava 提供了 Joiner 类用于将多个对象拼接成字符串,如果我们需要一个反向的操作,就要用到 Splitter 类。Splitter 能够将一个字符串按照指定的分隔符拆分成可迭代遍历的字符串集合,Iterable<String>

Splitter 的 API 和 Joiner 类似,使用 Splitter#on 指定分隔符,使用 Splitter#split 完成拆分。

Splitter.on(' ').split("1 2 3");//["1", "2", "3"]

Splitter 还支持使用正则表达式来描述分隔符。

Splitter.onPattern("\\s+").split("1 \t   2 3");//["1", "2", "3"]

Splitter 还支持根据长度来拆分字符串。

Splitter.fixedLength(3).split("1 2 3");//["1 2", " 3"]

Splitter.MapSplitter

与 Joiner.MapJoiner 相对,Splitter.MapSplitter 用来拆分被拼接了的 Map 对象,返回Map<String, String>

Splitter.on("#").withKeyValueSeparator(":").split("1:2#3:4");//{"1":"2", "3":"4"}

需要注意的是,不是所有由 MapJoiner 拼接出来的字符串,都能够被 MapSplitter 拆分,MapSplitter 对键值对个格式有着严格的校验。比如下面的拆分会抛出异常。

Splitter.on("#").withKeyValueSeparator(":").split("1:2#3:4:5");
//java.lang.IllegalArgumentException: Chunk [3:4:5] is not a valid entry

因此,如果希望使用 MapSplitter 来拆分 KV 结构的字符串,需要保证键-值分隔符和键值对之间的分隔符不会称为键或值的一部分。也许是出于类似方面的考虑,MapSplitter 被加上了 @Beta 注解,也许在不久的将来它会被移除,或者有大的变化。如果在应用中有可能用到 KV 结构的字符串,我一般推荐使用 JSON 而不是 MapJoiner + MapSplitter。

源码分析

源码来自 Guava 18.0。Splitter 类源码约 600 行,依旧大部分是注释和函数重载。Splitter 的实现中有十分明显的策略模式和模板模式,有各种神乎其技的方法覆盖,还有 Guava 久负盛名的迭代技巧和惰性计算。

不得不说,平时翻阅一些基础类库,总是感觉 “这种代码我也能写”,“这代码写的还没我好”,“在工具类中强依赖日志组件,人干事?”,如果 IDE 配上弹幕恐怕全是吐槽,难有让人精神为之一振的代码。阅读 Guava 的代码,每次都有新的惊喜,各种神技巧黑科技让我五体投地,写代码的脑洞半径屡次被 Guava 撑大。

成员变量

Splitter 类有 4 个成员变量,strategy 用于帮助实现策略模式,omitEmptyStrings 用于控制是否删除拆分结果中的空字符串,通过 Splitter#omitEmptyStrings 设置,trimmer 用于描述删除拆分结果的前后空白符的策略,通过 Splitter#trimResults 设置,limit 用于控制拆分的结果个数,通过 Splitter#limit 设置。

策略模式

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

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

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

然后在重载入参为 CharMatcher 的 Splitter#on 的时候,传入一个覆盖了 Strategy#iterator 方法的策略实例,返回值是 SplittingIterator 这个专用的迭代器。然后 SplittingIterator 是个抽象类,需要覆盖实现 separatorStart 和 separatorEnd 两个方法才能实例化。这两个方法是 SplittingIterator 用到的模板模式的重要组成。

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;
        }
      };
    }
  });
}

阅读源码的过程在,一个神奇的 continue 的用法让我震惊了,赶紧 Google 一番之后发现这种用法一直都有,只是我不知道而已。这段代码出自 Splitter#on 的字符串重载。

return new SplittingIterator(splitter, toSplit) {
  @Override public int separatorStart(int start) {
    int separatorLength = separator.length();
    positions:
    for (int p = start, last = toSplit.length() - separatorLength; p <= last; p++) {
      for (int i = 0; i < separatorLength; i++) {
        if (toSplit.charAt(i + p) != separator.charAt(i)) {
          continue positions;
        }
      }
      return p;
    }
    return -1;
  }
  @Override public int separatorEnd(int separatorPosition) {
    return separatorPosition + separator.length();
  }
};

这里的 continue 可以直接跳出内循环,然后继续执行与 positions 标签平级的循环。如果是 break,就会直接跳出 positions 标签平级的循环。以前用 C 的时候在跳出多重循环的时候都是用 goto 的,没想到 Java 也提供了类似的功能。

这段 for 循环如果我来实现,估计会写成这样,虽然功能差不多,大家的内循环都不紧凑,但是明显没有 Guava 的实现那么高贵冷艳,而且我的代码的计算量要大一些。

for (int p = start, last = toSplit.length() - separatorLength; p <= last; p++) {
  boolean match = true;
  for (int i = 0; i < separatorLength; i++) {
    match &= (toSplit.charAt(i + p) == separator.charAt(i))
  }
  if (match) {
    return p;
  }
}

惰性迭代器与模板模式

惰性求值是函数式编程中的常见概念,它的目的是要最小化计算机要做的工作,即把计算推迟到不得不算的时候进行。Java 虽然没有原生支持惰性计算,但是我们依然可以通过一些手段享受惰性计算的好处。

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

AbstractIterator 使用一个私有的枚举变量 state 来记录当前的迭代进度,比如是否找到了下一个元素,迭代是否结束等等。

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

AbstractIterator 给出了一个抽象方法 computeNext,计算下一个元素。由于 state 是私有变量,而迭代是否结束只有在调用 computeNext 的过程中才知道,于是我们有了一个保护的 endOfData 方法,允许 AbstractIterator 的子类将 state 设置为 State#DONE。

AbstractIterator 实现了迭代器最重要的两个方法,hasNext 和 next。

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

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

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

next 则是先判断是否还有下一个元素,属于防御式编程,先对自己做保护;然后把状态复原到还没找到下一个元素,然后返回结果。至于为什么先把 next 赋值给 result,然后把 next 置为 null,最后才返回 result,我想这可能是个面向 GC 的优化,减少无意义的对象引用。

private boolean tryToComputeNext() {
  state = State.FAILED; // temporary pessimism
  next = computeNext();
  if (state != State.DONE) {
    state = State.READY;
    return true;
  }
  return false;
}

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

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

SplittingIterator 还是一个抽象类,虽然实现了 computeNext 方法,但是它又定义了两个虚函数 separatorStart 和 separatorEnd,分别返回分隔符在指定下标之后第一次出现的下标,和指定下标后面第一个不包含分隔符的下标。之前的策略模式中我们可以看到,这两个函数在不同的策略中有各自不同的覆盖实现,在 SplittingIterator 中,这两个函数就是模板函数。

接下来我们看看 SplittingIterator 的核心函数 computeNext,注意这个函数一直在维护的两个内部全局变量,offset 和 limit。

@Override protected String computeNext() {
  /*
   * The returned string will be from the end of the last match to the
   * beginning of the next one. nextStart is the start position of the
   * returned substring, while offset is the place to start looking for a
   * separator.
   */
  int nextStart = offset;
  while (offset != -1) {
    int start = nextStart;
    int end;

    int separatorPosition = separatorStart(offset);
    if (separatorPosition == -1) {
      end = toSplit.length();
      offset = -1;
    } else {
      end = separatorPosition;
      offset = separatorEnd(separatorPosition);
    }
    if (offset == nextStart) {
      /*
       * This occurs when some pattern has an empty match, even if it
       * doesn't match the empty string -- for example, if it requires
       * lookahead or the like. The offset must be increased to look for
       * separators beyond this point, without changing the start position
       * of the next returned substring -- so nextStart stays the same.
       */
      offset++;
      if (offset >= toSplit.length()) {
        offset = -1;
      }
      continue;
    }
    while (start < end && trimmer.matches(toSplit.charAt(start))) {
      start++;
    }
    while (end > start && trimmer.matches(toSplit.charAt(end - 1))) {
      end--;
    }
    if (omitEmptyStrings && start == end) {
      // Don't include the (unused) separator in next split string.
      nextStart = offset;
      continue;
    }
    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();
      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--;
    }
    return toSplit.subSequence(start, end).toString();
  }
  return endOfData();
}

进入 while 循环之后,先找找 offset 之后第一个分隔符出现的位置,if 分支处理没找到的情况,else 分支处理找到了的情况。然后下一个 if 处理的是第一个字符就是分隔符的特殊情况。然后接下来的两个 while 就开始根据 trimmer 来对找到的元素做前后处理,比如去除空白符之类的。再然后就是根据需要去除那些是空字符串的元素,trim完之后变成空字符串的也会被去除。最后一步操作就是判断 limit,如果还没到 limit 的极限,就让 limit 自减,否则就要调整 end 指针的位置标记 offset 为 -1 然后重新 trim 一下。下一次再调用 computeNext 的时候就发现 offset 已经是 -1 了,然后就返回 endOfData 表示迭代结束。

整个 Splitter 最有意思的部分基本上就是这些了,至于 split 函数,其实就是用匿名类函数覆盖技巧调用了一下策略模式中被花样覆盖实现了的 Strategy#iterator 而已。

public Iterable<String> split(final CharSequence sequence) {
  checkNotNull(sequence);
  return new Iterable<String>() {
    @Override public Iterator<String> iterator() {
      return splittingIterator(sequence);
    }
    @Override public String toString() {
      return Joiner.on(", ")
          .appendTo(new StringBuilder().append('['), this)
          .append(']')
          .toString();
    }
  };
}

按理说实例化 Iterable 接口只需要实现 iterator 函数即可,这里覆盖了 toString 想必是为了方便打印吧?

MapSplitter 的实现中规中矩,使用 outerSplitter 拆分键值对,使用 entrySplitter 拆分键和值,拆分键和值前中后各种校验,然后返回一个不可修改的 Map。

最后说一下 Splitter 中一个略显画蛇添足的 API,Splitter#splitToList。

public List<String> splitToList(CharSequence sequence) {
  checkNotNull(sequence);
  Iterator<String> iterator = splittingIterator(sequence);
  List<String> result = new ArrayList<String>();
  while (iterator.hasNext()) {
    result.add(iterator.next());
  }
  return Collections.unmodifiableList(result);
}

这个函数其实就是吭哧吭哧把惰性迭代器跑了一遍生成完整数据存放到 ArrayList 中,然后又用 Collections 把这个列表变成不可修改列表返回出去,一点都不酷。

时间: 2024-11-03 21:41:33

Guava 是个风火轮之基础工具(2)的相关文章

Guava 是个风火轮之基础工具(4)

前言 Guava 是 Java 开发者的好朋友.虽然我在开发中使用 Guava 很长时间了,Guava API 的身影遍及我写的生产代码的每个角落,但是我用到的功能只是 Guava 的功能集中一个少的可怜的真子集,更别说我一直没有时间认真的去挖掘 Guava 的功能,没有时间去学习 Guava 的实现.直到最近,我开始阅读Getting Started with Google Guava,感觉有必要将我学习和使用 Guava 的一些东西记录下来. Preconditions Preconditi

Guava 是个风火轮之基础工具(1)

前言 Guava 是 Java 开发者的好朋友.虽然我在开发中使用 Guava 很长时间了,Guava API 的身影遍及我写的生产代码的每个角落,但是我用到的功能只是 Guava 的功能集中一个少的可怜的真子集,更别说我一直没有时间认真的去挖掘 Guava 的功能,没有时间去学习 Guava 的实现.直到最近,我开始阅读 Getting Started with Google Guava,感觉有必要将我学习和使用 Guava 的一些东西记录下来. Joiner 我们经常需要将几个字符串,或者字

Guava 是个风火轮之基础工具(3)

前言 Guava 是 Java 开发者的好朋友.虽然我在开发中使用 Guava 很长时间了,Guava API 的身影遍及我写的生产代码的每个角落,但是我用到的功能只是 Guava 的功能集中一个少的可怜的真子集,更别说我一直没有时间认真的去挖掘 Guava 的功能,没有时间去学习 Guava 的实现.直到最近,我开始阅读 Getting Started with Google Guava,感觉有必要将我学习和使用 Guava 的一些东西记录下来. Charsets Charsets 是一个常量

PS基础工具操作方式与方法介绍

本教程像飞特的PS初学者介绍一下ps软件的基础工具的操作方法.如果你耐心的多做几遍这个练习.         三联推荐:photoshop7.0迷你版免费下载    |  Photoshop CS5 中文免费下载  |  photoshop免费下载 我相信你会有很大的提高.这时介绍了"创建文件"."裁切方法""移动工具"."裁切工具". "对齐方式"."复制图层"."合并图层&

命令行基础工具的更佳替代品

命令行基础工具的更佳替代品 命令行听起来有时候会很吓人,特别是在刚刚接触的时候,你甚至可能做过有关命令行的噩梦.然而渐渐地,我们都会意识到命令行实际上并不是那么吓人,反而是非常有用.实际上,没有命令行正是每次我使用 Windows 时让我感到崩溃的地方.这种感觉上的变化是因为命令行工具实际上是很智能的. 你在任何一个 Linux 终端上所使用的基本工具功能都是很强大的, 但还远说不上是足够强大. 如果你想使你的命令行生涯更加愉悦, 这里有几个程序你可以下载下来替换原来的默认程序, 它还可以给你提

Guava 是个风火轮之函数式编程(1)

前言 函数式编程是一种历久弥新的编程范式,比起命令式编程,它更加关注程序的执行结果而不是执行过程.Guava 做了一些很棒的工作,搭建了在 Java 中模拟函数式编程的基础设施,让我们不用多费手脚就能享受部分函数式编程带来的便利. Java 始终是一个面向对象(命令式)的语言,在我们使用函数式编程这种黑魔法之前,需要确认:同样的功能,使用函数式编程来实现,能否在健壮性和可维护性上,超过使用面向对象(命令式)编程的实现? Function Function 接口是我们第一个介绍的 Guava 函数

如何在两小时内搞懂PHOTOSHOP基础工具?

  初学者面对Photoshop这个庞然大物,第一直观感受是不知道从何下手.那么多工具,学完这个忘那个,有木有什么教程可以一次性学完呢?今天这篇好文,只要童鞋们打起精神来,两个小时就可以搞懂基础的工具使用!绝对是新手速成Photoshop的一个好机会!赶紧来学习! 教程很有意思,从A--Z 依次讲解每个工具的使用,精辟彻底,不拖泥带水呦. 3D面板:作为Adobe Photoshop Extended的一部分,你可以在Adobe Photoshop CC中找到它.3D面板中展示了所有3D文件中允

[Google Guava] 2.3-强大的集合工具类:java.util.Collections中未包含的集合工具

原文链接 译文链接 译者:沈义扬,校对:丁一 尚未完成: Queues, Tables工具类 任何对JDK集合框架有经验的程序员都熟悉和喜欢java.util.Collections包含的工具方法.Guava沿着这些路线提供了更多的工具方法:适用于所有集合的静态方法.这是Guava最流行和成熟的部分之一. 我们用相对直观的方式把工具类与特定集合接口的对应关系归纳如下: 集合接口 属于JDK还是Guava 对应的Guava工具类 Collection JDK Collections2:不要和jav

Guava 是个风火轮之函数式编程(2)

前言 函数式编程是一种历久弥新的编程范式,比起命令式编程,它更加关注程序的执行结果而不是执行过程.Guava 做了一些很棒的工作,搭建了在 Java 中模拟函数式编程的基础设施,让我们不用多费手脚就能享受部分函数式编程带来的便利. Java 始终是一个面向对象(命令式)的语言,在我们使用函数式编程这种黑魔法之前,需要确认:同样的功能,使用函数式编程来实现,能否在健壮性和可维护性上,超过使用面向对象(命令式)编程的实现? Predicate Predicate 接口是我们第二个介绍的 Guava