Skip to main content
Object Oriented Design, Analysis and Programming

Arrays, Strings and String Handling in Java

Published: 2026-08-19
Level: postgraduate
Audience: Postgraduate students in Object Oriented Design, Analysis and Programming

This lecture covers arrays, Strings and String handling in Java, including declaration and access, the Arrays utility, jagged arrays, String pool and heap-stack memory, StringBuffer capacity, StringTokenizer and StringBuilder.

18.1 Arrays — Fundamentals, Declaration, Initialization and Access

This section follows the lecture sequence: Arrays Fundamentals (concept overview), example length illustration with arr holding 20 31 54 82 23, actual physical memory blocks, enhanced for loop that iterates, loop that retrieves elements, type Employee object array, members empNo fields, creates array retrieval via loop.

Hook — why manage 500 values with one name? Imagine a college must store the marks of 500 students for one exam. Without arrays you would write int m1, m2, m3, ..., m500; — 500 separate names, 500 separate statements to read, print and compare. With one array int[] marks = new int[500]; you hold all 500 values under one name and handle them with a four-line loop. That single shift — from many variables to one indexed collection — is why arrays are the first user-defined collection every Java programmer learns.

The explanation below adds the next layer of intuition.

Intuition + Analogy — an egg tray with numbered slots. Think of an array as an egg tray, not a grocery bag. An array (a collection of homogeneous data items — all of the same data type) is like a tray moulded to hold a dozen eggs: every compartment is the same size and shape, every position has a printed index, and you cannot put an apple in an egg slot. An element (one stored value) is one egg. The index or subscript is the slot number stamped on the tray, and the length or size is how many slots the tray has. Just as trays are manufactured per type — egg tray, ice-cube tray, test-tube rack — an array is declared per element type (int[], float[], char[]). The mapping is exact: arr is the tray, arr[i] is the egg at slot i, and arr.length is the tray capacity.

Where the analogy breaks: a real tray has a fixed physical spacing, but a Java array's "consecutive blocks" are logical contiguity in the heap managed by the JVM — you never compute byte offsets yourself, and Java checks every access at runtime. Also, an egg tray can hold null (empty slot); a primitive int[] cannot — empty slots hold 0, not "no egg."

Formalize — the three ideas that define an array.

1. Homogeneity and element type. If T is any Java type (int, float, char, Employee), then T[] is the type "array of T". Every element satisfies element ∈ T. You cannot store a char in an int[] without an explicit cast that the compiler will reject for narrowing.

2. Indexed access and bounds. For an array arr of length n = arr.length, the index i satisfies

and the element at position i is written . For the running example with five elements [20, 31, 54, 82, 23]:

arr.length is a final field, not a method — you write arr.length, never arr.length(). The check is performed on every access; arr[5] on this array throws ArrayIndexOutOfBoundsException immediately.

3. Two-step creation — declare then allocate.

Declaration (compile-time, no memory yet) gives the name and base type:

Both compile identically. Initialization (runtime, heap allocation) reserves consecutive slots:

new must know size now; without it there is no block to point to. All elements are zero-initialized at allocation: 0 for numeric types, false for boolean, '\u0000' for char, and null for reference types. Size is fixed for the life of the array — you cannot grow new int[5] to six slots; you must allocate a new array.

Relation to types. Primitive types (int, float, char) hold one value. Arrays are supported directly by the language as a way to group many values of one type under one reference, handled uniformly by loops and the java.util.Arrays helpers. Conceptually they are the simplest "collection" before ArrayList and the Collections Framework.

18.1.1 Declaration and Initialization — Linking a Name to Physical Memory

Declaration is the promise; initialization is the construction.

Three equivalent declaration forms for an integer array (all create a reference variable that can point to an int[], none allocate storage yet):

int[] arr;   // preferred — type is "array of int", name is arr
int arr[];   // C-style, same meaning, carried for familiarity
int []arr;   // whitespace variant, same meaning

For object types the pattern is identical: Employee[] staff; or String[] names;.

Initialization links the name to a physical heap block and freezes the length:

arr = new int[5];          // five consecutive int slots, all 0 initially
// combined form seen in professional code:
int[] arr = new int[5];
// initialized with values in one line (size inferred):
int[] arr2 = {20, 31, 54, 82, 23};  // length 5, contents as listed
// alternative initializer syntax:
int[] arr3 = new int[]{20, 31, 54, 82, 23};

Memory picture after arr = new int[5];arr itself is a 4- or 8-byte reference living on the stack (or as a field in an object), pointing to a heap object with a header, a length field holding 5, and five int slots back-to-back. Visualize:

stack:  arr ──► heap: [ header | length=5 | 0 | 0 | 0 | 0 | 0 ]
                indexes:              0   1   2   3   4

Without new, arr remains null and any arr[i] dereference throws NullPointerException. The "five consecutive blocks" phrase in the lecture maps to this heap object; "consecutive" matters because it gives random access via , a guarantee Java inherits from the underlying array implementation even though you never compute the address yourself.

Dynamic allocation via new is why every Java array is created at runtime and why the size expression new int[n] can itself be a variable n computed earlier — int n = sc.nextInt(); int[] a = new int[n]; is perfectly legal.

18.1.2 Accessing Array Elements — Direct Indexing and Loops

For a single variable int b = 7; you read and write the name b. For an array you read and write arr[i] — the name plus an index expression in brackets.

Direct assignment is exact but does not scale:

arr[0] = 10;
arr[1] = 20;
System.out.println(arr[2]); // 54 in the running example

For length 500, writing 500 such lines duplicates logic, invites copy errors, and defeats automation. Loops factor the index:

Simple for loop — explicit index, full control for reading and writing:

for (int i = 0; i < arr.length; i++) {
    System.out.println("Element at index " + i + " : " + arr[i]);
}

Walk-through with arr.length = 5: i starts at 0, test 0 < 5 true → print arr[0]; i++1, test true → print arr[1]; ... i=4 print arr[4]; i++5, test 5 < 5 false → stop. This is the canonical idiom because i < arr.length adapts automatically if the size changes — never hard-code i < 5 when arr.length is available.

For-each (enhanced for) loop — values only, no index writes:

for (int x : arr) {
    System.out.println(x);
}

x takes each element value in order; you cannot assign x = 10 to change arr[i]. Use it when you only need to display or sum.

Labelled for loop — a label before a loop (outer: for(...)) lets an inner loop break outer; or continue outer; to jump out of nested iteration. The lecture names it for completeness; with one-dimensional arrays you rarely need it, but it matters immediately when traversing jagged arrays with nested loops and an early-exit condition.

Population from external sources replaces the right-hand side of arr[i] = ...:

Scanner sc = new Scanner(System.in);
for (int i = 0; i < arr.length; i++) {
    arr[i] = sc.nextInt();       // from user
    // or: arr[i] = Integer.parseInt(args[i]);  // from command line
    // or: arr[i] = fileScanner.nextInt();      // from file
}

18.1.3 Worked Examples

Example 1 — Primitive int[] with manual build and loop retrieval.

Program (class ABC with public static void main):

class ABC {
    public static void main(String[] args) {
        int[] arr = new int[5];   // length 5, all 0
        arr[0] = 10;
        arr[1] = 20;
        arr[2] = 30;
        arr[3] = 40;
        arr[4] = 50;
        // state: [10, 20, 30, 40, 50]
        for (int i = 0; i < arr.length; i++) {
            System.out.println("Element at index " + i + " : " + arr[i]);
        }
    }
}

Execution trace:

  • After new int[5]: arr = [0, 0, 0, 0, 0].
  • After five assignments: arr = [10, 20, 30, 40, 50].
  • Loop iteration table:
i test i < 5 arr[i] printed line
0 true 10 Element at index 0 : 10
1 true 20 Element at index 1 : 20
2 true 30 Element at index 2 : 30
3 true 40 Element at index 3 : 40
4 true 50 Element at index 4 : 50
5 false loop exits

Sense-check: five lines printed, last index 4 = length - 1, sum if you add them.

Scaled version replacing manual lines:

Scanner sc = new Scanner(System.in);
for (int i = 0; i < arr.length; i++) arr[i] = sc.nextInt();

Same loop shape, input now comes from the keyboard — this is why loops are non-negotiable once n > 10.

Common variant that still works:

int[] arr = {10, 20, 30, 40, 50}; // declaration + initializer in one
for (int v : arr) System.out.println(v);

Produces the same five values without any explicit index.

A second concrete trace makes the pattern concrete.

Example 2 — Object array Employee[] (array of references).

Define the element type:

class Employee {
    int empNo;
    String name;
    Employee(int empNo, String name) {
        this.empNo = empNo;
        this.name = name;
    }
}

Build the array in ABC.main:

Employee[] arr = new Employee[5]; // array of 5 Employee references, all null initially
arr[0] = new Employee(1, "Ankit");
arr[1] = new Employee(2, "Bebbo");
arr[2] = new Employee(3, "Sunny");
arr[3] = new Employee(4, "Riya");
arr[4] = new Employee(5, "Karan");

Memory after new Employee[5]: not five employees, but five reference slots —

arr ──► [ null | null | null | null | null ]
         0      1      2      3      4

After each new Employee(...): a distinct heap Employee object is created and its reference stored in one slot. Contrast with Employee e1 = new Employee(6, "John") which is a single standalone reference; arr[0] is the same idea but the name of the reference is computed (arr[0] instead of e1). This is why the lecture stresses "arr[0] is itself a named object reference."

Retrieval loops identically, but through a dot:

for (int i = 0; i < arr.length; i++) {
    System.out.println("Element at index " + i + " : " + arr[i].empNo + " " + arr[i].name);
}

Trace:

i arr[i].empNo arr[i].name output
0 1 Ankit Element at index 0 : 1 Ankit
1 2 Bebbo Element at index 1 : 2 Bebbo
2 3 Sunny Element at index 2 : 3 Sunny
3 4 Riya Element at index 3 : 4 Riya
4 5 Karan Element at index 4 : 5 Karan

Sense-check: arr.length still 5; the loop body changed from arr[i] to arr[i].empNo.

Pitfall to watch in this trace: forgetting to construct a slot, e.g. leaving arr[3] as null and then executing arr[3].name throws NullPointerException — the array and the objects inside it are separate allocations. Every new Employee[5] must be followed by five new Employee(...) before you read through the array.

Visual intuition for both examples: draw a column of boxes. Left column — indexes 0 to 4 in a vertical stack. Right column — current contents. For int[] the right column holds numbers; for Employee[] the right column holds arrows pointing to separate Employee boxes each containing two fields (empNo, name). Axes are simple: horizontal position is index increasing downward, content is value/reference. The takeaway is that int[] is a tray of values, Employee[] is a tray of signposts.

Assumptions & Scope — when this works and when it breaks.

Applies when: element type is uniform, length is known at allocation time and stays fixed, random access by index is needed, iteration is sequential via arr.length. Performance of arr[i] is and cache-friendly because of contiguity.

Breaks when: you need to grow after creation — use ArrayList<T> or Arrays.copyOf to create a new larger array; you need mixed types — Java forbids int and String in one int[] (use Object[] only as an escape hatch, losing type safety); you rely on == to compare two arrays — it compares references, not contents (see 18.2.6); you expect deep cloning from arr.clone() on an Employee[] — it clones the tray, not the employees inside (shallow copy).

The visual picture clarifies why the preceding assumption matters before examining common errors.

Pitfalls — the four mistakes students repeat.

  1. Off-by-one and ArrayIndexOutOfBoundsException. Valid is 0 to arr.length - 1. Writing arr[arr.length] or looping with i <= arr.length always fails.
  1. Using arr.length() with parentheses. length for arrays is a field (arr.length); length() is a method for String. The compiler error "cannot find symbol: method length()" is the clue.
  1. Reading before filling object arrays. new Employee[5] gives [null, null, null, null, null]. Any arr[i].empNo before arr[i] = new Employee(...) throws NullPointerException.
  1. Confusing reference and value semantics. int[] a = {1,2,3}; int[] b = a; b[0]=99; now a[0] is also 99 because a and b point to the same heap array. For independent copies use Arrays.copyOf.

18.1.4 Student Questions and Answers

Q: Is the index always starting from zero? Does length mean the total number of elements? A: Yes — this is not a convention you can change. In Java the first element is always at index 0, the next at 1, continuing up to size - 1. For an array new int[5] the valid indexes are 0, 1, 2, 3, 4 and the total count — called length or size — is 5. The inequality is the only correct bounds test. Several students asked this in different phrasings and the answer was identical each time; the off-by-one risk is exactly why the professor repeated it.

The explanation below adds the next layer of intuition.

Q: Why prefer loops over direct indexing such as arr[0] = 10? A: Direct indexing is exact for a tiny, fixed array where you can list each slot by hand and see each value. It fails to scale. For an array of size 50 or 500, writing 500 assignments (arr[0]=..., arr[1]=..., ...) is impractical, error-prone, and must be edited whenever the size changes. A loop with arr[i] handles any size with the same three lines, works uniformly for reading from any source (Scanner, file, command-line), and is the only realistic way to initialize or display a large array. The professor's example with five manual assignments (10,20,30,40,50) was explicitly labeled "for illustration only — next, we automate with a loop."

The explanation below adds the next layer of intuition.

Q: Can we create an array of objects the same way as an integer array? A: Yes, and the syntax is deliberately parallel. int[] arr = new int[5] holds five integers (values); Employee[] arr = new Employee[5] holds five Employee objects (references). In both cases arr[i] names slot i. For primitives you assign a value (arr[0]=10); for objects you assign a newly constructed object (arr[0]=new Employee(1, "Ankit")) whose constructor initializes the two data members empNo and name for that slot. Organization is the advantage: instead of Employee e1, e2, e3, ... as separate variables, the array gathers them as arr[0] through arr[4], each itself a fully named reference that can be looped over with arr[i].empNo and arr[i].name.

Recap + Bridge. An array is a homogeneous, fixed-length, index-from-zero tray whose length is arr.length and whose elements are arr[i] with . Declaration names it, new builds it, and a for loop with i < arr.length is the standard bridge to every operation that follows. That loop idiom is exactly what the Arrays utility class (18.2) automates — printing without looping, sorting, searching and copying with one call — and what jagged arrays (18.3) repeat in two dimensions with arr[i].length.

Real-world and domain note: homogeneous arrays back every bulk numeric pipeline — daily sensor time-series, transaction ledgers, image rows, student score sheets, and database fetch buffers. Object arrays such as Employee[] model real business entities where each slot is a record with multiple fields; they underpin early domain models before generics and ArrayList<Employee> replace them for resizable collections. The same arr[i] mechanism reappears in systems programming for buffer management and windowing over large data arrays where a single allocation and indexed walk is faster than per-element objects.

18.1.5 Industry Applications

In production Java, primitive arrays drive high-throughput numeric code because they are contiguous and avoid per-element boxing. Examples include ingesting sensor streams from IoT gateways (one double[] per window), closing-price arrays for moving-average calculations in trading, and pixel row buffers in image processing. Object arrays like Employee[] or Order[] model entity sets in business applications — payroll batches, course-registration lists, hospital patient rosters — where each slot carries a rich object rather than a scalar. When collection size must vary at runtime, teams promote T[] to ArrayList<T>, but the underlying storage remains an array (ArrayList wraps an Object[]).

18.1.6 Exam Notes

Exam note: Expect "distinguish declaration from initialization" and "write a loop that works for any length." Be ready to: (a) write the three declaration forms and state that only new int[5] reserves five consecutive heap blocks and fixes the size; (b) write the indexed traversal for (int i = 0; i < arr.length; i++) and explain why i <= arr.length - 1 is equivalent but i <= arr.length overruns; (c) predict NullPointerException versus ArrayIndexOutOfBoundsException for object arrays. Marks are often allocated to spotting arr.length() (wrong) versus arr.length (correct).

18.2 The Arrays Utility Class — Printing, Sorting, Searching, Copying and Filling

Follow the Arrays utility flow in order: Arrays Utility overview, toString display via Arrays, meters supplied complete sort of A to 1 2 3 4 5 7, step element middle computation, Compute middle halving, middle truncated binary search, Since discard logic for comparison, remain does deepEquals nested check.

Java provides a helper class java.util.Arrays with static methods that operate on arrays. The class name Arrays (with an s) is itself a class, so an array such as int[] arr is an object of that underlying array type. The eight core method families are toString, sort, binarySearch, equals, deepEquals, copyOf, copyOfRange, and fill. Each family is overloaded for byte, char, double, float, int, long, short, and Object — the same code shape works for any element type.

Hook — why not hand-write loops for everything? If you have an array of ten million ints, writing your own bubble-sort, your own binary search, and your own element-by-element copy is both slow to write and slow to run. Arrays.sort, Arrays.binarySearch, Arrays.copyOf, and Arrays.fill are library-tuned: they use dual-pivot quicksort, optimized native copies, and tight loops that outperform a textbook for. One line replaces twenty.

The explanation below adds the next layer of intuition.

Intuition — the utility class as a toolbox clipped to the tray. If section 18.1's array is the egg tray, java.util.Arrays is the clipped-on toolbox: a magnifier (toString lets you see all eggs at once), a sorter (re-orders eggs), a finder (binarySearch locates one by value), a duplicator (copyOf/copyOfRange), a filler (re-inks a run of slots), and a comparator (equals/deepEquals). You never create an Arrays object; you call Arrays.methodName(...) directly because every method is static.

18.2.1 Printing Arrays — Why arr Alone Prints an Object ID

Consider int[] arr = {1, 2, 3}. If you write:

System.out.println(arr);

the output is not 1 2 3. It is something like [I@15db9742 — the object ID or reference number of the array object. That is because printing arr without an index talks about the array at the broad level, not about its individual elements. arr[0] means the first element; plain arr means the whole array object at the broad-to-deep distinction, and printing the broad reference shows its identity. [I is the JVM's internal name for int[]; the hex after @ is the hash of the object's heap address.

The fix is the static method Arrays.toString:

System.out.println(Arrays.toString(arr)); // [1, 2, 3]

Arrays.toString walks arr[i] for i = 0 to arr.length - 1, converts each element with String.valueOf, joins with ", " and wraps with [ ]. It is the standard way to print all elements in one go, complementing element-by-element access through a for loop with arr[i]. For multi-dimensional arrays use Arrays.deepToString.

Formalize — Arrays.toString signature and contract. For int[]:

with special handling for null (returns "null") and empty (returns "[]"). The method never mutates a; it is a pure view. The overload set covers all primitive array types and Object[].

18.2.2 Sorting — Full Sort and Partial (Range) Sort

An array A with elements [2, 3, 5, 1, 4, 7] has length 6 and indexes 0 through 5.

Formalize — Arrays.sort overloads and exclusive upper bound.

Crucially, fromIndex is inclusive, toIndex is exclusive. The lecture describes Arrays.sort(A, 0, 4) as "start at index 0 and sort the next 4 elements," which matches exactly: elements at indexes 0, 1, 2, 3 participate, indexes 4, 5 do not. Standard phrasing in the docs is "from fromIndex inclusive to toIndex exclusive" — they are the same interval.

Complexity for primitive arrays is dual-pivot quicksort, average , worst case with optimizations; for Object[] it is TimSort (stable).

Worked example — partial versus full sort with explicit state.

Initial state:

A = [2, 3, 5, 1, 4, 7]
     0  1  2  3  4  5   (indexes)

Partial sort: Arrays.sort(A, 0, 4)

  • Window: A[0..3] = [2, 3, 5, 1] → sorted → [1, 2, 3, 5].
  • Outside window: A[4]=4, A[5]=7 untouched.

Result:

A = [1, 2, 3, 5, 4, 7]

Printed with Arrays.toString(A) this is [1, 2, 3, 5, 4, 7]. The session session notes the output voiceover as "1, 2, 3, 5 followed by 4 and 7 are coming" — exactly this layout.

Full sort: Arrays.sort(A) on the current [1, 2, 3, 5, 4, 7]

  • Window: all six elements → sorted → [1, 2, 3, 4, 5, 7].
Arrays.sort(A);
System.out.println(Arrays.toString(A)); // [1, 2, 3, 4, 5, 7]

Sense-check: length unchanged (6), set of values unchanged (just permuted), ascending order holds: for every valid i.

18.2.3 Searching — Arrays.binarySearch

After sorting, the array [1, 2, 3, 4, 5, 7] is searched:

int idx = Arrays.binarySearch(A, 5);
System.out.println("Binary search for 5 is " + idx); // 4

The output 4 means element 5 is at index 4.

Formalize — binary search contract and middle formula.

Precondition: is sorted ascending. For sorted array of length , with low and high as inclusive bounds, the middle index is

Java computes this as mid = (low + high) >>> 1 (unsigned right shift) to avoid overflow of low + high. If key == A[mid], return mid; if key < A[mid], set high = mid - 1 and repeat left; if key > A[mid], set low = mid + 1 and repeat right. If not found, the method returns , a negative value encoding where the key would be inserted.

Time cost is ; a linear scan for (i ... if A[i]==key) costs .

Note on the lecture's "(0+5)/2 = 2.5 truncated to 2": that description computes the mid for the full range as (integer division). The standard implementation with low=0, high=5 gives mid=2, element A[2]=3, matching the walkthrough.

Worked example — binary search for 5 in [1, 2, 3, 4, 5, 7] step by step.

step low high mid = (low+high)/2 A[mid] compare 5 vs A[mid] action
1 0 5 3 discard left half , new low = 3
2 3 5 5 found → return 4

Verification: A[4] is indeed 5. Search for a missing key, e.g. 6, would terminate with low > high and return (insertion point 5, encoded as ).

Sense-check: probes; we used 2. That beats scanning all 6. For , binary search needs about 20 probes versus 1,000,000 for linear.

18.2.4 Copying — copyOf and copyOfRange

Formalize — copying with exclusive upper bounds.

If newLength > n, extra slots are zero/null-padded; if newLength < n, the copy is truncated.

Both from inclusive, to exclusive — exactly the sorting convention.

System-level System.arraycopy(src, srcPos, dest, destPos, length) is the fastest native primitive; Arrays.copyOf delegates to it.

Worked examples — concrete slices.

Given A = [1, 2, 3, 4, 5, 7] (already sorted, length 6):

Whole copy:

System.out.println(Arrays.toString(Arrays.copyOf(A, A.length))); // [1, 2, 3, 4, 5, 7]
System.out.println(Arrays.toString(Arrays.copyOf(A, 3)));        // [1, 2, 3] — truncated
System.out.println(Arrays.toString(Arrays.copyOf(A, 8)));        // [1,2,3,4,5,7,0,0] — padded

Range copy: Arrays.copyOfRange(A, 1, 4)

  • Window indexes 1, 2, 3 (since 4 is exclusive).
  • Values: A[1]=2, A[2]=3, A[3]=4[2, 3, 4].
System.out.println(Arrays.toString(Arrays.copyOfRange(A, 1, 4))); // [2, 3, 4]

The lecture emphasizes "1 to 4 means indexes 1, 2, 3 participate" — precisely the exclusive-upper-bound rule.

Sense-check: length of range copy is to - from = 3, which matches 3 elements printed.

18.2.5 Filling — Arrays.fill

Formalize — fill overloads.

Range overload follows the same inclusive-exclusive convention.

Worked examples — whole versus tail fill.

Starting from A = [1, 2, 3, 4, 5, 7] (n=6):

Tail fill: Arrays.fill(A, 4, A.length, 1)from=4 inclusive, to=6 exclusive → indexes 4, 5:

before: [1, 2, 3, 4, 5, 7]
index:   0  1  2  3  4  5
after:  [1, 2, 3, 4, 1, 1]
                    ^  ^  filled with 1

Whole fill: Arrays.fill(A, 1) → every slot:

before: [1, 2, 3, 4, 1, 1]
after:  [1, 1, 1, 1, 1, 1]

Sense-check: after a whole fill, Arrays.toString(A) is six 1's joined as [1, 1, 1, 1, 1, 1].

18.2.6 Equality — == Versus Arrays.equals Versus Arrays.deepEquals

Formalize — three levels of equality.

  1. ar1 == ar2 (reference equality): true iff ar1 and ar2 point to the same heap object (same ID). No element access.

  1. Arrays.equals(ar1, ar2) (shallow value equality): true iff ar1.length == ar2.length and for every i, (for primitives) or Objects.equals(ar1[i], ar2[i]) (for Object[]).
  1. Arrays.deepEquals(ar1, ar2) (recursive value equality): for nested arrays (Object[] containing int[] etc.), recursively descends until primitive values or non-array objects are reached, then compares values. Returns true only if the entire tree of values matches.

Predict-the-output 1 — == vs equals on flat arrays.

int[] ar1 = {1, 2, 3};
int[] ar2 = {1, 2, 3};
if (ar1 == ar2)
    System.out.println("Same");
else
    System.out.println("Not same");
System.out.println(Arrays.equals(ar1, ar2));

Memory:

ar1 ──► [1,2,3] at id1
ar2 ──► [1,2,3] at id2   (different object, same contents)

ar1 == ar2id1 != id2 → false → prints Not same. Arrays.equals(ar1, ar2) walks i=0:1==1, i=1:2==2, i=2:3==3 → true → prints true. Fix: always use Arrays.equals for flat value comparison.

Predict-the-output 2 — equals vs deepEquals on nested arrays.

int[] inAr1 = {1, 2, 3};
int[] inAr2 = {1, 2, 3};
Object[] ar1 = {inAr1, inAr2}; // two references inside
Object[] ar2 = {inAr2, inAr1}; // same two, swapped order? actually same values, same order in session's second example as {inAr2}, but deep matters
if (Arrays.equals(ar1, ar2))
    System.out.println("Same");
else
    System.out.println("Not same");  // prints Not same

if (Arrays.deepEquals(ar1, ar2))
    System.out.println("Same");      // prints Same when values match at deepest level
else
    System.out.println("Not same");

Wait — why does Arrays.equals(ar1, ar2) still say Not same even though inAr1 and inAr2 both hold [1,2,3]?

Because ar1[0] holds a reference to the int[] object inAr1; ar2[0] holds a reference to a different int[] object inAr2. Arrays.equals on Object[] compares elements with equals(), and for int[], equals() is not overridden — it falls back to == (reference). So inAr1.equals(inAr2) under Arrays.equals is actually inAr1 == inAr2 → false (different IDs). The comparison never reaches the integers 1,2,3.

Arrays.deepEquals fixes this: it detects that elements are themselves arrays and recursively applies Arrays.equals (or itself) to them, drilling to the bottom where integers 1,2,3 are compared by value. Only the deep version returns true when nested structure holds the same ultimate values.

Ordering nuance: In the session's nested example ar1 = {inAr1, inAr2} and ar2 = {inAr2, inAr1}, even deepEquals with swapped inner order would still compare positionally: ar1[0]=inAr1 vs ar2[0]=inAr2 values [1,2,3]==[1,2,3] true, ar1[1]=inAr2 vs ar2[1]=inAr1 also true, so deepEquals returns true because each corresponding pair has equal values despite being different objects. The lecture's point is the level of comparison, not permutation: == checks IDs, equals checks one level of values but stops at the intermediate reference, deepEquals reaches the concrete integers.

18.2.7 Worked Examples Summary

A compact map of the session's demonstrations:

  • Printing: System.out.println(arr)[I@... (ID); Arrays.toString(arr)[1, 2, 3].
  • Partial sort: Arrays.sort(A, 0, 4) on [2,3,5,1,4,7] sorts [2,3,5,1] to [1,2,3,5] leaving [4,7] untouched → [1,2,3,5,4,7].
  • Full sort: Arrays.sort(A) produces [1,2,3,4,5,7].
  • Binary search for 5 in the sorted [1,2,3,4,5,7] returns index 4 via two halvings.
  • copyOf(A, A.length) copies whole array; copyOfRange(A,1,4) yields [2,3,4] (exclusive to).
  • fill(A,4,A.length,1) fills tail with 1[1,2,3,4,1,1]; fill(A,1) fills all with 1.
  • ar1 == ar2 compares IDs and yields false; Arrays.equals compares flat values; Arrays.deepEquals compares nested values.

Assumptions & Scope — when each helper applies.

Requires: binarySearch assumes the array is already sorted (otherwise the result is undefined); sort(from,to) assumes 0 <= from <= to <= n or it throws IllegalArgumentException; copyOfRange assumes from <= to.

Breaks when: you sort an Object[] whose elements do not implement Comparable and you call the no-comparator sortClassCastException; you use binarySearch on an unsorted array and mistake a negative return for "not found" vs "would be inserted at…"; you compare nested arrays with == or equals and conclude "not equal" incorrectly.

The visual picture clarifies why the preceding assumption matters before examining common errors.

Pitfalls — the four traps that cost marks.

  1. Printing bare arr. Newcomers expect values and get [I@hash. Remember arr is an object — you must ask for a view via Arrays.toString.
  1. Exclusive upper bound. In sort(A,0,4), copyOfRange(A,1,4), fill(A,4,n,1), the to index 4 never participates. Writing "indexes 0 to 4 inclusive" is the standard error; correct is "0 to 3."
  1. == for content. == never checks contents for arrays — not even for String[]. Use Arrays.equals for flat, Arrays.deepEquals for nested.
  1. Searching before sorting. binarySearch on [2,3,5,1,4,7] without a prior sort returns an arbitrary position — the symptom looks like a wrong index rather than an explicit error.

Visual intuition: picture each utility as a strip operation. Draw A as six adjacent cells. Highlight the sort window [0,4) in yellow, show arrows reordering values inside only; show binarySearch as a shrinking highlight around mid; show copyOfRange(1,4) as a scissors cut extracting cells 1-3; show fill(4,n,1) as a paint roller over the tail. Axes are index (left→right) vs value (cell content). The takeaway is windowed operations are always half-open [from, to).

18.2.8 Student Questions and Answers

Q: When I print an array directly with System.out.println(arr), why do I see a strange code like [I@...? A: Because arr without an index is a reference to the array object itself, not to its elements. System.out.println(Object) prints the object's identity (getClass().getName() + "@" + hash). To see the elements you must either loop with arr[i] element by element, or — idiomatically — call Arrays.toString(arr) which builds "[1, 2, 3]" from the elements. Several students hit this at the same moment and the professor labeled it the "broad vs deep" distinction: arr is the tray, arr[i] is one egg.

The explanation below adds the next layer of intuition.

Q: In Arrays.sort(A, 0, 4), do the 0 and 4 mean indexes or counts? A: They designate a half-open index interval: fromIndex = 0 inclusive, toIndex = 4 exclusive. The effect is the same as "start at index 0 and sort the next 4 elements (indexes 0,1,2,3)", which is how the lecture walked through it. Standard documentation says "fromIndex inclusive, toIndex exclusive" — so for A of length 6, sort(A,0,4) leaves indexes 4 and 5 (4 and 7) untouched, and sort(A,1,4) would sort exactly indexes 1,2,3.

The explanation below adds the next layer of intuition.

Q: Why does == return false even when two arrays hold the same numbers, and when should we use equals versus deepEquals? A: == compares object IDs (heap references), so two distinct arrays ar1 at id1 and ar2 at id2 are never == even when every ar1[i]==ar2[i]. Arrays.equals fixes this for flat (one-dimensional) arrays by comparing elements one by one. For nested arrays such as Object[] ar1 = {inAr1, inAr2} where each element is itself an int[], Arrays.equals still compares the inner int[] objects by reference (== on the inner arrays) and therefore fails even though the ultimate integers match. Arrays.deepEquals is needed because it recurses past intermediate references to the bottom-level integers and compares values there. Hierarchy: == → references, equals → one level of values, deepEquals → all nested levels.

Recap + Bridge. Arrays is the toolbox that makes array practice tractable: toString for display, sort/binarySearch for ordering and fast lookup, copyOf/copyOfRange for slicing, fill for bulk initialization, and equals/deepEquals for value-level comparison at the correct depth. Those window conventions ([from, to)) recur immediately in jagged arrays (18.3), where arr[i].length sets to per row, and in string buffer capacity growth (18.5), where fresh windows are allocated when the old one overflows.

Real-world and domain note: in production data pipelines, Arrays.sort and binarySearch replace hand-rolled sorts over million-element int[] windows from logs or time-series; copyOfRange carves sliding windows without manual loops; fill pre-conditions buffers with sentinel values (e.g. Arrays.fill(buf, -1) before a sparse fill). The equals/deepEquals hierarchy is exercised in testing frameworks (assertArrayEquals delegates to these helpers) and in serialization checks where nested record arrays must be compared deeply.

18.2.9 Industry Applications

At large scale, array utilities replace hand-written loops with one-line, native-accelerated calls. Data engineers sort multi-million-entry int[] or double[] from sensor feeds with Arrays.sort before selecting percentiles via binarySearch; ETL jobs use copyOfRange to carve a date-window out of a year-long array for reprocessing; server buffers are reset with Arrays.fill(cache, 0) or Arrays.fill(tmp, 4, n, -1) to mark uncomputed tails. Equality helpers underpin correctness checks: unit tests assert Arrays.equals(expected, actual) for flat responses and Arrays.deepEquals(expected, actual) for nested result sets (e.g. Object[][] rows from JDBC).

18.2.10 Exam Notes

Exam note: Be ready to predict output for three comparison layers and three array operations. Memorize: (a) hierarchy == (references) → Arrays.equals (flat values) → Arrays.deepEquals (nested values); (b) half-open windows from inclusive, to exclusive for sort, copyOfRange, fill; (c) binarySearch preconditon (sorted) and negative return encoding when not found. The session used several predict-the-output questions exactly on these seams — practice them with concrete indexes 0..5 as above.

18.3 Jagged Arrays — Two-Dimensional Arrays with Variable Column Counts

Up to this point the discussion assumed regular (rectangular) two-dimensional arrays. A jagged array — also called a ragged array — is a two-dimensional array whose rows can have different numbers of columns. It is defined as an array whose elements are themselves arrays, possibly of different sizes.

Hook — what if every row needs a different length? A timetable with 3 sections where section A has 2 subjects, section B has 5 subjects, and section C has 1 subject cannot be forced into a rectangle without wasting cells. Nor can a social graph where person A has 2 friends and person B has 200 friends. A uniform table is the wrong shape. A jagged array is a table whose stair-step edge fits the data.

The explanation below adds the next layer of intuition.

Intuition + Analogy — a staircase of shelves, not a uniform grid. A regular 2D array is a full bookshelf grid: 3 rows, each row exactly 4 shelves. A jagged array is a staircase: row 0 has 2 pigeonholes, row 1 has 3, row 2 has 1. Each row is still an array; the outer array simply holds rows of different lengths. If a 1D array is an egg tray, a jagged 2D array is a cart holding egg trays of different sizes.

Where the analogy breaks: bookshelves have a fixed frame; a jagged array can allocate each row independently at runtime and even reassign arr[i] = new int[newSize] later, reshaping the staircase after construction. Also, rows not yet allocated hold null, not an empty shelf — accessing them before sizing throws NullPointerException.

Formalize — arrays of arrays.

In Java a 2D array is literally an array whose components are arrays. For T[][] arr:

Regular (rectangular) allocation creates all rows at once:

Jagged allocation creates rows separately:

Here only the outer dimension 2 (rows) is fixed. Components arr[0] and arr[1] are still null until sized:

Now , but column counts differ. For a larger example with , the picture is:

arr ──► [ arr[0] ] ──► [  *              ] length 1
        [ arr[1] ] ──► [  *  *           ] length 2
        [ arr[2] ] ──► [  *  *  *        ] length 3
        [ arr[3] ] ──► [  *  *  *  *     ] length 4

Access always needs two indexes: arr[i][j] with bounds and . Using a fixed inner bound like j < 5 for a jagged array is the characteristic error.

18.3.1 Declaration — Fixing Only the Row Count

int[][] arr = new int[2][];

Here 2 fixes the number of rows. The second bracket is left blank [] because column sizes are not yet chosen — the compiler creates an array of two int[] references, both null:

arr[0] = null
arr[1] = null

Then each row is sized individually:

arr[0] = new int[3]; // row 0 gets 3 columns — arr[0] now [0,0,0]
arr[1] = new int[2]; // row 1 gets 2 columns — arr[1] now [0,0]

Now arr[0] is an array of length 3 and arr[1] is an array of length 2. The structure looks like a staircase rather than a rectangle. Equivalent alternative syntax accepted by the compiler:

int arr[][] = new int[2][];
int[] arr2[] = new int[2][];

All mean the same: outer array of int[]. Reassigning arr[0] = new int[10] later is legal — the staircase can be reshaped per row at runtime. This per-row control is exactly why Java documents multidimensional arrays as "arrays of arrays" (T6 Chapter 3) and recommends leaving inner dimensions blank when sizes will vary.

18.3.2 Populating and Retrieving — Nested Loops with Per-Row Length

A worked example uses a counter count initialized to 0 and nested loops. This is the canonical idiom — outer walks rows, inner walks the specific row's length:

int count = 0;
for (int i = 0; i < arr.length; i++) {           // arr.length is number of rows (2)
    for (int j = 0; j < arr[i].length; j++) {    // arr[i].length is columns in row i
        arr[i][j] = count++;
    }
}

Worked trace for arr above (arr[0].length=3, arr[1].length=2).

iteration i j arr[i].length assigned arr[i][j] count after
1 0 0 3 arr[0][0] = 0 1
2 0 1 3 arr[0][1] = 1 2
3 0 2 3 arr[0][2] = 2 3
4 1 0 2 arr[1][0] = 3 4
5 1 1 2 arr[1][1] = 4 5

Final logical table:

row 0: [0, 1, 2]   (3 columns)
row 1: [3, 4]      (2 columns)

If populated with int[][] twoD = new int[4][]; twoD[0]=new int[1]; twoD[1]=new int[2]; twoD[2]=new int[3]; twoD[3]=new int[4]; the same loop with k=0 filling twoD[i][j]=k++ (T6's TwoDAgain example) yields:

0
1 2
3 4 5
6 7 8 9

showing the staircase growing by entries per row.

Retrieval mirrors population — the inner bound must be arr[i].length, not a constant:

for (int i = 0; i < arr.length; i++) {
    for (int j = 0; j < arr[i].length; j++) {
        System.out.print(arr[i][j] + " ");
    }
    System.out.println(); // newline per row
}

Output for the two-row example:

0 1 2 
3 4 

If you mistakenly write j < arr.length or j < 3, the first row may print correctly but the second will either miss elements or throw ArrayIndexOutOfBoundsException. The key pattern arr[i].length (length of row i) replaces a fixed column count — this phrase is exactly what the professor asked students to memorize.

A variant using Arrays.toString per row for debugging:

for (int i = 0; i < arr.length; i++) {
    System.out.println(Arrays.toString(arr[i]));
}
// [0, 1, 2]
// [3, 4]

Assumptions & Scope — when jagged fits and when a class fits better.

Applies when: rows naturally differ — adjacency lists (graph[node].length == degree(node)), student-course registrations, weekly schedules with variable events per day, triangular matrices, sparse rows where rectangular would waste most cells.

Breaks when: rows need uniform column semantics (e.g. a matrix for linear algebra where m[i][j] must exist for every j < n) — use new int[m][n]; rows must grow frequently — consider ArrayList<ArrayList<Integer>> or List<int[]>; you need to serialize or sort by row length — rectangular arrays have simpler invariants and library support.

The visual picture clarifies why the preceding assumption matters before examining common errors.

Pitfalls — the two jagged-specific errors.

  1. Null rows. int[][] a = new int[2][]; a[0][0]=5; throws NullPointerException because a[0] is still null. Always size each row a[i] = new int[size] before first access.
  1. Fixed inner bound. for (j=0; j < 5; j++) assumes every row has 5 columns. For a jagged array row 0 may have 3, row 1 may have 2 — the loop overruns. Only j < arr[i].length is correct. The same trap appears with arr.length vs arr[i].length confusion: outer is rows, inner is columns of that row.
  1. Reference aliasing between rows. arr[0] = arr[1]; makes both rows point to the same inner array; mutating arr[0][0] then also changes arr[1][0]. This is shared-reference behavior, not a copy — use Arrays.copyOf(arr[1], arr[1].length) for independent rows.

Visual intuition: sketch a left-to-right index axis per row, but shorten each row's bar to its own arr[i].length. Row 0's bar extends to 3, row 1's to 2. The "staircase" profile — steps of decreasing or increasing length — is the one-sentence takeaway. For the -row case, the area of cells is versus for a rectangle — jagged saves of allocations when rows are actually short.

18.3.3 Student Questions and Answers

Q: Is a jagged array a different data structure from a 2D array? A: No — it is a 2D array, just with variable column counts per row. The declaration new int[2][] fixes only the row count; each row is then created as new int[3] or new int[2] with its own size. Conceptually there is no new mechanism: you still declare with int[][], you still access with arr[i][j], and you still use two nested loops. The only difference is the inner bound: rectangular uses a constant n (j < n), jagged uses arr[i].length so each row contributes exactly as many columns as it owns. The professor's phrase — "if you understand one-dimensional arrays, jagged arrays are not a big thing" — maps directly to this: jagged is just "array of 1D arrays, each possibly different length."

Recap + Bridge. A jagged array (int[][] arr = new int[rows][] plus arr[i] = new int[colsForThatRow]) is the natural shape for variable-length rows. The double loop for i < arr.length / for j < arr[i].length is the only correct walk. That per-row .length idea resurfaces immediately: a String's length and a StringBuffer's capacity (18.4–18.5) are also length-like fields with different mutation rules — String length is fixed and immutable, StringBuffer capacity grows on demand.

Real-world and domain note: jagged arrays model inherently ragged data — adjacency lists for sparse graphs (each node's neighbor list has degree d_i), inverted indexes where each term posts to a different number of documents, student-course enrollments, calendar days with variable appointments, and compressed sparse rows for matrices where most entries are zero. Backend engineers often promote jagged patterns to List<List<T>> for dynamic growth, but the in-memory layout remains "array of arrays."

18.3.4 Industry Applications

Jagged arrays back systems where rows naturally differ: graph engines store adjacency as int[][] adj where adj[v].length is the out-degree; e-learning platforms store per-student course IDs (courses[s].length varies by enrollment); workforce schedulers store per-day shift assignments with variable staffing. Choosing jagged over rectangular saves heap and iteration time when the average row length is much smaller than the maximum — precisely the sparse, irregular datasets where graph traversal, path-finding (BFS over adjacency lists), and sparse-matrix kernels benefit most. When serialization to JSON or DB persistence is needed, each jagged row maps cleanly to a variable-length JSON array.

18.3.5 Exam Notes

Exam note: Expect to write the declaration int[][] arr = new int[2][]; arr[0] = new int[3]; arr[1] = new int[2]; and the exact double-loop with arr.length for rows and arr[i].length for columns in row i. Be ready to fill via count++ and predict printed tables, and to spot the null-row error (a[0][j] before a[0]=new int[...]) and the fixed-inner-bound error (j < n vs j < arr[i].length). These two bugs are favourite output-prediction and "fix the code" items.

18.4 Strings in Java — Immutability, the String Constant Pool, and Heap-Stack Memory

Lecture order for strings: Strings Java overview, normal sharing of literals in SCP, assignment java concatenation, Student Questions and Answers discussion.

Java strings are objects, not just character arrays. A String — a sequence of characters — in Java is immutable, meaning once a string object such as "Java" is created, its character content cannot be changed in place. Any operation that appears to modify it actually creates a new string.

Hook — why can "Java" == "Java" be true while two identical int[] arrays are never ==? Both compare references with ==, but strings cheat: the JVM shares one copy of each distinct literal and gives every String variable holding "Java" the same signpost. Arrays never share. That single sharing rule explains most string surprises in Java, including why == sometimes seems to work for strings and then suddenly does not.

The explanation below adds the next layer of intuition.

Intuition + Analogy — printed books versus photocopies. Think of the String Constant Pool (SCP) (a special area inside the heap that stores string literals, sometimes heard as "string constant tool") as a library shelf for printed books. A stack (last-in first-out store for local references and call frames) entry s1 is a call-number slip in your pocket; the heap (bulk object store whose reclamation is managed by the garbage collector) holds the actual book. The first time anyone asks for book "Java", the library prints one copy and puts it on the SCP shelf. The next person asking for "Java" gets a second slip pointing to the same copy — no second print run. Asking with new String("Java") is ordering a personal photocopy — you get your own book even though the text is identical. Immutability (cannot alter an existing book's pages in place) means that when you want "JavaJ2EE" you never scribble J2EE into the existing "Java" copy; the library prints a fresh book "JavaJ2EE" and moves your slip to it. The old book stays pristine on the shelf.

Where the analogy breaks: library slips are paper you can copy; Java references are fixed-width pointers managed by the JVM. And the library must be asked to reclaim books; in Java the garbage collector reclaims unreachable heap objects automatically — you never call "return book" yourself for heap strings.

18.4.1 The String Constant Pool (SCP)

The SCP exists to optimize space and improve performance. It is a table of interned literals inside the heap (in modern JVMs, inside the heap's string table backed by native memory, logically part of the heap). On class loading, every literal "..." encountered is interned: the JVM checks the SCP first, creates the character array once if absent, and shares that single instance.

Formalize — literal versus new and reference sharing.

For declarations:

String s1 = "Java";              // literal
String s2 = "Java";              // same literal
String s3 = new String("Java");  // explicit heap object

Execution model:

  1. String s1 = "Java" — literal "Java" not yet in SCP → create one SCP entry (char sequence J,a,v,a plus hash) at heap address say 2048. Stack slot s1 at address 1000 stores pointer 2048.
   stack 1000: s1 ──► heap SCP 2048: "Java"
  1. String s2 = "Java" — SCP lookup finds "Java" already at 2048. No new heap allocation. Stack slot s2 at 1054 stores the same pointer 2048.
   stack 1054: s2 ──► heap SCP 2048: "Java"  (shared)

Now s1 and s2 alias the identical object: one character array, two references.

  1. String s3 = new String("Java")new forces a distinct heap allocation outside the sharing path. A new String object with its own character copy is created (at say 2150), even though contents equal "Java". Stack slot s3 stores 2150.
   stack: s3 ──► heap 2150: "Java" (separate copy, not the SCP 2048 copy)

String values are char[] (historically) or byte[] plus coder in modern JDKs, but the sharing logic is unchanged. Calling s.intern() explicitly inserts a heap string into the SCP and returns the pooled reference — this is how runtime-created strings can join the sharing.

That the lecture's session writes "string constant tool" is a speech-recognition variant of "pool"; the semantics are identical.

18.4.2 Equality — == Versus equals() for Strings

Because of SCP sharing:

s1 == s2        // true  — both references point to the same SCP entry at 2048
s2 == s3        // false — s3 points to 2150, s2 to 2048 — different objects
s2.equals(s3)   // true  — equals() walks character by character

Formalize — two equality levels.

== is a single pointer comparison. equals() iterates the underlying char (or byte) sequences. That is why s1==s2 true is a sharing artifact, not a content proof, and why application code must always use equals() for correctness — s2==s3 would be a bug if you meant "same text."

Worked check — predicting outputs for the three comparisons.

Given the three declarations above:

  • System.out.println(s1 == s2);true — shared SCP, same pointer.
  • System.out.println(s2 == s3);false — separate heaps, different pointers.
  • System.out.println(s2.equals(s3));true — characters J,a,v,a match at every position 0..3.

Additional check after immutability step (18.4.3): after s1 = s1 + "J2EE" (see below), System.out.println(s1 == s2);false (no longer same object), but s1.equals("JavaJ2EE")true and s2.equals("Java")true independently.

Sense-check: == can be true only when both references were derived from the same literal without new intervening, and without a reassignment that moved one reference to a new value. Content equality is always equals().

18.4.3 Immutability in Action and the Stack-Heap Mapping

Formalize — immutability as assignment, not mutation.

A String in Java is immutable: after construction its character sequence and length never change. Methods that look mutating (concat, +, substring, replace, toUpperCase) do not overwrite the receiver; they allocate and return a different String object. The variable can be reassigned to that new object — that is assignment of the reference, not mutation of the heap bytes.

Consequences: s.length() never changes for a given object; two references sharing the same immutable string cannot surprise each other by changing it; thread sharing of string literals is safe without synchronization.

Consider:

String s1 = "Java";
String s2 = "Java";
s1 = s1 + "J2EE"; // intended "modification"

Worked trace — immutability via repointing.

State after two literals:

stack 1000: s1 ──► heap 2048: "Java"
stack 1054: s2 ──► heap 2048: "Java"   (shared, s1 == s2 true)

Execution of s1 = s1 + "J2EE":

  • The expression s1 + "J2EE" builds a new character sequence J,a,v,a,J,2,E,E length 8. Because String is immutable, the original "Java" at 2048 is not overwritten. A new string "JavaJ2EE" is created (at say 3056) — in the lecture's heap-SCP mapping this is again an SCP-accessible heap entry; technically the concatenated value is a fresh heap String.
  heap 3056: "JavaJ2EE"  (new object)
  heap 2048: "Java"      (unchanged, still alive because s2 points to it)
  • Assignment s1 = ... changes s1's stack slot at 1000 to hold pointer 3056 instead of 2048. s2 at 1054 still holds 2048.
stack 1000: s1 ──► heap 3056: "JavaJ2EE"
stack 1054: s2 ──► heap 2048: "Java"

Now:

s1 == s2   // false — different heap objects 3056 vs 2048
s1.equals("JavaJ2EE") // true
s2.equals("Java")     // true

The old string "Java" stays in memory as long as s2 (or the SCP) references it; if it becomes unreachable, the garbage collector reclaims it. This is the meaning of "you can change which string a variable references, but you cannot change the character content of an existing string object in place."

Memory-diagram summary (lecture addresses):

  • Stack (LIFO — last-in first-out): call frames, local references, program counters. Pushing a method frame allocates its locals (s1 at 1000, s2 at 1054); popping frees them. The most recently entered method returns first.
  • Heap (general heap; session describes its allocation queue as FIFO / priority-queue-like contrasting with stack LIFO): all new objects, arrays, and string data. SCP physically inside the heap: literals "Java" at 2048 and "JavaJ2EE" at 3056. Heap is reclaimed by the garbage collector automatically after execution — no manual free() in Java. The lecture emphasizes s1, s2 references live in the stack slots, the character data lives in the heap entries they point to; changing s1 changes which heap entry its slot points to.

The interchange cut by the lecture break that referenced "s1 == s2 is true for literals but s2.equals(s3) is true even across new" is exactly the two-level equality point above: literal-literal sharing makes == accidentally true, but equals() is the reliable content check that holds across both literal and new construction.

Assumptions & Scope — when immutability and SCP help and when they cost.

Help when: many identical literals occur (parsers, web handlers, logging frameworks) — sharing saves heap and makes interning-based == checks fast when safe; thread sharing is free because no writer can corrupt a String; keys in HashMap<String,...> are safe because the hash never changes.

Cost/breaks when: you build a string incrementally in a loop with s += x — each + creates a new String and discards the old one, causing copies and heap churn. For incremental assembly use StringBuffer/StringBuilder (18.5, 18.7). Also, relying on == for content is a scope error: it is only safe for interned literals, never for strings computed at runtime (new, substring, +, readLine()) unless explicitly intern()-ed.

Note on session's "heap is FIFO/priority-queue-like": this is a pedagogical contrast to stack LIFO, not a precise GC scheduling claim. Modern collectors are generation-based, but the essential distinction — stack is scoped LIFO, heap is long-lived and automatically reclaimed — is sound.

The visual picture clarifies why the preceding assumption matters before examining common errors.

Pitfalls — three frequent string traps.

  1. Using == instead of equals(). if (input == "yes") fails when input came from Scanner.nextLine() — the scanned value is a fresh heap string not in the SCP with the same characters but a different reference. Always write if ("yes".equals(input)).
  1. Thinking new String("Java") joins the pool. It does not — new always allocates a distinct object. Only the literal "Java" is pooled automatically; new String(...) copies it. Two new String("Java") calls give two different == identities even though equals() is true.
  1. Mutating a shared literal. You cannot — but s1 = s1 + "X" looks like mutation. Forgetting that this is a new allocation leads to the surprise s1 == s2 flipping from true to false after concatenation, and to performance cliffs in loops.

Visual intuition: draw a two-region diagram. Top region (stack) — two named boxes s1@1000, s2@1054 holding arrows. Bottom region (heap/SCP) — boxes 2048:"Java", 3056:"JavaJ2EE". Initially both arrows point at 2048. After s1 = s1 + "J2EE", the s1 arrow swings to 3056, s2 stays. Label the arrow as "reference" and the bottom boxes as "immutable character data." The one-sentence takeaway is that reassignment swings arrows, never rewrites boxes.

18.4.4 Student Questions and Answers

Q: Are strings created by new String("Java") also placed in the SCP? (Girish) A: The literal "Java" spelled inside new String("Java") is in the SCP — the class loader interns every literal it sees. The object created by new however is a separate copy outside that shared entry. Sharing happens only when new is not used. So String s1 = "Java"; String s2 = "Java"; share the single SCP entry at 2048 and s1==s2 is true, while String s3 = new String("Java"); gets its own heap object at 2150 and therefore s2==s3 is false even though s2.equals(s3) is true. Those references can be made to share again by writing s3 = s3.intern(); which returns the SCP's 2048 copy.

The explanation below adds the next layer of intuition.

Q: If strings are immutable, how does s1 = s1 + "J2EE" work? A: It does not mutate the original "Java" object at 2048. The + operator builds a new String "JavaJ2EE" at 3056 and the assignment s1 = ... moves s1's pointer from 2048 to 3056. The original "Java" remains unchanged in the SCP/heap and is still pointed to by s2. After the assignment s1==s2 becomes false because they now point to different objects — that is immutability in action: you can repoint which string a variable holds, but you cannot alter the characters inside an existing String in place.

The explanation below adds the next layer of intuition.

Q: What are stack and heap and how do they relate to string references? (Girish / Ashutosh exchange) A: Stack and heap are the two runtime allocation areas. The stack stores local variables, nested function call frames, and program counters and operates LIFO — last call pushed is first popped when the method returns, so the most recent entry executes and unwinds first. The heap is where new objects live — arrays, String character data, the SCP entries — described in the session as queue / priority-queue-like (FIFO) to contrast with stack LIFO, and automatically cleared by the garbage collector after execution. For the example, s1 and s2 references live in the stack (illustrative addresses 1000 and 1054) and each holds a pointer value (2048 or 3056) that designates the heap SCP entry holding the actual characters. When you reassign s1, only its stack slot changes (1000 now holds 3056); the heap entry at 2048 is untouched. This pointer indirection picture is exactly the object-reference model of Java.

Recap + Bridge. String literals share one immutable copy in the SCP (heap), new String forces a separate copy, == tests reference sharing and equals() tests character equality. s1 = s1 + "X" swings a stack arrow to a new heap box; it never edits the old box. The next concept, StringBuffer (18.5), exists precisely because that "allocate new box on every +" is heap-expensive — it replaces the tray of immutable books with a growable notepad that appends in place, and StringTokenizer / StringBuilder (18.6–18.7) complete the mutable versus immutable design story.

Real-world and domain note: the SCP optimisation is visible in parsers, compilers, XML/JSON decoders, HTTP header handling and logging frameworks where the same header names and log prefixes appear millions of times — sharing one "Content-Type" or "INFO" object instead of millions of duplicates saves heap and GC time. Application correctness still depends on equals() — notably, web request parameters and database strings are never literals and must not be compared with == even though literal-literal == accidentally works in test code.

18.4.5 Industry Applications

Systems that handle many repeated literals — HTTP servers canonicalizing header names, compilers interning identifiers, parsers holding grammar keywords, and logging frameworks emitting repeated levels (INFO, ERROR) — benefit directly from SCP sharing because one pooled copy replaces millions of duplicated char sequences, cutting heap footprint and GC pressure. Where safe (interned literals only), code can use faster == identity checks, but production Java universally mandates equals() for content comparison because runtime-derived strings (readLine(), substring(), StringBuilder.toString()) are never pooled by default. Security-sensitive code (authentication tokens, passwords) deliberately avoids interning to prevent pooled copies from lingering in memory.

18.4.6 Exam Notes

Exam note: A common trap asks for s1==s2 versus s1.equals(s2) after mixing literal assignment, new String, and s1 = s1 + "...". Remember: literal–literal with same text → same SCP entry → == true; literal vs new with same text → different references so == false but equals() true; reassigning s1 = s1 + "..." creates a new heap string and makes the previous s1==s2 false. Also expect to sketch the stack (addresses 1000, 1054 holding references) vs heap (SCP entries 2048, 3056 holding characters) and to state the GC/LIFO vs FIFO contrast in one sentence.

18.5 StringBuffer — Mutable, Growable Character Sequences

Capacity walkthrough in lecture order: StringBuffer Mutable basics, Programming leading example where default 20 holds Programming 16 then expands, request least ensureCapacity.

StringBuffer represents a growable and writable (mutable) sequence of characters. Unlike String, which is immutable, a StringBuffer can accommodate more characters, insertions, or appends by automatically expanding its capacity. Think of String as a printed book you cannot edit and StringBuffer as the editor's notepad — you can append, insert and delete in place.

Hook — what if you must assemble a megabyte of text one piece at a time? Building a log line with String s = ""; for (...) s += chunk; creates a fresh String on every +=, discarding the previous one — heap churn that is in copies. StringBuffer/StringBuilder do the same task with one buffer that grows only occasionally, copying characters once into the new capacity. That is why every incremental string assembly in Java eventually reaches for one of them.

The explanation below adds the next layer of intuition.

Intuition + Analogy — a notepad with spare blank pages. A String is a finished booklet — pages sewn in, page count fixed. A StringBuffer is a ring-bound notepad: it has a length (pages actually written) and a capacity (total pages including blank spares at the back). When you run out of blank pages you splice in a larger refill. The textbook's picture (T6 Chapter 17) is that a StringBuffer "often has more characters preallocated than are actually needed, to allow room for growth." That spare room is the capacity slack capacity - length.

Where the analogy breaks: a notepad can be shrunk by tearing pages out; a StringBuffer's capacity() never shrinks automatically — only trimToSize() requests it, and growth copies the entire notepad into a bigger ring.

18.5.1 Constructors

Formalize — four construction paths and the 16-char slack rule.

A StringBuffer (mutable sequence) is constructed by specifying how much room the ring starts with; the current written length and the allocated capacity are distinct:

  • new StringBuffer() — empty with capacity 16 (no characters yet, 16 blank pages).
  • new StringBuffer(int capacity) — empty with exactly capacity blank pages (if capacity < 0NegativeArraySizeException).
  • new StringBuffer(String str) — contents initialized to str, capacity .
  • new StringBuffer(CharSequence chars) — same as above for any CharSequence.

Two accessors track the state:

Invite errors: confusion between length() and capacity()length() is what you see (toString().length()), capacity() is what you can store before the next growth copy.

Example from the textbook's StringBufferDemo (capacity 21 for "Hello" because ) versus the lecture's simplified default 20 for "Java" (lecture models an empty 16 plus 4 characters of "Java" rounded to 20 for teaching clarity — both obey the rule, only the illustration numbers differ by the chosen base capacity).

Three concrete forms used in this lecture:

StringBuffer sbEmpty = new StringBuffer();          // capacity 16
StringBuffer sb5     = new StringBuffer(5);         // capacity 5
StringBuffer sbJava  = new StringBuffer("Java");    // lecture reports capacity 20 (=4 + 16)

18.5.2 Capacity and Automatic Expansion — Old Capacity Times Two Plus Two

A StringBuffer has a capacity — how many characters it can hold without reallocating. If the number of characters to store exceeds the current capacity, the capacity increases by the rule:

In implementation this is newCapacity = (oldCapacity << 1) + 2, but the arithmetic is identical. ensureCapacity uses max(requestedMinimum, newCapacity) semantics — it grows just enough to satisfy the request, using the formula above if that still falls short.

Worked example 1 — default capacity 20 (lecture's illustration), appending to cross the boundary.

Setup:

StringBuffer sb = new StringBuffer("Java"); // "Java" = 4 chars
System.out.println(sb.capacity()); // 20
System.out.println(sb.length());   // 4

Step A — append " Programming" (12 chars: space + "Programming" = 1 + 11 = 12):

sb.append(" Programming"); // total chars = 4 + 12 = 16
System.out.println(sb);          // "Java Programming"
System.out.println(sb.length()); // 16
System.out.println(sb.capacity()); // 20  (16 <= 20, no expansion)

Character count: J(1) a(2) v(3) a(4) _(5) P(6) r(7) o(8) g(9) r(10) a(11) m(12) m(13) i(14) n(15) g(16). Since , the notepad still has blank pages — no refill needed.

Step B — append a further suffix that pushes to 21 characters (session counts to 21: ; the exact suffix string varies as EA / Expert depending on session phrasing, the count 21 is what matters):

sb.append(" EA.."); // now length 21 (lecture: 16+5)
System.out.println(sb.length());   // 21
System.out.println(sb.capacity()); // 42

Why 42? Boundary crossed: so refill triggers:

Now 21 <= 42, so blank pages remain. Copy cost was one allocation of a char[] of size 42 and one arraycopy of the 21 characters — much cheaper than creating five intermediate Strings with +=.

Sense-check: capacity trace 20 → 20 → 42; after the burst the buffer can now absorb up to further characters before the next growth. The jump is always old×2+2, never old+extra.

A second concrete trace makes the pattern concrete.

Worked example 2 — explicit initial capacity 5, small appends.

StringBuffer sb1 = new StringBuffer(5); // capacity 5, length 0
sb1.append("program");  // 7 chars: p(1) r(2) o(3) g(4) r(5) a(6) m(7)
System.out.println(sb1.length());   // 7
System.out.println(sb1.capacity()); // 12

Capacity reasoning: old 5 cannot hold 7, so

Now 7 <= 12, expansion done, surplus .

Continue:

sb1.append("ming"); // +4 chars → 7+4 = 11 total; "programming" = 11? actually "program"=7 plus "ming"=4 → "programm"+"ming" = 11
System.out.println(sb1.length());   // 11
System.out.println(sb1.capacity()); // 12 (still fits: 11 <= 12)

No second expansion yet; one more character would trigger .

Sense-check: program length 7, ming length 4, so "programming" can also be verified as p1 r2 o3 g4 r5 a6 m7 m8 i9 n10 g11? The session's split of "program" (7) plus "ming" (4) intentionally walks through the boundary one suffix at a time to show 5 → 12 then staying at 12.

Variant verification with a single append append("programming") (length 11) to an initial 5:

  • Need 11 > 5 so one expansion suffices, same final 12.

A second concrete trace makes the pattern concrete.

Worked example 3 — ensureCapacity (pre-reservation).

StringBuffer sb = new StringBuffer("Java"); // capacity 20, length 4
System.out.println(sb.capacity()); // 20
sb.ensureCapacity(25);             // "make sure we can hold at least 25"
System.out.println(sb.capacity()); // 42
sb.ensureCapacity(30);             // already 42 >= 30
System.out.println(sb.capacity()); // 42 (no shrink, no second growth)

Reasoning for the 25 request:

  • Current → insufficient.
  • Apply growth once: .
  • Now → satisfied. Implementation actually computes max(25, 42) = 42; if the caller had asked for 100, the rule would have been iterated — 20→42 still so a second jump to still , then to — but the lecture's example stops at the single 42 that already covers 25.

Sense-check: ensureCapacity never shrinks (ensureCapacity(10) on a 42 buffer leaves 42), and it never truncates length. It is the standard way to pre-allocate before a known large burst (e.g. sb.ensureCapacity(estimatedSize)) to avoid repeated old×2+2 copies inside a loop.

Under the hood both automatic expansion and ensureCapacity delegate to Arrays.copyOf / System.arraycopy to move the existing char[] into the new larger char[].

Visual intuition: draw a strip of boxes: capacity is total boxes, shaded length are filled with letters, blank capacity-length are empty tail. Append fills tail boxes left-to-right. When the write head passes the right edge, a second strip twice as long plus two appears, the shaded prefix is copied across, and writing continues. Axes are character position (horizontal) vs occupancy (shaded/blank). The one-sentence takeaway is that growth is generous (doubling plus two) so repeated appends amortize to near-linear cost.

Assumptions & Scope — when StringBuffer is the right tool.

Applies when: you assemble characters incrementally — log lines, SQL/HTML, protocol frames — and want a single mutable buffer instead of intermediate Strings; you need thread-shared mutation with synchronization guarantees (see 18.7); you want explicit capacity control to reduce GC pauses.

Breaks when: you only concatenate two or three strings once — s1 + s2 or String.join is clearer and the JIT optimizes it; you need constant-time character replacement by index with byte-level control — char[] or ByteBuffer may be more direct; you mistake capacity for "maximum forever" — it is just "until next growth."

The visual picture clarifies why the preceding assumption matters before examining common errors.

Pitfalls — three capacity misconceptions.

  1. Conflating length() and capacity(). length() is what toString() returns; capacity() is spare room the programmer normally ignores. Tests that assert new StringBuffer(5).capacity() == 5 and new StringBuffer(5).append("hello").length() == 5 are asking exactly this.
  1. Expecting capacity to shrink automatically. Appending then deleteing does not reduce capacity. Only trimToSize() attempts compaction. Repeatedly growing and trimming in a loop wastes copies.
  1. Thinking growth is old + needed. It is old×2+2, not old + lengthNeeded. On the exam, given old = 20 and needed 25, the answer is 42, not 25 — the buffer overshoots deliberately to reduce future reallocations. Memorize the formula verbatim.

18.5.3 Student Questions and Answers

Q: Does ensureCapacity always double the capacity? (repeated student worry) A: No — it ensures at least the requested capacity and does nothing if current already suffices. If insufficient, the buffer grows using the rule, possibly iterated, but the result is max(requestedMinimum, growthByFormula). In the example, 20 was insufficient for 25, so the rule gave , and so it stopped at 42, which comfortably covers 25. If the buffer already held 42, then ensureCapacity(30) would leave it at 42; no change, no shrink. Exactly this "at least" wording appears in the JDK doc line quoted in the slides: "ensureCapacity specifies the minimum size of the buffer."

Recap + Bridge. StringBuffer is the mutable, growable alternative to immutable String: length() is content, capacity() is allocated slack, and overflow grows as (or enough to meet ensureCapacity's minimum). Those capacity traces (20 → 42, 5 → 12) are the numerical spine of every StringBuffer exam item. Next, StringTokenizer (18.6) shows how a string once built gets split back into words, and StringBuilder (18.7) revisits mutability with the synchronization knob removed.

Real-world and domain note: incremental string assembly is ubiquitous — structured log lines (timestamp + level + message), SQL/JSON/HTML generation, protocol frame building, and test report generation. Using StringBuffer/StringBuilder there avoids the heap churn of looped + on immutable strings; ensureCapacity(expectedSize) is the standard micro-optimization in hot paths where the final size is predictable (e.g. pre-sizing to 256 before appending a known record). In performance-sensitive servers, the old×2+2 trade-off between memory overhead and copy frequency is directly visible in GC logs — fewer, larger reallocations mean fewer old-gen promotions.

18.5.4 Industry Applications

In production code, StringBuffer and StringBuilder serve as the accumulation buffer for any piecewise text assembly: structured logging (sb.append(time).append(' ').append(level).append(msg)), dynamic SQL (sb.append("SELECT ...").append(whereClause)), HTML/XML templating, command-line help generation, and protocol encoding where fields are appended conditionally. Because each append writes into a pre-allocated char[] rather than allocating a fresh String, GC pressure and latency variance drop measurably in high-throughput services. The legacy choice between the two is governed by thread sharing (see 18.7); migration tooling often rewrites StringBuffer to StringBuilder where the buffer is method-local and no sharing crosses thread boundaries.

18.5.5 Exam Notes

Exam note: Memorize and the length() vs capacity() distinction. Given an initial capacity and a sequence of appends, compute character counts step by step and predict when the capacity jumps and to what value. Favourite patterns: default 20 holding 16 stays 20 but 21 becomes 42; new StringBuffer(5) with "program" (7 chars) becomes 12; ensureCapacity(25) from 20 becomes 42; ensureCapacity(10) from 42 stays 42. Show the trace table (cap, len, needed, newCap) for full marks.

18.6 StringTokenizer — Breaking a String into Tokens

Tokenizer sequence: StringTokenizer Breaking, Splitting Java example, self disappears after cut, included default behavior for delimiters.

StringTokenizer — from java.util — breaks a string into tokens, meaning the separated words or substrings that remain after splitting by a delimiter. Given a sentence as a single string, it extracts the individual words. In the parsing pipeline it is the lexer — the first phase that groups raw characters into meaningful pieces before grammar or semantics are applied.

Hook — what if your input is one long line and you need the words? A file gives you "name:Ankit;score:98;grade:A" as a single String, but your program needs four separate fields. Manually scanning with indexOf and substring is tedious and error-prone. A tokenizer does the scanning once and hands you the fields one by one, delimiter by delimiter.

The explanation below adds the next layer of intuition.

Intuition + Analogy — word chopping with scissors. Think of a delimiter (separator character) as the scissors' cut mark and a token (one separated piece) as one paper snippet after cutting. new StringTokenizer("Java Programming with Objects") lays the sentence on a cutting mat with default cut marks at spaces and snips at each space; the snippets Java, Programming, with, Objects are the tokens, and the space itself is discarded like scrap. Passing "," as delimiter moves the scissors to commas; passing true as the third flag collects the scrap itself as a snippet (ABC, +, DEF).

Where the analogy breaks: real scissors destroy the original strip; tokenization never mutates the source String — the StringTokenizer holds an internal cursor (pos) that walks through the original characters. Also, delimiters are character sets (each character is independently a cut mark), not full multi-character strings — delimiter " ,;" means "cut at space OR comma OR semicolon", not "cut at the three-character string."

18.6.1 Constructors

Formalize — three construction levels from the textbook (T6 Chapter 20 / slides).

  • str — source string that will be tokenized (never modified).
  • delimiters — string whose characters individually are separators. Example ",;:" means any of ,, ;, : separates tokens. The default (one-argument form) is whitespace: space ' ', tab '\t', newline '\n', carriage return '\r', form feed '\f'.
  • delimAsToken — if false (default), delimiter characters are discarded; if true, each delimiter character is itself returned as a single-character token. This is the mechanism Girish asked about for "including the delimiter."

All three constructors copy str and the delimiter set; subsequent changes to the original String (which is immutable anyway) do not affect the tokenizer's scan. The class also implements Enumeration<Object>, so hasMoreElements()/nextElement() mirror hasMoreTokens()/nextToken() for legacy code.

18.6.2 Core Methods

Formalize — the four methods you must fluently use.

  • int countTokens() — number of tokens still not consumed from the current cursor. It decreases as nextToken() advances. So countTokens() before a loop is the total token count; inside the loop it counts the remainder; after exhaustion it is 0.
  • boolean hasMoreTokens() — true iff at least one more token exists from pos to end. Equivalent to countTokens() > 0.
  • String nextToken() — returns the next token (skipping delimiters) and advances the internal cursor past it. Throws NoSuchElementException if no token remains.
  • String nextToken(String delim) — temporarily switches the delimiter set to delim for this extraction only, then proceeds as above. This is how the lecture's nextToken("a") variant per-call changes the scissors without constructing a new tokenizer. The default delimiter set after this call remains whatever was last explicitly set.

Idiom for full consumption:

while (st.hasMoreTokens()) {
    String tok = st.nextToken();
    // process tok
}

Or legacy:

while (st.hasMoreElements()) {
    String tok = (String) st.nextElement();
}

The lecture also notes hasMoreTokens() plus a counter i equals countTokens() initially — both reach the same total when no delimiter switch occurs.

18.6.3 Worked Examples

Example 1 — default space delimiter, full iteration with while and counts.

Source:

StringTokenizer st = new StringTokenizer("Java Programming with Objects");
int j = st.countTokens(); // snapshot before iteration
int i = 0;
while (st.hasMoreTokens()) {
    System.out.println(st.nextToken());
    i++;
}
System.out.println("i = " + i);                // 4
System.out.println("j = " + j);                // 4
System.out.println(st.countTokens());          // 0 — cursor at end

Scan with default whitespace:

str:  J a v a _ P r o g r a m m i n g _ w i t h _ O b j e c t s
          ^ cut              ^ cut    ^ cut
tokens: [Java] [Programming] [with] [Objects]   (4 tokens)

Each nextToken() prints one token on its own line:

Java
Programming
with
Objects
i = 4
j = 4
0

Teaching point from Rajesh's question — a token is one snippet (Java is one token, Programming is the next) after splitting by the active delimiters. j == i == 4 after exhaustion; countTokens() was 4 before, 0 after. Forgetting that countTokens() decays makes students expect it to stay 4 throughout — it does not; it mirrors the remaining tokens.

Sense-check: four words separated by three spaces → four tokens. If you change the string to " Java Programming " (extra spaces), StringTokenizer still returns four — consecutive delimiters are coalesced, never generating empty tokens between them.

A second concrete trace makes the pattern concrete.

Example 2 — custom delimiter "a", delimiter discarded (default).

Same logical text, new scissors:

StringTokenizer st = new StringTokenizer("Java Programming with Objects");
while (st.hasMoreTokens()) {
    System.out.println(st.nextToken("a"));
}

Behavior: nextToken("a") switches delimiter set to the single character a (and A is not a — case-sensitive). Scan of "Java Programming with Objects" cutting at a:

J a v a   P r o g r a m m i n g   w i t h   O b j e c t s
  ^   ^           ^     ^
cuts at each 'a'  (Java has two a's, Programming has two)

Resulting pieces (delimiters dropped):

  • Token1: J (from start to before first a in Java)
  • Token2: v (between the two a's inside Java"Ja" → cuts give J | v)
  • Token3: Pr / " Pr" segment — space plus "Pr" from " Programming..." up to the next a
  • Token4: remaining tail after the last a
  • Additional splits inside "Programming" (the a before "mming") extend the list; the session groups these into a numbered walkthrough with totals i==4, j==4, countTokens()==0 at end, focusing on the first four labeled snippets J as one, v as two, space-plus-Pr as three, tail as four.

Key teaching: delimiter "a" itself never appears among the printed tokens — it is the scrap discarded after each cut. To include it, see Example 4.

Verification with a more explicit trace: If we instead tokenize "Java" alone with a:

"Java" → tokens: [J] [v]  ]? actually splits: J | v | ""  → "J", "v" (trailing empty discarded)

More completely, "aab" with delimiter "a" → tokens: ["b"] (leading and consecutive a produce no empty tokens).

Sense-check: a-delimited count differs from space-delimited count exactly because the cut set changed — token count is a function of (str, delim), not of str alone.

A second concrete trace makes the pattern concrete.

Example 3 — single nextToken() without a loop (partial consumption).

StringTokenizer st = new StringTokenizer("ABC, DEF, GHI"); // commas and spaces in source
System.out.println(st.nextToken()); // prints "ABC,"  (default space delimiter)

With default whitespace, the first token is "ABC," including the trailing comma — the comma is not whitespace, so it stays attached to the token. Remaining untaken tokens are "DEF," and "GHI", so st.countTokens() is now 2.

Switch to comma as delimiter:

StringTokenizer st2 = new StringTokenizer("ABC, DEF, GHI", ",");
System.out.println(st2.nextToken()); // "ABC"
System.out.println(st2.nextToken()); // " DEF" (space + DEF — comma consumed, space not)
System.out.println(st2.nextToken()); // " GHI"

Now "," is the cut, so "ABC" is the first token (no comma). The space after the comma stays because it is not a delimiter in this construction. To cut at both comma and space, delimiters would be ", " (both characters active).

Teaching seam from the slides: new StringTokenizer("ABC, DEF, GHI", ",") → "ABC" only for the first token demonstrates exactly this: changing the delimiter set changes what is retained as token skin.

Sense-check: one nextToken() without a while consumes exactly one token; remaining tokens are still retrievable — countTokens() tells you how many.

A second concrete trace makes the pattern concrete.

Example 4 — including the delimiter as a token with true flag.

StringTokenizer st = new StringTokenizer("ABC+DEF+GHI", "+", true);
System.out.println(st.nextToken()); // ABC
System.out.println(st.nextToken()); // +
System.out.println(st.nextToken()); // DEF
System.out.println(st.nextToken()); // +
System.out.println(st.nextToken()); // GHI

The three-argument constructor new StringTokenizer(str, delim, true) says "treat + as the delimiter but also emit the delimiter itself as a token." Sequence is content, delimiter, content, delimiter, content — 5 tokens total. Without true:

StringTokenizer st2 = new StringTokenizer("ABC+DEF+GHI", "+");
System.out.println(st2.countTokens()); // 3  (ABC, DEF, GHI — '+' discarded)

This answered Girish's question: by default the separator is ignored (not returned), with true it appears as its own token. The same holds for any delimiter string — new StringTokenizer(line, ";:", true) would interleave ; or : among content tokens.

Full output of Example 4 as a while:

ABC
+
DEF
+
GHI

Sense-check: token count with true is contentTokens + delimiterOccurrences; without it is just contentTokens. That is why the textbook emphasizes the third parameter exists precisely for parsers that must reconstruct the original string or distinguish delimiter kinds.

Legacy Enumeration mapping (for completeness):

StringTokenizer st = new StringTokenizer("a b");
while (st.hasMoreElements()) {
    String tok = (String) st.nextElement(); // same as nextToken()
}

And nextToken(delim) switching inside a loop can produce surprising counts — once you call nextToken("a"), the tokenizer's active delimiter set becomes "a" for subsequent nextToken() calls as well, unless you switch back — the session's Examples 1→2 transition assumes a fresh tokenizer per example, which is the safe pattern.

Visual intuition: draw the source string as a character ribbon with cut marks according to the delimiter set. Tokens are the colored segments between marks; delimiters are the black tick marks. With delimAsToken=false the ticks are erased after cutting; with true each tick is itself a colored mini-segment between content pieces. Axes are character index (left→right) and segmentation (colors). The one-sentence takeaway is that changing delim moves every tick, and the true flag decides whether ticks are kept.

Assumptions & Scope — when StringTokenizer fits and when modern APIs win.

Applies when: you need a quick, low-overhead word split on single-character delimiters and sequential forward iteration is sufficient; you are maintaining legacy code that already uses StringTokenizer and Enumeration.

Breaks/replaced when: you need multi-character delimiter strings ("::") as a unit — StringTokenizer would cut at : or : individually, not at "::"; you need regular expressions (split on \\s+ or \\W+); you need empty tokens ("a,,b"["a","","b"]StringTokenizer discards it, String.split preserves it); you need random access or streaming. For new code the textbook recommends String.split or Scanner, and the SDK doc marks StringTokenizer as legacy-compatible ("described here primarily for benefit of those working with legacy code").

Also note: countTokens() is O(n) in some implementations (scans remaining tokens without consuming), so calling it inside a hot loop is wasteful — cache the first call.

The visual picture clarifies why the preceding assumption matters before examining common errors.

Pitfalls — three tests that fail often.

  1. Assuming empty tokens are returned. StringTokenizer("a__b", "_") with "_" as delimiter returns ["a","b"], not ["a","","b"]. Consecutive delimiters are coalesced. String.split("_", -1) would keep empties; StringTokenizer never does.
  1. Treating delimiters as a string phrase. new StringTokenizer(src, "and") does not split at the word and; it splits at any of a, n, d. To split at a fixed phrase use split(Pattern.quote("and")).
  1. Reading past the end. Calling nextToken() when hasMoreTokens() is false throws NoSuchElementException. Unlike Scanner.hasNext() idiom where forgetting the guard is common, the exception here is immediate; always guard with hasMoreTokens() or check countTokens() > 0.
  1. Delimiter switching persists. After st.nextToken("a"), subsequent plain nextToken() uses a, not the original delimiters. Students who expect it to revert to spaces are surprised — create a fresh StringTokenizer per delimiter set.

18.6.4 Student Questions and Answers

Q: What does token mean? (Rajesh) A: A token is one separated piece of the original string after cutting at the delimiters. For "Java Programming with Objects" with the default space delimiter there are four tokens: Java is one token, Programming is the next, with is the next, Objects is the last. Counting: before iteration countTokens() == 4, each nextToken() shortens the remainder by one, and after the four prints countTokens() == 0. A token is not a character — it is a substring between delimiters. Re-asking the same doubt: if you write while (st.hasMoreTokens()) System.out.println(st.nextToken()) you print exactly those four snippets.

The explanation below adds the next layer of intuition.

Q: Is the delimiter itself kept or discarded? How do I include it as a token? (Girish, Ashutosh) A: By default the delimiter is used only to split and is discarded. In the a-delimited example the a characters disappear from the output — J, v, Pr and the tail contain no a. If you pass true as the third argument, such as new StringTokenizer("ABC+DEF+GHI", "+", true), the delimiter + is itself returned as a token, so the output alternates between content tokens and delimiter tokens: ABC, +, DEF, +, GHI. Without the flag the same string would yield only ABC, DEF, GHI. This single flag is the entire "include versus ignore" mechanism — there is no per-call flag for other tokens once the tokenizer is constructed.

Recap + Bridge. A StringTokenizer is a cursor-with-scissors over an immutable String: choose delimiters, then repeatedly hasMoreTokens()/nextToken() to walk tokens; countTokens() decays; nextToken(delim) switches scissors; the true flag keeps the scissors scraps as tokens. That "read-only source plus cursor" picture is exactly how StringBuilder/StringBuffer differ next (18.7) — a Tokenizer never writes the source; a buffer rewrites its own mutable backing while exposing the same CharSequence view.

Real-world and domain note: tokenizers underpin any parsing of delimited text before a proper parser is justified — CSV rows split by commas, TSV by tabs, log lines split by spaces or colons, HTTP header values split by ;, and tiny DSLs that need a lightweight lexer. Where performance matters or delimiters are regular expressions, modern code migrates to String.split("\\s*,\\s*") or a Scanner; the tokenizer's speed advantage is marginal today, but its conceptual simplicity — "one token at a time, discarded delimiters" — remains the pedagogical foundation for scanners, grammars and the stream API.

18.6.5 Industry Applications

Tokenizers are the first pass over delimited input in data ingest: ETL jobs split CSV lines (new StringTokenizer(line, ",") historically, now line.split(",", -1) to keep empties), log processors split by spaces/colons/dashes to extract timestamps and levels, and protocol parsers separate fields before handing them to a grammar parser. In compilers, a tokenizer (lexer) groups characters into keywords and identifiers before parsing — StringTokenizer is the Java standard library's most direct illustration of that lexer step. Modern services often replace it with Pattern.split or Commons CSV for quoted fields with embedded commas, but the token-by-token cursor model persists in streaming parsers and Scanner.

18.6.6 Exam Notes

Exam note: Be ready to count tokens given a string and a delimiter, to state what countTokens() returns before versus after iteration, and to predict output for nextToken() with and without the true delimiter flag. Also rehearse: default delimiters (whitespace), that delimiters are a character set not a phrase, and that consecutive delimiters produce no empty tokens. A typical mark item: "For StringTokenizer st = new StringTokenizer(\"ABC+DEF+GHI\", \"+\", true) what does the second nextToken() return?" — answer +.

18.7 StringBuilder — Mutable Like StringBuffer but Without Synchronization

StringBuilder also represents a mutable sequence of characters and is functionally very similar to StringBuffer: both provide an alternative to immutable String by allowing the character sequence to be expanded and mutated in place. The difference is a single design knob: synchronization (thread safety).

Hook — why have two nearly identical classes? If they both grow, append and insert in the same way, why does the SDK ship both? Because the synchronized one pays a cost for every operation even when only one thread is using it. The second class drops that cost. The exam loves asking which to pick and why.

The explanation below adds the next layer of intuition.

Intuition + Analogy — the notepad with a lock versus the desk pad. Reuse the notepad from 18.5. A StringBuffer (synchronized, thread-safe) is a notepad locked in a conference room: only one colleague can write at a time, others wait at the door — safe, but slow when you are alone. A StringBuilder (not synchronized, not thread-safe) is your private desk pad: you write freely with no lock, fast, but unsafe if two colleagues scribble on it concurrently and overwrite each other's strokes. A String is the printed book that cannot be changed at all. All three expose the same CharSequence view.

Where the analogy breaks: real waiting is physical queuing; thread synchronization is a JVM monitor (synchronized keyword) on the buffer object itself, with nanosecond-scale acquisition rather than human waiting. And unsafe concurrent use of StringBuilder does not always produce a clean error — it may silently produce garbled text, so the bug is subtle, not immediately visible like two pens on one page.

Formalize — the synchronization contract and its cost.

  • StringBuffer — every mutation method (append, insert, delete, replace, reverse) is synchronized. At most one thread can execute such a method on the same buffer at a time; others block until the monitor is released. This guarantees serializability of character sequences — the buffer's invariant (characters plus length plus capacity) remains consistent even when the program is "divided into multiple threads that run concurrently" (the session's definition of multi-threading — dividing a main program into sub-programs (threads) that execute in parallel for performance gain).
  • StringBuilder — none of the methods are synchronized. In a multi-threaded environment where several threads append to the same builder concurrently without external locking, the internal char[] and count can be raced: one thread's write can be lost, the count can become inconsistent, and the buffer can end up with a mix of characters that matches neither thread's intent. No exception is guaranteed — corruption may be silent.

Rule of thumb distilled in the lecture and in T6 Chapter 17:

Performance difference is modest per call (a monitor enter/exit) but compounds in hot loops that append thousands of times — exactly the log/HTML/SQL assembly paths where these classes are used. Modern JITs can sometimes elide the lock for thread-local buffers, but the documented guarantee remains the selection rule.

18.7.1 Constructors

Formalize — construction mirrors StringBuffer with one default-size note.

StringBuilder defines four constructors that parallel StringBuffer's, each tracking length() vs capacity() as before:

  • new StringBuilder() — no characters, initial capacity 16 (same 16-char slack as StringBuffer(), before any append).
  • new StringBuilder(int capacity) — no characters, specified initial capacity (often chosen after estimating final size to avoid growth).
  • new StringBuilder(CharSequence seq) — contents equal to seq, capacity .
  • new StringBuilder(String str) — contents equal to str, capacity . A previously immutable String supplied here becomes mutable once held by the builder — assignment to the builder copies the characters into the builder's char[], and subsequent append/insert mutate that copy, never the original String.

The lecture emphasizes the last point: passing a String into a builder/buffer is the bridge from the immutable world (18.4) to the mutable world (18.5/18.7) — the string's characters become writable once inside the builder.

Example:

StringBuilder sb1 = new StringBuilder();              // cap 16, len 0
StringBuilder sb2 = new StringBuilder(40);            // cap 40, len 0
StringBuilder sb3 = new StringBuilder("Hello");       // len 5, cap 5+16=21
StringBuilder sb4 = new StringBuilder((CharSequence)"Hi"); // via CharSequence

Capacity growth for StringBuilder uses the same old×2+2 rule and ensureCapacity semantics as StringBuffer — the only difference is the absence of synchronized.

Note on textbook constant: quote from T6 Chapter 17 page 465 — "StringBuilder is similar to StringBuffer except for one important difference: it is not synchronized, which means that it is not thread-safe. The advantage of StringBuilder is faster performance." This sentence is the exam's expected answer verbatim.

Worked example — StringBuilder in a single-threaded loop.

StringBuilder sb = new StringBuilder(16); // capacity 16, length 0
sb.append("Hello");          // Hello length 5 -> cap still 16
sb.append(" ");              // length 6
sb.append("World");          // length 11 -> cap still 16
System.out.println(sb.toString()); // Hello World
System.out.println(sb.length());   // 11
System.out.println(sb.capacity()); // 16 (no growth yet: 11 <= 16)
sb.append("! This is a builder test."); // pushes length to 35
System.out.println(sb.capacity()); // 34? compute: 16*2+2=34 insufficient for 35 -> 34*2+2=70
System.out.println(sb.toString()); // Hello World! This is a builder test.

Trace: initial new StringBuilder() gives capacity 16. Appending Hello (5) then space (1) then World (5) = 11 total, still 11 <= 16 so no reallocation. A further append of 24 characters makes 35 > 16 -> first growth 162+2=34 still 35 > 34 -> second growth 342+2=70 which holds 35 with slack 35. The same sequence on a StringBuffer would allocate the same capacities but each append would acquire the monitor.

Sense-check: StringBuilder growth mirrors StringBuffer growth (old*2+2), only the lock is missing.

18.7.2 Relationship to String Manipulation Methods

Many string manipulation concerns — String methods, tokenizer behaviour, builder/buffer operations — share similar names (append, insert, substring, indexOf) and overlapping behaviours. The session defers exhaustive enumeration to the slides and advises self-practice, because the important conceptual takeaway is already covered: immutability of String versus mutability of StringBuffer and StringBuilder, and the synchronization guarantee that separates the latter two. The remaining methods are used exactly as illustrated in the textbook's catalog: append(...) adds at the tail, insert(pos, ...) splices at pos, delete(start, end) removes, replace, reverse, charAt/setCharAt, getChars, and trimToSize/ensureCapacity manage capacity — all behaving identically between StringBuilder and StringBuffer except the monitor around them.

Assumptions & Scope — when each builder/buffer/string wins.

Applies/choose when:

  • Need a one-time assembled result (+ of two or three strings, String.join) → stay with String.
  • Build piecemeal in a loop or conditionally → StringBuilder by default.
  • Build piecemeal but the buffer is shared across threads without external locking → StringBuffer (rare — modern practice usually confines a StringBuilder per thread/request instead).

Breaks when: you assume StringBuilder is implicitly thread-safe because it looks like StringBuffer — it is not. Concurrent unsynchronized appends can lose writes and throw ArrayIndexOutOfBoundsException inside the builder due to a raced count. Also, assuming StringBuilder avoids all copying is wrong — it still copies on capacity overflow just like StringBuffer.

The visual picture clarifies why the preceding assumption matters before examining common errors.

Pitfalls — the two distinctions examiners target.

  1. "Both are mutable so they are identical." Missing the synchronization sentence loses marks. Always state StringBuffer is synchronized (thread-safe) while StringBuilder is not, and name multi-threading as the deciding context. The lecture says "the detailed impact of synchronization will become clear when multi-threading is covered later" — for now, memorizing the choice rule suffices.
  1. Using StringBuilder for cross-thread aggregation. Example error: a servlet field StringBuilder shared = new StringBuilder(); appended to by every request thread. Fix: either StringBuffer or, preferably, a request-local StringBuilder (StringBuilder sb = new StringBuilder() inside doGet), or external synchronized(shared) wrapping.
  1. Unnecessary StringBuffer in single-threaded code. It works but pays a redundant lock per append. Linters flag StringBuffer sb = new StringBuffer() inside a method that never escapes the thread — replacing with StringBuilder is a standard performance tidy.

Visual intuition: picture two identical desk pads side by side, one with a tiny lock icon. Both have a char[] strip (capacity boxes) and a count pointer. A single writer using the locked pad pauses at the lock gate on each stroke; the same writer using the open pad writes straight through. With two writers, the open pad shows interleaved half-letters; the locked pad forces them to take turns and keeps the final text coherent. Axes are operation sequence (time) vs buffer content (character order). The one-sentence takeaway is that the lock guarantees coherence at the price of per-operation waiting.

18.7.3 Student Questions and Answers

Q: If StringBuffer and StringBuilder are so similar, when should we choose one over the other? A: Choose StringBuffer when multiple threads may access the same character sequence and you need synchronization guarantees — its methods are synchronized so concurrent appends cannot corrupt the buffer. Choose StringBuilder in single-threaded contexts or when the builder is confined to one thread/request, because without synchronization it is faster — no monitor enter/exit on every append. Both solve the immutability cost of repeated String concatenation (18.4), and both grow with the same old×2+2/ensureCapacity rules (18.5); the single technical separator is the synchronization guarantee. The lecture explicitly notes that the full impact of "divided into multiple threads running concurrently" becomes concrete when the multi-threading chapter arrives, but the choice criterion should already be noted now.

Recap + Bridge. StringBuilder is StringBuffer with the synchronized lock removed: identical mutable, growable CharSequence API, faster for single-threaded work, unsafe when shared across threads. Together with String (immutable, SCP-pooled) and StringTokenizer (read-only scanner) they complete Java's string-handling toolkit: String for fixed values, tokenizer for splitting, builder/buffer for assembling. Knowing which member varies on which axis — immutability distinguishes String from the other two, synchronization distinguishes StringBuffer from StringBuilder — is the entire module compressed into one comparison.

Exam-ready comparison table:

class mutable? thread-safe? initial capacity growth use
String No (immutable) Yes (immutable sharing is safe) — (length == content) never (new String per +) fixed text, literals, keys
StringBuffer Yes Yes (synchronized) 16 or str.length()+16 or explicit old×2+2 shared mutable buffer across threads
StringBuilder Yes No 16 or str.length()+16 or explicit old×2+2 default mutable builder, single thread

This is the exact triple the session labels "the favourite comparison question across three axes — immutability/mutability, capacity growth, and synchronization."

Real-world and domain note: in high-throughput servers that assemble responses from many threads, legacy code historically promoted StringBuffer for shared buffers and Vector/Hashtable for shared collections for the same reason — intrinsic synchronization. Modern practice confines a StringBuilder to a single thread or request scope (local variable, ThreadLocal, or StringBuilder per task) and synchronizes externally only where sharing is unavoidable. This shift — from synchronized shared object to thread-confined unsynchronized builder — mirrors the wider move from StringBuffer/Vector to StringBuilder/ArrayList and is visible in performance profiles as reduced lock contention and fewer monitor acquisitions.

18.7.4 Industry Applications

In high-throughput services, string assembly is request-scoped: each HTTP handler creates a local StringBuilder to build the response body, log entry or JSON payload, then discards it. Because the builder never escapes the thread, no lock is needed and StringBuilder outperforms StringBuffer measurably. Shared aggregation — e.g. a single buffer appended to by multiple worker threads polling a queue — historically used StringBuffer for its synchronization guarantee; today engineers typically prefer a StringBuilder per producer plus a final merge, or a BlockingQueue<String> rather than a shared mutable buffer at all. Migration scripts that replace StringBuffer with StringBuilder where escape analysis proves thread confinement show up regularly in performance backlogs, and profiling tools flag StringBuffer constructed inside a method as a lint recommendation to switch.

18.7.5 Exam Notes

Exam note: The favourite comparison is "String vs StringBuffer vs StringBuilder" across three axes — immutability/mutability, capacity growth, and synchronization. Memorize:

  • String is immutable and backed by the SCP (shared literal pool); operations create new Strings.
  • StringBuffer and StringBuilder are mutable, growable, share the old×2+2 / ensureCapacity capacity model, and both accept a String to convert it to mutable form.
  • Only StringBuffer is synchronized (thread-safe for multi-threaded shared access); StringBuilder is not and is therefore faster for the common single-threaded case; its default capacity when empty is 16.

Be ready to state why StringBuilder is preferred in single-threaded code and to show a tiny two-thread interleaving trace that explains how an unsynchronized append can lose a write.

Exam Guidance Summary

  • Arrays — declaration vs initialization: expect to write the three declaration forms (int[] arr, int arr[]) and the initialization arr = new int[5] that reserves five consecutive blocks. Know that size is required at initialization and fixes the length.
  • Arrays — access patterns: know the indexed loop for (int i = 0; i < arr.length; i++) with arr[i], and distinguish it from the for-each and labelled-for alternatives.
  • Arrays — object arrays: Employee[] arr = new Employee[5] with arr[0] = new Employee(1, "Ankit") is a distinct concept from int[]. The constructor parameters initialise per-slot fields.
  • Arrays class — output prediction: the session used several predict-the-output questions. Practise: System.out.println(arr) prints an object ID versus Arrays.toString(arr) prints values; Arrays.sort(A, 0, 4) partially sorts; Arrays.sort(A) fully sorts; Arrays.binarySearch(A, key) returns the index after sorting; Arrays.copyOf and copyOfRange with exclusive upper bounds; Arrays.fill with and without range indexes.
  • Arrays class — equality trap: == compares references (always Not same for two distinct arrays even with identical values); Arrays.equals compares flat contents; Arrays.deepEquals is required for nested arrays such as Object[] {int[], int[]} where equals still compares inner references.
  • Jagged arrays: int[][] arr = new int[2][]; arr[0] = new int[3]; arr[1] = new int[2]; and the nested loop using arr.length for rows and arr[i].length for columns per row.
  • Strings — SCP and equality: String s1 = "Java"; String s2 = "Java"; share the SCP so s1 == s2 is true; String s3 = new String("Java"); forces a new copy so s2 == s3 is false but s2.equals(s3) is true; s1 = s1 + "J2EE" creates "JavaJ2EE" and repoints s1, leaving s1 == s2 false.
  • Strings — heap/stack mapping: SCP strings live in the heap; references s1, s2 live in the stack (e.g., 1000, 1054 versus heap 2048, 3056). Garbage collection reclaims heap; stack is LIFO.
  • StringBuffer — capacity rule: memorize . Examples: default 20 holding 16 stays 20 but 21 becomes 42; 5 with "program" (7 chars) becomes 12; ensureCapacity(25) from 20 becomes 42.
  • StringTokenizer — token counting and delimiter inclusion: countTokens() before loop versus 0 after exhaustion; default space versus explicit "a" or ","; true flag in new StringTokenizer(str, delim, true) includes the delimiter as its own token.
  • StringBuffer vs StringBuilder vs String: core comparison across immutability, growth, and synchronization — only StringBuffer is synchronized; StringBuilder defaults to capacity 16 when empty.

Key Industry Applications

  • Collections of homogeneous data (sensor streams, financial records, academic scores) modelled as primitive arrays, and entity collections (employees, orders, accounts) modelled as object arrays such as Employee[].
  • High-volume array processing using Arrays.sort, Arrays.binarySearch, Arrays.copyOf/copyOfRange, and Arrays.fill instead of hand-written loops, enabling concise handling of large data windows and buffer management.
  • Graph adjacency lists, variable-length records, and per-row variable datasets modelled with jagged arrays int[][] where arr[i].length differs per row.
  • Systems with many repeated literals (parsers, web handlers, logging) benefitting from SCP sharing, while application correctness still relies on equals() over == for content checks.
  • Incremental string assembly (logs, queries, HTML) using mutable StringBuffer/StringBuilder to avoid the heap cost of repeated immutable String concatenation, with StringBuffer chosen for multi-threaded shared builders and StringBuilder for single-threaded speed.
  • Text parsing and lightweight lexing via StringTokenizer, splitting delimited input (CSV, logs, protocol fields) by spaces, commas, or custom delimiters, optionally preserving the delimiter as a token for reconstruction.

OODAP Lecture 18 notes · Arrays, Strings and String Handling in Java

Object Oriented Design, Analysis and Programming Architecture· postgraduate· 2026-08-19

Sections Breakdown

1Arrays — Fundamentals, Declaration, Initialization and Access

Homogeneous collections indexed from 0 to length-1, declared as T[] and allocated with new, accessed via loops and demonstrated with primitive and Employee object arrays.

2The Arrays Utility Class — Printing, Sorting, Searching, Copying and Filling

java.util.Arrays toolbox: toString display, half-open sort/copy/fill windows, binarySearch with mid = (low+high)/2, and three-level equality == vs equals vs deepEquals.

3Jagged Arrays — Two-Dimensional Arrays with Variable Column Counts

2D arrays as arrays of arrays with per-row length arr[i].length and staircase declaration new int[rows][] then per-row new int[cols].

4Strings in Java — Immutability, the String Constant Pool, and Heap-Stack Memory

Immutable String sharing in the heap SCP, literal vs new reference equality vs equals content equality, assignment as repointing, and stack-heap mapping with LIFO vs GC-collected heap.

5StringBuffer — Mutable, Growable Character Sequences

Mutable StringBuffer with distinct length vs capacity, growth rule newCapacity = old*2+2 and ensureCapacity, illustrated with 20->42 and 5->12 traces.

6StringTokenizer — Breaking a String into Tokens

Lexer that splits a String by delimiter characters into tokens, with countTokens/hasMoreTokens/nextToken, delimiter set vs single delimiter, and delimAsToken true to keep separators.

7StringBuilder — Mutable Like StringBuffer but Without Synchronization

StringBuilder matches StringBuffer API but without synchronized, faster single-threaded and unsafe when shared; choose Buffer for shared access, Builder otherwise.

8Exam Guidance Summary

Consolidated exam predictions covering array forms, Arrays helpers windows and equality hierarchy, jagged declaration, SCP sharing, stack-heap mapping, capacity formula, token counts and Buffer/Builder/String choice.

9Key Industry Applications

Industry mappings for homogeneous and object arrays, Arrays helpers at scale, jagged graphs, SCP sharing, incremental buffer assembly and tokenizer parsing.

Postgraduate students in Object Oriented Design, Analysis and Programming

Exam Revision Notes

Below is the distilled, exam-ready core. Every entry comes from the full explanation above. Use this section for rapid review; return to the main notes when a point needs more context.

Arrays — Fundamentals, Declaration, Initialization and Access

Must-know: arr[i] with 0 <= i < arr.length; declaration creates name, new reserves consecutive blocks and fixes size.

⚠️ Top pitfall: Off-by-one arr[arr.length] and using arr.length() instead of arr.length.

Self-check: For new int[5] what are valid indexes and what does arr.length return?

Connects to: 18.2, 18.3

The Arrays Utility Class — Printing, Sorting, Searching, Copying and Filling

Must-know: Half-open [from,to) for sort/copyOfRange/fill; binarySearch requires sorted; == checks IDs, equals flat values, deepEquals nested values.

⚠️ Top pitfall: Inclusive upper bound mistake and binarySearch on unsorted array; comparing nested arrays with == or equals.

Self-check: In Arrays.sort(A,0,4) which indexes are sorted and what does binarySearch return when key missing?

Connects to: 18.1, 18.3

Jagged Arrays — Two-Dimensional Arrays with Variable Column Counts

Must-know: int[][] arr = new int[2][] then arr[0]=new int[3]; loops use arr.length for rows and arr[i].length for columns.

⚠️ Top pitfall: Null row before per-row allocation and fixed inner bound j < n instead of j < arr[i].length.

Self-check: Write nested loops that correctly populate and print a jagged array with rows of 3 and 2 columns.

Connects to: 18.1, 18.4

Strings in Java — Immutability, the String Constant Pool, and Heap-Stack Memory

Must-know: Literals share one SCP entry so s1==s2 true; new forces separate copy so s2==s3 false but equals true; s1=s1+J2EE repoints s1 to new object.

⚠️ Top pitfall: Using == for string content; thinking new String joins the pool.

Self-check: Predict s1==s2, s2==s3, s2.equals(s3) after String s1=Java; s2=Java; s3=new String(Java);

Connects to: 18.5, 18.7

StringBuffer — Mutable, Growable Character Sequences

Must-know: Capacity grows as new = old*2+2; ensureCapacity guarantees at least the requested minimum.

⚠️ Top pitfall: Confusing length and capacity; expecting capacity to shrink automatically.

Self-check: sb capacity 20, need 21 -> new capacity? 5 with program 7 -> new capacity?

Connects to: 18.4, 18.7

StringTokenizer — Breaking a String into Tokens

Must-know: Default whitespace delimiters; countTokens decays; delimiters are discarded unless true flag; nextToken(delim) switches delimiters.

⚠️ Top pitfall: Expecting empty tokens, treating delimiter string as phrase, and persisting delimiter switch.

Self-check: StringTokenizer st=new StringTokenizer(ABC+DEF+GHI,+,true) second nextToken returns what?

Connects to: 18.5, 18.7

StringBuilder — Mutable Like StringBuffer but Without Synchronization

Must-know: StringBuilder = StringBuffer minus synchronized; Buffer thread-safe, Builder faster single-threaded.

⚠️ Top pitfall: Sharing a StringBuilder across threads without locking; using StringBuffer where no sharing exists.

Self-check: Which is synchronized and what is the correct choice for a method-local builder?

Connects to: 18.4, 18.5

Was this lecture useful?

Loading comments…
🤖

BitsNotes AI Assistant

Subject Notes Assistant

Configure AI Chat

Choose how to access the chatbot
Have your own API key?

Switch to "Bring Your Own Key" tab above for unlimited access with any OpenAI-compatible provider.

🔑 Enter API key above to fetch live models from provider, or enter model name manually.
OpenAI-Compatible API Support

Choose any provider preset (Gemini, DeepSeek, Kimi, GLM, MiniMax, Qwen, OpenAI, Groq, Ollama, etc.) or enter a custom endpoint URL.

Security & Privacy First

Your API key is sent directly from your browser to your specified provider. BitsNotes servers never store or see your key.