Return to lecture notes index

15-110 Lecture 9 (Monday, July 13, 2010)

Examples

After the discussion below, we developed the following examples:

Interfaces

So far, we've made use of "class reference variables". These reference variables identify an object -- and are limited to identifying objects defined by a specific class. We can also make use of "interface reference variables". These variables are limited to containing references to objects that implement a specific interface, regardless of the class.

So, consider the following:


  CalculatorInterface c = new SimpleCalculator();
  

Notice that, above, we have a "CalculatorInterface" reference identifying a SimpleCalculator. This is useful because it allows us to write code that manipulates Objects, without unnecessarily limiting their types. In the case above, we can implement a whole bunch of Calculators by referring to them as CalculatorInterface without restricting their methods just to a single class object. In this case, we can create a new calculator like a financial calculator which will implement a whole new range of methods but still guarantee the interface methods. Let's go ahead and write the financial calculator and took a more in depth look at this later.

Extends

Now we want to write a new calculator to do some basic financial calculations, however we've already written a simple calculator that can perform all the basic operations that we will still need in our financial calculator as well. There's a way we can implement this fairly easily.

  class FinancialCalculator extends SimpleCalculator {

  public static void main (String[] args) {
   
     FinancialCalculator sc = new FinancialCalculator();
     
     sc.setNumber (10);
     sc.requestOperation('+');
     sc.setNumber (5);
     System.out.println (sc.requestOperation('='));
   
    }
  }
  

Notice that we can still call all the methods we wrote in SimpleCalculator. This is because we added those extra two words in the class definition "extends SimpleCalculator" This means that FinancialCalculator is also a SimpleCalculator and has inherited all of the simple calculator methods.

Let's go ahead an add an addTax method in the FinancialCalculator:

  class FinancialCalculator extends SimpleCalculator {

  public double addTax (double taxRate) {
  
    accumulator = accumulator * (1.0 + taxRate);
  
    return accumulator;
  }


  public static void main (String[] args) {  
     FinancialCalculator sc = new FinancialCalculator();
     
     sc.setNumber (10);
     sc.requestOperation('+');
     sc.setNumber (5);
     System.out.println (sc.requestOperation('='));
     
     sc.addTax (0.05);
     System.out.println (sc.requestOperation('='));
     
   
    }



  }
  

CalculatorInterface vs SimpleCalculator vs FinancialCalculator

So we learned earlier that we can create a FinancialCalculator or SimpleCalculator and store it with a CalculatorInterface reference variable. We can do the same thing with FinancialCalculator. This is because FinancialCalculator also inherited all the CalculatorInterface required methods from SimpleCalculator. Similarly we can also make a FinancialCalculator and store it with a SimpleCalculator reference variable. Here they are in code:


  CalculatorInterface c1 = new SimpleCalculator();
  SimpleCalculator c2 = new SimpleCalculator();
  CalculatorInterface fc1 = new FinancialCalculator();
  SimpleCalculator fc2 = new FinancialCalculator();
  FinancialCalculator fc3 = new FinancialCalculator();
  

Now let's take a look at the operations we can perform with each of these calculators. First off, let's take a look at the simple calculators. Well c2 is just like what we implemented in the main method of our original simple calculator class, and it can perform all the simple calculator operations. c2, however, is referenced as a CalculatorInterace and so the only methods it is guaranteed to have are those specified by the interface (getValue(), setNumber(), and requestOperation()). In other words you can not call compute() on c2.

fc1, fc2, and fc3 act similarly. fc3 can perform all the operations including addTax(). fc2, however, can only implement all the methods specified by SimpleCalculator and fc1 only those in the interface.

The Comparable Interface

Java defines an interface, Comparable, as follows:

  interface Comparable {
    public int compareTo(Object o);
  }
  

This interface provides a uniform way to Compare implementing Objects, known as Comparables. An Object implements Comparable by doing the following:

Let's take a look at an example compareTo method for our Apple class. We'll compare them by their weight.

  public int compareTo(Object o) {
    Apple a  = (Apple) o;
    
    int difference = this.color.compareTo(a.color);
  
    if (difference != 0)
      return difference;
      
    return (this.weigthOz - a.weightOz);
  
  } 
  

Natural Ordering

Remember from our prior discussions that natural ordering is a pretty way of saying, "ordered using the compareTo() method of a Comparable." If we call the version of the sort method that does not make use of a Comparator, it makes use of the fact that the Objects are Comparable and invokes the compareTo() method.


Please note...Comparable vs Comparator.

A Comparable object is one that can compare itself to another instance of the same class. It is this ability that makes the object Comparable.

A Comparator is an object that compares other things. It, itself, is not (necessarily) Comparable, but it does that the ability to compare other objects. Comparator objects are, in essence, function objects.

Comparables only take one argument, the object to compare to themselves. Comparators take two arguments, as they compare them to each other. The Comparator, itself, is not being compared.


Remember, there is only one natural ordering for any particular type of Comparable object, the one defined by the compareTo() method. n some sense, this is the primary ordering. Next, we'll talk about how to sort things in other ways.

Comparators

What happens if we want to order objects in different ways at different times? For example we want to manage student records, in the common case, by keeping them alphabetic by name. But, sometimes, we want to sort them by GPA -- for example, to send out "naughty and nice" letters at the end of each semester? Or, by expected graduation date, to prioritize advising appointments? This is made difficult if the only way to sort objects is via the single, unchangeable, intrinsic "natural ordering" defined by their compareTo() method.

Well, this is where Comparators enter the picture. The Comparator interface defines a single method, compare(). Well it also defines equals(), but we'll talk about that in a second -- it operates upon the Comparator, itself, not other Objects.

interface Comparator  {
  
  public int compare (T o1, T o2);
  public boolean equals (Object o);
}

Or, if you find its pre-generics version easier to read:

interface Comparator {
  
  public int compare (Object o1, Object o2);
  public boolean equals (Object o);
}

The compare() method does exactly what you think it does. It works just like the compareTo() method of a Comparable -- except that it accepts both operands as arguments, rather than only one.

The idea here is that we can define multiple Comparators for a single object type. Then, we can supply which ever one we want, whenever we want. Notice the other version fo the sort(...) method: The one that takes the Comparator as the second argument. We can pass any Comparator we'd like in there, and off we go.

Of course, if the particular Comparator that you pass in doesn't match the type of the array you pass in -- you'll blow up at runtime.

It is important to note that a Compatator's compareTo() method, unlike a compareTo() method does not have access to an instance's private members. Remember, the compareTo() method is actually defined by the class of Object upon which is operating. The Comparable is usually an entirely separate class of object -- and, as such, has no access to another class of object's private members.

You could, of course, define a class such that it implements both the Comparable and Comparator interfaces and then pass it in as its own Comparator. But since there can only be one method with the matching compare() signature, this would give you only a second way to compare -- and would be very confusing to any reader.

It is perfectly reasonable to implement the Comparable interface with a compareTo() method that works one way, and one or more Comaprable classes with compare() methods that work differently. In fact, this is the beautify of the system.

Quick Example

Imagine some class, such as the one below:

class Student {
  private String name;
  private double gpa;

  public String getName() {
    return name;
  }

  public double getGpa() {
    return gpa;
  }

  // Blah, blah, blah
}

We could implement two different Comparators, as follows.

class NameComparator implements Comparator {
  public int compare (Object o1, Object o2) {
    Student s1 = (Student) o1;
    Student s2 = (Student) o2;

    return (s1.getName().compareTo(s2.getName());
  }
}

class GpaComparator implements Comparator {
  public int compare (Object o1, Object o2) {
    double gpa1 = ((Student)o1).getGpa();
    double gpa2 = ((Student)o2).getGpa();

    if (gpa1 == gpa2) return 0;
    if (gpa1 > gpa2) return 1;
    else return -1;
  }
}