DSA Study Sheet · Java

String Operations & Fast Input for Java DSA

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.

Section 1

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
Section 2

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.

Why it matters Immutability is why 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.
"ab" original + "c" "abc" (new) + "d" "abcd" (new) Each concatenation copies everything so far into a fresh object — the old ones are discarded.
Fig. 1 — Concatenation never mutates; it allocates.
⚠ Pitfall — == vs .equals() == 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
💡 Tip — the digit trick A digit character converts to its integer value by subtracting '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.

D S A R O X 0 1 2 3 4 5 6 substring(2, 5) grabs indices 2,3,4 → "ARO" — index 5 is the stop, not taken.
Fig. 2 — Indices sit between characters; the end is exclusive.
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
★ Trick — length is just j minus i Because the end is exclusive, the length of 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.

"12,7,90,3" split(",") join(",", a) "12" "7" "90" "3" String[] — a[0]="12", a[1]="7", a[2]="90", a[3]="3"
Fig. 3 — 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"
⚠ Pitfall — split takes a REGEX, not a literal The argument to 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).
💡 Tip — trailing empties disappear "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.
💡 Tip — splitting on whitespace Words separated by one or more spaces: 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.
⚠ Pitfall — replace is literal, replaceAll is 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);
💡 Tip — palindrome without reversing You rarely need to build the reversed string to check a palindrome. Two pointers from both ends comparing 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).

D S A free append('R') writes into spare capacity — no new object Buffer over-allocates and grows in chunks; appends are amortized O(1).
Fig. 4 — StringBuilder appends into reserved space instead of copying.

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
⚠ Pitfall — the O(n²) concatenation loop 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();
Why it matters — interview classic "What's the difference between String, StringBuilder, and StringBuffer?" is asked in almost every Java interview. The crisp answer: 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.

'c' a char c - 'a' = 2 001 00 012 34 freq[c - 'a']++ bumps bucket 2 int[26]
Fig. 6 — Subtracting '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
★ Trick — anagram check in one array Two strings are anagrams exactly when their letter counts match. Instead of two arrays, use one: ++ 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.
★ Trick — sort the letters for an anagram key Two words are anagrams exactly when their sorted characters match, so a sorted string is a canonical key: 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.
💡 Tip — pick the base and the size Lowercase only? 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');
}
💡 Tip — StringBuilder is a ready-made stack For "remove adjacent duplicates", backspace-style edits, or "undo the last char", treat 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))
💡 Tip — normalize while you scan For case-insensitive or "alphanumeric only" problems (palindrome checks, word counts), test and lowercase each char in the same pass: 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"
⚠ Pitfall — parseInt is strict 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.

slower / easier faster / more manual Scanner BufferedReader StreamTokenizer custom byte reader
Fig. 5 — The input upgrade path: pick the leftmost one that's fast enough.
// 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
★ Trick — the first thing to check when reading is slow If the logic is right but a solution is slow while reading many numbers, don't optimize the algorithm first — swap 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.
⚠ Pitfall — nextInt() then nextLine() Mixing token reads with line reads on 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());
💡 Tip — StringTokenizer beats split() for speed For breaking a line into tokens in a hot path, 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.
Why it matters 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; }
⚠ Pitfall — nval is a double, and negatives/words need care 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;
    }
}
Why it matters Notice the two core ideas reused from earlier: a buffer (read many bytes per system call) and the digit trick (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.
💡 Tip — output is half the battle Fast input is wasted if you 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 */ }
💡 Tip — don't assume one value per line When numbers are laid out freely (counts and data wrapped however the setter liked), pull every value through one shared 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?
Once created, a String's characters can't change; any "modification" produces a new object. The benefit is safe sharing: because nobody can mutate it, a String can be used as a 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?
Because String is immutable, each += 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()?
Its argument is a regular expression, not a literal string. So splitting on "." 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()?
Use 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?
Replace 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?
It reads raw bytes into a buffer and builds numbers digit-by-digit (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?
Subtract the character '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

  1. String is immutable. Every "edit" allocates a new object — the root cause of most string performance traps.
  2. Compare with .equals(), never ==. == tests reference identity, not content.
  3. Substring end is exclusive: substring(i, j) has length j - i.
  4. split takes a regex. Escape special chars ("\\.") and use -1 limit to keep trailing empties.
  5. Reverse via StringBuilder or a two-pointer char-array swap; there's no String.reverse().
  6. Build text with StringBuilder, not += in a loop — O(n) vs O(n²).
  7. 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.
  8. Ask the Character class, don't hand-roll ranges: isDigit, isLetter, isLetterOrDigit, toLowerCase; convert with Integer.parseInt / String.valueOf.
  9. Input upgrade path: Scanner → BufferedReader → StreamTokenizer → custom byte reader. Start at BufferedReader by default, and keep the §12 recipes handy.
  10. When reading is the bottleneck, swap Scanner for BufferedReader first — highest-leverage single change.
  11. 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.