15-100 Lecture 6 (Wednesday, January 28, 2008)

Files From Today's Class

Today's Example

In class today, we developed a simple example designed to reinforce what we already know about developing class specification in Java, as well as to introduce some more nomenclature, the toString() method, and a process for developing and testing class specs. We also talked a bit more about the nature of String objects.

We'll talk about toString() today, among otehr things, it enables println() to work upon objects

The Process

After working through the design of a class in my head and constructing informal notes describing its design, I often approach implementing the specification using a process designed to help me code and debug quickly. After discussing this process, we used it to develop today's example.

The process:

  1. Implement a "class skeleton". In other words, write the class specification, but leave out the instance and static variables, and static methods such as main(). Do not actually implement the methods -- leave the body, everything between the {-bracket and }-bracket empty. If the method returns an Object type, such as String, have it return null. If it returns a primitive type, have it return soemthing, something invalid if possible. This is just to get it to compile. We'll replace it with real code later.

    Compile to test

  2. Add the instance and static variable

    Compile to test

  3. Add the constructor or constructors and a main() method that calls each of them

    Compile and run to test -- no output expected, yet

  4. Implement the toString() method. This will enable real testing. In the main(), print out the newly initialized object(s).

    Compile and run to test -- begin checking output for correctness now.

  5. Implement the getter methods, those that just get properties. In the main(), gather the properties of one of the objects and print them out by hand. When run, compare the output to the earlier output using toString() -- they should match.

    Compile and run to test

  6. One at a time, or in very small groups, add the simplest "setter" methods, those that simply reassign a value based on arguments. Add code to call them and to print out the newly changed object. The output shold reflect the changd state.

    Compile and run to test

  7. Repeat with more complicated methods. After implementing each method, add code to the test driver to make sure it is correct. Be especially sure to check interesting cases.

    Compile and run to test

Nomenclature

Style

Notice that in our example today we follow certain stylistic conventions. Most are not enforced by Java, but are good programming practice.

The null Reference

In creating the method skeleton today, we needed to "fake out" return values in order to get things to compile before we actually implemented the method. When we did this for toString(), we could have returned, for example, an empty String, represented by "".

But, instead, we took it as an opportunity to introduce null. Remember that, given the declaration "String s", "s" is not a String. Instead, it is a reference to a String. But, what is "s" if there is no String object and, as a consequence, "s" can't identify one? In this case, "s" is said to be null. A nul reference is, in effect, an "empty" reference -- a reference to nothing. It is sometimes said to be a reference to a non-existent object. A null reference is symbolized by the Java keyword, null.

The toString() method

The toString(), like the equals() method we discussed last class, is inherited from the Object class. And, like the equals() method, we'll need to override it to get it to do the correct thing.

The toString() method simply reduces the important properties of an Object into a String. Sometimes this is called serialization. The toString() method takes no arguments as it needs none -- it is producing a representation of the Object as it already is. And, it, no surprise, returns a String.

As a "for example", the toString() method is what enables the System.out.println() method to work on object types. println() knows how to print the few, simple predefined primitive types. But, it doesn't know which are the important properties of arbitrary object types, not does it know how to format them. So, if println() is asked to print an Object type, it first calls toString() upon that type. It then prints the resulting String. Strings are the only Object type that println intrinsically knows how to print -- for the rest, it invokes toString() to get a String representation.

Immutability of Strings, Garbage Collection, and the Append Operator

In Java, Strings are immutable. Once created, they cannot be changed. Instead, we simply create a new String each time we would otherwise change an old one. This is okay, because Java is a garbage collected language. What that means is that unreferenced objects are automatically freed, so it isn't necessary for the programmer to clean up unused objects within the code. This makes it much more practical for various language features to implicitly create objects in ways that are not obctious.

Consider the following line of code:

  String s = "Hello" + " " + "World"
  

We begin with the String "Hello". The + operator asks it, the object referenced on the left, to create a new object that is just like itself, but with a " " appended. This does not change either "Hello" or " ". Instead it creates a new String "Hello ". '"Hello" + " "' is an expression that evaluates to a reference to this newly created object. The next part of the expression '+ "World"' asks for another String to be created, "Hello World" and the expresion evaluates to a reference to this newly created String. This reference is then assigned to the variable "s". The temporary String, "Hello ", is simply available for garabage collection.

The equals() method

Last class, we discussed the equals() method and when it should be used instead of the == operator. We made use of the equals() method defined upon the String class. We also discussed the fact that, although it is inherited form the Object class, it needs to be overridden within each individual class.

Today, we implemente dour own equals() method for the first time. We did it using what I like to call the "Laundry list" idiom. The basic idea is that we compare ach property of the two instances, one at a time. If we encounter a property that doesn't match, we immediately return false and are done. If, after comparing each property, one at a time, we don't find any mismatch, we return true -- the objects are equivalent if we cannot distinguish them.

In implementing this method, we observed that the signature that we inherit is "public boolean equals(Object)" and, so, we must keep exactly this signature to overload the method. If we create a method with the more specific signature, "public boolean equals(BusinessCard)", it will actually work in most cases, but won't actually replace the original. Instead, it will suplement it.

If we don't overload the method, and instead pass in a "BusinessCard" rather than an Object, it will work in the case where we are asked to compare two BusinessCards. But, in the case where the programmer makes a mistake and tried soemthing like, "gregsCard.equals(elephant)", the the new method won't match -- but the inherited equals() method will. Since it just does an ==, it will always return false. This will likely result in defective code that continues to run for a while, making debugging difficult.

So, instead, we keep the original method, but use a "type cast" to implement runtime type checking as follows:

  public boolean equals(Object o) {
    BusinessCard bc = (BusinessCard) o;

    ...
  }
  
Notice that we "cast" the Object reference to a BusinessCard reference by placing the "(BusinessCard)" annotation before it. This basically says, "Java, I promise you this Object is a BusinessCard. For right now, treat the reference like a BusinessCard reference". This works because, although an Object is not necessarily a BusinessCard -- it could be a Train, Plane, Automobile, or String -- it could be a BusinessCard as a BusinessCard, like any other Object we create is a type of Object.

If, at runtime, we haven't told any lies, this code will run just fine. But, if it doesn't actually turn out to be a BusinessCard, java will call the bluff -- and "throw a ClassCastException". In other words, it will die with a runtime error.

The source code from today's example, linked at the top, or included at the very bottom of this page, shows this method fully fleshed out.

The Class Skeleton

class BusinessCard {

  public String getName() { return ""; }

  public String getEmail() { return ""; }

  public int getYearsOfService() { return -1; }

  public void incrementYearsOfService() { }

  public void changeEmail(String email) { }

  public String toString() { return ""; }

  public boolean equals (Object o) { return false; }
}
  

The Complete Example

class BusinessCard {

  private String name;
  private String email;
  private int yearsOfService;

  public BusinessCard(String name, String email, int yearsOfService) {
    this.name = name;
    this.email = email;
    this.yearsOfService = yearsOfService;
  } 

  public String getName() {
    return name;
  }

  public String getEmail() {
    return email;
  }

  public int getYearsOfService() {
    return yearsOfService;
  }

  public void incrementYearsOfService() {
    yearsOfService = yearsOfService + 1;
  }

  public void changeEmail(String email) {
    this.email = email;
  }

  public String toString() {
    return name + ", " + email + ", " + yearsOfService + "yrs";
  }

  public boolean equals (Object o) {
    BusinessCard bc = (BusinessCard) o;

    if (!this.name.equals(bc.name))
      return false;

    if (!this.email.equals(bc.email))
      return false;

    if (this.yearsOfService != bc.yearsOfService)
      return false;

    return true;
  }


  public static void main (String[] args) {
    BusinessCard gregCard = new BusinessCard ("Gregory Kesden", 
                                              "gkesden@cs.cmu.edu", 10);

    System.out.println (gregCard);
    System.out.println (gregCard.getName() + ", " + gregCard.getEmail() + ", " +
                        gregCard.getYearsOfService() + "yrs");

    gregCard.incrementYearsOfService();
    gregCard.changeEmail("gkesden@andrew.cmu.edu");
    System.out.println (gregCard);

    
    BusinessCard gregClone= new BusinessCard ("Gregory Kesden", 
                                              "gkesden@andrew.cmu.edu", 11);

    if (gregCard.equals(gregClone))
      System.out.println ("The two cards (gregCard,gregClone)  are equivalent");
    else
      System.out.println ("The two cards (gregCard,gregClone) are NOT equivalent");

    BusinessCard gregYear= new BusinessCard ("Gregory Kesden", 
                                              "gkesden@andrew.cmu.edu", 10);
    if (gregCard.equals(gregYear))
      System.out.println ("The two cards (gregCard,gregYear)  are equivalent");
    else
      System.out.println ("The two cards (gregCard,gregYear) are NOT equivalent");

    BusinessCard gregEmail= new BusinessCard ("Gregory Kesden", 
                                              "gkesden@cs.cmu.edu", 11);
    if (gregCard.equals(gregEmail))
      System.out.println ("The two cards (gregCard,gregEmail)  are equivalent");
    else
      System.out.println ("The two cards (gregCard,gregEmail) are NOT equivalent");
  }
  

 
}