Guava Joiner 拼接字符串
Joiner
Guava 是Google 对Java的内置类型进行增强和扩展的工具。
Joiner.on(", ").join(Iterator<> iter)
Joiner.on(" - ").join(Object[] objects)
Joiner.on(" / ").join(first, second, rest..)
Joiner.on(" : ").skipNulls().join()
1. on()方法制定拼接符号,如:test1-test2-test3 中的 “-“ 符号
2. join()方法可以传递三种格式的参数
迭代对象
数组
多个单一的拼接对象
3. 可以指定当为空时跳过,不指定为空则报错
-----------------------------------------------------------------------------------------------------------------------
我们经常需要将几个字符串,或者字符串数组、列表之类的东西,拼接成一个以指定符号分隔各个元素的字符串,比如把 [1, 2, 3] 拼接成 "1 2 3"。
在 Python 中我只需要简单的调用 str.join 函数,就可以了,就像这样。
' '.join(map(str, [1, 2, 3]))
到了 Java 中,如果你不知道 Guava 的存在,基本上就得手写循环去实现这个功能,代码瞬间变得丑陋起来。
Guava 为我们提供了一套优雅的 API,让我们能够轻而易举的完成字符串拼接这一简单任务。还是上面的例子,借助 Guava 的 Joiner 类,代码瞬间变得优雅起来。
Joiner.on(' ').join(1, 2, 3);
被拼接的对象集,可以是硬编码的少数几个对象,可以是实现了 Iterable 接口的集合,也可以是迭代器对象。
除了返回一个拼接过的字符串,Joiner 还可以在实现了 Appendable 接口的对象所维护的内容的末尾,追加字符串拼接的结果。
StringBuilder sb = new StringBuilder("result:");
Joiner.on(" ").appendTo(sb, 1, 2, 3);
System.out.println(sb);//result:1 2 3
Guava 对空指针有着严格的限制,如果传入的对象中包含空指针,Joiner 会直接抛出 NPE。与此同时,Joiner 提供了两个方法,让我们能够优雅的处理待拼接集合中的空指针。
如果我们希望忽略空指针,那么可以调用 skipNulls 方法,得到一个会跳过空指针的 Joiner 实例。如果希望将空指针变为某个指定的值,那么可以调用 useForNull 方法,指定用来替换空指针的字符串。
Joiner.on(' ').skipNulls().join(1, null, 3);//1 3
Joiner.on
(' ').useForNull("None").join(1, null, 3);//1 None 3
需要注意的是,Joiner 实例是不可变的,skipNulls 和 useForNull 都不是在原实例上修改某个成员变量,而是生成一个新的 Joiner 实例。
Joiner.MapJoiner
MapJoiner 是 Joiner 的内部静态类,用于帮助将 Map 对象拼接成字符串。
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 不过是它和它的重载的封装。
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;
}
这段代码的第一个技巧是使用 if 和 while 来实现了比较优雅的分隔符拼接,避免了在末尾插入分隔符的尴尬;第二个技巧是使用了自定义的 toString 方法而不是 Object#toString 来将对象序列化成字符串,为后续的各种空指针保护开了方便之门。
注意到一个比较有意思的 appendTo 重载。
public final StringBuilder appendTo(StringBuilder builder, Iterator<?> parts) {
try {
appendTo((Appendable) builder, parts);
} catch (IOException impossible) {
throw new AssertionError(impossible);
}
return builder;
}
在 Appendable 接口中,append 方法是会抛出 IOException 的。然而 StringBuilder 虽然实现了 Appendable,但是它覆盖实现的 append 方法却是不抛出 IOException 的。于是就出现了明知不可能抛异常,却又不得不去捕获异常的尴尬。
这里的异常处理手法十分机智,异常变量命名为 impossible,我们一看就明白这里是不会抛出 IOException 的。但是如果 catch 块里面什么都不做又好像不合适,于是抛出一个 AssertionError,表示对于这里不抛异常的断言失败了。
另一个比较有意思的 appendTo 重载是关于可变长参数。
public final <A extends Appendable> A appendTo(
A appendable, @Nullable Object first, @Nullable Object second, Object... rest)
throws IOException {
return appendTo(appendable, iterable(first, second, rest));
}
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];
}
}
};
}
如果是我来实现,可能是简单粗暴的创建一个 ArrayList 的实例,然后把这两个变量一个数组的全部元素放到 ArrayList 里面然后返回。这样子代码虽然短了,但是代价却不小:为了一个小小的重载调用而产生了 O(n) 的时空复杂度。
看看人家 G 社的做法。要想写出这样的代码,需要熟悉顺序表迭代器的实现。迭代器内部维护着一个游标,cursor。迭代器的两大关键操作,hasNext 判断是否还有没遍历的元素,next 获取下一个元素,它们的实现是这样的。
public boolean hasNext() {
return cursor != size();
}
public E next() {
checkForComodification();
try {
int i = cursor;
E next = get(i);
lastRet = i;
cursor = i + 1;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
hasNext 中关键的函数调用是 size,获取集合的大小。next 方法中关键的函数调用是 get,获取第 i 个元素。Guava 的实现返回了一个被覆盖了 size 和 get 方法的 AbstractList,巧妙的复用了由编译器生成的数组,避免了新建列表和增加元素的开销。
空指针处理
当待拼接列表中可能包含空指针时,我们用 useForNull 将空指针替换为我们指定的字符串。它是通过返回一个覆盖了方法的 Joiner 实例来实现的。
public Joiner useForNull(final String nullText) {
checkNotNull(nullText);
return new Joiner(this) {
@Override CharSequence toString(@Nullable 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");
}
};
}
首先是使用复制构造函数保留先前初始化时候设置的分隔符,然后覆盖了之前提到的 toString 方法。为了防止重复调用 useForNull 和 skipNulls,还特意覆盖了这两个方法,一旦调用就抛出运行时异常。为什么不能重复调用 useForNull ?因为覆盖了 toString 方法,而覆盖实现中需要调用覆盖前的 toString。
在不支持的操作中抛出 UnsupportedOperationException 是 Guava 的常见做法,可以在第一时间纠正不科学的调用方式。
skipNulls 的实现就相对要复杂一些,覆盖了原先全功能 appendTo 中使用 if 和 while 的优雅实现,变成了 2 个 while 先后执行。第一个 while 找到 第一个不为空指针的元素,起到之前的 if 的功能,第二个 while 功能和之前的一致。
。
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");
}
};
}
拼接键值对
MapJoiner 实现为 Joiner 的一个静态内部类,它的构造函数和 Joiner 一样也是私有,只能通过 Joiner#withKeyValueSeparator 来生成实例。类似地,MapJoiner 也实现了 appendTo 方法和一系列的重载,还用 join 方法对 appendTo 做了封装。MapJoiner 整个实现和 Joiner 大同小异,在实现中大量使用 Joiner 的 toString 方法来保证空指针保护行为和初始化时的语义一致。
MapJoiner 也实现了一个 useForNull 方法,这样的好处是,在获取 MapJoiner 之后再去设置空指针保护,和获取 MapJoiner 之前就设置空指针保护,是等价的,用户无需去关心顺序问题。
Guava Joiner 拼接字符串的更多相关文章
- 为什么 Java 8 中不再需要 StringBuilder 拼接字符串
为什么 Java 8 中不再需要 StringBuilder 拼接字符串 来源:codeceo 发布时间:2016-12-27 阅读次数:427 0 在Java开发者中,字符串的拼接占用资源高往往 ...
- knockoutJS学习笔记01:从拼接字符串到编写模板引擎
开篇 关于knockout的文章,园里已经有很多大神写过了,而且都写得很好.其实knockout学习起来还是很容易的,看看官网的demo和园里的文章,练习练习就可以上手了(仅限使用,不包含研究源码). ...
- Java使用占位符拼接字符串
大家知道,在C#编程中,可以用占位符来拼接字符串,用起来非常的方便. 特别是需要进行大量的参数拼接的时候,比如: Console.WriteLine(String.Format("该域名{0 ...
- razor 拼接字符串
在asp.net引擎中 拼接字符串可以这样写 <script src="~/script/<%=scriptname%>.js"></script&g ...
- 谈JavaScript组合拼接字符串的效率 --转载
JavaScript组合拼接字符串的效率.在脚本开发过程中,经常会按照某个规则,组合拼接出一个大字符串进行输出.比如写脚本控件时控制整个控件的外观的HTML标签输出,比如AJAX里得到服务器端的回传值 ...
- c++拼接字符串效率比较(+=、append、stringstream、sprintf)
转自:http://www.cnblogs.com/james6176/p/3222671.html c++拼接字符串效率比较(+=.append.stringstream.sprintf) 最近写的 ...
- 数据库(MSSQLServer,Oracle,DB2,MySql)常见语句以及问题(续1之拼接字符串)
上一篇文章http://www.cnblogs.com/valiant1882331/p/4056403.html写的太长了,所以就换了一篇,链接上一节继续 字符串的拼接 MySql中可以使用&quo ...
- js - 在拼接字符串中动态submit当前form
今天在做一个项目的时候, mapabc中的inforWindow中,如果是超链接a,不直接响应. 后来的解决方案是动态产生form,并调用summit方法.如下 自定义一个js函数: function ...
- sql server中单引号拼接字符串(书写错误会出现错误"浮点值 XXXX 超出了计算机表示范围(8 个字节)。“XX”附近有语法错误。")
" ' "(单引号)的运用:在sql server中,两个" ' "(单引号)在拼接字符串的情况下运用,就是表示拼接上了一个" ' "单引号 ...
随机推荐
- Visual studio C++ MFC之点击按钮(菜单栏)生成新窗口
背景 当前做的APP有菜单栏,菜单栏有一项需要对下位机相关参数进行设置,则必须弹出一个窗口来实现设置操作.本篇即对点击菜单栏生成新的窗口,在新的窗口内完成相应计划后结束新窗口并返回原窗口的方法进行简述 ...
- Eclipse Mylyn成为顶级项目
http://www.infoq.com/cn/news/2010/09/eclipse-mylyn/ 作为应用程序的生命周期管理工具,Eclipse Mylyn项目已经被提升为顶级的Eclipse项 ...
- 插入数据返回插入的主键Id
ADO.Net中Sql语句: insert into RoomType(TypeName,Price,AddBed,BedPrice,Remark)output inserted.ID values( ...
- NSDate相差8小时
NSDate *date = [NSDate date]; NSTimeZone *zone = [NSTimeZone systemTimeZone]; NSInteger interval = ...
- MySql(一):linux 安装mysql数据库——yum安装法
mysql数据库有多种安装方式,本文只介绍在Linux服务器上最实用.最快捷的mysql server安装方法.一.Linux服务器yum安装(CentOS6.3 64位)所有在服务器上执行的命令,都 ...
- Atitit.uml2 api 的编程代码实现设计uml开发 使用eclipse jar java 版本
Atitit.uml2 api 的编程代码实现设计uml开发 使用eclipse jar java 版本 1. clipse提供了UML的底层Java包, 1 2. MDTUML2Getting St ...
- 88. Merge Sorted Array【easy】
88. Merge Sorted Array[easy] Given two sorted integer arrays nums1 and nums2, merge nums2 into nums1 ...
- valgrind的编译和使用
ubuntu 平台: valgrind 3.8.1 一. 编译 ./configure --prefix=/home/frank/test/valgrind/PC/local 报错:checking ...
- 李洪强iOS开发之大神必备的Xcode插件
iOS开发大神必备的Xcode插件 写在前面 工欲善其事,必先利其器,iOS开发中不仅要学会Xcode的基本操作,而且还得学会一些Xcode的使用技巧,如掌握常用的快捷键等,还有就是今天要说到的Xco ...
- Java序列化的几种方式
本文着重解说一下Java序列化的相关内容. 假设对Java序列化感兴趣的同学能够研究一下. 一.Java序列化的作用 有的时候我们想要把一个Java对象变成字节流的形式传出去,有的时候我们想要从 ...