大家好,我是沉默王二。
很多初學(xué)編程的同學(xué),經(jīng)常給我吐槽,說:“二哥,你在敲代碼的時(shí)候會(huì)不會(huì)有這樣一種感覺,寫著寫著看不下去了,覺得自己寫出來的代碼就好像屎一樣?”
這里我必須得說一句,初入“江湖”的時(shí)候,確實(shí)會(huì)覺得自己的代碼寫得很爛,但這么多年下來,這種感覺已經(jīng)蕩然無存了。
(吹嘛,我也會(huì),哈哈)
那,怎么才能讓寫出來的代碼不那么爛呢?
我的一個(gè)經(jīng)驗(yàn)就是,“拿來主義”,盡量不去重復(fù)造輪子。使用那些已經(jīng)被驗(yàn)證過,足夠優(yōu)質(zhì)的開源庫不僅能夠讓我們的代碼變得優(yōu)雅,還能夠讓我們?cè)诓粩嗟氖褂眠^程當(dāng)中,學(xué)習(xí)到編程的精髓。
洋務(wù)運(yùn)動(dòng)的時(shí)候,有一句很響亮的口號(hào)叫做,“師夷長技以制夷”。先去用,再去學(xué),自然而然就會(huì)變得牛逼。同學(xué)們,你們說,是不是這個(gè)理?
我今天推薦的這款開源庫,名字叫做 strman-java,GitHub 上標(biāo)星 1.3k,一款超贊的字符串處理工具庫,基于 Java 8,語法非常簡(jiǎn)潔。
接下來,我們來看看怎么用。
Maven 項(xiàng)目只需要在 pom.xml 文件中添加以下依賴即可。
<dependency>
<groupId>com.shekhargulati</groupId>
<artifactId>strman</artifactId>
<version>0.4.0</version>
</dependency>
好了,可以肆無忌憚地調(diào)用 strman-java 的 API 了。我會(huì)在介紹的時(shí)候插入一些源碼的介紹,方便同學(xué)們更深一步的學(xué)習(xí),盡量做到“知其然知其所以然”。

01、append
把可變字符串參數(shù)添加到指定的字符串尾部。
Strman.append("沉","默","王","二");
結(jié)果如下所示:
沉默王二
append 對(duì)應(yīng)的方法是 prepend,把可變字符串參數(shù)前置到指定的字符串前面,使用方法如下。
Strman.prepend("沉","默","王","二");
結(jié)果如下所示:
默王二沉
02、appendArray
把字符串?dāng)?shù)組添加到指定的字符串尾部。
String [] strs = {"默","王","二"};
Strman.appendArray("沉",strs);
結(jié)果如下所示:
沉默王二
append 內(nèi)部其實(shí)調(diào)用的 appendArray,來看一下源碼:
public static String append(final String value, final String... appends) {
return appendArray(value, appends);
}
當(dāng)使用可變參數(shù)的時(shí)候,實(shí)際上是先創(chuàng)建了一個(gè)數(shù)組,該數(shù)組的大小就是可變參數(shù)的個(gè)數(shù),然后將參數(shù)放入數(shù)組當(dāng)中,再將數(shù)組傳遞給被調(diào)用的方法。
通過觀察反編譯后的字節(jié)碼,就能看得到。
Strman.append("沉","默","王","二");
實(shí)際等同于:
Strman.append("沉", new String[]{"默", "王", "二"});
再來看一下 appendArray 方法的源碼:
public static String appendArray(final String value, final String[] appends) {
StringJoiner joiner = new StringJoiner("");
for (String append : appends) {
joiner.add(append);
}
return value + joiner.toString();
}
內(nèi)部用的 StringJoiner,Java 8 時(shí)新增的一個(gè)類。構(gòu)造方法有兩種。
第一種,指定分隔符:
public StringJoiner(CharSequence delimiter) {
this(delimiter, "", "");
}
第二種,指定分隔符、前綴、后綴:
public StringJoiner(CharSequence delimiter,
CharSequence prefix,
CharSequence suffix) {
this.prefix = prefix.toString();
this.delimiter = delimiter.toString();
this.suffix = suffix.toString();
}
雖然也可以在 StringBuilder 類的幫助下在每個(gè)字符串之后附加分隔符,但 StringJoiner 提供了更簡(jiǎn)單的方法來實(shí)現(xiàn),無需編寫大量的代碼。
03、at
獲取指定索引處上的字符。
Strman.at("沉默王二", 0);
Strman.at("沉默王二", -1);
Strman.at("沉默王二", 4);
結(jié)果如下所示:
Optional[沉]
Optional[二]
Optional.empty
也就是說,at 可以處理 -(length-1) 到 (length-1) 之內(nèi)的索引(當(dāng)索引為負(fù)數(shù)的時(shí)候?qū)哪┪查_始查找),如果超出這個(gè)范圍,將會(huì)返回 Optional.empty,避免發(fā)生空指針。
來看一下源碼:
public static Optional<String> at(final String value, int index) {
if (isNullOrEmpty(value)) {
return Optional.empty();
}
int length = value.length();
if (index < 0) {
index = length + index;
}
return (index < length && index >= 0) ? Optional.of(String.valueOf(value.charAt(index))) : Optional.empty();
}
本質(zhì)上,是通過 String 類的 charAt() 方法查找的,但包裹了一層 Optional,就巧妙地躲開了煩人的空指針。
Optional 是 Java 8 時(shí)新增的一個(gè)類,該類提供了一種用于表示可選值而非空引用的類級(jí)別解決方案。

04、between
按照指定起始字符和截止字符來返回一個(gè)字符串?dāng)?shù)組。
String [] results = Strman.between("[沉默王二][一枚有趣的程序員]","[", "]");
System.out.println(Arrays.toString(results));
結(jié)果如下所示:
[沉默王二, 一枚有趣的程序員]
來看一下源碼:
public static String[] between(final String value, final String start, final String end) {
String[] parts = value.split(end);
return Arrays.stream(parts).map(subPart -> subPart.substring(subPart.indexOf(start) + start.length()))
.toArray(String[]::new);
}
java.util.Arrays 類是為數(shù)組而生的專用工具類,基本上常見的對(duì)數(shù)組的操作,Arrays 類都考慮到了,stream() 方法可以將數(shù)組轉(zhuǎn)換成流:
String[] intro = new String[] { "沉", "默", "王", "二" };
Arrays.stream(intro);
Java 8 新增的 Stream 流在很大程度上提高了開發(fā)人員在操作集合(Collection)時(shí)的生產(chǎn)力。要想操作流,首先需要有一個(gè)數(shù)據(jù)源,可以是數(shù)組或者集合。每次操作都會(huì)返回一個(gè)新的流對(duì)象,方便進(jìn)行鏈?zhǔn)讲僮?,但原有的流?duì)象會(huì)保持不變。
map() 方法可以把一個(gè)流中的元素轉(zhuǎn)化成一個(gè)新流中的元素,它可以接收一個(gè) Lambda 表達(dá)式作為參數(shù)。Lambda 表達(dá)式描述了一個(gè)代碼塊(或者叫匿名方法),可以將其作為參數(shù)傳遞給構(gòu)造方法或者普通方法以便后續(xù)執(zhí)行。
考慮下面這段代碼:
() -> System.out.println("沉默王二")
來從左到右解釋一下,() 為 Lambda 表達(dá)式的參數(shù)列表(本例中沒有參數(shù)),-> 標(biāo)識(shí)這串代碼為 Lambda 表達(dá)式(也就是說,看到 -> 就知道這是 Lambda),System.out.println("沉默王二") 為要執(zhí)行的代碼,即將“沉默王二”打印到標(biāo)準(zhǔn)輸出流。
toArray() 方法可以將流轉(zhuǎn)換成數(shù)組,你可能比較好奇的是 String[]::new,它是什么東東呢?來看一下 toArray() 方法的源碼。
<A> A[] toArray(IntFunction<A[]> generator);
也就是說 String[]::new 是一個(gè) IntFunction,一個(gè)可以產(chǎn)生所需的新數(shù)組的函數(shù),可以通過反編譯字節(jié)碼看看它到底是什么:
String[] strArray = (String[])list.stream().toArray((x$0) -> {
return new String[x$0];
});
也就是相當(dāng)于返回了一個(gè)指定長度的字符串?dāng)?shù)組。
05、chars
返回組成字符串的單個(gè)字符的數(shù)組。
String [] results = Strman.chars("沉默王二");
System.out.println(Arrays.toString(results));
結(jié)果如下所示:
[沉, 默, 王, 二]
來看一下源碼:
public static String[] chars(final String value) {
return value.split("");
}
內(nèi)部是通過 String 類的 split() 方法實(shí)現(xiàn)的。
06、charsCount
統(tǒng)計(jì)字符串中每個(gè)字符出現(xiàn)的次數(shù)。
Map<Character, Long> map = Strman.charsCount("沉默王二的妹妹叫沉默王三");
System.out.println(map);
結(jié)果如下所示:
{的=1, 默=2, 三=1, 妹=2, 沉=2, 叫=1, 王=2, 二=1}
是不是瞬間覺得這個(gè)方法有意思多了,一步到位,統(tǒng)計(jì)出字符串中各個(gè)字符出現(xiàn)的次數(shù),來看一下源碼吧。
public static Map<Character, Long> charsCount(String input) {
return input.chars().mapToObj(c -> (char) c).collect(groupingBy(identity(), counting()));
}
String 類的 chars() 方法是 Java 9 新增的,它返回一個(gè)針對(duì)基本類型 int 的流:IntStream。
mapToObj() 方法主要是將 Stream 中的元素進(jìn)行裝箱操作, 轉(zhuǎn)換成一個(gè)引用類型的值, 它接收一個(gè) IntFunction 接口, 它是一個(gè) int -> R 的函數(shù)接口。
collect() 方法可以把流轉(zhuǎn)成集合 Map。
07、collapseWhitespace
用單個(gè)空格替換掉多個(gè)連續(xù)的空格。
Strman.collapseWhitespace("沉默王二 一枚有趣的程序員");
結(jié)果如下所示:
Strman.collapseWhitespace("沉默王二 一枚有趣的程序員")
來看一下源碼:
public static String collapseWhitespace(final String value) {
return value.trim().replaceAll("\\s\\s+", " ");
}
內(nèi)部先用 trim() 方法去掉兩側(cè)的空格,然后再用正則表達(dá)式將多個(gè)連續(xù)的空格替換成單個(gè)空格。
08、contains
驗(yàn)證指定的字符串是否包含某個(gè)字符串。
System.out.println(Strman.contains("沉默王二", "沉"));
System.out.println(Strman.contains("Abbc", "a", false));
結(jié)果如下所示:
true
true
第三個(gè)參數(shù) caseSensitive 是可選項(xiàng),如果為 false 則表明不區(qū)分大小寫。
來看一下源碼:
public static boolean contains(final String value, final String needle, final boolean caseSensitive) {
if (caseSensitive) {
return value.contains(needle);
}
return value.toLowerCase().contains(needle.toLowerCase());
}
內(nèi)部通過 String 類的 contains() 方法實(shí)現(xiàn),如果不區(qū)分大小寫,則先調(diào)用 toLowerCase() 方法轉(zhuǎn)成小寫。
09、containsAny
驗(yàn)證指定的字符串是否包含字符串?dāng)?shù)組中任意一個(gè)字符串,或更多。
System.out.println(Strman.containsAny("沉默王二", new String [] {"沉","三"}));
System.out.println(Strman.containsAny("沉默王二", new String [] {"沉默","三"}));
System.out.println(Strman.containsAny("沉默王二", new String [] {"不","三"}));
結(jié)果如下所示:
true
true
false
來看一下源碼:
public static boolean containsAny(final String value, final String[] needles, final boolean caseSensitive) {
return Arrays.stream(needles).anyMatch(needle -> contains(value, needle, caseSensitive));
}
Stream 類提供了三個(gè)方法可供進(jìn)行元素匹配,它們分別是:
anyMatch(),只要有一個(gè)元素匹配傳入的條件,就返回 true。allMatch(),只有有一個(gè)元素不匹配傳入的條件,就返回 false;如果全部匹配,則返回 true。noneMatch(),只要有一個(gè)元素匹配傳入的條件,就返回 false;如果全部匹配,則返回 true。
10、endsWith
驗(yàn)證字符串是否以某個(gè)字符串結(jié)尾。
System.out.println(Strman.endsWith("沉默王二","二"));
System.out.println(Strman.endsWith("Abbc", "A", false));
結(jié)果如下所示:
true
false
來看一下源碼:
public static boolean endsWith(final String value, final String search, final int position,
final boolean caseSensitive) {
int remainingLength = position - search.length();
if (caseSensitive) {
return value.indexOf(search, remainingLength) > -1;
}
return value.toLowerCase().indexOf(search.toLowerCase(), remainingLength) > -1;
}
內(nèi)部通過 String 類的 indexOf() 方法實(shí)現(xiàn)。
11、ensureLeft
確保字符串以某個(gè)字符串開頭,如果該字符串沒有以指定的字符串開頭,則追加上去。
System.out.println(Strman.ensureLeft("沉默王二", "沉"));
System.out.println(Strman.ensureLeft("默王二", "沉"));
結(jié)果如下所示:
沉默王二
沉默王二
來看一下源碼:
public static String ensureLeft(final String value, final String prefix, final boolean caseSensitive) {
if (caseSensitive) {
return value.startsWith(prefix) ? value : prefix + value;
}
String _value = value.toLowerCase();
String _prefix = prefix.toLowerCase();
return _value.startsWith(_prefix) ? value : prefix + value;
}
內(nèi)部通過 String 類的 startsWith() 方法先進(jìn)行判斷,如果結(jié)果為 false,則通過“+”操作符進(jìn)行連接。
ensureLeft 對(duì)應(yīng)的還有 ensureRight,同理,這里不再贅述。
12、base64Encode
把字符串進(jìn)行 base64 編碼。
Strman.base64Encode("沉默王二");
結(jié)果如下所示:
5rKJ6buY546L5LqM
Base64 是一種基于 64 個(gè)可打印字符來表示二進(jìn)制數(shù)據(jù)的表示方法。來看一下源碼:
public static String base64Encode(final String value) {
return Base64.getEncoder().encodeToString(value.getBytes(StandardCharsets.UTF_8));
}
內(nèi)部是通過 Base64 類實(shí)現(xiàn)的,Java 8 新增的一個(gè)類。
base64Encode 對(duì)應(yīng)的解碼方法是 base64Decode,使用方法如下所示:
Strman.base64Decode("5rKJ6buY546L5LqM")
如果不可解碼的會(huì),會(huì)拋出 IllegalArgumentException 異常。
Exception in thread "main" java.lang.IllegalArgumentException: Last unit does not have enough valid bits
at java.base/java.util.Base64$Decoder.decode0(Base64.java:763)
at java.base/java.util.Base64$Decoder.decode(Base64.java:535)
at java.base/java.util.Base64$Decoder.decode(Base64.java:558)
at strman.Strman.base64Decode(Strman.java:328)
at com.itwanger.strman.Demo.main(Demo.java:58)
13、binEncode
把字符串轉(zhuǎn)成二進(jìn)制的 Unicode(16 位)。
Strman.binEncode("沉默王二");
結(jié)果如下所示:
0110110010001001100111101101100001110011100010110100111010001100
binEncode 對(duì)應(yīng)的方法是 binDecode,把二進(jìn)制的 Unicode 轉(zhuǎn)成字符串,使用方法如下所示:
Strman.binDecode("0110110010001001100111101101100001110011100010110100111010001100");
14、first
返回字符串的前 N 個(gè)字符。
System.out.println(Strman.first("沉默王二", 0));
System.out.println(Strman.first("沉默王二", 1));
System.out.println(Strman.first("沉默王二", 2));
結(jié)果如下所示:
Optional[]
Optional[沉]
Optional[沉默]
如果 N 為負(fù)數(shù)的話,將會(huì)拋出 StringIndexOutOfBoundsException 異常:
Exception in thread "main" java.lang.StringIndexOutOfBoundsException: begin 0, end -1, length 4
at java.base/java.lang.String.checkBoundsBeginEnd(String.java:3319)
at java.base/java.lang.String.substring(String.java:1874)
at strman.Strman.lambda$first$9(Strman.java:414)
at java.base/java.util.Optional.map(Optional.java:265)
at strman.Strman.first(Strman.java:414)
at com.itwanger.strman.Demo.main(Demo.java:68)
針對(duì) N 為負(fù)數(shù)的情況,我覺得沒有之前的 at 方法處理的巧妙。
來看一下源碼:
public static Optional<String> first(final String value, final int n) {
return Optional.ofNullable(value).filter(v -> !v.isEmpty()).map(v -> v.substring(0, n));
}
內(nèi)部是通過 String 類的 substring() 方法實(shí)現(xiàn)的,不過沒有針對(duì) n 小于 0 的情況做處理。
ofNullable() 方法可以創(chuàng)建一個(gè)即可空又可非空的 Optional 對(duì)象。
filter() 方法的參數(shù)類型為 Predicate(Java 8 新增的一個(gè)函數(shù)式接口),也就是說可以將一個(gè) Lambda 表達(dá)式傳遞給該方法作為條件,如果表達(dá)式的結(jié)果為 false,則返回一個(gè) EMPTY 的 Optional 對(duì)象,否則返回過濾后的 Optional 對(duì)象。
map() 方法可以按照一定的規(guī)則將原有 Optional 對(duì)象轉(zhuǎn)換為一個(gè)新的 Optional 對(duì)象,原有的 Optional 對(duì)象不會(huì)更改。
first 對(duì)應(yīng)的的是 last 方法,返回字符串的后 N 個(gè)字符。
15、head
返回字符串的第一個(gè)字符。
Strman.head("沉默王二");
結(jié)果如下所示:
Optional[沉]
來看一下源碼:
public static Optional<String> head(final String value) {
return first(value, 1);
}
內(nèi)部是通過調(diào)用 first() 方法實(shí)現(xiàn)的,只不過 N 為 1。
16、unequal
檢查兩個(gè)字符串是否不等。
Strman.unequal("沉默王二","沉默王三");
結(jié)果如下所示:
true
來看一下源碼:
public static boolean unequal(final String first, final String second) {
return !Objects.equals(first, second);
}
內(nèi)部是通過 Objects.equals() 方法進(jìn)行判斷的,由于 String 類重寫了 equals() 方法,也就是說,實(shí)際上還是通過 String 類的 equals() 方法進(jìn)行判斷的。
17、insert
把字符串插入到指定索引處。
Strman.insert("沉默二","王",2);
結(jié)果如下所示:
沉默王二
來看一下源碼:
public static String insert(final String value, final String substr, final int index) {
if (index > value.length()) {
return value;
}
return append(value.substring(0, index), substr, value.substring(index));
}
如果索引超出字符串長度,直接返回原字符串;否則調(diào)用 append() 方法將指定字符串插入到對(duì)應(yīng)索引處。
18、repeat
對(duì)字符串重復(fù)指定次數(shù)。
Strman.repeat("沉默王二", 3);
結(jié)果如下所示:
沉默王二沉默王二沉默王二
來看一下源碼:
public static String repeat(final String value, final int multiplier) {
return Stream.generate(() -> value).limit(multiplier).collect(joining());
}
Stream.generate() 生成的 Stream,默認(rèn)是串行(相對(duì) parallel 而言)但無序的(相對(duì) ordered 而言)。由于它是無限的,在管道中,必須利用 limit 之類的操作限制 Stream 大小。
collect(joining()) 可以將流轉(zhuǎn)成字符串。
19、leftPad
返回給定長度的新字符串,以便填充字符串的開頭。
Strman.leftPad("王二","沉默",6);
結(jié)果如下所示:
沉默沉默沉默沉默王二
來看一下源碼:
public static String leftPad(final String value, final String pad, final int length) {
if (value.length() > length) {
return value;
}
return append(repeat(pad, length - value.length()), value);
}
內(nèi)部會(huì)先調(diào)用 repeat() 方法進(jìn)行補(bǔ)位,然后再調(diào)用 append() 方法拼接。
leftPad 方法對(duì)應(yīng)的是 rightPad,填充字符串的末尾。
19)removeEmptyStrings,從字符串?dāng)?shù)組中移除空字符串。
String [] results = Strman.removeEmptyStrings(new String[]{"沉", " ", " ", "默王二"});
System.out.println(Arrays.toString(results));
結(jié)果如下所示:
[沉, 默王二]
來看一下源碼:
public static String[] removeEmptyStrings(String[] strings) {
if (Objects.isNull(strings)) {
throw new IllegalArgumentException("Input array should not be null");
}
return Arrays.stream(strings).filter(str -> str != null && !str.trim().isEmpty()).toArray(String[]::new);
}
通過 Stream 的 filter() 方法過濾掉了空格。
20、reverse
反轉(zhuǎn)字符串。
Strman.reverse("沉默王二");
結(jié)果如下所示:
二王默沉
來看一下源碼:
public static String reverse(final String value) {
return new StringBuilder(value).reverse().toString();
}
內(nèi)部是通過 StringBuilder 類的 reverse() 方法進(jìn)行反轉(zhuǎn)的。
21、safeTruncate
對(duì)字符串進(jìn)行截?cái)啵粫?huì)破壞單詞的完整性。
Strman.safeTruncate("Java is the best",13,"...");
結(jié)果如下所示:
Java is...
來看一下源碼:
public static String safeTruncate(final String value, final int length, final String filler) {
if (length == 0) {
return "";
}
if (length >= value.length()) {
return value;
}
String[] words = words(value);
StringJoiner result = new StringJoiner(" ");
int spaceCount = 0;
for (String word : words) {
if (result.length() + word.length() + filler.length() + spaceCount > length) {
break;
} else {
result.add(word);
spaceCount++;
}
}
return append(result.toString(), filler);
}
先調(diào)用 words() 方法對(duì)字符串進(jìn)行單詞分割,然后按照長度進(jìn)行截?cái)?,最后調(diào)用 append() 方法填充上補(bǔ)位符。
safeTruncate 對(duì)應(yīng)的是 truncate,可能會(huì)破壞單詞的完整性,使用方法如下所示:
Strman.truncate("Java is the best",13,"...")
結(jié)果如下所示:
Java is th...
來看一下源碼:
public static String truncate(final String value, final int length, final String filler) {
if (length == 0) {
return "";
}
if (length >= value.length()) {
return value;
}
return append(value.substring(0, length - filler.length()), filler);
}
就是單純的切割和補(bǔ)位,沒有對(duì)單詞進(jìn)行保護(hù)。
22、shuffle
對(duì)字符串重新洗牌。
Strman.shuffle("沉默王二");
結(jié)果如下所示:
王默二沉
來看一下源碼:
public static String shuffle(final String value) {
String[] chars = chars(value);
Random random = new Random();
for (int i = 0; i < chars.length; i++) {
int r = random.nextInt(chars.length);
String tmp = chars[i];
chars[i] = chars[r];
chars[r] = tmp;
}
return Arrays.stream(chars).collect(joining());
}
調(diào)用 chars() 方法把字符串拆分為字符串?dāng)?shù)組,然后遍歷對(duì)其重排,最后通過 Stream 轉(zhuǎn)成新的字符串。
23、其他方法
Strman 中還有很多其他巧妙的字符串處理方法,比如說把字符串按照指定的前后綴進(jìn)行包裹 surround 等等,同學(xué)們可以參考 Strman 的官方文檔進(jìn)行學(xué)習(xí):
PS:最近有小伙伴私信我要一份優(yōu)質(zhì)的 Java 教程,我在 GitHub 花了很長時(shí)間才找到了一份,115k star,真的非常不錯(cuò),來看一下目錄:



花了三個(gè)半小時(shí)把這份教程整理成 PDF 后,我發(fā)給了小伙伴,他“啪”的一下就發(fā)過來了私信,很快啊,“二哥,你也太用心了,這份教程的質(zhì)量真的高,不服不行!”
如果你也對(duì)這份 PDF 感興趣的話,可以通過下面的方式獲取。
多說一句,遇到好的資源,在讓它吃灰的同時(shí),能學(xué)一點(diǎn)就賺一點(diǎn),對(duì)吧?知識(shí)是無窮無盡的,但只要我們比其他人多學(xué)到了那么一點(diǎn)點(diǎn),那是不是就超越了呢?
點(diǎn)個(gè)贊吧,希望更多的人看得到!