Return to the lecture notes index

Lecture #9 (Monday, February 6, 2006)

References Review

We started class by looking at the following code fragment.


int i;
char c;
double d;
String s;

Kesden asked the class what i, c and d were, and the class correctly responded that these variables were primitive types. However, many people were unsure about s. The common mistake was that s was a String. Although this seems intuitive, s is actually what is called a reference, and effectively stores where the actual String object is found in memory.

Lets look briefly about how objects are stored in memory in Java. Unlike other languages like C, you do not have direct access to memory addresses. When you create objects, Java internally stores a list containing the addressses of those objects, like an array. The variable s is actually just an integer that specifies which address in the list points to the object you want. An advantage of this representation is that even as objects become extremely large, they can still be manipulated quickly and easily using references, which are very small.

Comparint References

Now lets reexamine the need for objects to have a .equals method. Given the following code...

int i1 = 4;
int i2 = 4;

String s1 = new String("a");
String s2 = new String("b");

The comparison i1==i2 will obviously be true, but the comparison s1==s2 will be false. This is because two different strings were created, and each is stored in a different location in memory. So each has its own listing in Java's internal memory tables. s1 and s2 are just the indices of their repective memory addresses. So of course, they're not the same numbers. The point is, when you compare s1 == s2 it is exactly the same as comparing i1 == i2 , Java never even looks at the actual objects themselves. It just compares the references.

This is why we need to implement .equals methods in all of our classes. The generic Object class that all other classes are implicitly derived from does contain its own .equals method which will be used if the derived class doesn't have its own. Generally, when we create a class, we want to override the old .equals method and replace it with one that works specifically for this class.

Lets take the String class as an example.



public class String{

  /// stuff
 
  public Boolean equals (String s){
    // check equality.
  }

  /// stuff
}


You can work out the actual code for this yourself, but essentially what we care about is that the two strings have the same length, and each contains all the same characters in the same positions.

However, lets talk about the .equals method shown above. We'll assume that if its given another String as an argument, it will fuction correctly. In most cases, you only want to be comparing two Strings for equality. It doesn't really make any sense to compare a String with a Cat or Dog. Using the method above, if the the method were called with another type of Object as an argument, Java would instead use the generic Object class's .equals, which has the correct type. This will always (correctly) return false, since a String can't possibly be equal to a Cat. However, you should consider the fact that if you were ever even trying to compare a String to a Cat, it would probably be a bug in your code. However, using the above equals method, it would never cause a runtime error or an exception to be thrown. However, while your debugging your code, actual run time erros can be your best friend, as they'll let you know exactly whats wrong with your code and where. So let's change the equals method to do something slightly more useful.


public boolean equals (Object o)
{
  String s = (String)o;
  // check equality
}

This equals method is generalized enough that you will never need to rely on the Object class's equals method. This method will immediately attempt to cast the given object to a String. If it fails, a ClassCastException will be thrown when you try to run your program, letting you know that you just tried to cast something that could not be casted into that specific type of Object, and it tells you where in your code you did it. This can be extremely helpful while debugging and could potentially save you a lot of time.

This style of programming is called defensive programming. If you can design your code such that errors will manifest themselves as run time exceptions, rather than just unusual behavior, you will not only be able to identify errors more easily, but also fix them faster.