本文是對 Guava 中 Joiner 的學(xué)習(xí)介紹。歡迎加入學(xué)習(xí)項目: LearningGuava喊暖。
使用示例
以下參考:官方文檔伊约。
開發(fā)過程中嘁信,用分隔符連接字符串序列可能是一個比較繁瑣的過程,但本不應(yīng)該如此抡爹。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
,最終會調(diào)用對象的 toString()
方法欧穴。
// 使用在對象上,會調(diào)用對象的 toString() 函數(shù)
result = Joiner.on(",").join(Arrays.asList(1, 5, 7));
Assert.assertEquals(result, "1,5,7");
對于 Map
,可以使用這樣的代碼:
// MapJoiner 的使用泵殴,將 map 轉(zhuǎn)換為字符串
Map map = ImmutableMap.of("k1", "v1", "k2", "v2");
result = Joiner.on("; ").withKeyValueSeparator("=").join(map);
Assert.assertEquals(result, "k1=v1; k2=v2");
源碼分析
以下參考:Guava 是個風(fēng)火輪之基礎(chǔ)工具(1)涮帘。
初始化方法
Joiner
的構(gòu)造方法被設(shè)置成了私有,需要通過靜態(tài)的 on(String separator)
或者 on(char separator)
函數(shù)初始化笑诅。
拼接基本函數(shù)
Joiner
了中最為核心的函數(shù)就是 <A extends Appendable> A appendTo(A appendable, Iterator<?> parts)
调缨。作為全功能函數(shù)疮鲫,其它所有的字符串拼接最終都會調(diào)用這個函數(shù)。
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
源碼中傳入的是實現(xiàn)該接口的StringBuilder
弦叶。 - 因為是公共方法俊犯,無法保證
appendable
值不為空,所以要先檢查該值是否為空湾蔓。 -
if ... while ...
的結(jié)構(gòu)確保末尾不會添加多余的分隔符瘫析。 - 通過本地
toString
方法,而不是直接調(diào)用對象的toString
方法默责,這種做法提供了空指針保護贬循。
不可能發(fā)生的異常
在源碼中,有個地方的處理值得關(guān)注一下:
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
在實現(xiàn)的時候并不會拋出改異常杖虾,所以為了適應(yīng)這個接口,這里不得不捕捉異常媒熊。這樣捕捉后的斷言處理也就可以理解了奇适。
巧妙的可變長參數(shù)轉(zhuǎn)換
有一個添加的重載函數(shù)如下所示:
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
函數(shù)將參數(shù)變?yōu)橐粋€可以迭代的序列,該函數(shù)如下所示芦鳍。
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];
}
}
};
}
通過實現(xiàn) AbstractList
的方式嚷往,巧妙地復(fù)用了編譯器生成的數(shù)組,減少了對象創(chuàng)建的開銷柠衅。這樣的實現(xiàn)需要對迭代器有深入的了解皮仁,因為要確保實現(xiàn)能夠滿足迭代器接口各個函數(shù)的語義。
Joiner 二次創(chuàng)建
因為 Joiner
創(chuàng)建后就是不可更改的了菲宴,所以為了實現(xiàn) useForNull
和 skipNulls
等語義贷祈,源碼會再次創(chuàng)建一個匿名類,并覆蓋相應(yīng)的方法喝峦。
useForNull
函數(shù)匯中為了防止重復(fù)調(diào)用 useForNull
和 skipNulls
势誊,還特意覆蓋了這兩個方法,一旦調(diào)用就拋出運行時異常谣蠢。為什么不能重復(fù)調(diào)用 useForNull
粟耻?因為覆蓋了 toString
方法,而覆蓋實現(xiàn)中需要調(diào)用覆蓋前的 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
函數(shù)實現(xiàn)如下所示勋颖。個人比較奇怪的是 skipNulls
中為什么不禁止重復(fù)調(diào)用 skipNulls
函數(shù)。
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");
}
};
}