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:
@@ -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