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

简介:

前言

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

Charsets

Charsets 是一个常量工厂,给出了 6 个Java 承诺了全平台支持的字符集,类似的静态工厂在 Apache 的类库中也有提供。如果没有静态变量,我们要么使用 Charset#forName 方法,传入一个字符串来获取指定的字符集,要么自己定义一个类似的工厂类。

使用 Charset#forName 的问题就在于用户需要关注入参字符串的拼写,一旦拼写错误就会出现意料之外的事情。

到了 Java 7 中,JDK 提供了一个官方的静态工厂类 java.nio.charset.StandardCharsets,Guava 也推荐使用 Java 7 及以上的用户使用 StandardCharsets。

Strings

在 Guava 中,以名词的复数形式命名的类,基本上都是静态工厂。Strings 就是这么一个用来操作字符串的方法工厂。

Strings 提供了空指针、空字符串的判断和互换方法。

Strings.isNullOrEmpty("");//true
Strings.nullToEmpty(null);//""
Strings.nullToEmpty("a");//"a"
Strings.emptyToNull("");//null
Strings.emptyToNull("a");//"a"

对于防御式编程,可以在拿到字符串入参之后,调用一下 Strings#nullToEmpty 将可能的空指针变成空字符串,然后也就不用担心字符串引发的 NPE,或者字符串拼接时候出现的 "null" 了。

Strings 还提供了常见的字符串前后拼接同一个字符直到达到某个长度,或者重复拼接自身 n 次。

Strings.padStart("7", 3, '0');//"007"
Strings.padStart("2010", 3, '0');//"2010"
Strings.padEnd("4.", 5, '0');//"4.000"
Strings.padEnd("2010", 3, '!');//"2010"
Strings.repeat("hey", 3);//"heyheyhey"
Strings 的最后一组功能是查找两个字符串的公共前缀、后缀。
Strings.commonPrefix("aaab", "aac");//"aa"
Strings.commonSuffix("aaac", "aac");//"aac"

源码分析

源码来自 Guava 18.0。Strings 类的源码大约 240 行,大部分的函数实现中规中矩,值得关注的是 Strings#repeat。代码注释赫然写着,如果你修改了这里的代码,必须同步更新 Benchmark!看来这段代码是经过极致优化了的,让我不禁想起当年楼教主比赛时“我去上个厕所,不要动键盘”的霸气。

public static String repeat(String string, int count) {
  checkNotNull(string);  // eager for GWT.
  if (count <= 1) {
    checkArgument(count >= 0, "invalid count: %s", count);
    return (count == 0) ? "" : string;
  }
  // IF YOU MODIFY THE CODE HERE, you must update StringsRepeatBenchmark
  final int len = string.length();
  final long longSize = (long) len * (long) count;
  final int size = (int) longSize;
  if (size != longSize) {
    throw new ArrayIndexOutOfBoundsException("Required array size too large: " + longSize);
  }
  final char[] array = new char[size];
  string.getChars(0, len, array, 0);
  int n;
  for (n = len; n < size - n; n <<= 1) {
    System.arraycopy(array, 0, array, n, n);
  }
  System.arraycopy(array, 0, array, n, size - n);
  return new String(array);
}

真正的代码从霸气注释开始。开头的 3 行代码,int 升级 long 然后降级 int,是为了确保字符串 repeat 之后没有超过 String 的长度限制,而先强制提升然后截断的方法,能够高效的判断溢出,这种手法在 C 语言中也是常见的。由于这里是判断 int 溢出,可以升级到 long,如果判断 long 溢出,就只能用除法了。

然后这里没有用 StringBuilder,而是出于性能考虑用了 char[],直接申请目标大小的数组。循环复制字符串的时候,复制源的长度指数增长,以最快的速度结束循环。System#arraycopy 是个 native 方法,也就是用 C 来实现的,性能上似乎更值得信赖一点。

另外一段让我涨姿势的代码是查找相同前缀的。

public static String commonPrefix(CharSequence a, CharSequence b) {
  checkNotNull(a);
  checkNotNull(b);
  int maxPrefixLength = Math.min(a.length(), b.length());
  int p = 0;
  while (p < maxPrefixLength && a.charAt(p) == b.charAt(p)) {
    p++;
  }
  if (validSurrogatePairAt(a, p - 1) || validSurrogatePairAt(b, p - 1)) {
    p--;
  }
  return a.subSequence(0, p).toString();
}

static boolean validSurrogatePairAt(CharSequence string, int index) {
  return index >= 0 && index <= (string.length() - 2)
      && Character.isHighSurrogate(string.charAt(index))
      && Character.isLowSurrogate(string.charAt(index + 1));
}

整个函数本来很简单的,但是 while 后面还跟着一个莫名其妙的 if,这是什么东西!函数名里面居然出现了我不认识的单词,英语水平暴露了!

一番 Google 之后发现,这里其实是判断最后两个字符是不是合法的“Java 平台增补字符”。看起来这些增补字符占了 2 个字节,然后要用判断高位低位之类的。。仔细看了函数的头注释,里面也提到 taking care not to split surrogate pairs,然后就明了了。

CharMacher

一提起字符串操作,我们都会想起一个神奇的符号,StringUtil。不仅仅 Apache Common 有一个 StringUtil,Spring 也有一个类似的 StringUtils,然后各个公司、各个项目也会造个轮子,或者重写,或者继承来实现自己的一些特殊的字符串操作。

随着 StringUtil 无节制的发展,StringUtil 里面充斥着 allAscii, collapse, collapseControlChars, collapseWhitespace, indexOfChars, lastIndexNotOf, numSharedChars, removeChars, removeCrLf, replaceChars, retainAllChars, strip, stripAndCollapse, stripNonDigits 等等函数。这些函数本质上是两个概念的点积:

  1. 如何界定匹配的字符?
  2. 要对匹配的字符做什么?

为了解决这种野蛮增长,Guava 带来了 CharMacher。一个 CharMacher 实例本身,界定了一个匹配字符的集合,而 CharMacher 实例的方法,解决了要对匹配字符做什么的问题。然后我们就可以用最小化的 API 来处理字符匹配和字符操作,把 M×NM×N 的复杂度下降到了 M+NM+N

CharMacher 自带常量工厂,域定义了一系列常用的字符集合,比如 CharMatcher#ASCII 匹配 ASCII 码,CharMatcher#DIGIT 匹配 Unicode 的数字 0~9,还有其他常量如 JAVA_DIGIT、JAVA_LETTER 等。

CharMacher 提供了一系列的静态方法用于构造自定义的字符集合。

CharMatcher#is 得到界定单个匹配字符的实例,CharMatcher#isNot 正好与前者逻辑反。CharMatcher#anyOf 生成存在量词,CharMatcher#noneOf 生成否定全称量词。CharMatcher#inRange 范围量词,闭区间。

本质上 CharMacher 继承自 Predicate,是专门字符对象的断言,因此 Predicate 享有的与或非等等操作,CharMacher 也有。我们可以用 CharMatcher#and、CharMatcher#or、CharMatcher#negate 来完成 CharMacher 的与或非,对于匹配字符集合来说就是交并补。

如果上述两种构造 CharMacher 的手段还是太弱没法描述我们想要的匹配器,没关系,我们还有大招:初始化一个重载了 CharMatcher#matches 方法的匿名类实例,或者显式继承 CharMacher 然后实现 matches 方法。

接下来我们看看 CharMacher 有哪些实例方法可用。根据函数的返回值和名称我们能够轻易将这些方法分为 3 类。

第一类是判定型函数,判断 CharMacher 和入参字符串的匹配关系。

CharMatcher.is('a').matchesAllOf("aaa");//true
CharMatcher.is('a').matchesAnyOf("aba");//true
CharMatcher.is('a').matchesNoneOf("aba");//true

第二类是计数型函数,查找入参字符串中第一次、最后一次出现目标字符的位置,或者目标字符出现的次数,比如 CharMatcher#indexIn,CharMatcher#lastIndexIn 和 CharMatcher#countIn。

第三类就是对匹配字符的操作。我们能对字符串中的匹配字符做什么操作呢?基本上就是移除、仅保留、替换、前后修剪、collapse(不知道怎么翻译比较恰当) 等等,我们可以轻易地使用 CharMatcher#removeFrom、CharMatcher#retainFrom、CharMatcher#replaceFrom、CharMatcher#trimFrom、CharMatcher#collapseFrom 等等一系列正交的方法来实现。

源码分析

源码来自 Guava 18.0。CharMatcher 类源码约 1400 行,大致上分为常量、内部类、静态工厂、非静态工厂以及文本处理例程。

CharMatcher 整个类都被打上了 @Beta 注解,还有一句注释,Possibly change from chars to code points; decide constants vs. methods,看得我云里雾里的。不管怎么说,CharMatcher 自从 Guava 1.0 就一直存在了,虽然有着 Beta 注解,个人感觉被移除或者过时的可能性很小。

继承与组合

前不久我在一篇博文中看到这样一种说法(具体出处找不到了),那些可以直接实例化的类,应该被声明为 final,这样可以强制其他开发者使用组合而不是继承来复用代码。那些可以被继承的类,应该是抽象类,要么在实例化的时候补全虚函数的实现,要么通过继承实现虚函数。

对照 Guava 的代码,还真有点类似的意思。那些工厂类,基本上都被声明为了 final,而 CharMatcher 则是一个抽象类,在实例化的时候覆盖实现各种虚函数,也被各种内部类、外部类继承。

CharMatcher 中有 3 个私有静态类,CharMatcherAndCharMatcherAnd、CharMatcherOr、CharMatcher$NegatedMatcher,用于描述 CharMatcher 的与或非关系。这 3 个类与 CharMatcher 之间既是继承,又是聚合。突然感觉 UML 好难画。

其他 4 个静态类,则是为了实现某种匹配模式,而对 CharMatcher 作出了特化处理,比如 RangesMatcher 专门匹配一段连续的字符范围,FastMatcher 专门匹配那些无法通过预处理获得性能提升的字符(们)。

代码即文档

我在写代码的时候总是把“自注释”、“代码即文档”等等挂在嘴边,但是真正能做到的代码却不多。有的时候把自己的代码拿出来,一眼看去尽是 IDE 自动生成的毫无意义的变量名,真是惭愧。

下面这段代码的参数命名比较有趣,值得借鉴。

public static CharMatcher inRange(final char startInclusive, final char endInclusive) {
  checkArgument(endInclusive >= startInclusive);
  String description = "CharMatcher.inRange('" +
      showCharacter(startInclusive) + "', '" +
      showCharacter(endInclusive) + "')";
  return inRange(startInclusive, endInclusive, description);
}

不需要任何文档,我们从函数名和参数名就能够看出,这段代码返回的 CharMatcher 匹配的是一个闭区间内的字符。如果换了别人来实现,能用 start 和 end 做参数名的已经算是很不错了,大部分估计用 c1、c2 敷衍了事,更不用说加上起到关键作用的 Inclusive 了。

总体而言,CharMatcher 的代码虽然长,却没有特别深奥或者精彩的片段,less is more。

目录
相关文章
|
6月前
|
数据采集 编解码 Java
解锁Hutool魔法箱:Java开发者不可错过的神奇工具集
解锁Hutool魔法箱:Java开发者不可错过的神奇工具集
141 0
|
3月前
|
开发者 前端开发 Apache
【绝不错过!】揭秘Wicket大神级插件,带你飞越编程极限,探索Web应用开发新大陆!
【8月更文挑战第31天】Apache Wicket是一个成熟的Java Web框架,以其组件化体系结构、直观的API及对MVC的支持著称。其活跃社区贡献了大量插件和扩展,显著提升了Wicket的功能性。本文推荐几个实用插件,如**Wicket Ajax Support**,可轻松添加Ajax功能,提升用户体验;**Bootstrap for Wicket**则将Bootstrap与Wicket结合,美化应用界面。
37 0
|
存储 JSON Kubernetes
证书管理工具 cfssl 浅尝
证书管理工具 cfssl 浅尝
339 0
|
前端开发 测试技术 API
干掉丑陋的 Swagger,堪称开发者的瑞士军刀
“为什么改了这个没告诉我”,“实际功能和文档上说的不一样啊”。这些话大家做开发的想必耳朵都听出老茧了。真不是故意的,有时候任务比较急,就先改了代码,想着以后再同步文档,然后就给忘了。项目更新又全靠社交软件通知,人一多难免有一两个没及时沟通到的。确实给合作的小伙伴带来麻烦,但说实话开发商也挺委屈的。
|
人工智能 Go
神级插件Bito介绍及使用
神级插件Bito介绍及使用
|
缓存 安全 JavaScript
别再造轮子了,Google 开源的 Guava 工具库真心强大!
别再造轮子了,Google 开源的 Guava 工具库真心强大!
|
缓存 安全 Java
别再重复造轮子了,推荐使用 Google Guava 开源工具类库,真心强大!
别再重复造轮子了,推荐使用 Google Guava 开源工具类库,真心强大!
342 0
别再重复造轮子了,推荐使用 Google Guava 开源工具类库,真心强大!
|
存储 缓存 监控
真正的缓存之王,Google Guava 只是弟弟(一)
前面刚说到Guava Cache,他的优点是封装了get,put操作;提供线程安全的缓存操作;提供过期策略;提供回收策略;缓存监控。当缓存的数据超过最大值时,使用LRU算法替换。这一篇我们将要谈到一个新的本地缓存框架:Caffeine Cache。它也是站在巨人的肩膀上-Guava Cache,借着他的思想优化了算法发展而来。 本篇博文主要介绍Caffine Cache 的使用方式,以及Caffine Cache在SpringBoot中的使用。
真正的缓存之王,Google Guava 只是弟弟(一)
|
存储 缓存 NoSQL
真正的缓存之王,Google Guava 只是弟弟(二)
真正的缓存之王,Google Guava 只是弟弟(二)
|
JavaScript 前端开发 Java
不要再重复造轮子了,Hutool这款开源工具类库贼好使(2)
不要再重复造轮子了,Hutool这款开源工具类库贼好使
787 0
不要再重复造轮子了,Hutool这款开源工具类库贼好使(2)
下一篇
无影云桌面