Java Strings & Fast Input
The everyday Java you forget mid-problem — immutability and .equals, substring/split/join, StringBuilder, characters as array indices (counting, anagrams, shifts), the Character tests and number conversions, and the input upgrade path with ready-to-paste recipes.
Assumes you know
After this you should be able to
- Transform strings without accidental quadratic work
- Parse input with a reusable fast-reader pattern
Everything you need to slice, build, compare, and count with strings in a DSA problem, plus the input-reading upgrade path that keeps large inputs fast.
Quick Reference#
The whole sheet at a glance. Telegraphic by design; everything here is unpacked below. Watch the ★/⚠/💡 lines.
A. String basics (immutable)#
String = immutable seq of chars (UTF-16). Any "edit" → NEW object. len → s.length() // method, () needed char at i → s.charAt(i) find → s.indexOf(x) / lastIndexOf(x) // -1 if absent slice → s.substring(i) / s.substring(i, j) // [i, j) contains → s.contains(sub) | startsWith / endsWith case → toUpperCase / toLowerCase trim edges → strip() (Unicode) / trim() (ASCII) ⚠EQUALITY: use .equals(), NEVER == (== compares references) ⚠substring(i,j) end is EXCLUSIVE; len = j - i 💡char c = s.charAt(i); int d = c - '0'; // digit char → int
B. Split & Join#
split s.split(regex) → String[] // ARG IS REGEX! ⚠split(".") splits on EVERY char (. = any). Escape: split("\\.") 💡split(regex, -1) keeps trailing empty tokens join String.join(delim, parts) → one String chars → str: new String(charArr) | String.valueOf(charArr) str → chars: s.toCharArray()
C. Reverse#
reverse new StringBuilder(s).reverse().toString() 💡no String.reverse(). SB is the idiom. DOODLE: box "abc" → arrow → box "cba"; two fingers swap ends inward
D. Mutable string = StringBuilder#
StringBuilder = editable char buffer. Build, then toString() ONCE. append(x) · insert(i,x) · deleteCharAt(i) · delete(i,j) setCharAt(i,c) · charAt(i) · reverse() · length() ⚠Building a String in a loop with + = O(n²). Use SB = O(n). 💡StringBuffer = SB but synchronized (slower). Single thread → SB. DOODLE: growing box, append arrows pushing chars in the right end
E. Comparison table#
String | StringBuilder | StringBuffer mutable? NO | YES | YES thread-safe - | NO | YES speed - | FAST | slower use for fixed text | building/edit | rarely (MT)
F. Chars: count, test, convert#
char is an int → index an array with it. count letters: int[] f = new int[26]; f[c-'a']++; 💡mixed / ASCII → int[128] or int[256], f[c]++ anagram: ++ for A, -- for B → all zero ⇒ match anagram key: Arrays.sort(s.toCharArray()) → group by it shift w/ wrap: (c-'a'+k) % 26 + 'a' test Character.isDigit / isLetter / isLetterOrDigit / isWhitespace num Integer.parseInt(s) · String.valueOf(n) · parseInt(s,2)=base 💡SB as stack: charAt(len-1) peek, deleteCharAt(len-1) pop
G. Fast input (upgrade path)#
Scanner easy, SLOW (regex + boxing). Small input only. BufferedReader read whole LINES fast; split/parse yourself. br.readLine(); Integer.parseInt(...); new StringTokenizer(line) StreamTokenizer token-by-token numbers; st.nextToken(); st.nval Custom DataInputStream read bytes yourself = fastest. 💡Order of speed: Scanner < BufferedReader ≤ StreamTokenizer < custom ⚠Output: use StringBuilder / PrintWriter, flush at END. Never print in a tight loop. ★Slow on 10⁵+ ints? Swap Scanner → BufferedReader FIRST. Usually fixes it. 💡recipes below (§12): read n ints, a grid, T cases, until EOF DOODLE: 4 boxes L→R, "slow/easy" → "fast/manual", arrow labeled speed
Detailed Walkthrough#
Two things quietly make correct string code slow on large inputs: building output with
+ inside a loop, and reading input with Scanner. This reference fixes both and
lays out the string moves you actually reach for while solving — slicing, building, comparing, and
counting with characters.
1. What a String really is#
A String is a fixed, read-only sequence of characters. Think of it like a word printed on a laminated card: you can read it, copy it, cut a piece out onto a new card, but you can never rub out one letter and write another on the same card. In Java this property is called immutability — once created, a String's contents never change.
So what happens when you write s = s + "!"? Java doesn't edit s. It builds a
brand-new String holding the old contents plus "!", and points s at it. The old
one is left for the garbage collector. This one fact explains almost every String performance trap you'll
hit.
String is safe to share (as map keys, across threads) without copying
— nobody can mutate it under you. It's also why concatenating in a loop is a performance bomb: each
+ copies the whole string so far.
== asks "are these the same object?" not "do they hold the same text?".
"ab" == new String("ab") is false. Always compare content with
s.equals(t) (or s.equalsIgnoreCase(t)). The reason == sometimes
looks right is string literal interning — but never rely on it.
The everyday read-only toolkit#
| Operation | Call | Notes |
|---|---|---|
| Length | s.length() | Method with () — not .length (that's arrays) |
| Char at index | s.charAt(i) | Returns a char, 0-based |
| Find first | s.indexOf(x) | Returns index, or -1 if absent |
| Find last | s.lastIndexOf(x) | Search from the right |
| Substring | s.substring(i, j) | Range [i, j) — end excluded |
| Contains | s.contains(sub) | Boolean; also startsWith / endsWith |
| Case | s.toUpperCase() | Returns a new string |
| Trim edges | s.strip() | Unicode-aware; trim() is ASCII-only |
| To char array | s.toCharArray() | Your gateway to mutation |
| Compare order | s.compareTo(t) | <0, 0, >0 — lexicographic, for sorting |
'0':
int d = s.charAt(i) - '0';. This works because '0'..'9' are consecutive in the
character code table. To go back, char c = (char)(d + '0');. This shows up constantly in
string-to-number and big-integer problems.
2. Substring — cutting a slice#
substring returns a new string covering a range of the original. Picture a
ruler laid along the word, with the tick marks between the letters. substring(i, j)
cuts from tick i up to (but not including) tick j. That "not including" is where
everyone slips.
String s = "DSAROX"; s.substring(2, 5); // "ARO" (length = 5 - 2 = 3) s.substring(2); // "AROX" (from 2 to end) s.substring(6); // "" (empty, legal) s.substring(7); // StringIndexOutOfBoundsException
substring(i, j) is always j - i. No
off-by-one arithmetic needed. When you want "the next k characters from position i", write
substring(i, i + k) and the count is exactly k.
3. Split & Join — text to pieces and back#
Splitting chops a string into an array on a separator; joining glues an array back with a separator between items. Think of a train: splitting uncouples the cars at the couplings, joining hooks them back together with couplings between.
split goes string → array; join goes array →
string.String line = "12,7,90,3"; String[] parts = line.split(","); // ["12","7","90","3"] String back = String.join("-", parts); // "12-7-90-3"
split is a regular expression. So "a.b.c".split(".") gives an
empty array — . means "any character" in regex. To split on a literal dot, escape it:
split("\\."). Same trap for |, *, +, (,
[, $. When in doubt, escape or use Pattern.quote(sep).
"a,b,,,".split(",") returns ["a","b"] — Java drops trailing empty tokens by
default. If you need them (e.g. parsing CSV with blank final fields), pass a negative limit:
split(",", -1) keeps them all.
line.trim().split("\\s+"). The \\s+
collapses runs of spaces/tabs so you don't get empty tokens between them. When speed matters, prefer
StringTokenizer over split — it skips compiling a regex.
s.replace("a", "b") swaps every literal "a" (no escaping). But
s.replaceAll("a.b", "x") treats the first argument as a regex — the same trap as
split. To delete every dot, use s.replace(".", ""); replaceAll(".", "")
erases the whole string.
Converting between String and char[]#
char[] cs = s.toCharArray(); // String -> char[] String t = new String(cs); // char[] -> String String u = String.valueOf(cs); // same thing, null-safe style
4. Reverse — and why there's no String.reverse()#
Reversing flips a string end-for-end. Because String is immutable, it
deliberately offers no reverse() — that would imply mutation. The canonical move is to
hand the string to a StringBuilder, which is mutable, and reverse there.
String r = new StringBuilder(s).reverse().toString();
If you're doing something custom (reverse only part, reverse words, palindrome check), fall back to a char array and a two-pointer swap:
char[] a = s.toCharArray(); int i = 0, j = a.length - 1; while (i < j) { char t = a[i]; a[i]=a[j]; a[j]=t; i++; j--; } String r = new String(a);
charAt(i) and charAt(j) is O(n) time and O(1) extra space — cheaper than
allocating a reversed copy.
5. The mutable string: StringBuilder#
A StringBuilder is an editable buffer of characters — the mutable counterpart to
String. The analogy: String is ink on paper, StringBuilder is a
whiteboard. You scribble, erase, insert, and rearrange freely; when you're done you take one photo of the
board (toString()) to get a finished, immutable String.
This is the tool you reach for whenever you're building text piece by piece or editing it in place. It over-allocates its internal array and grows in chunks, so appends are amortized O(1).
Core methods#
| Method | Does |
|---|---|
append(x) | Add to the end (any type: char, int, String…) |
insert(i, x) | Insert at position i |
setCharAt(i, c) | Overwrite one char in place |
charAt(i) | Read one char |
deleteCharAt(i) | Remove one char |
delete(i, j) | Remove range [i, j) |
reverse() | Flip in place |
length() / setLength(n) | Size; setLength(0) clears it |
toString() | Freeze into an immutable String |
String out = ""; for(...) out += x; looks innocent but each += copies the entire
string built so far. For n appends that's 1+2+3+…+n = O(n²) work, and on large inputs it's
dramatically slow. Build with a StringBuilder and call toString() once at the end
— that's O(n).
// GOOD: O(n) StringBuilder sb = new StringBuilder(); for (int i = 0; i < n; i++) sb.append(arr[i]).append(' '); String result = sb.toString();
String is immutable; StringBuilder is a mutable
buffer for single-threaded building; StringBuffer is the same but every method is
synchronized, making it thread-safe and slower. In DSA you use StringBuilder ~99%
of the time.
String vs StringBuilder vs StringBuffer#
| String | StringBuilder | StringBuffer | |
|---|---|---|---|
| Mutable | No | Yes | Yes |
| Thread-safe | n/a (immutable) | No | Yes (synchronized) |
| Speed | — | Fastest to build | Slower |
| Use when | Text won't change | Building/editing (default) | Shared across threads (rare) |
6. Working with characters — counting, tests & conversions#
A char is really a small integer, so it can index an array directly. Subtracting
a base letter turns a character into a slot: c - 'a' maps
'a'…'z' onto 0…25. That single move powers
most counting-based string problems — frequency tables, anagram checks, "first unique character"
— in O(n) time and O(1) extra space.
'a' turns a letter into a 0–25 slot; the array does
the counting.int[] freq = new int[26]; for (char c : s.toCharArray()) freq[c - 'a']++; // tally each letter
++ for every char of the first string, -- for every char of the second, then check
the array is all zeros. The same one-array idea drives ransom-note, permutation-in-string, and
"group anagrams" problems.
char[] a = s.toCharArray(); Arrays.sort(a); String key = new String(a);. Group words by that
key in a HashMap<String, List<String>> and "group anagrams" falls out in a few
lines.
new int[26] with base 'a'. Mixed case or symbols? Use
new int[128] (ASCII) or new int[256] and index by the raw char:
freq[c]++. Sizing the array to the alphabet keeps it O(1) space no matter how long the string
gets.
Shifting a letter, with wrap-around#
Rotating a letter through the alphabet (a Caesar shift, or "move each letter forward by k") is the same
circular-index trick from modular arithmetic: map to 0–25, add the shift, wrap with
% 26, map back.
char shift(char c, int k) { return (char) ((c - 'a' + k) % 26 + 'a'); }
StringBuilder as a stack: peek with sb.charAt(sb.length() - 1) and pop with
sb.deleteCharAt(sb.length() - 1). You get O(1) push/pop and the finished string for free via
toString() — no separate stack class needed.
Testing a character: the Character helpers#
Before parsing or validating, you often need to ask what a character is. The Character
class answers directly — no character ranges to memorize.
| Question | Call |
|---|---|
| Digit 0–9? | Character.isDigit(c) |
| Letter? | Character.isLetter(c) |
| Letter or digit? | Character.isLetterOrDigit(c) |
| Whitespace? | Character.isWhitespace(c) |
| Upper / lower case? | Character.isUpperCase(c) / isLowerCase(c) |
| Flip case | Character.toLowerCase(c) / toUpperCase(c) |
| Digit char → its value | c - '0' (or Character.getNumericValue(c)) |
if (Character.isLetterOrDigit(c)) sb.append(Character.toLowerCase(c)); — cleaner than a
stack of >= 'a' && <= 'z' comparisons.
Number ↔ String#
Turning text into numbers and back shows up in almost every parsing problem.
int n = Integer.parseInt("42"); // String -> int (throws on bad input) long big = Long.parseLong("9000000000"); String s = Integer.toString(n); // int -> String String t = String.valueOf(n); // same, works for any type int b = Integer.parseInt("1011", 2); // binary string -> 11 String h = Integer.toString(255, 16); // 255 -> "ff"
Integer.parseInt throws NumberFormatException on stray spaces, a leading
+, or an empty string — trim() the line first. A leading - is
fine. For a single digit character, skip parsing entirely and use c - '0'.
7. Reading input: why Scanner betrays you#
Input reading is often the real bottleneck, not your algorithm. Scanner is the
friendly one you learned first, and it's fine for a handful of values. But it parses with regular
expressions and boxes primitives, so on hundreds of thousands of tokens it becomes shockingly slow —
frequently the sole reason an otherwise-correct solution times out.
Think of it as a checkout line. Scanner is a cashier who inspects and re-bags every single item
individually — safe, but glacial when the cart is huge. The faster readers below are like scanning
items in bulk off a conveyor belt.
// Level 0 - Scanner: fine for small input, slow at scale import java.util.Scanner; Scanner sc = new Scanner(System.in); int n = sc.nextInt(); String w = sc.next(); // one token String line = sc.nextLine(); // rest of line
Scanner for BufferedReader. On large integer inputs this alone often
cuts read time by 5–10×. It's the single highest-leverage change you can make to input
handling.
Scanner bites everyone: nextInt() leaves the
newline in the buffer, so the next nextLine() returns an empty string. Either read everything
as lines and parse, or add an extra nextLine() to consume the leftover newline. Cleaner fix:
use BufferedReader and control the parsing yourself.
8. BufferedReader — the workhorse upgrade#
BufferedReader reads big chunks of input at once into a buffer, then hands you whole lines.
You do the parsing (splitting, parseInt), which is exactly why it's fast: no regex tokenizing,
no autoboxing per value, no per-character system calls.
import java.io.*; import java.util.*; BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); int n = Integer.parseInt(br.readLine().trim()); // single int on a line // a line of space-separated ints -> array StringTokenizer stk = new StringTokenizer(br.readLine()); int[] a = new int[n]; for (int i = 0; i < n; i++) a[i] = Integer.parseInt(stk.nextToken());
StringTokenizer is faster than
String.split() because it doesn't compile a regex. Pattern: one StringTokenizer
per line, then pull tokens with nextToken(). Default delimiters are whitespace, which is
usually what you want.
BufferedReader hits the sweet spot: nearly as fast as hand-rolled byte reading, but with
readable, bug-resistant code. For the vast majority of problems this is the right default. Reach for
anything faster only when the constraints are genuinely enormous (millions of tokens with a tight limit).
9. StreamTokenizer — when input is mostly numbers#
StreamTokenizer streams the input token by token and can parse numbers for you. If a problem
is a flood of integers with no tricky formatting, this is clean and fast: it hands you each number directly,
no split or parseInt needed.
import java.io.*; StreamTokenizer st = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in))); st.nextToken(); int n = (int) st.nval; // nval is a double int[] a = new int[n]; for (int i = 0; i < n; i++) { st.nextToken(); a[i] = (int) st.nval; }
StreamTokenizer stores every number in st.nval as a double, so cast
to int/long and beware precision on very large longs. By default it also parses
- as part of a number, which is usually fine, but if the input mixes words and numbers you read
strings from st.sval and check st.ttype. For anything with strings or mixed
formats, BufferedReader is simpler.
10. The custom fast reader — maximum speed#
The fastest approach reads raw bytes from the stream and assembles numbers by hand. You skip character decoding, regex, tokenizing, and boxing entirely. It's overkill for most problems, but for the rare monster input — millions of integers under a tight limit — it's the tool that guarantees reading isn't your bottleneck. The pattern below is a well-known idiom worth keeping in your back pocket.
import java.io.*; class FastReader { private final DataInputStream in; private final byte[] buf = new byte[1 << 16]; private int ptr = 0, len = 0; FastReader() { in = new DataInputStream(System.in); } private int readByte() throws IOException { if (ptr == len) { len = in.read(buf, 0, buf.length); ptr = 0; } return len == -1 ? -1 : buf[ptr++]; } int nextInt() throws IOException { int c, x = 0; boolean neg = false; do { c = readByte(); } while (c <= ' '); // skip spaces/newlines if (c == '-') { neg = true; c = readByte(); } while (c > ' ') { x = x * 10 + (c - '0'); c = readByte(); } return neg ? -x : x; } }
c - '0' to build the number). That's the whole secret to fast I/O —
batch the reads and parse without allocating. Understand this once and you understand why every faster
reader is faster.
System.out.println inside a loop — each call can flush and is
slow. Accumulate results in a StringBuilder and print once at the end, or wrap output in a
PrintWriter (new PrintWriter(new BufferedWriter(...))) and flush()
after the loop. Same buffering principle as input.
11. Which reader do I actually use?#
| Reader | Speed | Effort | Reach for it when |
|---|---|---|---|
Scanner | Slow | Trivial | Tiny input, quick prototype, not performance-sensitive |
BufferedReader + StringTokenizer | Fast | Low | Default for almost all problems (mixed strings/numbers) |
StreamTokenizer | Fast | Low | Input is overwhelmingly numbers, simple format |
| Custom byte reader | Fastest | High | Millions of tokens + tight time limit; nothing else fits |
Rule of thumb: start at BufferedReader. Drop down to a custom reader only after
it's proven necessary.
12. Common input recipes#
The handful of shapes that cover most problems. All assume
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
// one int on its own line int n = Integer.parseInt(br.readLine().trim()); // n space-separated ints on one line -> array StringTokenizer st = new StringTokenizer(br.readLine()); int[] a = new int[n]; for (int i = 0; i < n; i++) a[i] = Integer.parseInt(st.nextToken()); // an r x c grid of characters char[][] g = new char[r][]; for (int i = 0; i < r; i++) g[i] = br.readLine().toCharArray(); // T test cases int T = Integer.parseInt(br.readLine().trim()); while (T-- > 0) { /* read and solve one case */ } // read until end of input String line; while ((line = br.readLine()) != null) { /* process line */ }
StringTokenizer that you refill from br.readLine() whenever it runs out.
Then the code stops caring how the input happens to be split across lines.
Interview & Practice Q&A#
Question visible; tap to reveal the answer. Each answer is short enough to say out loud.
Why is String immutable in Java, and what's one concrete benefit?
HashMap key or passed across threads without defensive copying, and its hash code can be
cached.What's the difference between == and .equals() for strings?
== compares object references (are they the same object in memory), while
.equals() compares actual character content. Two strings with identical text can be
different objects, so == may return false when you expect true. Always use
.equals() for content comparison.Explain String vs StringBuilder vs StringBuffer.
String is immutable. StringBuilder is a mutable character buffer
optimized for building/editing text in a single thread. StringBuffer is the same as
StringBuilder but its methods are synchronized, making it thread-safe at the cost of speed.
For DSA, use StringBuilder.Why is building a string with += in a loop slow, and what's the fix?
+= copies the entire string built so far
into a new object, giving O(n²) total work for n appends. The fix is a StringBuilder:
append in the loop (amortized O(1) each) and call toString() once at the end, making it
O(n).What's the gotcha with String.split()?
"." matches every character; you must escape it as "\\.". Also, trailing empty
tokens are dropped by default — pass a negative limit like split(",", -1) to keep
them.How do you reverse a string, and why is there no String.reverse()?
new StringBuilder(s).reverse().toString(), or do a two-pointer swap on
s.toCharArray(). There's no String.reverse() because String is immutable
— a reverse method would imply mutating it in place, which the type deliberately forbids.Your solution is correct but slow while reading input. First move?
Scanner with BufferedReader (plus
StringTokenizer or parseInt). Scanner uses regex and autoboxing per token,
which is very slow at scale; BufferedReader reads in large buffered chunks and often speeds reading up
several-fold without touching the algorithm.Why is a custom byte-based reader faster than BufferedReader?
x = x*10 + (c - '0')), skipping character decoding, regex tokenizing, and object
allocation entirely. It's the fastest but the least convenient, so it's reserved for inputs with
millions of tokens under tight limits.How do you convert a digit character to its integer value?
'0': int d = c - '0';. This works because
the digit characters '0' through '9' have consecutive character codes. To
convert back, add '0' and cast: char c = (char)(d + '0');.Key Takeaways#
- String is immutable. Every "edit" allocates a new object — the root cause of most string performance traps.
- Compare with
.equals(), never==.==tests reference identity, not content. - Substring end is exclusive:
substring(i, j)has lengthj - i. splittakes a regex. Escape special chars ("\\.") and use-1limit to keep trailing empties.- Reverse via StringBuilder or a two-pointer char-array swap; there's no
String.reverse(). - Build text with StringBuilder, not
+=in a loop — O(n) vs O(n²). - A char is an index:
freq[c - 'a']++counts letters in O(1) space; one+/-array checks anagrams, and(c-'a'+k)%26+'a'shifts with wrap-around. - Ask the
Characterclass, don't hand-roll ranges:isDigit,isLetter,isLetterOrDigit,toLowerCase; convert withInteger.parseInt/String.valueOf. - Input upgrade path: Scanner → BufferedReader → StreamTokenizer → custom byte reader. Start at BufferedReader by default, and keep the §12 recipes handy.
- When reading is the bottleneck, swap Scanner for BufferedReader first — highest-leverage single change.
- Buffer your output too: accumulate in a StringBuilder / PrintWriter and flush once at the end.
Self-contained study reference · theme-aware · built for Java DSA revision.