Experienced Java programmers will quickly recognize that the following is not how to check for String equality:
public class StringToConstantCompareDemo {
public static final String SOME_CONSTANT = "Hello";
public int compareStringToConstant(String input) {
if (input == SOME_CONSTANT) {
return 1;
} else {
return 0;
}
}
}
Instead this example checked if input is a reference to the same object as SOME_CONSTANT. Or as stated in the Java Language Specification:
“While == may be used to compare references of type String, such an equality test determines whether or not the two operands refer to the same String object. The result is false if the operands are distinct String objects, even if they contain the same sequence of characters. The contents of two strings s and t can be tested for equality by the method invocation s.equals(t).”
The code should have been:
public int compareStringToConstant(String input) {
if (SOME_CONSTANT.equals(input)) {
return 1;
} else {
return 0;
}
}
Okay, let’s say we know not to put an == for String comparison. We sometimes make mistakes though. One way to protect yourself from mistakes is to write unit tests.
What might a unit test for this function look like?
public class StringCompareTest {
@Test
public void stringIsEqualsToHello() {
StringToConstantCompareDemo demo = new StringToConstantCompareDemo();
Assert.assertEquals(1, demo.compareStringToConstant(
StringToConstantCompareDemo.SOME_CONSTANT));
}
}
If we ran this test, it would still pass with the == in the method. Why? Because we passed it a reference to SOME_CONSTANT. Let’s try again:
@Test
public void stringsAreEqual() {
StringToConstantCompareDemo demo = new StringToConstantCompareDemo();
Assert.assertEquals(1, demo.compareStringToConstant("Hello"));
}
Hmm…. it still passes. That seems a little odd. Let’s look at why.
During compilation, String constants of the same value are “interned”, or made to reference the same object in memory. See this section of the Java Language Specification for more details. This optimization helps preserve memory, but it foils the intent of our test case.
However, there’s one more thing we can try in our test case:
@Test
public void stringsAreEqual() {
StringToConstantCompareDemo demo = new StringToConstantCompareDemo();
Assert.assertEquals(1, demo.compareStringToConstant(new String("Hello")));
}
Now we’ve got it. With the == in the method, the test case fails and with .equals the test case passes.
The problem here is that if I knew about this behavior and thought to write new String(…) in my test case, I probably didn’t compare the String with == in the first place! Therefore, it’s unlikely that I would have ever found this bug with a unit test.
An alternative is to use a static code analyzer, such as PMD or FindBugs, which can locate this issue and many others. You shouldn’t expect a static analyzer to replace rigorous unit testing. They can only look for common programming mistakes that would be suspicious in any program. They cannot validate the logic of your code. However, unit testing and static analysis combined can detect a wider range of problems than either one alone.