Imagine you are building a text-based adventure game where players can type commands like "/quit" to leave, "/help" to get assistance, or "take sword" to pick up items. Your game logic needs to check if a player’s input matches these commands. Similarly, suppose you have an in-game list of players and you want to display it in alphabetical order: you need to compare names lexicographically. Both tasks—checking equality and ordering—rely on comparing strings.
This method checks if two strings contain exactly the same sequence of characters. The method signature might seem odd - why Object instead of String? This is because equals comes from the Object class that all Java classes inherit from. String overrides this method to provide string-specific comparison:
This method compares strings in dictionary (lexicographic) order. It comes from the Comparable<String> interface, which requires any comparable type to provide this method:
As a new Java student, you might wonder "Why do we care about Object and interfaces? Why not just compare strings directly?" These are excellent questions! For simplicity in our MyString class, we’ll implement simpler versions that mirror the logic without matching the exact signatures. This lets us focus on the core concepts of string comparison while avoiding some of Java’s more advanced features.
Now that you’ve seen how Java’s built-in methods work, it’s time to see how our MyString class can implement its own versions. This leads us to reviewing our data definition from Step 1 and then proceeding with Step 2 of the Design Recipe.
Before we specify how methods will work (Step 2), we must recall what data we are working with—this is the essence of Step 1. In the previous section, we created a simple class MyString to store up to 100 characters. Our data definition said each MyString has:
We already implemented basic inspection methods (length(), isEmpty(), charAt()) based on these data decisions. Here is a quick reminder of what the skeleton for MyString looks like, focusing on the relevant data fields and constructor:
public class MyString {
private char[] chars = new char[100];
private int usedLength;
public MyString(String source) {
if (source.length() > 100) {
usedLength = 100;
} else {
usedLength = source.length();
}
for (int i = 0; i < usedLength; i++) {
chars[i] = source.charAt(i);
}
}
// More methods (length, isEmpty, charAt...) go here
}
With these fundamentals in place, we are ready to design methods that compare our MyString objects with Java String objects for equality or ordering, following the Design Recipe’s Step 2.
Subsection5.3.4Common Pitfalls in String Comparison
In Java, strings are objects, not primitives. A variable of type String does not hold text directly; it holds a reference to a String object in memory. Consequently, the operator == compares whether two variables point to the same object rather than checking if they contain the same text. Beginners often write code like:
Remember, strings in Java are immutable and stored by reference. That means changing a string actually creates a new string object. This also means == checks reference identity, while .equals() checks the text content itself.
Next, we’ll use these lessons about .equals(...) vs == to help us define clear, bug-free comparison methods in our MyString class, starting with Step 2: writing the method signatures and purpose statements.
Step 2 of the Design Recipe asks us to write down exactly what our methods look like (signatures) and why they exist (purpose statements). This is where we decide:
The most direct way to capture this in Java is through JavaDoc comments. JavaDoc is a specialized comment style that begins with /** and uses tags like @param, @return, @throws. IDEs (like VS Code) automatically parse these comments and can show them when you hover over a method, making it easier for you—and your classmates or teammates—to recall what each method is for. JavaDoc can also generate a website of your documentation if you run the javadoc tool. For more details, see this Baeldung article on JavaDoc.
Let’s see how we might create meaningful JavaDoc for two methods: equals(String other) and compareTo(String other). We will build it up iteratively, to illustrate how we refine the content.
/**
* Checks if this MyString equals some text.
*/
public boolean equals(String other) {
// ...
return false;
}
While this is a start, it does not tell us how we check equality (case-sensitive?), what happens if other is null, or what "equals" means in detail. A student reading this might wonder, "Wait, do we check lengths? Are we ignoring uppercase differences?" Let’s refine it:
/**
* Checks if this MyString has exactly the same characters
* as the given String, in the same order.
* This comparison is case-sensitive.
* Example: equals("HELLO") vs equals("Hello") returns false
*
* @param other The String to compare;
* if null, the result is always false.
* @return true if both have identical characters
* and the same length, false otherwise
*/
public boolean equals(String other) {
// ...
return false;
}
Now anyone reading can see: (1) we compare character by character, (2) it is case-sensitive with a concrete example, and (3) we return false if other is null. This clarity helps us implement the method correctly and helps future maintainers or classmates understand exactly how it works.
Note: This is a simplified version that takes String other, not Object other. In real Java String, equals overrides public boolean equals(Object obj) from Object. We use String here so it’s easier for beginners to see what’s happening. Later courses will cover more advanced topics like method overriding and exception handling.
/**
* Compares this MyString to another.
*/
public int compareTo(String other) {
// ...
return 0;
}
But the student who encounters this might wonder, "How do we decide bigger or smaller? Are we ignoring uppercase vs. lowercase? What if the strings differ in length but share a common prefix?" Let’s refine:
/**
* Performs a lexicographical (dictionary) comparison
* between this MyString and another String.
* Case-sensitive by default.
* Example: "apple".compareTo("banana") returns negative
* "cat".compareTo("CAT") returns positive
*
* @param other The String to compare;
* if null, we may consider this MyString greater.
* @return a negative number if this < other,
* 0 if they are the same,
* and a positive number if this > other
*/
public int compareTo(String other) {
// ...
return 0;
}
This version clarifies that the ordering is case-sensitive with concrete examples, how we handle null, and how we interpret positive/negative return values—key details needed to use the method confidently in sorting logic.
Once our signatures and purpose statements (Step 2) are settled, it is easier to create test cases (Step 3). Let’s outline a few scenarios to confirm what we expect. Feel free to expand this table as you discover new edge cases during implementation!
Depends on ASCII code difference between ’c’ and ’C’
"apple"
"banana"
false
negative (since ’a’ < ’b’)
"hello"
null
false
positive (our design: treat null as "less")
"cat"
"cater"
false
negative (since "cat" is a prefix but shorter)
""
""
true
0 (empty strings are equal)
""
"a"
false
negative (empty string is less than any non-empty)
This table forms the basis of our tests. Whenever we implement or modify equals or compareTo, we should confirm they pass these examples. Now we are set to write a skeleton (Step 4) and then fill in the implementation (Step 5).
Before writing the complete code, it helps to sketch out the control flow in a skeleton. This ensures we know exactly which checks or loops we will write. For example:
public class MyString {
// Data & constructor from previous sections
/**
* Checks if this MyString matches 'other' exactly (case-sensitive).
*/
public boolean equals(String other) {
// 1) If other is null, return false
// 2) If other.length() != usedLength, return false
// 3) For i in [0..usedLength-1]:
// if chars[i] != other.charAt(i), return false
// 4) If all match, return true
return false; // skeleton placeholder
}
/**
* Lexicographically compares this MyString to 'other'.
*/
public int compareTo(String other) {
// 1) If other is null, decide 'this' is greater => return positive
// 2) Let minLen = the smaller of usedLength and other.length()
// 3) For i in [0..minLen-1]:
// if chars[i] != other.charAt(i), return difference
// 4) If all matched so far, return usedLength - other.length()
return 0; // skeleton placeholder
}
}
This blueprint is enough to show your instructor or peers and ask, "Does this logic look right?" If they spot a missing step, it is easier to correct in the skeleton stage than after fully coding.
Now let’s carefully code each comparison method. We will follow our skeleton, explaining each step and testing as we go. First, let’s do equals in isolation.
// Part 1: equals(String other) - with explanations:
/**
* Checks if this MyString matches 'other' exactly (case-sensitive).
* Example: equals("HELLO") vs equals("Hello") returns false
* @param other The String to compare (null means automatically false)
* @return true if lengths match and every character matches
*/
public boolean equals(String other) {
// Step A: Check null
if (other == null) {
// If 'other' is null, there's no way it equals 'this'
return false;
}
// Step B: Check length
if (other.length() != usedLength) {
// If their lengths differ, they can't be the same
return false;
}
// Step C: Check each character
for (int i = 0; i < usedLength; i++) {
if (chars[i] != other.charAt(i)) {
// If any character doesn't match, the strings differ
return false;
}
}
// Step D: If we pass all checks, they're equal
return true;
}
This step-by-step style ensures you understand exactly why each part is there: null checks, length checks, character comparison. After implementing each method, test it to verify it behaves correctly. If a test fails, this is your signal to enter the refinement phase:
public static void main(String[] args) {
MyString test = new MyString("cat");
// Test equals() with different cases - each test maps to a table row
System.out.println("Testing equals(\"cat\"): " + test.equals("cat")); // Row 1: exact match
System.out.println("Testing equals(\"Cat\"): " + test.equals("Cat")); // Row 2: case difference
// Implementation Notes:
// 1. Changes from original design:
// - None needed for equals() so far
// 2. Additional edge cases found:
// - Mixed case comparisons work as expected
// 3. Optimization opportunities:
// - Could add early exit on length mismatch
}
This helps confirm that our equality checks behave correctly before we move on to compareTo. If tests fail, we document the refinements made in our implementation notes.
// Part 2: compareTo(String other) - with explanations:
/**
* Lexicographically compares this MyString to 'other' (case-sensitive).
* If 'other' is null, we treat 'this' as greater.
* Example: "apple".compareTo("banana") returns negative
* "cat".compareTo("CAT") returns positive
*
* @param other The String to compare
* @return negative if this < other, 0 if equal, positive if this > other
*/
public int compareTo(String other) {
// Step A: Handle null
if (other == null) {
// We decided that null is considered "less"
// so 'this' is "greater"
return 1;
}
// Step B: Find the minimum length to compare
int minLen = Math.min(usedLength, other.length());
// Step C: Compare characters up to minLen
for (int i = 0; i < minLen; i++) {
int diff = chars[i] - other.charAt(i);
if (diff != 0) {
// If they're different, that difference decides
// whether 'this' is less or greater
return diff;
}
}
// Step D: If all matched so far, the shorter is considered less
// e.g. "cat" vs "cater"
return usedLength - other.length();
}
After implementing compareTo, we should test it thoroughly and document any refinements needed:
// In main, add tests for compareTo - each maps to a table row:
System.out.println("Testing compareTo(\"cat\"): " + test.compareTo("cat")); // Row 1: equal strings
System.out.println("Testing compareTo(\"cater\"): " + test.compareTo("cater")); // Row 5: prefix case
System.out.println("Testing compareTo(\"\"): " + test.compareTo("")); // Row 7: empty string case
// Implementation Notes:
// 1. Changes from original design:
// - Clarified that shorter strings are "less than" longer ones
// 2. Additional edge cases found:
// - Empty string comparisons need special handling
// 3. Future enhancements to consider:
// - Case-insensitive versions
// - Exception handling for errors
// - Using testing frameworks for more thorough validation
Remember: whenever tests reveal unexpected behavior or new edge cases, update your test table (Step 3) and skeleton (Step 4) to reflect these discoveries. This refinement process ensures your final implementation handles all cases correctly.
Here is a final version of our MyString class, complete with a main(...) method that tests equals and compareTo using the table we proposed in Step 3. The implementation includes documentation of refinements made during testing. Feel free to modify, experiment, or break it intentionally to see how each comparison behaves!
In this section, we focused on Step 2: Method Signatures & Purpose for string comparison, but we demonstrated how it interacts with all other steps in the Design Recipe:
Step 2 (Spotlight): Defining equals(String other) and compareTo(String other) with meaningful JavaDoc that clarifies how they behave, especially regarding null and case sensitivity.
Step 5: Implementing, testing, and refining each comparison method. When tests reveal new edge cases or unexpected behavior, we update our documentation and implementation notes to reflect these discoveries.
These steps help you avoid the most frequent mistake in Java string comparison: using == instead of .equals(...). They also clarify how to order strings, including what "less than" or "greater than" means. Finally, they highlight how a well-structured JavaDoc and implementation notes can guide everyone’s understanding and your IDE’s tooltips. As you build on what we’ve started, consider trying case-insensitive versions or exploring how to handle punctuation or Unicode comparisons. But for now, you’ve laid a strong foundation in comparing strings carefully and clearly, in a way that your peers can understand and rely on.