strings: Java Strings deep dive companion code (pool/intern, indy concat vs StringBuilder JMH, text blocks, string templates)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01KqJyCidz3ZgRyHABv2GVJh
This commit is contained in:
@@ -17,6 +17,7 @@ article; each module's own README has that article's version table, quickstart,
|
|||||||
| [`exceptions`](exceptions/) | Java Exception Handling Deep Dive: Checked vs Unchecked, Suppression, and What Exceptions Actually Cost |
|
| [`exceptions`](exceptions/) | Java Exception Handling Deep Dive: Checked vs Unchecked, Suppression, and What Exceptions Actually Cost |
|
||||||
| [`regex`](regex/) | Java Regex Tutorial: Pattern, Matcher, Groups, Lookarounds and ReDoS |
|
| [`regex`](regex/) | Java Regex Tutorial: Pattern, Matcher, Groups, Lookarounds and ReDoS |
|
||||||
| [`serialization`](serialization/) | Java Serialization in 2026: Why It's Dangerous and What Replaced It |
|
| [`serialization`](serialization/) | Java Serialization in 2026: Why It's Dangerous and What Replaced It |
|
||||||
|
| [`strings`](strings/) | Java Strings Deep Dive: Interning, StringBuilder, Text Blocks, and the Concatenation Benchmark |
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
<module>exceptions</module>
|
<module>exceptions</module>
|
||||||
<module>regex</module>
|
<module>regex</module>
|
||||||
<module>serialization</module>
|
<module>serialization</module>
|
||||||
|
<module>strings</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# strings
|
||||||
|
|
||||||
|
Companion code for the ankurm.com post *"Java Strings Deep Dive: Interning, StringBuilder, Text
|
||||||
|
Blocks, and the Concatenation Benchmark."* Module `strings` in `java-core-examples`.
|
||||||
|
|
||||||
|
All explanation lives in the post; this module holds the runnable evidence and the captured output.
|
||||||
|
|
||||||
|
## Versions
|
||||||
|
|
||||||
|
| Component | Version |
|
||||||
|
|---|---|
|
||||||
|
| JDK | 25.0.4.1+1 (Temurin, LTS) |
|
||||||
|
| JMH | 1.37 |
|
||||||
|
| JUnit Jupiter | 5.11.0 |
|
||||||
|
| Hardware | 2 vCPU x86-64 VM, shared with other jobs while measuring (timings are indicative, not a leaderboard) |
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export JDK25_HOME=/path/to/jdk-25
|
||||||
|
./scripts/run-all.sh # rebuilds and regenerates everything in output/
|
||||||
|
```
|
||||||
|
|
||||||
|
`run-all.sh` ends with the JMH run, which takes a few minutes. JMH refuses to start if another JMH
|
||||||
|
process holds `/tmp/jmh.lock`; wait for it rather than forcing it, or the numbers are worthless.
|
||||||
|
|
||||||
|
## What is in here
|
||||||
|
|
||||||
|
| File | Shows | Output |
|
||||||
|
|---|---|---|
|
||||||
|
| `StringPoolDemo` | `==` vs `equals`, constant folding, runtime concatenation, `intern()` (including the already-pooled `"java"` surprise) | `01` |
|
||||||
|
| `TextBlockDemo` | closing-delimiter position, indentation stripping, trailing spaces, `\s`, `\<newline>`, escape timing, `formatted()` | `02` |
|
||||||
|
| `ConcatLoopDemo` | `+=` in a loop allocates a new String each time; characters copied grows quadratically | `03` |
|
||||||
|
| `ConcatShapes` | the same `+` expression compiled two ways; `javap` shows `invokedynamic` vs `StringBuilder` | `04`, `05` |
|
||||||
|
| `src/broken/StringTemplate.java` | `STR."..."` is a compile error on JDK 25, with and without `--enable-preview` | `06` |
|
||||||
|
| `run-all.sh` flags step | `StringTableSize`, `CompactStrings`, `OptimizeStringConcat`, `UseStringDeduplication` defaults | `07` |
|
||||||
|
| `ConcatBenchmark`, `LoopConcatBenchmark` | JMH: `+` vs `StringBuilder` vs `String.format`; Latin-1 vs UTF-16; `+=` loop vs builder at 100/1000/10000 | `08` |
|
||||||
|
| `StringBehaviourTest` | 8 assertions behind the claims above | `09` |
|
||||||
|
|
||||||
|
`ConcatBenchmark` uses 2 forks; `LoopConcatBenchmark` uses 1 fork; both 3 warmup and 5 measurement
|
||||||
|
iterations of 1 s. Re-running moves the numbers by tens of percent but not the ordering.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
literal == literal : true
|
||||||
|
literal == new String : false
|
||||||
|
literal.equals(new String) : true
|
||||||
|
literal == c.intern() : true
|
||||||
|
constant + literal == "hello" : true
|
||||||
|
runtime concat == "hello" : false
|
||||||
|
runtime concat.intern() == "hello": true
|
||||||
|
built "java".intern() == the built instance : false
|
||||||
|
built unseen string .intern() == the built instance: true
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
--- closing delimiter on its own line, aligned with the text ---
|
||||||
|
{\n
|
||||||
|
.."id":.1\n
|
||||||
|
}\n
|
||||||
|
|
||||||
|
--- closing delimiter right after the last line (no final newline) ---
|
||||||
|
{\n
|
||||||
|
.."id":.1\n
|
||||||
|
}
|
||||||
|
--- closing delimiter 2 columns left of the text: 2 spaces of indentation survive ---
|
||||||
|
..left\n
|
||||||
|
|
||||||
|
--- trailing spaces are stripped, but \s and \040 survive ---
|
||||||
|
x....\n
|
||||||
|
y.\n
|
||||||
|
|
||||||
|
--- backslash at end of line joins the lines ---
|
||||||
|
one.line\n
|
||||||
|
|
||||||
|
--- escapes are processed after indentation is stripped ---
|
||||||
|
tab: |\n
|
||||||
|
|
||||||
|
--- double quotes need no escape; only a run of three does ---
|
||||||
|
say."hi".and."""\n
|
||||||
|
|
||||||
|
two identical text blocks are the same pooled instance: true
|
||||||
|
equals the hand-written literal: true
|
||||||
|
--- formatted() is the built-in way to interpolate ---
|
||||||
|
Hello,.Ann!.You.are.30.\n
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
iteration 0: length=1 value=0
|
||||||
|
iteration 1: length=2 value=01
|
||||||
|
iteration 2: length=3 value=012
|
||||||
|
iteration 3: length=4 value=0123
|
||||||
|
iteration 4: length=5 value=01234
|
||||||
|
characters copied in total across the loop results: 15
|
||||||
|
n=20000: final length=20000, characters in all intermediate strings=200010000
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
$ javac ConcatShapes.java && javap -c -p ConcatShapes (method one)
|
||||||
|
public static java.lang.String one(java.lang.String, int);
|
||||||
|
Code:
|
||||||
|
0: aload_0
|
||||||
|
1: iload_1
|
||||||
|
2: invokedynamic #7, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;I)Ljava/lang/String;
|
||||||
|
7: areturn
|
||||||
|
|
||||||
|
$ javac -XDstringConcat=inline ConcatShapes.java && javap -c -p ConcatShapes (method one)
|
||||||
|
public static java.lang.String one(java.lang.String, int);
|
||||||
|
Code:
|
||||||
|
0: new #7 // class java/lang/StringBuilder
|
||||||
|
3: dup
|
||||||
|
4: invokespecial #9 // Method java/lang/StringBuilder."<init>":()V
|
||||||
|
7: ldc #10 // String user=
|
||||||
|
9: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
|
||||||
|
12: aload_0
|
||||||
|
13: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
|
||||||
|
16: ldc #16 // String n=
|
||||||
|
18: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
|
||||||
|
21: iload_1
|
||||||
|
22: invokevirtual #18 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
|
||||||
|
25: invokevirtual #21 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
|
||||||
|
28: areturn
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
$ javap -c -v ConcatShapes | grep -A3 BootstrapMethods
|
||||||
|
BootstrapMethods:
|
||||||
|
0: #39 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
|
||||||
|
Method arguments:
|
||||||
|
#37 user=\u0001 n=\u0001
|
||||||
|
InnerClasses:
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
$ javac StringTemplate.java
|
||||||
|
src/broken/StringTemplate.java:4: error: illegal escape character
|
||||||
|
System.out.println(STR."Hello, \{name}");
|
||||||
|
^
|
||||||
|
1 error
|
||||||
|
|
||||||
|
$ javac --enable-preview --release 25 StringTemplate.java
|
||||||
|
src/broken/StringTemplate.java:4: error: illegal escape character
|
||||||
|
System.out.println(STR."Hello, \{name}");
|
||||||
|
^
|
||||||
|
1 error
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
bool CompactStrings = true {pd product} {default}
|
||||||
|
bool OptimizeStringConcat = true {C2 product} {default}
|
||||||
|
uintx StringTableSize = 65536 {product} {default}
|
||||||
|
bool UseStringDeduplication = false {product} {default}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
Benchmark (loop) Mode Cnt Score Error Units
|
||||||
|
ConcatBenchmark.explicitBuilder N/A avgt 10 20.838 ± 2.629 ns/op
|
||||||
|
ConcatBenchmark.formattedMethod N/A avgt 10 164.278 ± 12.164 ns/op
|
||||||
|
ConcatBenchmark.plusExpression N/A avgt 10 20.448 ± 7.264 ns/op
|
||||||
|
ConcatBenchmark.plusLatin1 N/A avgt 10 15.791 ± 4.953 ns/op
|
||||||
|
ConcatBenchmark.plusUtf16 N/A avgt 10 23.185 ± 4.275 ns/op
|
||||||
|
ConcatBenchmark.stringFormat N/A avgt 10 285.885 ± 133.980 ns/op
|
||||||
|
LoopConcatBenchmark.loopBuilder 100 avgt 5 270.251 ± 128.322 ns/op
|
||||||
|
LoopConcatBenchmark.loopBuilder 1000 avgt 5 2268.140 ± 888.134 ns/op
|
||||||
|
LoopConcatBenchmark.loopBuilder 10000 avgt 5 24468.200 ± 8712.080 ns/op
|
||||||
|
LoopConcatBenchmark.loopBuilderPresized 100 avgt 5 194.101 ± 168.069 ns/op
|
||||||
|
LoopConcatBenchmark.loopBuilderPresized 1000 avgt 5 1682.224 ± 135.261 ns/op
|
||||||
|
LoopConcatBenchmark.loopBuilderPresized 10000 avgt 5 20622.063 ± 16702.078 ns/op
|
||||||
|
LoopConcatBenchmark.loopPlusEquals 100 avgt 5 2021.521 ± 648.577 ns/op
|
||||||
|
LoopConcatBenchmark.loopPlusEquals 1000 avgt 5 73764.547 ± 11968.016 ns/op
|
||||||
|
LoopConcatBenchmark.loopPlusEquals 10000 avgt 5 8939940.357 ± 2388632.875 ns/op
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-------------------------------------------------------------------------------
|
||||||
|
Test set: com.ankurm.strings.StringBehaviourTest
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
Tests run: 8, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.252 s -- in com.ankurm.strings.StringBehaviourTest
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>com.ankurm</groupId>
|
||||||
|
<artifactId>java-core-examples</artifactId>
|
||||||
|
<version>1.0</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>strings</artifactId>
|
||||||
|
<name>strings</name>
|
||||||
|
<description>Java Strings: the string pool and intern(), invokedynamic concatenation vs StringBuilder (JMH), text blocks, and why string templates were withdrawn.</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<jmh.version>1.37</jmh.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<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>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter</artifactId>
|
||||||
|
<version>5.11.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<finalName>benchmarks</finalName>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.13.0</version>
|
||||||
|
<configuration>
|
||||||
|
<release>25</release>
|
||||||
|
<!--
|
||||||
|
Same JDK-25-stops-discovering-annotation-processors-implicitly trap documented in
|
||||||
|
the jmm module: JMH's @Benchmark-method-to-*_jmh.java generator needs to be declared
|
||||||
|
explicitly here or the build silently produces a jar with nothing runnable in it.
|
||||||
|
-->
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.openjdk.jmh</groupId>
|
||||||
|
<artifactId>jmh-generator-annprocess</artifactId>
|
||||||
|
<version>${jmh.version}</version>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>3.2.5</version>
|
||||||
|
</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>
|
||||||
|
<transformers>
|
||||||
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
|
<mainClass>org.openjdk.jmh.Main</mainClass>
|
||||||
|
</transformer>
|
||||||
|
</transformers>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
Executable
+42
@@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regenerates every file in ../output/. Requires JDK25_HOME.
|
||||||
|
set -euo pipefail
|
||||||
|
[[ -z "${JDK25_HOME:-}" ]] && { echo "JDK25_HOME must be set" >&2; exit 1; }
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
OUT=output; mkdir -p "$OUT"
|
||||||
|
export JAVA_HOME="$JDK25_HOME"
|
||||||
|
mvn -q -f ../pom.xml -pl strings -am package
|
||||||
|
J="$JDK25_HOME/bin/java"
|
||||||
|
run() { echo "==> $1"; "$J" -cp target/classes "com.ankurm.strings.$1" 2>&1 | grep -v "Picked up" > "$OUT/$2"; }
|
||||||
|
run StringPoolDemo 01-string-pool.txt
|
||||||
|
run TextBlockDemo 02-text-blocks.txt
|
||||||
|
run ConcatLoopDemo 03-concat-loop.txt
|
||||||
|
|
||||||
|
echo "==> javap: indy vs inline StringBuilder"
|
||||||
|
SRC=src/main/java/com/ankurm/strings/ConcatShapes.java
|
||||||
|
rm -rf /tmp/cs-indy /tmp/cs-inline
|
||||||
|
"$JDK25_HOME/bin/javac" -d /tmp/cs-indy "$SRC" 2>&1 | grep -v "Picked up" || true
|
||||||
|
"$JDK25_HOME/bin/javac" -XDstringConcat=inline -d /tmp/cs-inline "$SRC" 2>&1 | grep -v "Picked up" || true
|
||||||
|
{ echo '$ javac ConcatShapes.java && javap -c -p ConcatShapes (method one)'
|
||||||
|
"$JDK25_HOME/bin/javap" -c -p -cp /tmp/cs-indy com.ankurm.strings.ConcatShapes 2>&1 | grep -v "Picked up" | sed -n '/String one/,/^$/p'
|
||||||
|
echo '$ javac -XDstringConcat=inline ConcatShapes.java && javap -c -p ConcatShapes (method one)'
|
||||||
|
"$JDK25_HOME/bin/javap" -c -p -cp /tmp/cs-inline com.ankurm.strings.ConcatShapes 2>&1 | grep -v "Picked up" | sed -n '/String one/,/^$/p'
|
||||||
|
} > "$OUT/04-javap-indy-vs-inline.txt"
|
||||||
|
{ echo '$ javap -c -v ConcatShapes | grep -A3 BootstrapMethods'
|
||||||
|
"$JDK25_HOME/bin/javap" -v -p -cp /tmp/cs-indy com.ankurm.strings.ConcatShapes 2>&1 | grep -v "Picked up" | grep -A4 '^BootstrapMethods'
|
||||||
|
} > "$OUT/05-bootstrap-method.txt"
|
||||||
|
|
||||||
|
echo "==> StringTemplate (expected compile failures)"
|
||||||
|
{ echo '$ javac StringTemplate.java'
|
||||||
|
"$JDK25_HOME/bin/javac" -d /tmp/lc src/broken/StringTemplate.java 2>&1 | grep -v "Picked up" || true
|
||||||
|
echo; echo '$ javac --enable-preview --release 25 StringTemplate.java'
|
||||||
|
"$JDK25_HOME/bin/javac" --enable-preview --release 25 -d /tmp/lc src/broken/StringTemplate.java 2>&1 | grep -v "Picked up" || true
|
||||||
|
} > "$OUT/06-string-template-compile-error.txt"
|
||||||
|
|
||||||
|
echo "==> JVM string flags"
|
||||||
|
"$J" -XX:+PrintFlagsFinal -version 2>/dev/null | grep -E " (StringTableSize|UseStringDeduplication|CompactStrings|OptimizeStringConcat) " | sed 's/ */ /g' > "$OUT/07-jvm-flags.txt"
|
||||||
|
|
||||||
|
echo "==> JMH"
|
||||||
|
"$J" -jar target/benchmarks.jar 'ConcatBenchmark|LoopConcatBenchmark' -rf text -rff "$OUT/08-jmh-raw.txt" 2>&1 | grep -v "Picked up" > /dev/null
|
||||||
|
cp target/surefire-reports/com.ankurm.strings.StringBehaviourTest.txt "$OUT/09-tests.txt"
|
||||||
|
echo Done
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
public class StringTemplate {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
String name = "Ann";
|
||||||
|
System.out.println(STR."Hello, \{name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.ankurm.strings;
|
||||||
|
|
||||||
|
import org.openjdk.jmh.annotations.*;
|
||||||
|
import org.openjdk.jmh.infra.Blackhole;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does an explicit StringBuilder beat a single + expression (compiled to invokedynamic)?
|
||||||
|
* Also: String.format / formatted() for comparison, and Latin-1 vs UTF-16 operands.
|
||||||
|
*/
|
||||||
|
@BenchmarkMode(Mode.AverageTime)
|
||||||
|
@OutputTimeUnit(TimeUnit.NANOSECONDS)
|
||||||
|
@Warmup(iterations = 3, time = 1)
|
||||||
|
@Measurement(iterations = 5, time = 1)
|
||||||
|
@Fork(2)
|
||||||
|
@State(Scope.Thread)
|
||||||
|
public class ConcatBenchmark {
|
||||||
|
|
||||||
|
public String name = "ann";
|
||||||
|
public int n = 12345;
|
||||||
|
public String latin = "abc";
|
||||||
|
public String wide = "abc\u20ac"; // contains a char outside Latin-1 -> UTF16 coder
|
||||||
|
|
||||||
|
// ---- single expression -------------------------------------------------------------
|
||||||
|
@Benchmark public String plusExpression() { return "user=" + name + " n=" + n + " ok"; }
|
||||||
|
@Benchmark public String explicitBuilder() {
|
||||||
|
return new StringBuilder().append("user=").append(name).append(" n=").append(n).append(" ok").toString();
|
||||||
|
}
|
||||||
|
@Benchmark public String stringFormat() { return String.format("user=%s n=%d ok", name, n); }
|
||||||
|
@Benchmark public String formattedMethod() { return "user=%s n=%d ok".formatted(name, n); }
|
||||||
|
|
||||||
|
// ---- Latin-1 vs UTF16 operands -----------------------------------------------------
|
||||||
|
@Benchmark public String plusLatin1() { return latin + latin + latin; }
|
||||||
|
@Benchmark public String plusUtf16() { return wide + wide + wide; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.ankurm.strings;
|
||||||
|
|
||||||
|
/** Counts how many intermediate String objects += in a loop creates (observed via identity of each result). */
|
||||||
|
public class ConcatLoopDemo {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
int n = 5;
|
||||||
|
String s = "";
|
||||||
|
long chars = 0;
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
s += i; // allocates a brand-new String every iteration
|
||||||
|
chars += s.length();
|
||||||
|
System.out.println("iteration " + i + ": length=" + s.length() + " value=" + s);
|
||||||
|
}
|
||||||
|
System.out.println("characters copied in total across the loop results: " + chars);
|
||||||
|
int big = 20_000;
|
||||||
|
long total = 0;
|
||||||
|
String t = "";
|
||||||
|
for (int i = 0; i < big; i++) { t += 'x'; total += t.length(); }
|
||||||
|
System.out.println("n=" + big + ": final length=" + t.length() + ", characters in all intermediate strings=" + total);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.ankurm.strings;
|
||||||
|
|
||||||
|
/** Source whose bytecode is inspected with javap, compiled twice: default (invokedynamic) and -XDstringConcat=inline. */
|
||||||
|
public class ConcatShapes {
|
||||||
|
public static String one(String name, int n) {
|
||||||
|
return "user=" + name + " n=" + n;
|
||||||
|
}
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println(one("ann", 3));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.ankurm.strings;
|
||||||
|
|
||||||
|
import org.openjdk.jmh.annotations.*;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/** What += in a loop costs as the loop grows, compared with one StringBuilder (plain and presized). */
|
||||||
|
@BenchmarkMode(Mode.AverageTime)
|
||||||
|
@OutputTimeUnit(TimeUnit.NANOSECONDS)
|
||||||
|
@Warmup(iterations = 3, time = 1)
|
||||||
|
@Measurement(iterations = 5, time = 1)
|
||||||
|
@Fork(1)
|
||||||
|
@State(Scope.Thread)
|
||||||
|
public class LoopConcatBenchmark {
|
||||||
|
|
||||||
|
@Param({"100", "1000", "10000"})
|
||||||
|
public int loop;
|
||||||
|
|
||||||
|
@Benchmark public String loopPlusEquals() {
|
||||||
|
String s = "";
|
||||||
|
for (int i = 0; i < loop; i++) s += "x";
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
@Benchmark public String loopBuilder() {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < loop; i++) sb.append("x");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
@Benchmark public String loopBuilderPresized() {
|
||||||
|
StringBuilder sb = new StringBuilder(loop);
|
||||||
|
for (int i = 0; i < loop; i++) sb.append("x");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.ankurm.strings;
|
||||||
|
|
||||||
|
/** == versus equals() on strings: the literal pool, compile-time constants, runtime concatenation, intern(). */
|
||||||
|
public class StringPoolDemo {
|
||||||
|
static final String CONST = "hel"; // compile-time constant variable
|
||||||
|
static final String NOT_CONST = new String("hel"); // not a constant
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
String a = "hello";
|
||||||
|
String b = "hello";
|
||||||
|
String c = new String("hello");
|
||||||
|
System.out.println("literal == literal : " + (a == b));
|
||||||
|
System.out.println("literal == new String : " + (a == c));
|
||||||
|
System.out.println("literal.equals(new String) : " + a.equals(c));
|
||||||
|
System.out.println("literal == c.intern() : " + (a == c.intern()));
|
||||||
|
|
||||||
|
String folded = CONST + "lo"; // folded by javac into the constant "hello"
|
||||||
|
String runtime = NOT_CONST + "lo"; // built at run time
|
||||||
|
System.out.println("constant + literal == \"hello\" : " + (folded == a));
|
||||||
|
System.out.println("runtime concat == \"hello\" : " + (runtime == a));
|
||||||
|
System.out.println("runtime concat.intern() == \"hello\": " + (runtime.intern() == a));
|
||||||
|
|
||||||
|
// "java" is already in the pool (the JDK itself uses the literal), so intern() returns the pooled one, not ours.
|
||||||
|
String java = new StringBuilder("ja").append("va").toString();
|
||||||
|
System.out.println("built \"java\".intern() == the built instance : " + (java.intern() == java));
|
||||||
|
// A string nobody has seen before: intern() adds THIS instance to the pool and returns it.
|
||||||
|
String unseen = new StringBuilder("never-seen-").append(System.nanoTime()).toString();
|
||||||
|
System.out.println("built unseen string .intern() == the built instance: " + (unseen.intern() == unseen));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package com.ankurm.strings;
|
||||||
|
|
||||||
|
/** Text block rules: incidental indentation, closing-delimiter position, trailing spaces, \s, line continuation, escape timing. */
|
||||||
|
public class TextBlockDemo {
|
||||||
|
// Prints the string with newlines shown as \n and spaces shown as dots, so whitespace is visible.
|
||||||
|
static void show(String label, String s) {
|
||||||
|
System.out.println("--- " + label + " ---");
|
||||||
|
System.out.println(s.replace("\n", "\\n\n").replace(" ", "."));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
String a = """
|
||||||
|
{
|
||||||
|
"id": 1
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
show("closing delimiter on its own line, aligned with the text", a);
|
||||||
|
|
||||||
|
String b = """
|
||||||
|
{
|
||||||
|
"id": 1
|
||||||
|
}""";
|
||||||
|
show("closing delimiter right after the last line (no final newline)", b);
|
||||||
|
|
||||||
|
String c = """
|
||||||
|
left
|
||||||
|
""";
|
||||||
|
show("closing delimiter 2 columns left of the text: 2 spaces of indentation survive", c);
|
||||||
|
|
||||||
|
String d = """
|
||||||
|
x \s
|
||||||
|
y\040
|
||||||
|
""";
|
||||||
|
show("trailing spaces are stripped, but \\s and \\040 survive", d);
|
||||||
|
|
||||||
|
String e = """
|
||||||
|
one \
|
||||||
|
line
|
||||||
|
""";
|
||||||
|
show("backslash at end of line joins the lines", e);
|
||||||
|
|
||||||
|
String f = """
|
||||||
|
tab:\t|
|
||||||
|
""";
|
||||||
|
show("escapes are processed after indentation is stripped", f);
|
||||||
|
|
||||||
|
String g = """
|
||||||
|
say "hi" and ""\"
|
||||||
|
""";
|
||||||
|
show("double quotes need no escape; only a run of three does", g);
|
||||||
|
|
||||||
|
String same = """
|
||||||
|
{
|
||||||
|
"id": 1
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
System.out.println("two identical text blocks are the same pooled instance: " + (a == same));
|
||||||
|
System.out.println("equals the hand-written literal: " + a.equals("{\n \"id\": 1\n}\n"));
|
||||||
|
|
||||||
|
String h = """
|
||||||
|
Hello, %s! You are %d.
|
||||||
|
""".formatted("Ann", 30);
|
||||||
|
show("formatted() is the built-in way to interpolate", h);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package com.ankurm.strings;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class StringBehaviourTest {
|
||||||
|
static final String CONST = "hel";
|
||||||
|
static final String NOT_CONST = new String("hel");
|
||||||
|
|
||||||
|
@Test void literalsArePooledNewStringIsNot() {
|
||||||
|
String a = "hello";
|
||||||
|
assertSame(a, "hello");
|
||||||
|
assertNotSame(a, new String("hello"));
|
||||||
|
assertSame(a, new String("hello").intern());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test void compileTimeConstantsAreFoldedRuntimeConcatIsNot() {
|
||||||
|
assertSame("hello", CONST + "lo");
|
||||||
|
assertNotSame("hello", NOT_CONST + "lo");
|
||||||
|
assertSame("hello", (NOT_CONST + "lo").intern());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test void internReturnsTheAlreadyPooledInstanceOrAdoptsOurs() {
|
||||||
|
String pooled = new StringBuilder("ja").append("va").toString();
|
||||||
|
assertNotSame(pooled, pooled.intern()); // "java" was in the pool before this test ran
|
||||||
|
String unseen = new StringBuilder("never-seen-").append(System.nanoTime()).toString();
|
||||||
|
assertSame(unseen, unseen.intern()); // first intern() adopts this very instance
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test void textBlockIsAnOrdinaryPooledString() {
|
||||||
|
String t = """
|
||||||
|
hi
|
||||||
|
""";
|
||||||
|
assertSame("hi\n", t);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test void closingDelimiterPositionDecidesTrailingNewlineAndMargin() {
|
||||||
|
String onOwnLine = """
|
||||||
|
a
|
||||||
|
""";
|
||||||
|
String attached = """
|
||||||
|
a""";
|
||||||
|
String leftOfText = """
|
||||||
|
a
|
||||||
|
""";
|
||||||
|
assertEquals("a\n", onOwnLine);
|
||||||
|
assertEquals("a", attached);
|
||||||
|
assertEquals(" a\n", leftOfText);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test void trailingSpacesStrippedUnlessEscaped() {
|
||||||
|
String t = """
|
||||||
|
x \s
|
||||||
|
y\040
|
||||||
|
""";
|
||||||
|
assertEquals("x \ny \n", t); // "x" + 3 literal spaces + \s, then "y" + \040
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test void lineContinuationAndEscapeTiming() {
|
||||||
|
assertEquals("one line\n", """
|
||||||
|
one \
|
||||||
|
line
|
||||||
|
""");
|
||||||
|
assertEquals("tab:\t|\n", """
|
||||||
|
tab:\t|
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test void plusInLoopAllocatesNewStringEachTime() {
|
||||||
|
String s = "";
|
||||||
|
String prev;
|
||||||
|
for (int i = 0; i < 3; i++) { prev = s; s += "x"; assertNotSame(prev, s); }
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < 3; i++) sb.append("x");
|
||||||
|
assertEquals(s, sb.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user