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.
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.
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.
Subsubsection5.2.3.1Attempt #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.
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.
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.
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.
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.
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.
Subsection5.2.6Step 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.
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.
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:
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:
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.
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.
Subsection5.2.9Putting 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
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:
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.
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.