Skip to main content

Section 5.2 Basic String Inspection

Subsection 5.2.1 Introduction & Motivating Narrative

Imagine you’re building a chat application where users type short messages. You need to check if a message is empty (so you can ignore it), determine its length (to enforce a maximum size), and occasionally inspect specific characters (for commands like "/roll" or "@someone"). Java’s String class provides convenient methods like length(), isEmpty(), and charAt(...). But how do these methods work under the hood? And how can we guarantee we never access an invalid index?
In this section, we’ll explore how Java implements these inspection methods and, in line with the Design Recipe, create a simplified version called "MyString." By focusing on Step 1: Data Definition, we’ll clarify the internal structure of a string-like class. Along the way, we’ll also touch on Step 2: Method Signatures, identify a minimal Step 3: Examples and Tests, and briefly outline a Step 4: Template for the class, and then build each method step by step. By the end, you’ll see how these inspection methods flow naturally from a precise definition of what a string is in code.

Subsection 5.2.2 Java’s Built-In Inspection Methods

Let’s begin by examining how Java’s String class meets our inspection needs:
  • str.length() — Returns the number of characters in str. For example, "Hello".length() is 5.
  • str.isEmpty() — Checks if str contains zero characters. If str.length() == 0, it returns true; otherwise, false.
  • str.charAt(i) — Returns the character at index i, where 0 <= i < str.length(). An out-of-range i triggers a StringIndexOutOfBoundsException.
Here’s a brief snippet showing their usage:
public class DemoStringInspection {
    public static void main(String[] args) {
        String msg = "Hello";
        System.out.println("msg.length() -> " + msg.length());    // 5
        System.out.println("msg.isEmpty() -> " + msg.isEmpty());  // false
        System.out.println("msg.charAt(1) -> " + msg.charAt(1));  // 'e'

        String emptyMsg = "";
        System.out.println("emptyMsg.length() -> " + emptyMsg.length());   // 0
        System.out.println("emptyMsg.isEmpty() -> " + emptyMsg.isEmpty()); // true
    }
}
It’s straightforward. But to understand why these methods behave as they do, we’ll apply Step 1 (Data Definition) from the Design Recipe in our own "MyString." We’ll also reveal relevant method signatures (Step 2) and a skeleton (Step 4) before implementing each method individually.

Subsection 5.2.3 Spotlight: Step 1 (Data Definition)

To design our MyString (or any string-like class), we must decide how to represent its data internally. Our goals are to handle character-by-character storage, enable quick indexing, and track the total length. Let’s look at three "attempts" to see why storing characters in an array is the most sensible approach, even if it initially introduces a naive maximum size.

Subsubsection 5.2.3.1 Attempt #1: Just Store a Java String

A quick solution might be, "If we want a string, let’s just have a String text field." While this does store text, it misses our educational objective: learning how methods like length() and charAt(...) work from scratch. We’d be using Java’s built-in String rather than defining our own structure. This violates Step 1 of the Design Recipe because it doesn’t define our own underlying data—it just delegates to existing code.
private String text; // Attempt #1

Subsubsection 5.2.3.2 Attempt #2: Individual Variables per Character

Another idea might be, "Let’s store each character in its own field." For example:
private char first;
private char second;
private char third;
// ... etc.
This quickly becomes unmanageable: we never know how many character fields we’ll need, and it doesn’t scale for arbitrary-length strings. If someone needs 10 or 100 characters, do we keep declaring fields indefinitely? Clearly unsustainable.

Subsubsection 5.2.3.3 Final Approach: A Fixed Character Array (Up to 100 Chars)

The best next step is to store all characters in a single array. For simplicity, we’ll impose a limit of 100 characters in this demo:
private char[] chars = new char[100];
private int usedLength = 0;
We also track usedLength to indicate how many of these slots are actually in use. If a string is four characters long, usedLength is 4, and positions beyond index 3 remain unused. While this approach is straightforward, it’s obviously limited—someone needing 101 characters has no room. Real Java String uses a more flexible, immutable representation. Alternatively, ArrayList<Character> could be used, though it would consume extra memory by "boxing" each character. For our Design Recipe practice, a char[100] plus usedLength clarifies how each character is stored, accessed, and counted.
  • length() simply returns usedLength.
  • isEmpty() checks if usedLength == 0.
  • charAt(i) must ensure 0 <= i < usedLength before returning chars[i].
Our "failed attempts" and final choice illustrate why Step 1 is powerful: once we define how our string data is organized, the rest of the class follows naturally—even with a naive upper limit. In the next sections, we’ll refine these methods and see how the rest of the Design Recipe fits in.

Subsection 5.2.4 Step 2: Method Signatures & Purpose

Though our main emphasis is Step 1, it’s worth seeing how the data definition informs each method’s signature and purpose. For our MyString class, we might write:
// In a hypothetical MyString class...

/**
 * Returns how many characters are used in this MyString.
 */
public int length() { ... }

/**
 * Checks if this MyString has zero characters used.
 */
public boolean isEmpty() { ... }

/**
 * Returns the char at position index (0 <= index < usedLength).
 * Throws an error if out-of-range.
 */
public char charAt(int index) { ... }
Each method’s purpose follows our data definition: "a string is a fixed array of up to 100 characters plus a count of used characters." So length returns an int, isEmpty returns a boolean, and charAt expects an int parameter. Once the data is defined, the signatures and purpose statements nearly write themselves.

Subsection 5.2.5 Step 3: Examples & Tests (Minimal)

We can outline a few examples to confirm correctness. Below is a table for the built-in String, but it equally applies to our re-implementation:
Table 5.2.1. Inspection Methods Example Table
Input length() isEmpty() charAt(1)
"Hello" 5 false ’e’
"" (empty string) 0 true (invalid - out of bounds)
"Cat" 3 false ’a’
"I love Java!" 12 false ' ' (space)
" " (space) 1 false (invalid - out of bounds)
"123" 3 false ’2’
These examples offer a clear reference for the behavior we expect: from ordinary words to empty strings. They set a baseline for what the next implementation steps must achieve.

Subsection 5.2.6 Step 4: Skeleton for Our "MyString" Class

Having established our data definition (Step 1), method signatures (Step 2), and sample tests (Step 3), let’s present a class skeleton. This corresponds to Step 4: Skeleton / Method Template in the Design Recipe.
public class MyString {

    // Data (from our final approach in Step 1)
    private char[] chars = new char[100];  // fixed size of 100
    private int usedLength;                // tracks actual characters used

    // Constructor
    // - copies characters from a regular Java String
    public MyString(String original) {
        // 1) verify length <= 100
        // 2) copy chars from original
        // 3) set usedLength
    }

    public int length() {
        // return usedLength as discussed in examples
        return 0; // placeholder
    }

    public boolean isEmpty() {
        // check if usedLength == 0 as shown in our test table
        return false; // placeholder
    }

    public char charAt(int index) {
        // verify 0 <= index < usedLength
        // return chars[index] if valid
        return 'X'; // placeholder
    }

    // We'll implement each method next
}
This skeleton directly mirrors our earlier reasoning: the data definition from Step 1, the method signatures from Step 2, and the tests from Step 3. Each method’s logic follows from how we defined the string structure. In the next section, we’ll fill in the actual method bodies.

Subsection 5.2.7 Step 5: Method Implementation

Now let’s implement each method of MyString according to our data definition and example table. Pay particular attention to how each method directly stems from our design choices.

Subsubsection 5.2.7.1 Constructor

First, we need a way to instantiate MyString. The constructor enforces our 100-character cap and properly initializes the internal array:
public MyString(String original) {
    if (original.length() > 100) {
        // Enforce our 100-character limit
        usedLength = 100;
    } else {
        usedLength = original.length();
    }
    
    // Copy characters into our array
    for (int i = 0; i < usedLength; i++) {
        chars[i] = original.charAt(i);
    }
}

Subsubsection 5.2.7.2 The length() Method

We already track the string’s length in usedLength. Thus, length() is trivial:
public int length() {
    return usedLength;
}

Subsubsection 5.2.7.3 The isEmpty() Method

A string is empty if it has zero characters. Given our data definition, that’s just:
public boolean isEmpty() {
    return (usedLength == 0);
}

Subsubsection 5.2.7.4 The charAt() Method

This method must validate the index before accessing the array. Unlike Java’s String, we’ll return a special character instead of throwing an exception:
public char charAt(int index) {
    if (index < 0 || index >= usedLength) {
        return '?';  // Our placeholder for invalid indices
    }
    return chars[index];
}

Subsection 5.2.8 Step 6: Reflection & Next Steps

By stating that "a string is a sequence of n characters at indices 0..n-1," we avoided guesswork when implementing length(), isEmpty(), and charAt(...). That’s Step 1 of the Design Recipe in action:
  • No out-of-bounds surprises: Our data definition clearly marks any index >= length or < 0 as invalid.
  • Defining "empty": If n == 0, no valid positions exist, so isEmpty() is true and charAt(0) cannot succeed.
In the next section (Section 2), we’ll focus on Step 2 (Method Signatures & Purpose) in the realm of string equality and ordering. We’ll still use the same foundational data definition—"a string is length n with positions 0..n-1." This stable core continues to guide how other string methods should behave, including comparing or sorting them.
If you’d like more practice right now, try:
  • Index validation: Write a loop that calls charAt(i) for i = 0..(length), observing how it signals an error once i == length().
  • Adding tests: For your MyString, test words like "Cat," "Umbrella," or anything else—perhaps indexing the last character. This reinforces the 0..n-1 rule.

Subsection 5.2.9 Putting It All Together: A Runnable MyString

Now that we’ve walked through each method, here’s a self-contained version of the MyString class with a main method. Each code block tests a different example, covering the scenarios we outlined in Step 3 (Examples & Tests). Run each example to confirm that all methods behave as expected
Example 1: "Hello"
Example 2: "" (empty string)
Example 3: "Cat"
Example 4: "I love Java!"
Example 5: " " (space)
Example 6: "123"

Subsection 5.2.10 Summary & Recap

This section showed how a well-defined Step 1 (Data Definition) naturally leads to simple inspection methods. Defining our string as "a fixed character array of size 100 plus a count of used characters" made the implementations straightforward:
  • length() returns usedLength
  • isEmpty() checks if usedLength == 0
  • charAt(index) returns chars[index] if valid
We explored two alternative approaches (using Java’s own String or individual character fields) before settling on an array-based solution. We then proceeded through the Design Recipe steps: defining method signatures (Step 2), creating a class skeleton (Step 4), and implementing each method.
In Section 2, we’ll dive into string comparison, including equals vs ==, case sensitivity, and lexicographical ordering. While the implementation remains array-based, we’ll shift our focus to how different aspects of method design and testing come to life.

Subsection 5.2.11 Check Your Understanding

Exercises Exercises

1. Multiple-Choice: Java String Immutability.
Which statement best explains why Java String objects are considered immutable, in contrast to our MyString implementation?
  • Because calling toUpperCase() modifies the original String in-place, but MyString does not.
  • No. Actually, toUpperCase() returns a new String; it never changes the original in place.
  • Because once a String is created, its internal character data can’t be altered, whereas our MyString could be changed (e.g., by returning ’?’ on invalid indices).
  • Correct. Java String never modifies the existing object’s contents, while MyString here is not strictly immutable.
  • Because Java’s String is a primitive type, and primitives are always immutable.
  • No. String is a reference type, not a primitive. Its immutability is a design choice in Java.
  • Because Java automatically uses intern() for every String, ensuring immutability.
  • Interning is an optimization for string literals but not the reason String is immutable.
2. Multiple-Choice: The Importance of Step 1 (Data Definition).
According to the Design Recipe, how does Step 1 (Data Definition) guide the creation of inspection methods like length(), isEmpty(), and charAt()?
  • It primarily covers user interface design, ensuring the MyString class has a visually appealing API.
  • No. Step 1 is about the underlying data representation, not UI aesthetics.
  • It focuses on optimizing runtime efficiency first, then clarifies what methods we can implement.
  • Not quite. Step 1 defines the raw data structure; optimization is a separate concern.
  • It prevents us from adding any methods until we finish the entire Design Recipe.
  • No. Step 1 informs method design but doesn’t block you from writing skeleton methods as you go.
  • It forces us to specify exactly how data (the string’s characters and length) is stored, so the methods have a clear basis for operating on that data.
  • Exactly. Step 1 clarifies the internal representation (array + length), making these methods straightforward.
3. Multiple-Choice: Fixed Array Size.
Why did our MyString implementation use a fixed-size array of 100 characters?
  • Because Java String also uses a fixed-size array of exactly 100 characters internally.
  • No. Java’s String can handle strings of arbitrary length and doesn’t have such a limit.
  • Because arrays in Java must always have a fixed size of 100 or less.
  • No. Arrays can be any length you specify when you create them.
  • For educational simplicity (aligning with Step 1 in the Design Recipe), even though it’s an artificial limitation.
  • Correct! Limiting the size to 100 makes it easier to illustrate core ideas in a small example.
  • Because real-world string implementations never need more than 100 characters.
  • No. Real applications often need more than 100 characters.
4. Multiple-Choice: charAt Behavior.
How does our MyString.charAt() implementation differ from Java’s built-in String.charAt()?
  • Our version allows negative indices, while Java’s doesn’t.
  • No. We also treat negative indices as invalid, returning '?' in that case.
  • Our version returns null for invalid indices, while Java’s returns '?'.
  • No. Our version returns '?' (not null); Java’s version throws an exception for invalid indices.
  • Our version returns '?' for invalid indices, whereas Java’s version throws a StringIndexOutOfBoundsException.
  • Correct! We chose '?' for simplicity; Java uses exceptions to handle errors.
  • The two implementations behave identically in every way.
  • No. They differ on how they handle out-of-range indices.
You have attempted of activities on this page.