Skip to main content

Section 5.3 String Comparison Methods

Subsection 5.3.1 Introduction & Motivation

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.

Subsection 5.3.2 String Comparison Methods

The Java String class provides two essential methods for comparing strings. Let’s explore how they work and why they’re designed this way:
Key String Comparison Methods:
equals(Object other)
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:
String command = "quit";
if (command.equals("/quit")) {  // returns false
    System.out.println("Exiting game...");
}
compareTo(String other)
This method compares strings in dictionary (lexicographic) order. It comes from the Comparable<String> interface, which requires any comparable type to provide this method:
String player1 = "Alice";
String player2 = "Bob";
if (player1.compareTo(player2) < 0) {  // returns negative number
    System.out.println("Alice comes before Bob");
}
The method returns:
• A negative number if the first string comes before the second
• Zero if the strings are equal
• A positive number if the first string comes after the second
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.

Subsection 5.3.3 Step 1: Data Definition (Review)

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:
  • A char[] chars of length 100
  • An integer usedLength that tracks how many positions in that array are actually in use
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.

Subsection 5.3.4 Common 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:
String s1 = "Hello";
String s2 = new String("Hello");

if (s1 == s2) {
    // ...
}
Because s1 and s2 are different objects, (s1 == s2) is false. But if you wrote:
if (s1.equals(s2)) {
    // This would be true, since both contain "Hello"
}
you’d get the correct result for text comparison.
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.

Subsection 5.3.5 Step 2: Method Signatures & 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:
  • Parameter types: For instance, should we compare MyString to another MyString or to a standard Java String? We will choose String here for clarity.
  • Return types: Should we return boolean or int? That depends on what question we want answered. Equality needs a boolean; ordering needs an int.
  • Behavior & purpose: What exactly happens if other is null or shorter/longer? How do we decide "less than" or "greater than"? Are we case-sensitive?
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.

Subsection 5.3.6 Building JavaDoc in Stages: equals(String other)

Suppose we initially write something minimal:
/**
 * 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.

Subsection 5.3.7 Building JavaDoc in Stages: compareTo(String other)

Next, we do the same for ordering. We initially might just say:
/**
 * 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.

Subsection 5.3.8 Step 3: Examples & Tests

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!
Table 5.3.1. Test Table for equals(...) & compareTo(...)
MyString (this) String (other) equals(...)? compareTo(...) result
"cat" "cat" true 0
"cat" "Cat" false 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).

Subsection 5.3.9 Step 4: Skeleton / Method Template

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.

Subsection 5.3.10 Step 5: Method Implementation (with Explanations)

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.

Subsubsection 5.3.10.1 Implementing equals(String other)

// 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:
  • Review your test case assumptions - did you expect the wrong output?
  • Check your skeleton logic - did you miss a case?
  • Consider if you’ve discovered a new edge case not in your original test table
Here’s how we might test equals(...) immediately in a minimal main method:
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.

Subsubsection 5.3.10.2 Implementing compareTo(String other)

// 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.

Subsection 5.3.11 A Complete Runnable Example

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!

Subsection 5.3.12 Summary & Reflection

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 0: Recognizing the need to compare strings for equality or ordering in a project scenario (e.g., commands, sorting, filters).
  • Step 1: Building or recalling the data definition of MyString (a char[] plus usedLength).
  • 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 3: Writing a small table of test cases—critical for guiding our eventual code and verifying it later.
  • Step 4: Creating a skeleton outline so we know exactly which checks or loops we need before coding in detail.
  • 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.

Subsection 5.3.13 Check Your Understanding

Exercises Exercises

1. Multiple-Choice: equals(...) vs. ==.
In Java, what is the primary distinction between using == and .equals(...) when comparing two strings?
  • == checks if the two strings contain the same characters, while .equals checks if they are the same object in memory.
  • No, it’s actually the opposite: == checks for the same object reference, and .equals checks text content.
  • == checks if both string variables refer to the same object, whereas .equals checks if the strings contain identical text.
  • Correct! == is reference comparison, and .equals is content comparison.
  • They are interchangeable for all Java String objects—there is no difference.
  • No. Using == will often fail unexpectedly because it doesn’t compare text content.
  • In Java, == always throws an exception when used on strings, so .equals is the only legal comparison.
  • No. == is legal syntactically, but compares references, not text.
2. Multiple-Choice: compareTo(...) Outcomes.
Suppose str1.compareTo(str2) returns a positive number. Which statement best describes what that means?
  • str1 is considered "greater" than str2 in lexicographical order.
  • Correct! A positive result means str1 sorts after str2 alphabetically.
  • They contain the same content, so the method returns a positive number to indicate "equal."
  • No. If they’re equal, compareTo should return 0.
  • It means str1 is shorter than str2.
  • No. Shorter or longer by itself is not the entire story; you must compare character by character.
  • They must differ only in case (e.g., "Cat" vs "cat"), so compareTo returns positive when ignoring case differences.
  • No. compareTo is case-sensitive, and a positive result can happen for many reasons, not just case.
3. Multiple-Choice: Handling null in compareTo(...).
How does our MyString.compareTo(String other) implementation handle the situation when other is null?
  • It throws a NullPointerException immediately.
  • No. Our implementation explicitly checks for null and returns a positive value, not an exception.
  • It treats null as "less than" any valid string, returning a positive integer from the compareTo method.
  • Correct! The logic says if other is null, we consider the current MyString to be greater.
  • It automatically converts null to an empty string and compares accordingly.
  • No. We do not treat null as an empty string in the provided code snippet.
  • It returns 0, implying they are equal if other is null.
  • No. We explicitly decided to treat null as less (hence this is greater), so we return a positive integer, not zero.
4. Multiple-Choice: Data Definition & Comparison.
Why is a clear data definition (as in Step 1 of the Design Recipe) essential for building correct equals(...) and compareTo(...) methods in MyString?
  • Because we need to override Object.equals(...) precisely as Java does.
  • No. While overriding Object.equals is a valid design, Step 1 is about defining how data is stored and accessed, not necessarily about overriding.
  • It’s not important; we can guess how to compare characters on-the-fly.
  • No. Lacking a clear data definition leads to buggy implementations (e.g., forgetting length checks or ignoring null).
  • It specifies exactly how characters and length are stored, which makes it clear when two strings are "equal" and how to iterate for comparisons.
  • Exactly. Knowing how usedLength and chars are managed clarifies both equality and ordering logic.
  • It only matters for methods like length() or charAt(), but has no bearing on equals or compareTo.
  • Data definition is the foundation for all methods—especially comparison methods that rely heavily on character-by-character checks.
5. Short-Answer: JavaDoc & Clarity.
In your own words, why is it useful to write detailed JavaDoc for equals(...) and compareTo(...) before coding them?
Answer.
You have attempted of activities on this page.