GitHub 標星 1.3k+,一款超讚的用於字符串處理的 Java 8 庫,附帶源碼分析

大家好,我是沉默王二。

很多初學編程的同學,經常給我吐槽,說:“二哥,你在敲代碼的時候會不會有這樣一種感覺,寫着寫着看不下去了,覺得自己寫出來的代碼就好像屎一樣?”

這裏我必須得說一句,初入“江湖”的時候,確實會覺得自己的代碼寫得很爛,但這麼多年下來,這種感覺已經蕩然無存了。

(吹嘛,我也會,哈哈)

那,怎麼才能讓寫出來的代碼不那麼爛呢?

我的一個經驗就是,“拿來主義”,儘量不去重複造輪子。使用那些已經被驗證過,足夠優質的開源庫不僅能夠讓我們的代碼變得優雅,還能夠讓我們在不斷的使用過程當中,學習到編程的精髓。

洋務運動的時候,有一句很響亮的口號叫做,“師夷長技以制夷”。先去用,再去學,自然而然就會變得牛逼。同學們,你們說,是不是這個理?

我今天推薦的這款開源庫,名字叫做 strman-java,GitHub 上標星 1.3k,一款超讚的字符串處理工具庫,基於 Java 8,語法非常簡潔。

接下來,我們來看看怎麼用。

Maven 項目只需要在 pom.xml 文件中添加以下依賴即可。

<dependency>
    <groupId>com.shekhargulati</groupId>
    <artifactId>strman</artifactId>
    <version>0.4.0</version>
</dependency>

好了,可以肆無忌憚地調用 strman-java 的 API 了。我會在介紹的時候插入一些源碼的介紹,方便同學們更深一步的學習,儘量做到“知其然知其所以然”。

01、append

把可變字符串參數添加到指定的字符串尾部。

Strman.append("沉","默","王","二");

結果如下所示:

沉默王二

append 對應的方法是 prepend,把可變字符串參數前置到指定的字符串前面,使用方法如下。

Strman.prepend("沉","默","王","二");

結果如下所示:

默王二沉

02、appendArray

把字符串數組添加到指定的字符串尾部。

String [] strs = {"默","王","二"};
Strman.appendArray("沉",strs);

結果如下所示:

沉默王二

append 內部其實調用的 appendArray,來看一下源碼:

public static String append(final String value, final String... appends) {
    return appendArray(value, appends);
}

當使用可變參數的時候,實際上是先創建了一個數組,該數組的大小就是可變參數的個數,然後將參數放入數組當中,再將數組傳遞給被調用的方法

通過觀察反編譯後的字節碼,就能看得到。

Strman.append("沉","默","王","二");

實際等同於:

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

內部用的 StringJoiner,Java 8 時新增的一個類。構造方法有兩種。

第一種,指定分隔符:

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 類的幫助下在每個字符串之後附加分隔符,但 StringJoiner 提供了更簡單的方法來實現,無需編寫大量的代碼。

03、at

獲取指定索引處上的字符。

Strman.at("沉默王二", 0);
Strman.at("沉默王二", -1);
Strman.at("沉默王二", 4);

結果如下所示:

Optional[沉]
Optional[二]
Optional.empty

也就是說,at 可以處理 -(length-1)(length-1) 之內的索引(當索引爲負數的時候將從末尾開始查找),如果超出這個範圍,將會返回 Optional.empty,避免發生空指針。

來看一下源碼:

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

本質上,是通過 String 類的 charAt() 方法查找的,但包裹了一層 Optional,就巧妙地躲開了煩人的空指針。

Optional 是 Java 8 時新增的一個類,該類提供了一種用於表示可選值而非空引用的類級別解決方案。

04、between

按照指定起始字符和截止字符來返回一個字符串數組。

String [] results = Strman.between("[沉默王二][一枚有趣的程序員]","[", "]");
System.out.println(Arrays.toString(results));

結果如下所示:

[沉默王二, 一枚有趣的程序員]

來看一下源碼:

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 類是爲數組而生的專用工具類,基本上常見的對數組的操作,Arrays 類都考慮到了,stream() 方法可以將數組轉換成流:

String[] intro = new String[] { "沉", "默", "王", "二" };
Arrays.stream(intro);

Java 8 新增的 Stream 流在很大程度上提高了開發人員在操作集合(Collection)時的生產力。要想操作流,首先需要有一個數據源,可以是數組或者集合。每次操作都會返回一個新的流對象,方便進行鏈式操作,但原有的流對象會保持不變。

map() 方法可以把一個流中的元素轉化成一個新流中的元素,它可以接收一個 Lambda 表達式作爲參數。Lambda 表達式描述了一個代碼塊(或者叫匿名方法),可以將其作爲參數傳遞給構造方法或者普通方法以便後續執行。

考慮下面這段代碼:

() -> System.out.println("沉默王二")

來從左到右解釋一下,() 爲 Lambda 表達式的參數列表(本例中沒有參數),-> 標識這串代碼爲 Lambda 表達式(也就是說,看到 -> 就知道這是 Lambda),System.out.println("沉默王二") 爲要執行的代碼,即將“沉默王二”打印到標準輸出流。

toArray() 方法可以將流轉換成數組,你可能比較好奇的是 String[]::new,它是什麼東東呢?來看一下 toArray() 方法的源碼。

<A> A[] toArray(IntFunction<A[]> generator);

也就是說 String[]::new 是一個 IntFunction,一個可以產生所需的新數組的函數,可以通過反編譯字節碼看看它到底是什麼:

String[] strArray = (String[])list.stream().toArray((x$0) -> {
    return new String[x$0];
});

也就是相當於返回了一個指定長度的字符串數組。

05、chars

返回組成字符串的單個字符的數組。

String [] results = Strman.chars("沉默王二");
System.out.println(Arrays.toString(results));

結果如下所示:

[沉, 默, 王, 二]

來看一下源碼:

public static String[] chars(final String value) {
    return value.split("");
}

內部是通過 String 類的 split() 方法實現的。

06、charsCount

統計字符串中每個字符出現的次數。

Map<Character, Long> map = Strman.charsCount("沉默王二的妹妹叫沉默王三");
System.out.println(map);

結果如下所示:

{的=1, 默=2, 三=1, 妹=2, 沉=2, 叫=1, 王=2, 二=1}

是不是瞬間覺得這個方法有意思多了,一步到位,統計出字符串中各個字符出現的次數,來看一下源碼吧。

public static Map<Character, Long> charsCount(String input) {
    return input.chars().mapToObj(c -> (char) c).collect(groupingBy(identity(), counting()));
}

String 類的 chars() 方法是 Java 9 新增的,它返回一個針對基本類型 int 的流:IntStream。

mapToObj() 方法主要是將 Stream 中的元素進行裝箱操作, 轉換成一個引用類型的值, 它接收一個 IntFunction 接口, 它是一個 int -> R 的函數接口。

collect() 方法可以把流轉成集合 Map。

07、collapseWhitespace

用單個空格替換掉多個連續的空格。

Strman.collapseWhitespace("沉默王二       一枚有趣的程序員");

結果如下所示:

Strman.collapseWhitespace("沉默王二       一枚有趣的程序員")

來看一下源碼:

public static String collapseWhitespace(final String value) {
    return value.trim().replaceAll("\\s\\s+", " ");
}

內部先用 trim() 方法去掉兩側的空格,然後再用正則表達式將多個連續的空格替換成單個空格。

08、contains

驗證指定的字符串是否包含某個字符串。

System.out.println(Strman.contains("沉默王二", "沉"));
System.out.println(Strman.contains("Abbc", "a", false));

結果如下所示:

true
true

第三個參數 caseSensitive 是可選項,如果爲 false 則表明不區分大小寫。

來看一下源碼:

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

內部通過 String 類的 contains() 方法實現,如果不區分大小寫,則先調用 toLowerCase() 方法轉成小寫。

09、containsAny

驗證指定的字符串是否包含字符串數組中任意一個字符串,或更多。

System.out.println(Strman.containsAny("沉默王二", new String [] {"沉","三"}));
System.out.println(Strman.containsAny("沉默王二", new String [] {"沉默","三"}));
System.out.println(Strman.containsAny("沉默王二", new String [] {"不","三"}));

結果如下所示:

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 類提供了三個方法可供進行元素匹配,它們分別是:

  • anyMatch(),只要有一個元素匹配傳入的條件,就返回 true。

  • allMatch(),只有有一個元素不匹配傳入的條件,就返回 false;如果全部匹配,則返回 true。

  • noneMatch(),只要有一個元素匹配傳入的條件,就返回 false;如果全部匹配,則返回 true。

10、endsWith

驗證字符串是否以某個字符串結尾。

System.out.println(Strman.endsWith("沉默王二","二"));
System.out.println(Strman.endsWith("Abbc", "A", false));

結果如下所示:

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

內部通過 String 類的 indexOf() 方法實現。

11、ensureLeft

確保字符串以某個字符串開頭,如果該字符串沒有以指定的字符串開頭,則追加上去。

System.out.println(Strman.ensureLeft("沉默王二", "沉"));
System.out.println(Strman.ensureLeft("默王二", "沉"));

結果如下所示:

沉默王二
沉默王二

來看一下源碼:

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

內部通過 String 類的 startsWith() 方法先進行判斷,如果結果爲 false,則通過“+”操作符進行連接。

ensureLeft 對應的還有 ensureRight,同理,這裏不再贅述。

12、base64Encode

把字符串進行 base64 編碼。

Strman.base64Encode("沉默王二");

結果如下所示:

5rKJ6buY546L5LqM

Base64 是一種基於 64 個可打印字符來表示二進制數據的表示方法。來看一下源碼:

public static String base64Encode(final String value) {
    return Base64.getEncoder().encodeToString(value.getBytes(StandardCharsets.UTF_8));
}

內部是通過 Base64 類實現的,Java 8 新增的一個類。

base64Encode 對應的解碼方法是 base64Decode,使用方法如下所示:

Strman.base64Decode("5rKJ6buY546L5LqM")

如果不可解碼的會,會拋出 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

把字符串轉成二進制的 Unicode(16 位)。

Strman.binEncode("沉默王二");

結果如下所示:

0110110010001001100111101101100001110011100010110100111010001100

binEncode 對應的方法是 binDecode,把二進制的 Unicode 轉成字符串,使用方法如下所示:

Strman.binDecode("0110110010001001100111101101100001110011100010110100111010001100");

14、first

返回字符串的前 N 個字符。

System.out.println(Strman.first("沉默王二", 0));
System.out.println(Strman.first("沉默王二", 1));
System.out.println(Strman.first("沉默王二", 2));

結果如下所示:

Optional[]
Optional[沉]
Optional[沉默]

如果 N 爲負數的話,將會拋出 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)

針對 N 爲負數的情況,我覺得沒有之前的 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));
}

內部是通過 String 類的 substring() 方法實現的,不過沒有針對 n 小於 0 的情況做處理。

ofNullable() 方法可以創建一個即可空又可非空的 Optional 對象。

filter() 方法的參數類型爲 Predicate(Java 8 新增的一個函數式接口),也就是說可以將一個 Lambda 表達式傳遞給該方法作爲條件,如果表達式的結果爲 false,則返回一個 EMPTY 的 Optional 對象,否則返回過濾後的 Optional 對象。

map() 方法可以按照一定的規則將原有 Optional 對象轉換爲一個新的 Optional 對象,原有的 Optional 對象不會更改。

first 對應的的是 last 方法,返回字符串的後 N 個字符。

15、head

返回字符串的第一個字符。

Strman.head("沉默王二");

結果如下所示:

Optional[沉]

來看一下源碼:

public static Optional<String> head(final String value) {
    return first(value, 1);
}

內部是通過調用 first() 方法實現的,只不過 N 爲 1。

16、unequal

檢查兩個字符串是否不等。

Strman.unequal("沉默王二","沉默王三");

結果如下所示:

true

來看一下源碼:

public static boolean unequal(final String first, final String second) {
    return !Objects.equals(first, second);
}

內部是通過 Objects.equals() 方法進行判斷的,由於 String 類重寫了 equals() 方法,也就是說,實際上還是通過 String 類的 equals() 方法進行判斷的。

17、insert

把字符串插入到指定索引處。

Strman.insert("沉默二","王",2);

結果如下所示:

沉默王二

來看一下源碼:

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

如果索引超出字符串長度,直接返回原字符串;否則調用 append() 方法將指定字符串插入到對應索引處。

18、repeat

對字符串重複指定次數。

Strman.repeat("沉默王二", 3);

結果如下所示:

沉默王二沉默王二沉默王二

來看一下源碼:

public static String repeat(final String value, final int multiplier) {
    return Stream.generate(() -> value).limit(multiplier).collect(joining());
}

Stream.generate() 生成的 Stream,默認是串行(相對 parallel 而言)但無序的(相對 ordered 而言)。由於它是無限的,在管道中,必須利用 limit 之類的操作限制 Stream 大小。

collect(joining()) 可以將流轉成字符串。

19、leftPad

返回給定長度的新字符串,以便填充字符串的開頭。

Strman.leftPad("王二","沉默",6);

結果如下所示:

沉默沉默沉默沉默王二

來看一下源碼:

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

內部會先調用 repeat() 方法進行補位,然後再調用 append() 方法拼接。

leftPad 方法對應的是 rightPad,填充字符串的末尾。

19)removeEmptyStrings,從字符串數組中移除空字符串。

String [] results = Strman.removeEmptyStrings(new String[]{"沉", " ", "   ", "默王二"});
System.out.println(Arrays.toString(results));

結果如下所示:

[沉, 默王二]

來看一下源碼:

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

反轉字符串。

Strman.reverse("沉默王二");

結果如下所示:

二王默沉

來看一下源碼:

public static String reverse(final String value) {
    return new StringBuilder(value).reverse().toString();
}

內部是通過 StringBuilder 類的 reverse() 方法進行反轉的。

21、safeTruncate

對字符串進行截斷,但不會破壞單詞的完整性。

Strman.safeTruncate("Java is the best",13,"...");

結果如下所示:

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

先調用 words() 方法對字符串進行單詞分割,然後按照長度進行截斷,最後調用 append() 方法填充上補位符。

safeTruncate 對應的是 truncate,可能會破壞單詞的完整性,使用方法如下所示:

Strman.truncate("Java is the best",13,"...")

結果如下所示:

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

就是單純的切割和補位,沒有對單詞進行保護。

22、shuffle

對字符串重新洗牌。

Strman.shuffle("沉默王二");

結果如下所示:

王默二沉

來看一下源碼:

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

調用 chars() 方法把字符串拆分爲字符串數組,然後遍歷對其重排,最後通過 Stream 轉成新的字符串。

23、其他方法

Strman 中還有很多其他巧妙的字符串處理方法,比如說把字符串按照指定的前後綴進行包裹 surround 等等,同學們可以參考 Strman 的官方文檔進行學習:

https://github.com/shekhargulati/strman-java/wiki

PS:最近有小夥伴私信我要一份優質的 Java 教程,我在 GitHub 花了很長時間才找到了一份,115k star,真的非常不錯,來看一下目錄:

花了三個半小時把這份教程整理成 PDF 後,我發給了小夥伴,他“啪”的一下就發過來了私信,很快啊,“二哥,你也太用心了,這份教程的質量真的高,不服不行!”

如果你也對這份 PDF 感興趣的話,可以通過下面的方式獲取。

鏈接:https://pan.baidu.com/s/1rT0l5ynzAQLF--efyRHzQw 密碼:dz95

多說一句,遇到好的資源,在讓它喫灰的同時,能學一點就賺一點,對吧?知識是無窮無盡的,但只要我們比其他人多學到了那麼一點點,那是不是就超越了呢?

點個贊吧,希望更多的人看得到!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章