Java 拆分字串效能
1.概述
字串操作是大多數程式語言(包括 Java)中的常見操作。無論是解析日誌檔案、讀取 CSV 資料或處理使用者輸入,我們經常需要根據分隔符號將字串拆分成更小的部分。
雖然 Java 提供了多種拆分字串的方法,但它們的效能會根據所選方法和資料大小而有很大差異。
在本教程中,我們將探討在 Java 中拆分字串的不同方法,比較它們的效能,並提供選擇最有效方法的最佳實踐。
2. 字串拆分中效能為何如此重要
處理小字串時,效能差異可能無關緊要。然而,在處理大量文字資料的應用程式中,選擇正確的字串拆分方法會對速度和記憶體使用產生重大影響。
例如,讓我們檢查一些效能很重要的場景:
- 日誌處理:某些系統將數百萬個日誌行解析為字段,而基於正規表示式的拆分可能會減慢吸收速度。
- 資料解析:具有數百萬行的 CSV 或 TSV 檔案可以快速暴露出低效率拆分的成本。
- 高吞吐量服務:標記查詢參數或標頭的 API,每秒處理數千個請求,低效的分割會增加 CPU 負載並降低系統回應能力。
- 記憶體使用情況:頻繁的字串拆分會創造大量臨時對象,增加垃圾收集壓力。
選擇正確的字串拆分方法會影響生產系統的效能、可擴展性和回應能力。
3. Java 字串拆分方法
Java 提供了多種拆分字串的方法,每種方法都有各自的優點、限制和效能。雖然所有方法最終都會將字串分割成更小的部分,但它們的效率和易用性會因輸入大小、模式複雜度和記憶體限製而異。
3.1. 使用String.split()
String.split()
方法接受以正規表示式表示的分隔符,並在每次出現分隔符時斷開字串,並傳回一個子字串陣列。
為了演示,讓我們拆分一個字串:
public class SplitBasic {
public static void main(String[] args) {
String text = "apple,banana,orange,grape";
String[] fruits = text.split(",");
for (String fruit : fruits) {
System.out.println(fruit);
}
}
}
這裡,逗號是分隔符號。此方法掃描字串,找到與分隔符號相符的部分,並將字串切分成幾部分:
apple
banana
orange
grape
此外,由於此方法使用正規表示式,因此它可以處理比簡單分隔符號更複雜的情況:
public class SplitWhitespace {
public static void main(String[] args) {
String text = "apple banana\tgrape";
String[] parts = text.split("\\s+");
for (String part : parts) {
System.out.println(part);
}
}
}
上面, \\s+
正規表示式符合一個或多個空格字元:
apple
banana
grape
此外, String.split()
也接受一個限制參數,使我們能夠指定字串應拆分的次數:
public class SplitLimit {
public static void main(String[] args) {
String text = "a,b,c,d,e";
String[] parts = text.split(",", 3);
for (String part : parts) {
System.out.println(part);
}
}
}
這裡,字串只被分成三個部分;其餘部分c,d,e
保持不變:
a
b
c,d,e
當我們只需要記錄中的前幾個欄位時,上述方法很有用。
總而言之,由於此方法支援正規表示式語法,我們可以在一次呼叫中處理多個分隔符號和複雜模式。此外,它實現速度很快,非常適合執行小型任務。
另一方面,該方法會帶來正規表示式(regex)的開銷,這意味著即使是簡單的分隔符號(例如逗號)也可能需要進一步處理,這可能會降低速度。此外,當在大型資料集上頻繁使用時,每次呼叫都會建立新的陣列和子字串,這會增加垃圾收集的壓力。
3.2. 使用Pattern.split()
Java 中的Pattern
類別提供了一種使用已編譯正規表示式的方法。與每次呼叫都會編譯正規表示式的String.split()
不同, Pattern.split()
允許我們預先編譯一次模式,然後在多個運算中重複使用它。因此,在處理大型資料集或在循環中反覆拆分字串時,Pattern.split() 是更好的選擇。
例如,讓我們根據空格進行拆分:
import java.util.regex.Pattern;
public class PatternSplitExample {
public static void main(String[] args) {
String logEntry = "2025-09-18 10:35:22 INFO User=samuel Action=login Status=success";
Pattern whitespace = Pattern.compile("\\s+");
String[] fields = whitespace.split(logEntry);
for (String field : fields) {
System.out.println(field);
}
}
}
這裡,我們預先編譯了\\s+
模式一次,然後用它來分割字串:
2025-09-18
10:35:22
INFO
User=samuel
Action=login
Status=success
當循環處理數千行時,與使用String.split()
方法相比,它將節省大量時間。
它還可以處理複雜的分隔符號:
import java.util.regex.Pattern;
public class PatternSplitMultiDelimiter {
public static void main(String[] args) {
String text = "apple,banana;grape orange";
// Compile regex that matches comma, semicolon, or space
Pattern pattern = Pattern.compile("[,; ]");
String[] parts = pattern.split(text);
for (String part : parts) {
System.out.println(part);
}
}
}
上面,我們用逗號、分號或空格來分割字串:
apple
banana
grape
orange
使用Pattern.compile()
和Pattern.split()
通常比String.split()
方法更好,因為它避免了每次分割字串時編譯正規表示式的重複成本。
3.3. 使用String.indexOf()
和substring()
我們不需要依賴正規表示式或標記器,而是可以使用String.indexOf()
直接掃描字串中的分隔符號位置,然後使用substring()
來提取分隔符號位置之間的子字串,從而避免正規表示式處理、標記器中的物件建立和其他抽象的開銷。
為了演示,讓我們拆分一個逗號分隔的字串:
public class ManualSplitExample {
public static void main(String[] args) {
String text = "apple,banana,grape";
int start = 0;
int index;
while ((index = text.indexOf(",", start)) >= 0) {
String token = text.substring(start, index);
System.out.println(token);
start = index + 1;
}
String lastToken = text.substring(start);
System.out.println(lastToken);
}
}
上面,我們根據逗號分隔符號手動拆分字串:
apple
banana
grape
具體來說,我們使用indexOf(“,”, start)
從 start 位置開始搜尋逗號分隔符號。如果找到,則提取start
和分隔符號索引之間的子字串。然後,將start
移到分隔符號之後,繼續掃描,直到提取最後一個分隔符號後的最後一個標記。
對於具有簡單分隔符號的中小型字串,此方法可能是最快的,但與String.split()
相比,對於大型字串,效能會有所下降。它也避免了編譯和執行正規表示式的開銷。
此外,這種方法可以直接控制解析過程,使我們能夠處理諸如尾隨分隔符號、空標記和空格修剪等邊緣情況。
然而,由於不支援正規表示式, indexOf()
通常不適用於複雜的分隔符號模式,例如多個空格字元。此外,由於其實作方式相當依賴手動,如果我們忘記處理某些邊緣情況(例如字串開頭或結尾的分隔符號),則更容易引入錯誤。
4. 性能基準測試
現在我們已經探索了不同的方法,讓我們來測量它們的性能。在本例中,我們使用 Java Microbenchmark Harness 和 Maven 來執行此操作。
4.1. 建立Maven項目
首先,讓我們產生一個 Maven 專案:
$ mvn archetype:generate -DgroupId=com.baeldung -DartifactId=splitstringperformance -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
創建專案後,讓我們導航到split-string-performance
目錄並適當更新pom.xml
檔案:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>jmh-split-benchmark</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<jmh.version>1.36</jmh.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<finalName>benchmarks</finalName>
</properties>
<dependencies>
<!-- JMH Benchmarking -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>${jmh.version}</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
<scope>provided</scope>
</dependency>
<!-- JUnit 5 for unit testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Compiler plugin with JMH annotation processor -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<annotationProcessorPaths>
<path>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<!-- JMH Uber Jar with Shade plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.openjdk.jmh.Main</mainClass>
</transformer>
</transformers>
<finalName>benchmarks</finalName>
</configuration>
</execution>
</executions>
</plugin>
<!-- Surefire plugin for running JUnit 5 tests -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
</plugins>
</build>
</project>
處理完所有依賴關係後,我們就可以寫實際的測試了。
4.2. 實作拆分字串效能測試
接下來,我們導航到src/main/java/com/baeldung/splitstringperformance
。在此目錄中,讓我們建立一個名為SplitStringPerformance.java
的檔案:
package com.baeldung.splitstringperformance;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Fork(value = 1)
@Warmup(iterations = 5)
@Measurement(iterations = 10)
@State(Scope.Thread)
public class SplitStringPerformance {
@Param({"10", "1000", "100000"})
public int tokenCount;
private static final String DELIM = ",";
private String text;
private Pattern commaPattern;
@Setup(Level.Trial)
public void setup() {
StringBuilder sb = new StringBuilder(tokenCount * 8);
for (int i = 0; i < tokenCount; i++) {
sb.append("token").append(i);
if (i < tokenCount - 1) sb.append(DELIM);
}
text = sb.toString();
commaPattern = Pattern.compile(",");
}
@Benchmark
public void stringSplit(Blackhole bh) {
String[] parts = text.split(DELIM);
bh.consume(parts.length);
}
@Benchmark
public void patternSplit(Blackhole bh) {
String[] parts = commaPattern.split(text);
bh.consume(parts.length);
}
@Benchmark
public void manualSplit(Blackhole bh) {
List<String> tokens = new ArrayList<>(tokenCount);
int start = 0, idx;
while ((idx = text.indexOf(DELIM, start)) >= 0) {
tokens.add(text.substring(start, idx));
start = idx + 1;
}
tokens.add(text.substring(start));
bh.consume(tokens.size());
}
}
在這裡,我們使用上面的程式碼來對拆分逗號分隔字串的三種方法進行基準測試:
-
String.split()
-
Pattern.split()
-
String.indexOf()
和substring()
基準測試以三種輸入令牌大小運作:
-
10
-
1000
-
100000
這樣,我們就可以比較小型和大型資料集的效能。
4.3. 運行測試
此時,我們應該能夠執行基準測試。
首先,讓我們建立專案:
$ mvn -DskipTests package
該命令編譯主 Java 原始程式碼並在target
目錄中建立benchmarks.jar
檔。
建立文件後,讓我們執行基準測試並查看結果:
$ java -jar target/benchmarks.jar
...
Benchmark (tokenCount) Mode Cnt Score Error Units
SplitStringPerformance.manualSplit 10 avgt 10 0.334 ± 0.041 us/op
SplitStringPerformance.manualSplit 1000 avgt 10 46.469 ± 6.864 us/op
SplitStringPerformance.manualSplit 100000 avgt 10 22698.745 ± 4779.351 us/op
SplitStringPerformance.patternSplit 10 avgt 10 0.998 ± 0.267 us/op
SplitStringPerformance.patternSplit 1000 avgt 10 103.649 ± 19.582 us/op
SplitStringPerformance.patternSplit 100000 avgt 10 10929.489 ± 2556.689 us/op
SplitStringPerformance.stringSplit 10 avgt 10 0.606 ± 0.163 us/op
SplitStringPerformance.stringSplit 1000 avgt 10 51.525 ± 10.154 us/op
SplitStringPerformance.stringSplit 100000 avgt 10 5914.462 ± 1001.699 us/op
輸出結果得出兩個主要結論。首先,對於小字串,所有方法都很快。
然而,對於較大的字串, String.split()
總體上是最快的, Pattern.split()
由於正則表達式開銷而較慢,而使用indexOf()
和substring()
進行手動拆分在規模上表現最差。
5. 結論
在本文中,我們討論了 Java 中拆分字串的多種方法,例如String.split()
、 Pattern.split()
、 String.indexOf()
和substring()
。我們還測量並比較了每種方法的性能。
對於較小的輸入,所有選項都很快。但是,對於具有簡單分隔符號的較大輸入,精心編寫的手動掃描可以最大限度地減少分配。另一方面,為了方便起見, String.split()
是一個不錯的選擇,並且受益於內部快取。最後,對於重複的複雜模式,使用Pattern
進行預編譯通常是最佳選擇。
最後,本文的源代碼可在 GitHub 上取得。