Computer Science 212 - Tutorial 5

Objects and encapsulation

Copyright © 2005 by Dorothy L. Nixon. All rights reserved.



Table of contents

  1. What if an array is too small?
  2. Encapsulating an array - class StringBuffer as an example
  3. More about passing object references as parameters to a method
  4. Encapsulating a partially-filled, growable array of short integers
  5. Another example of a class which defines an object
  6. The equals methods of classes String, Time, and ShortSequence
  7. A very brief, very oversimplified introduction to hash codes
  8. More about exceptions



Example files (download as ZIP file here)

The example files may be downloaded in a ZIP file here.


Tutorial on objects and encapsulation


  1. What if an array is too small?
  2. Try running SortingDemoGUI1.java with the data file numbers4.txt, as follows:

    java SortingDemoGUI1 numbers.txt
    

    The bottom text field displays a message saying, "Too many numbers in file. Exceeds capacity 40."

    It would be highly desirable to be able to expand an array as necessary to accommodate a large set of input data. It would be desiarable for a program to be able to store large amounts of data without having to create huge arrays right from the start, so that the program doesn't eat up huge amounts of memory when it happens to be processing only small amounts of data.

    Unfortunately, once an array has been created with a particular length, it is not possible to make the array bigger.

    But we can replace a too-small array with a new and bigger array, as is done in SortingDemo2.java and SortingDemoGUI2.java. Compile this program and run it with numbers4.txt. Observe that it does NOT complain about the file being too big. If the original array turns out to be too small, a new array is allocated. The contents of the old array are copied to the new array, which then replaces the old array.

    Replacing one array with another array is known as reallocating the array. In this program, the reallocation is done as follows:

          if ( lengthFilled >= numbers.length )
          {
             short[] temp = new short[numbers.length
                                      + lengthFilled + 2];
             for (int i = 0; i < numbers.length; i++)
                temp[i] = numbers[i];
             numbers = temp;
          }  // if
    

    The reallocation involves three steps:

    1. A new array, larger than numbers, is created and assigned to a temporary array reference variable temp. (The details of how the new array size is computed are unimportant, as long as the new array is guaranteed to be bigger than the old array. It should be bigger enough that subsequent reallocations will not need to happen very often. At the same time, it shouldn't be so much bigger as to eat up vast amounts of memory unnecessarily.) The array reference temp is temporary because its scope ends at the end of the if block. Thus we say that temp will "go out of scope" once the if block is finished executing.
    2. After the new array pointed to by temp has been created, all the elements in array numbers are copied to array temp. Or, more precisely, the elements are copied from the array pointed to by array reference variable numbers to the array pointed to by array reference variable temp.
    3. Then, the array reference variable numbers is reassigned to point to the new array, the same array to which temp points. Thus, although the array reference temp goes out of scope at the end of the if block, the array it was pointing to will now be pointed to by array reference numbers, which will not go out of scope at that point. Furthermore, the remainder of the program will now treat the new array pointed to by numbers as if it were the old array. Thus, the array pointed to by numbers has effectively grown.

    After the array reference variable numbers is reassigned to the new array, then the old, smaller array no longer has any reference pointing to it. Thus the program no longer has any way to access the old array. As far as the program is concerned, the old array has been forgotten about.

    What happens to old arrays and other old objects that are forgotten about? The Java Virtual Machine automatically marks them for garbage collection. Garbage collection means that the object's region in the computer's memory is deallocated, so that the memory formerly used to store it can now be used for other things. When an old object is forgotten about and marked for garbage collection, it is not garbage-collected immediately. However, garbage collection is guaranteed to occur before the program uses up all the available memory.

    This is a big difference between Java and C++. In C++, you must explicitly deallocate any dynamically allocated region of memory before you make it inaccessible by removing the last pointer to it, or before the last pointer or reference to it goes out of scope. Otherwise you have what is known as a "memory leak" - the memory is simply lost to your program, and if it happens enough time with enough memory, the program will crash due to a lack of memory.

    But in Java, you don't need to worry about such details. The Java virtual machine automatically takes care of deallocation for you.


  3. Encapsulating an array - class StringBuffer as an example
  4. Nearly all programs of any significant size use arrays to store data, and it is usually not known in advance how many data items there will be of a given kind. Thus, partially-filled arrays are very common.

    For all these partially-filled arrays, it is necessary to keep track of how many elements are stored in them, and it is often necessary reallocate the array. It would be quite tedious if we had to write the code to take care of all these details every time we want to use a partially-filled array. It would be much more convenient to use objects that contain the arrays and automatically take care of the tedious details themselves.

    There are many classes of this kind in the Java library. One example is class StringBuffer, which implements a growable array of characters.

    The Java library has several classes that contain and manage arrays of characters. The best-known is our old friend class String. However, String objects are immutable. Once a String object has been created, it cannot be changed. If you look at the documentation for the String in the java.lang package, you will see that class String has many instance methods to give us information about the String objects for which they are called, or to generate a new String from an old String (e.g. the substring methods), or to compare one String object with another. But it does not have any methods that change, in any way, the sequence of characters stored inside the String object for which they are called.

    Look now at the documentation for class StringBuffer, also in the java.lang package. Class StringBuffer implements a mutable (changeable) sequence of characters. It has many instance methods for appending, inserting, and deleting characters inside the StringBuffer object. It also has an ensureCapacity method which replaces the StringBuffer object's internal array of characters with a new and bigger array. This method is called automatically whenever the StringBuffer object needs to grow, e.g. whenever you try to append or insert more characters than will fit in the current internal array.

    When a program needs to edit text, a StringBuffer object is much more convenient to use than a raw array of characters. By using StringBuffer objects, we save ourselves the trouble of writing code to keep track of the number of stored characters and reallocate the array. A StringBuffer object encapsulate a partially-filled array of charactersand takes care of all the tedious details itself.

    One of the advantages of organizing programs into methods and classes is reusability. Once a class or method has been written, it can be used again and again in many different programs. Furthermore, if a class is written to define objects, then you can use multiple objects of the same class in one program.

    The Java compiler automatically uses StringBuffer objects, behind the scenes, to implement the string concatenation (+) operator. For example, the following expression:

       text1 = text2 + text3
    

    is interpreted by the compiler as something like (though not exactly) the following:

       StringBuffer sb = new StringBuffer();
       sb.append(text2);
       sb.append(text3);
       text1 = sb.toString();
    


  5. More about passing object references as parameters to a method
  6. Because a StringBuffer object is mutable (changeable) in general, its internal state can be changed in (among other places) a method to which the StringBuffer object has been passed as a parameter, as exemplified in ReferenceParametersDemo.java. This program contains two methods demonstrating the effects of passing object references as parameters to methods. When you run the program, observe that the first method, modifyObject, changes the StringBuffer in a way that is detected in the main method after modifyObject is finished executing. On the other hand, the second method, modifyReference, does NOT change the state of the object that has been passed to it as a parameter. Rather, it makes changes to a different object to which the parameter has been reassigned. After the modifyReference method is finished executing, these changes cannot be detected by the final System.out.println statement in the main method.

    Method modifyObject demonstrates changing the state of an object that has been passed to the method as a parameter. In this case, the object is a StringBuffer, and its state is changed by inserting more characters. This change can be detected both inside the method and outside the method, after it has been called.

    On the other hand, method modifyReference demonstrates changing the value of a reference to an object that has been passed in as a parameter. The reference is assigned to a new object, as follows:

          sb = new StringBuffer(sb.toString());
    

    The above statement creates a new StringBuffer which is an exact copy of the old StringBuffer, but a distinct object, i.e. it resides in a different area in memory. Then, when the formal parameter sb is used subsequently within the method, it now refers to the new StringBuffer object, rather to the original StringBuffer which was passed in as an actual parameter. Hence, when sb is modified via a call to its insert method, the changes affect only the new StringBuffer object, not the old one. The original StringBuffer object remains unaffected by the call to insert. Hence this change is NOT detected outside the method, after the method has been called.

    Run the program again and observe that its behavior is consistent with the above.


  7. Encapsulating a partially-filled, growable array of short integers
  8. Consider now a class ShortSequence which encapsulates an array of short integers in much the same way that class StringBuffer encapsulates an array of characters.

    An object of class ShortSequence needs to store, inside itself, two data items: (1) an array of short integers and (2) a separate integer variable to keep track of the length of the meaningfully-defined portion, i.e. how many numbers are in the array. Furthermore, a ShortSequence object needs to maintain the array and the separate int variable DURING THE ENTIRE LIFETIME OF THE OBJECT, from the time it is instantiated until the time it is garbage-collected.

    These two data items need to be stored as instance variables, not just local variables or parameters. (Recall that local variables are declared inside a method and formal parameters are declared inside the parentheses of a method heading.) Local variables and formal parameters bothe are names of memory locations that are reserved only as long as the method is being executed. When a method is finished executing, its local variables and formal parameters are deallocated, i.e. the memory locations they name are no longer reserved for them. Hence, to store an ShortSequence object's array and keep track of the number of meaningfully-defined elements in the array, we can't use either a local variable or a formal parameter. We need another kind of variable, one that will continue exist exist not only when a method is being executed, but also in between method calls as well. For that purpose, we need instance variables. An instance variable belongs to an object, whereas local variables and formal parameters belong to a method only.

    Look now at ShortSequence.java, a class whose sole to purpose is define objects. (It does not define a main method and thus is not a program. Nor does it define any other static methods.) It defines a constructor and instance methods. In addition, it declares two variables just under the class heading, OUTSIDE of any method:

       private short[] numbers;
       private int lengthFilled = 0;
    

    Variables declared within a class but outside of any method are known as fields. There are two kinds of fields: instance variables and static variables. For now, we will focus on instance variables, of which the fields in class ShortSequence are examples. You will learn about static variables in a later lab assignment.

    Recall that most other program statements must occur within a method. Field declarations, optionally combined with assignments, are among the few kinds of statements that do not occur inside a method. All variables must be declared inside a class. (Note to those who have studied other programming languages: There is no such thing as a global variable in Java.)

    Instance variables can be accessed inside any instance method of the class, whereas local variables (declared inside a method body) and formal parameters (declared inside the parentheses of a method heading) can be accessed only within the method in which they are declared.

    Instance variables define how an object stores its data. Instance variables can store data continuously throughout an object's entire lifetime, from the time it is instantiated until the time it is garbage-collected. In contrast, local variables and formal parameters can store data only while a method is being executed. When the method is finished being executed, those variables are forgotten about.

    If a program has instantiated multiple objects of a class, then each object has its own version of the instance variables. For example, if a program has instantiated three ShortSequence objects, the three objects can store three different sequences of integers. Thus, each object contains its own distinct array of integer data, and, hence, each object also has its own value of lengthFilled, the variable which keeps track of how many elements are stored in the array. One ShortSequence object's value of lengthFilled may be different from another ShortSequence object's value of lengthFilled. The three objects occupy three distinct areas in memory, and each of those three areas in memory contains its own distinct memory locations referred to by the variable names numbers and lengthFilled.

    Observe the keyword private. Instance variables are normally declared private so that they cannot be accessed outside the class. If the instance variable lengthFilled were not declared private, then another class could access -- and change -- the lengthFilled variable of a newly-created ShortSequence object as follows:

       ShortSequence sequence = new ShortSequence(10);
       sequence.lengthFilled = 5;
    

    Note the syntax:   a non-private instance variable can be accessed as follows:

       objectReference.variableName
    

    The syntax is similar to a call to an instance method, but without the parentheses. When you forget the parentheses on a parameterless method call (e.g. for the length method of class String), the compiler will complain that you are trying to access the method as if it were an instance variable. Similarly, if you inadvertantly do include parentheses for an instance variable, the compiler will complain that you are trying to access the variable as if it were a method.

    Because the lengthFilled variable is declared private, it CANNOT be accessed in the above-described manner. If it were were allowed to be accessed from outside the class, this would defeat the whole point of defining a lengthFilled variable for each ShortSequence object in the first place. The whole point is to keep track of how many elements are stored. But an ShortSequence object's lengthFilled variable cannot keep track reliably if its value can be arbitrarily reset by other classes.

    In general, to ensure that an object can perform its own tasks reliably, instance variables are usually declared private, to prevent other classes from messing them up.

    The practice of declaring instance variables private, allowing them to be accessed only by methods of the class in which they are declared, is known as encapsulation. In a properly encapsulated object, the values of the instance variables cannot be changed by other classes except indirectly, via methods which ensure that only appropriate changes are made.

    Encapsulation of objects helps make programs much easier to debug. If one class in a program isn't working properly, encapsulation minimizes the bug's impact on other classes in the program and makes it much easier for the programmer to track down the class containing the bug. Without encapsulation, very large programs would be literally impossible to debug in any reasonable amount of time.

    Encapsulation is most helpful when the public methods of a class restrict the ways that the instance variables can be changed and generate programmer-friendly error messages when the object is misused in a way that would result in inappropriate values being given to the instance variables. As we will see, the public methods of class ShortSequence do generate such error messages.

    Look now at the constructor:

    public ShortSequence(int initialCapacity)
    {
       if ( initialCapacity < 0 )
          throw new IllegalArgumentException(
                             "Error: ShortSequence initial capacity "
                             + initialCapacity
                             + ", must be >= 0.");
    
       numbers = new short[initialCapacity];
    }  // constructor ShortSequence(int)
    

    The constructor is a special method which instantiates (creates) a new object of the class. It can be called as follows, for example:

       ShortSequence sequence = new ShortSequence(expectedLengthOfFile);
    

    A constructor always has the same name as the class. A constructor's heading differs from the headings of all other methods, in that it does not specify the type of a returned value (or contain the word void). That is because the value generated by a call to a constructor is always a new object of the class for which it is a constructor. Also, unlike an ordinary method which returns a value, a constructor does not contain a return statement.

    The constructor of class ShortSequence takes a parameter specifying the initial capacity, which must be greater than or equal to zero. If it is less than zero, the constructor throws an IllegalArgumentException.

    If the program has not terminated, then the constructor creates a new array with the specified capacity and assigns it to the instance variable numbers.

    The name of a constructor must be EXACTLY the same as the name of the class. If it is misspelled (e.g., in our present example, by not capitalizing the second 's' in ShortSequence), you will get a syntax error message like the following:

    ShortSequence.java:25: invalid method declaration; return type required
       public ShortSequence(int initialCapacity)
              ^
    1 error
    

    If the name of the constructor is not exactly the same as the name of the class, then the compiler will not recognize it as a constructor and, instead, will think your intended constructor is just another method. Therefore, the compiler will think its heading should contain the type of a return value (or void), just like any other method. If you ever get the above error message for a constructor, do not insert a return type. Instead, check the spelling of the name of the constructor, and make sure it exactly matches the name of the class.

    Look now at the instance methods. The first one is getLengthFilled:

       public int getLengthFilled()  { return lengthFilled; }
    

    This method takes no parameters and does nothing except return the value of the instance variable lengthFilled. Because the instance variable lengthFilled has been declared private, the getLengthFilled method is the only way that another class can find out the value of lengthFilled. For example, to print all the values stored in a ShortSequence object sequence, you could use a for-loop like the following:

       for ( int i = 0; i < sequence.getLengthFilled(); i++ )
          System.out.println(sequence.getNumberAt(i));
    

    instead of:

       for ( int i = 0; i < sequence.lengthFilled; i++ )
          System.out.println(sequence.getNumberAt(i));
    

    as it could if the instance variable lengthFilled were not private. The getLengthFilled() method allows other classes to find out the value of the private variable lengthFilled, but does not allow them to change it.

    Observe that the entire getLengthFilled method definition, including both the method heading and the method body, complete with opening and closing curly braces, is written on one line. It is common to do this for methods whose bodies consist of just one statement which is short enough that the entire method definition can easily fit on one line.

    The next instance method in class ShortSequence is another one-line method, getCapacity:

       public int getCapacity()  { return numbers.length; }
    

    This method returns the length (capacity) of the ShortSequence object's internal array, which is pointed to by the instance variable numbers. Note that it is NOT necessary for class ShortSequence to have a separate instance variable for the capacity, because this quantity can be gotten via numbers.length.

    Look now at the getNumberAt method:

    public short getNumberAt(int index)
    {
       if ( index < 0 || index >= lengthFilled )
          throw new IndexOutOfBoundsException(
                             "Error: ShortSequence index " + index
                             + " must be >=0 and < "
                             + lengthFilled + ".");
    
       return numbers[index];
    }  //method getNumberAt(int)
    

    This method returns the number stored at the specified index in the internal array, after checking to make sure that the index is within range.

    The above three get methods all allow other classes to get values of data stored in an ShortSequence object, but do not allow them to change the data in any way. The next instance method, add, does allow changes, but only an appropriate kind of change:

    public void add(short data)
    {
       if ( lengthFilled >= numbers.length )
          ensureCapacity( lengthFilled + 10 );
       numbers[lengthFilled++] = data;
    }  // method add(short)
    

    This method expands the capacity, if necessary, and then puts the specified short integer data item at the next available position in the internal array numbers. It also increments lengthFilled, the count of meaningful elements stored in the array. Note that the add method does not allow other classes to make just any old change to any old location in the array. It allows them ONLY to put a data item at the next unoccupied location.

    The add method contains a call to ensureCapacity, another instance method defined in class ShortSequence. Note that the method call has the following syntax:

       methodName(parameters)
    

    Instance methods can be called in the above manner, without specifying an object reference, only by other instance methods of the same class. On the other hand, if the method were being called from outside the class, or from a static method of the same class, then the required syntax would be:

       objectReference.methodName(parameters)
    

    The ensureCapacity method reallocates the array pointed to by the instance variable numbers. The capacity is expanded to the larger of (1) the minimum specified by the parameter minimumCapacity and (2) twice the current number of stored elements plus 2.

    public void ensureCapacity(int minimumCapacity)   
    {
       int newCapacity = Math.max( minimumCapacity,
                                   2 * lengthFilled + 10 );
       short[] temp = new short[newCapacity];
       for ( int i = 0; i < lengthFilled; i++ )
          temp[i] = numbers[i];
       numbers = temp;
    }  // method ensureCapacity(int)
    

    Consider now the insert method. Before you look at the code for it, let's first consider what this method should do.

    The insert method puts the specifed data element at the specified index without replacing any of the other data elements already stored in the internal array. The other data elements should remain intact. This means that some of the other data elements must move over, by one location, to make room for the old one. The original order of the data items should remain intact. For example, suppose an ShortSequence object referred to by an object reference variable sequence already contains the following sequence of integers, starting at location 0:

       10 55 95 33 40
    

    Suppose, then, the insert method is called as follows, to insert the number 100 at location 2:

       sequence.insert(2, 100);
    

    The data element currently at location 2 is 95. Hence that element, and all elements with higher indices, must each be moved over by one location. As a result, sequence will contain the following:

       10 55 100 95 33 40
    

    Before the data element is inserted, the insert method must first check (1) whether index is within range and (2) whether the array is full. If index is not within the range [0, lengthFilled), i.e. if it is less than zero or greater than or equal to lengthFilled, an exception should be thrown indicating that the index is out of bounds. If the index is within range but there is no more room in the array, then the array should be reallocated via a call to ensureCapacity before the element is inserted.

    Here's the code for the insert method:

    public void insert(int index, short data)
    {
       if ( index < 0 || index >= lengthFilled )
          throw new IndexOutOfBoundsException(
                             "IntSequence index " + index
                              + " must be >=0 and < "
                              + lengthFilled + ".");
    
       if ( lengthFilled == numbers.length )
          ensureCapacity(lengthFilled + 10);
       for ( int i = lengthFilled; i > index; i-- )
          numbers[i] = numbers[i-1];
       numbers[index] = data;
       lengthFilled++;
    }  // method insert(int, short)
    

    Class ShortSequence also has an equals method, a hashCode method, and a toString method. The equals, hashCode, and toString methods are three of the methods that every class in the Java language inherits automatically from class Object, the grand ancestor superclass of all classes in Java. In classes whose purpose is to define objects - especially objects whose main purpose is to simply represent some specialized kind of data (as is the case both for ShortSequence objects and for objects of our example class Time which will be discussed in the next section) - the equals and toString methods are usually re-defined to do something meaningful in terms of the particular kind of data which the class represents. And, in any class that re-defines the equals method, the hashCode method should be re-defined too for consistency with the equals method.

    In class ShortSequence, the equals method is defined to return true when the parameter object is another ShortSequence object containing a sequence of short integers identical to the sequence contained in the ShortSequence object for which the equals method has been called. It returns false when either (1) the parameter is null, (2) the parameter is not a ShortSequence object, or (3) it is a ShortSequence object containing a different sequence of short integers.

    The equals method will be discussed in more detail in a later section of this tutorial. Likewise the hashCode method.

    In class ShortSequence, the toString method returns a String which begins with "ShortSequence:" and then lists the contents of the array on a single line of text.

    You may test various methods of class ShortSequence using the following test programs:

    The above programs have, in their source code, some details you probably won't fully understand yet. But the programs' output is clear.


  9. Another example of a class which defines an object
  10. Compile and run the applications TimeTest1.java and TimeTest2.java. These programs both instantiate and test objects of a class Time, defined in Time.java. Objects of class Time represent a time of the day.

    Before you look at the source code of either class Time itself or the test programs, let's first generate and look at the Javadoc files. You will probably find class Time much easier to understand if you read all the descriptions of all the methods, in the Javadoc file, before you look at any of the source code. To generate the Javadoc files if you have not done so already, make sure your DOS window is looking at the directory where your source code files are, and then type, in your DOS window:

       javadoc -author *.java
    

    First, look at the description of the class itself, near the top of the Javadoc file for class Time.

    Then look at the summary and detailed descriptions of the constructors. Class Time has two constructors. Having more than one constructor for a class is known as constructor overloading. Read the Javadoc descriptions of the two constructors. One constructor has three parameters of type int specifying the time as an hour, minute, and second in 24-hour time. The other constructor has one parameter of type int specifying the time as a total number of seconds since midnight.

    Regardless of how a time was specified when a Time object was created, its value can be obtained from the Time object in both formats: as an hour, minute, and second, via the getHour, getMinute, and getSecond methods, or as a total number of seconds since midnight, via the getTotalSeconds method.

    Class Time has an equals method which determines whether a time object is equal in value to another object. A Time object is equal to another object if the other object is also of class Time and the two objects represent the same time in a day.

    Class Time has a toString method that returns a String indicating the 24-hour time in HH:MM:SS format. It also has a to12HourString method that returns a String indicating the same time as a 12-hour instead of 24-hour time.

    When you have read the entire Javadoc description, look next at the source code, not of class Time itself, but of the test program TimeTest1.java. Run this program again and make sure you understand what it does.

    TimeTest1.java and TimeTest1.java contains a static method testTime which tests most of the instance methods of class Time:

    public static void testTime(int h, int m, int s)
    {
        Time time = new Time(h, m, s);
        System.out.println(time.to12HourString() + "   "
                           + time + "   "
                           + time.getHour() + " hr, "
                           + time.getMinute() + " min, "
                           + time.getSecond() + " sec, or "
                           + time.getTotalSeconds() + " total seconds.");
        System.out.println("     Was " + h + " hr, " + m + " min, "
                           + s + " sec, or " + (3600*h + 60*m + s)
                           + " total seconds.");
    }  // method testTime
    

    The first statement instantiates a Time object. Note the difference between "time" and "Time." Remember, Java is case-sensitive. In this example, Time is the name of a class, whereas time is the name of a local variable. In Java, it is customary (though not a syntactic rule) to begin class names with a capital letter and to begin variable names and method names with a lower-case letter.

    Observe the first of the two System.out.println statements, especially the portion that has been bolded above. The object reference time appears between two plus signs, on the other sides of which are String literals. Whenever there is an object reference on the other side of a plus sign from a String, a String representation of the object is concatenated to the other String. The object's String representation is obtained via an automatic call to the object's toString method. In class Time, the toString returns a String representing the Time as a 24-hour time in HH:MM:SS format.

    Run the program again and observe how it tests various instance methods of class Time.

    Then, compile and run another test program, TimeTest3.java, but do not yet look at the source code of TimeTest3.java. This program tests the equals method of class Time.

    Then, before you look at the source code of class Time itself, generate a more detailed version of the Javadoc documentation, as follows:

       javadoc -author -private *.java
    

    i.e. include the command-line argument "-private" in addition to all the others. Then, in the docs folder, look at the documentation for class Time again.

    The "-private" command-line argument causes the creation of Javadoc files which list the private fields and methods of a class, as well as the public ones that are normally listed. Without the "-private" command-line argument, only the public fields and methods would be listed.

    Look first at the "Field Summary." It lists three instance variables for the hour, minute, and second. Note that hour stores a value in the range 0 to 23, and both minute and second store values in the range 0 to 59. Because the values of the instance variables are strictly limited to these very small numbers, we can perhaps (depending on the computer's architecture) save room in the computer's memory by declaring them to be of type byte (8 bits) rather than type int (32 bits). An integer in the range 0 to 6 can easily be represented by only 8 bits. The savings in memory space could be significant if, for example, we were to write a program using a large array of Time objects.

    (However, in many computers, using the shorter data types doesn't actually save space at all except in arrays of primitive data types. Declaring individual variables as type byte or type short would save space only with processors that use 8-bit or 16-bit memory locations. A 32-bit processor may pack 4 elements of an array of bytes into a 32-bit memory location, but will not likely pack individually-declared byte or short variables that way.)

    Look now at the following clause in the Javadoc description of the toString method:

    Overrides:     toString in class java.lang.Object

    The above clause is not taken from a Javadoc comment in the source code, but, instead, is generated automatically by Javadoc, which automatically inspects all superclasses of the classes for which Javadoc is generating an HTML document.

    As mentioned earlier, ALL classes in Java automatically have a toString instance method, whether or not you explicitly define one for the class. If your class does define a toString method, then your toString method is executed whenever toString is called for an object of your class. However, if your class does NOT contain a toString method definition, it is still possible to call toString for an object of your class. What happens, in that case, is that the Java Virtual Machine automatically calls a toString method which is defined in class Object, the grand superclass of all classes in Java. (Class Object is in the java.lang.Object package.) The toString method of class Object returns a String which is rather ugly and does not mean much to anyone except an advanced programmer. So, whenever you write a class that defines objects, it is usually a good idea to include a toString method. If your class does define a toString method, it is said to override the toString method in class Object. This means that your version will be executed instead of class Object's version when toString is called for an object of your class.

    There is a similar Overrides clause for the equals method. As is the case with the toString method, every object of every class automatically has an equals method, inherited from class Object. Class Object's version of the equals method behaves the same as the equality operator (==), i.e. it determines whether two object references point to the same object. A class should override the equals method if it is desired to be able to compare two distinct objects for equality of value. For example, class String defines its equals method to return true for two distinct String objects that contain the same sequence of characters. Similarly, class Time defines its equals method to return true for two distinct Time objects that represent the same time.

    Look now at the class heading, near the top of the Javadoc file Time.html:

    public class Time extends java.lang.Object

    This means that class Time inherits all the methods defined in class Object, such as toString. In other words, even if class Time does not contain definitions of those methods, they can still be called for an object of class Time as if they were defined in class Time itself. If class Time does not define them, then class Object's versions will be executed when the methods are called. On the other hand, if class Time does define them, then class Time's versions will be executed instead. In other words, class Time's own versions override the versions it inherits from class Object.

    You may recall that most of the GUI programs you've looked at so far have extends JFrame in their class headings. This means that they inherit methods from class JFrame -- and from all the other classes that class JFrame, in turn, inherits methods from, including class Object. If a class heading does not have an extends clause, then the class extends Object by default, which means that it inherits only from class Object and from no other class.

    Look now at the following diagram, which appears above the class heading in the Javadoc file:

    java.lang.Object
      |
      +--Time
    

    This diagram is called the class hierarchy for class Time. It lists all the classes from which class Time inherits methods, either directly or indirectly. In this particular class hierarchy diagram, there are no other classes listed besides classes Object and Time. This means there are no other classes from which class Time inherits methods besides class Object. For example, there is no other class from which class Object inherits methods, and from which, therefore, class Time would inherit those methods indirectly.

    A window, in contrast, would have a class hierarchy diagram like the following:

      java.lang.Object
      |
      +--java.awt.Component
         |
         +--java.awt.Container
            |
            +--java.awt.Window
               |
               +--java.awt.Frame
                  |
                  +--javax.swing.JFrame
                     |
                     +--SimpleGUI3
    

    This diagram would mean that class SimpleGUI3 extends JFrame (which is defined in the javax.swing package in the Java library), and that class JFrame, in turn, extends Frame (defined in the java.awt package), and that class Frame, in turn, extends Window, which in turn extends Container, which in turn extends Component, which extends Object (defined in the java.lang package). Thus, all JFrame subclasses inherit methods from all the listed classes.

    Another way of saying this is that SimpleGUI3 is a subclass of class JFrame, which in turn is a subclass of class Frame, etc. Conversely, class Object is the immediate superclass of class Component, which in turn is the immediate superclass of class Container, etc.

    Class Object is at the top of every class hierarchy. Class Object is the grand ancestor superclass of all classes in Java.

    Look now at the source code of class Time, in Time.java. Look first at the class heading, and compare it with the class heading in the Javadoc file. In the source code, the class heading does not contain the clause "extends Object". A class which extends Object, e.g. class Time, does not need an explicit extends clause in its class heading, whereas a class which extends any other class besides Object does need an explicit extends clause in its source code.

    In Time.java, look next at the constructors. Observe first how the thrown exceptions are specified.

    After error checking, the constructors assign values to the instance variables. The one-parameter constructor will need first to convert a total number of seconds to hours, minutes, and seconds.

    Look now at the three-parameter constructor. Note that the formal parameters and the instance variables have the same names. When an instance variable and a formal parameter are declared using the same name, they are two separate variables that happen to have the same name. Hence, within the body of the method, it is necessary to distinguish between them somehow. Within the body of the method, if you use a name which has been declared for both an instance variable and a formal parameter, the method will assume that you mean the formal parameter, NOT the instance variable. Hence, within such a method, the instance variable must be accessed using the following syntax, instead of just the variable name:

       objectReference.variableName
    

    However, we cannot use a reference to just any old Time object. We need to access the object which is currently being created by the constructor. Similarly, if we were writing an instance method with a parameter having the same name as an instance variable, we would need to access the object for which the method has been called. For this purpose, we must use the keyword this as our object reference. Within any constructor or instance method, the keyword this can be used as a reference to the object whose behavior is being defined by the method. In the three-parameter constructor of class Time, the keyword this is used to access the instance variables hour, minutes, and second, as follows:

           this.hour = (byte) hour;
           this.minute = (byte) minute;
           this.second = (byte) second;
    

    Recall that the instance variable is of type byte, whereas the parameter is of type int. Hence the need for an explicit conversion from int to byte, using the (byte) cast.

    Look now at the instance methods. Note that none of them do any error checking of the values of the instance variables. It is simply assumed that the instance variables have appropriate values. Making sure that an instance variable has an appropriate value is the responsibility of any methods which CHANGE the value of the instance variable, which is not the case for any of the instance methods of class Time. In class Time, the only methods that change the values of instance variables are the constructors. Hence, in class Time, the constructors are the only methods that should need to do any error checking.

    One important difference between class Time and our previous example, class ShortSequence, is that class Time does not contain any methods, besides its constructors, which modify the value of its instance variable, whereas class ShortSequence does contain such methods. Thus, class ShortSequence is said to define mutable objects, whereas class Time defines immutable objects. Similarly, recall that class String defines immutable objects, whereas class StringBuffer defines mutable objects.

    One of the instance methods is compareTo. The Time object for which the compareTo method has been called (i.e. this object) is compared with another Time object which has been passed in as a parameter. Before you look at the source code, look first at the Javadoc comment:

    /**
     * Compares this Time object to the parameter
     * Time object.  Times begin at midnight, so
     * the "earliest" time is midnight and the
     * "latest" time is one second before the
     * following midnight.
     *
     * @param other the Time with which this Time
     *              is being compared
     * @return a negative integer if this Time precedes 
     *         other, a positive integer if 
     *         other precedes this Time, or
     *         zero if the two times are equal.
     */
    public int compareTo(Time other)
    

    The compareTo method for class Time is defined analogously to the compareTo method for class String. In the Java library documentation, Look at the description of the String class's compareTo method which takes a parameter of type String. The description says, "The result is a negative integer if this String object lexicographically precedes the argument string. The result is a positive integer if this String object lexicographically follows the argument string. The result is zero if the strings are equal."

    The compareTo method of class Time behaves similarly except that we are comparing times rather than strings. Again we return a negative number if this Time is less than the parameter Time, a positive number if this Time is greater than the parameter Time, and zero if the strings are equal.

    If the two objects are NOT equal, note that the exact magnitude of the returned integer value does NOT matter. All that matters is that it is nonzero and has the correct sign.

    Below is one possible implementation of the compareTo method for the Time class:

    public int compareTo(Time other)
    {
       if ( hour != other.hour )
          return ( hour - other.hour );
       if ( minute != other.minute )
          return ( minute - other.minute );
       return ( second - other.second );
    }  // method compareTo
    

    To test the compareTo method of class Time, run TimeTest5.java.

    Class Time also has an equals method and a hashCode method which will be discussed in the next two sections of this tutorial.

    All of class Time's instance methods are public, meaning that they can be accessed from outside the class, and that, if class Time were part of a package, they could also be accessed from outside the package. (You will not be taught how to create packages in this course, but you can look it up in Deitel, Chapter 8, if you are interested.)

    Class Time also has a private static method, padToTwoDigits. A private method can be accessed only in the class within which it is defined. The padToTwoDigits method is private because it has been defined solely as a convenience to make two of the public methods a bit easier to write. The padToTwoDigits method is called within both the toString and to12HourHourString methods as an aid to generating strings in HH:MM:SS format. All the numbers in the string must be two digits long, which means that if they are less than ten, they must be padded with a leading zero. Since the generation of a two-digit string must be done three times each within both the toString and to12HourString methods, it makes sense to define a separate method to generate the two-digit string rather than write out the code to do it in six separate places. The padToTwoDigits method is defined as follows:

       private static String padToTwoDigits(byte b)
       {
          return ( b < 10 ) ? ("0" + b) : Byte.toString(b);
       }  // method padToTwoDigits
    

    The above is an abbreviation for:

       private static String padToTwoDigits(byte b)
       {
          String s;
          if ( b < 10 )
             s = "0" + b;
          else
             s = Byte.toString(b);
          return s;
       }  // method padToTwoDigits
    

    The ? : is known as the conditional operator and can be used to abbreviate simple if/else statements of the kind that do nothing more than select one of two possible values. The conditional operator is used also in the to12HourString method as follows:

          String AMorPM = ( hour < 12 ) ? " AM" : " PM";
    

    Here, it selects " AM" or " PM" depending on whether or not the hour (in 24-hour time) is less than 12.

    To test various instance methods of class Time, run TimeTest4.java, which automates testing using quite a few different values. Note that our choice of test values emphasizes boundary cases.


  11. The equals methods of classes String, Time, and ShortSequence
  12. As mentioned earlier, every object of every class automatically has an equals method, inherited from class Object. Class Object's version of the equals method behaves like the equality operator (==), i.e. it determines whether two object references point to the same object. However, this isn't what we normally would want. More often, what we are interested in knowing via the equals method is whether the two objects are in some sense equal in value. For example, two String objects are equal if they contain identical sequences of characters.

    For an example of the difference between the equality operator and the equals method as they both behave with String objects, look at StringEqualityDemo.java and compile and run it. It pops up a JOptionPane dialog box and outputs distinct messages for the following three cases: (1) the user clicking "Cancel," (2) the user clicking "OK" without typing anything, and (3) the user clicking "OK" after typing something in the text field.

    These options are detected as follows: The user clicking "Cancel" is signalled by the JOptionPane method showInputDialog returning a null value instead of an actual String object. On the other hand, when the user clicks "OK" without typing anything, the JOptionPane method showInputDialog returns an empty String. That being the case, we might expect the following Java code to do our job:

       String line = JOptionPane.showInputDialog(
                        "Type anything, or nothing:");
    
       if ( line == null )
          System.out.println("You clicked CANCEL.");
       else if ( line == "" )
          System.out.println(
                "You clicked OK without typing anything");
       else
          System.out.println("You typed: " + line);
    

    However, if our program were written as above, the second option wouldn't work. The condition line == "" doesn't check whether line is an empty String. It checks whether line is a reference to the same String object that our program is using to hold the empty string literal (""). Even if the String object line refers to happens to be empty, it will not be the very same String object that is being used to hold the empty string literal.

    So, to test whether a String object contains an empty string, we must compare it to the empty string literal ("") using the equals method, not the equality operator, as illustrated below:

       String line = JOptionPane.showInputDialog(
                        "Type anything, or nothing:");
    
       if ( line == null )
          System.out.println("You clicked CANCEL.");
       else if ( line == "" )
          System.out.println("This message will never be printed.");
       else if ( line.equals("") )
          System.out.println(
                "You clicked OK without typing anything");
       else
          System.out.println("You typed: " + line);
    

    Class String defines its equals method to return true for two distinct String objects that contain the same sequence of characters. So, whenever you need to test whether two separate strings are equal in the sense of containing the same sequence of characters, you should call the equals method rather than the equality operator (==).

    Similarly, if you ever wish to test whether two separate strings s1 and s2 are NOT equal in the sense of containing the same sequence of characters, you should use a negated call to the equals method, as follows:

       if ( ! (s1.equals(s2)) )
    

    rather than the inequality operator (!=):

       if ( s1 != s2 )
    

    Keep this in mind for objects of other classes too, not just String objects, if those classes define an equals method.

    A class should override the equals method of class Object if it is desired to be able to compare two distinct objects for equality of value. For example, class Time defines its equals method to return true for two distinct Time objects that represent the same time.

    Consider now the equals method of class Time. Look first at the Javadoc comment:

    /**
     * Determines whether this Time is equal in value
     * to the parameter object.  A Time object if equal
     * to another object if the other object is also of
     * class Time and the two objects represent the
     * same time in a day.
     *
     * @param other the object to which this Time is
     *          being compared for equality.
     * @return true if this Time is equal to the
     *          parameter object, false otherwise.
     */
    public boolean equals(Object other)
    

    The equals method takes a parameter of type Object, which may point to any object of any class whatsoever, because all classes have Object as a superclass. In general, any object can be pointed to by an object reference whose type is either the object's own class or a superclass of the object's class.

    To understand why this is so, think of the word "class" as meaning "category." A class defines the characteristics of some category of objects. A subclass defines the additional characteristics of some subcategory of objects of the original class. Thus, an object of a subclass IS also an object of the superclass. An object of class Time IS also an object of class Object i.e. it has all the capabilities defined in class Object, as well the additional capabilibies defined in class Time.

    To understand what bearing this has on object references, let's leave the world of programming for a moment and think about animals. "Human" and "dog" are both subclasses of "mammal," which in turn is a subclass of "vertebrate." A human or a dog IS also a mammal, and thus, in turn, IS also a vertebrate. All mammals have all the defining characteristics of a "vertebrate," as well as some additional characteristics that are part of the definition of a "mammal." Humans, in turn, have all the defining characteristics of a "mammal," plus some additional characteristics that make us human. One can point to a human and truthfully say, "that is a vertebrate," or "that is a mammal," as well as "that is a human." But one cannot truthfully point to a dog and say "that is a human."

    Similarly, an object reference points to an object, and the object reference's type says what kind of object it is pointing to. One can point to a Time object and truthfully say that it is of class Object, as well as class Time. So, if an object is of class Time, the type of the object reference can be either Time or Object.

    Look now at the source code for the equals method of class Time:

    public boolean equals(Object other)
    {
       return ( other != null
                   && getClass() == other.getClass()
                   && hour == ((Time) other).hour
                   && minute == ((Time) other).minute
                   && second == ((Time) other).second );
    }  // method equals
    

    The expression in parentheses after the word "return" is a compound boolean expression, consisting of a total of five boolean expressions joined together by "and" (&&) operators. Note that the double-ampersand operator performs short-circuit evaluation, meaning that if the expression on the left is false, it immediately knows the whole thing if false and doesn't bother to evaluate the expression on the right.

    First, in the leftmost boolean expression, this method checks to see whether the parameter is null. According to the description of the equals method of class Object, the equals method should return false if the parameter is null. It should NOT throw a NullPointerException. Luckily, if other is null, short-circuit evaluation prevents evaluation of all the boolean expressions to the right of the first double ampersand. If they were to be evaluated, they would throw a NullPointerException due to calls to instance methods of the nonexistent object other, or due to attempts to access instance variables of the nonexistent object.

    The next thing the equals method needs to check is whether the parameter object and this Time object are of the same class. This is checked via the following boolean expression:

       getClass() == other.getClass()
    

    The getClass() method is yet another of those methods that every class in the Java language inherits from class Object. It returns an object of class Class, which is a class of objects that the Java virtual machine uses to keep track of classes and other data types. Every data type, including every class, has a unique object of class Class associated with it. If two objects are of the same class, the getClass() object returns the same Class object for both of them. (And, since it returns the same Class object for both of them - NOT two separate though identical Class objects - it is okay to use the equality operator, rather than an equals method, to compare the values returned by the two calls to getClass. for this Time object and the parameter object.

    Besides calling the getClass method, another way to check the class of an object would be to use the instanceof operator, as follows:

       other instanceof Time
    

    Above is another boolean expression which checks whether the parameter points to an object of class Time, i.e. whether other points to an instance of class Time. Note that instanceof is a Java keyword. It is a boolean operator which expects an object reference on the left and a class name on the right, and evaluates to true if the object reference on the left is of the same class or a subclass of the class named on the right.

    An important difference between the instanceof operator and our comparison of calls to getClass is that an instanceof expression is true for subclasses of the class named on the right, whereas our equality comparison of calls to getClass is true only if the two objects are of the exact same class as each other, not a subclass. In an equals method, it is generally preferred to consider two objects to be equal only if they are of the exact same class.

    If the parameter object and this object are of the exact same class, then our equals method checks to see whether the instance variables of the two objects are equal. Note that it is possible, within a class, for one object to access the private instance variables of another object of the same class, though it is not possible to access those instance variables from another class.

    In order to compare the instance variables of two Time objects, the two object references must both be of type Time. But the parameter other is declared to be of type Object, not of type Time. Therefore, we need to use the cast operator (Time) to change the type of the object reference other to allow the comparison of instance variables of class Time.

    Note that permissible use of the cast operator is more restricted for object references than for primitive data types. The cast operator cannot be used to convert just any old object reference to any old class, nor can it be used to convert primitive data types to object references or vice versa.

    When applied to object references, the cast operator affects only the type of the reference, not the class of the object itself. Recall that an object can have references pointing to it that are of either its own class or any superclass. A cast operator can be used to convert a reference from any of the types allowed for an object to any of the other types allowed for that object.

    The cast operator will work correctly with an object reference only if the object is already of the class named by the cast. Thus, for example, if an object reference of type Object (which can point to objects of any class whatsoever) happens to be pointing to an object of class Time, then the cast operator can be used to convert the type of the object reference from type Object to type Time. But the cast operator could not be used, say, to convert an object reference of type String to an object reference of type Time, because neither String nor Time is a superclass of the other.

    In our present example, before we cast the object reference, we have already determined that the actual object pointed to by reference other is indeed of class Time. Recall that the && operator does short-circuit evaluation, meaning that if the boolean expression to the left of the && operator is false, then the Java Virtual Machine concludes that the combined expression is false without bothering to check the expression to the right of the operator. Thanks to short-circuit evaluation, we are assured that the (Time) cast gets applied only to objects that have already been determined to be of class Time. (Otherwise, we would get a ClassCastException runtime error message when trying to cast a non-Time object to type Time.) You'll learn more later about how to recognize when a class cast is and is not possible and appropriate.

    To test the equals method of class Time, run TimeTest3.java.

    Consider now the equals method of class ShortSequence, defined in ShortSequence.java. It takes a parameter of type Object and returns a boolean value which is true if the parameter object is equal in value to this object (the ShortSequence object for which the method has been called). The two objects should be considered equal if all of the following conditions hold: (1) the parameter isn't null, (2) the parameter object is of the exact same class as this object, and (3) both ShortSequence objects contain the same sequence of integers, i.e. they have the same length and their elements are equal at each index. For the length, we will consider the length of the actual sequence of numbers, which is the lengthFilled, not the capacity of the array itself, which we shall ignore.

    public boolean equals(Object other)
    {
       if ( other == null 
               || getClass() != other.getClass()
               || lengthFilled != ((ShortSequence) other).lengthFilled )
           return false;
       for (int i = 0; i < lengthFilled; i++)
           if ( numbers[i] != ((ShortSequence) other).numbers[i] )
              return false;
       return true;
    }  // method equals
    

    Instead of joining together a bunch of boolean expressions using the && operator as we did in class Time, the above implementation tests various conditions that would make the two objects unequal and returns false as soon as it finds that any of those conditions are true. Then, if the method has NOT found any of the inequality conditions to be true, the method returns true.

    You may test the equals method of class ShortSequence using ShortSequenceTest4.java.


  13. A very brief, very oversimplified introduction to hash codes
  14. Consider an electronic phone directory for Manhattan or Queens - or, bigger yet, for an electronic phone directory for the entire New York metropolitan area. One way to implement it would be to put all the records (containing name, address, and phone number) in an array. However, looking up names in such a huge array could be somewhat time-consuming, especially if new records are being added and deleted frequently enough to make it unfeasible to maintain the array in alphabetical order sorted by name. (Furthermore, as a practical matter, such huge arrays aren't normally loaded into memory all in one piece. They are normally stored on a disk and loaded into memory only piecemeal. This in itself can make searching the array very time-consuming.)

    There are several ways to make a faster lookup table, which you can learn about later, in a data structures course. One way is known as a hash table.

    A hash table uses hash codes. For now, don't worry about the details of what a hash table is, or what a hash code is, or how a hash table uses the hash codes. For now, all you need to know is the following:

    1. If two objects are equal according to the equals method, then they should also have the same hash code.
    2. Therefore, if you override (re-define) the equals method for a class, you should also override the hashCode method for that same class. The hashCode method is another method which every class inherits from class Object. You should re-define it in such a way that if two objects are equal according to your equals method, then the hashCode method returns the same value for both objects. Doing so is part of the "contract" for both the equals method and the hashCode methods (as described in the Sun Java documentation for class Object), and it is absolutely necessary in order for a hash table to be able to look up objects of your class.
    3. It is desirable - but NOT absolutely essential - that two objects that are unequal according to the equals method have different hash codes. (What IS essential is that if two objects are equal according to the equals method, they must have the SAME hash code. On the other hand, "collisions," i.e. equal hash code values for unequal objects, are acceptable though they should be as rare as possible.)
    4. It is also desirable - though not absolutely essential - that the possible hash code values for unequal objects be spread out very widely over the int range, preferably in such a way that the most likely values are spread out very widely over the int range too.

    For any class you write, the following hashCode method will work fine ONLY IF you have also overridden the toString method in such a way that if two objects are equal according to the equals method, then their toString methods return identical strings:

       public int hashCode()
       {
          return toString().hashCode();
       }
    

    The above is NOT, in general, the best possible way to write a hashCode method. But it's good enough for this course, at least for now. Just remember that it's okay ONLY IF the toString method has been defined in such a way that if two objects are equal according to the equals method, then their string representations as returned by the toString method are identical too.

    So, in any class where you define an equals method, you should also define a toString method - as well as a hashCode method - to be consistent with the equals method.

    The toString method - like the hashCode method - should NOT used any instance variables or other information that isn't ALSO used by the equals method. For example, in class ShortSequence.java, the equals method uses lengthFilled but does NOT use the length of the array numbers itself. Therefore, the toString and hashCode methods should NOT use the length of the array numbers either.


  15. More about exceptions
  16. Run TimeTest1.java andTimeTest2.java again. Try running both programs for both in-range and out-of-range values, and observe the error messages for out-of-range values. For TimeTest1.java, the hour should be between 0 and 23, inclusive, and the minute and second both be between 0 and 59, inclusive. For TimeTest2.java, the total number of seconds should be between 0 and 86399, incluisve. Observe also the error message you get for non-integer command-line arguments. For both non-integer command-line arguments and out-of-range integer values, observe that the error messages look like Java's typical runtime error messages, except that the IllegalArgumentException, for an out-of-range integer value, is more specific, having been generated by our Time class.

    Then compile and run another test program, TimeTest1A.java, for both correct and incorrect input. Again, try to generate both a NumberFormatException for non-integers and an IllegalArgumentException for out-of-range values. Observe that the error messages, in both cases, are much more user-friendly (as distinct from programmer-friendly).

    Look at the source code of both TimeTest1.java and TimeTest1A.java and compare their main methods. The two programs are identical except that TimeTest1A.java uses exception handling to "catch" the usual Java runtime error messages and replace them with user-friendlier messages.

    In the current assignment, you will not yet be expected to learn the details of how to handle exceptions. For now, all you will be expected to learn about exceptions is how to throw (generate) them. You have been shown a complete example of exception handling only to let you know one of the reasons why we bother to throw them in the first place. They can be used not only to generate programmer-friendly error messages, as we have seen, but also to help us generate user-friendly error messages as well. The main advantage of exception handling over other, more direct ways of generating user-friendly error messages is that exceptions allow flexibility in how the error message are displayed.

    In our present example programs, the NumberFormatException is thrown by the parseInt method of class Integer when it tries to interpret, as an int value, a String which does not represent an integer. The IllegalArgumentException is generated by the constructors of class Time when they are given inappropriate values as parameters.

    The exceptions IllegalArgumentException and NumberFormatException are both pre-defined in the Java library. Look again at Sun's documentation for the java.lang package. (Recall that the java.lang package contains the most commonly-used classes. All the classes in the java.lang package are automatically imported into every Java program without need for an explicit import statement.)

    On the page for the java.lang package, scroll down past the "Interface summary" and "Class summary," and you will arrive at the "Exception summary," and below that, the "Error summary." Among the Exceptions, click on the links for NumberFormatException and IllegalArgumentException.

    Note that the exceptions are defined as classes. Thus, in order to throw an exception, we must first instantiate an object of the exception's class. For example, the three-parameter constructor of class Time contains the statement:

        if ( hour < 0 || hour >= 24 )
           throw new IllegalArgumentException(
                  "Time: hour " + hour
                   + " must be >=0 and < 24.");
    

    This statement both instantiates the exception, by calling the constructor of class IllegalArgumentException, and then throws it. The above statement could be written as follows, more explicitly showing the instantiation and throwing of the exception as two separate steps:

        if ( hour < 0 || hour >= 24 )  {
           IllegalArgumentException iae =
                      new IllegalArgumentException(
                               "Time: hour " + hour
                               + " must be >=0 and < 24.");
           throw iae;
        }  // if
    

    But it is more common to combine the two steps in a single statement.

    Most Exception classes have two constructors, one of which takes no parameters and one of which takes a String parameter which is the suggested text of an error message, or at least part of an error message. When you write methods that throw exceptions, you should always call the constructor that takes a String parameter, and you should always give it a concise but reasonably specific and clear error message, as is done in the constructors of class Time.

    Run both TimeTest1.java and TimeTest1A.java again for out-of-range values. Observe that the String which we passed as a parameter to the IllegalArgumentException constructor appears as a part of both error messages, both the programmer-friendly one and the user-friendly one. In TimeTest1A.java, observe how the complete error message is generated by concatenating the phrase "User Error: " in front of the IllegalArgumentException's message.

          try  {
             int hour = Integer.parseInt(args[0]);
             int minute = Integer.parseInt(args[1]);
             int second = Integer.parseInt(args[2]);
             testTime(hour, minute, second);
          } catch ( NumberFormatException nfe ) {
             System.out.println("User error: You entered \""
                   + args[0] + " " + args[1] + " " + args[2]
                   + "\" - should be integers only.");
          } catch ( IllegalArgumentException iae ) {
             System.out.println("User error: " + iae.getMessage());
          }  // catch
    

    The exceptions are not handled in TimeTest1.java as they are in TimeTest1A.java Therefore, when you run TimeTest1.java, the Java Virtual Machine handles the exceptions in its own way, namely by generating its own error messages containing more information that may be useful to a programmer though intimidating to the average user, and then halting execution of the program.

    Don't worry about the other details of the try-catch block yet. You will not be asked to write a try-catch block until much later in this course. What you will be asked to do now, in the current assignment, is to throw exceptions, as is done in the constructors of class Time. For yet another example, here's the one-parameter constructor:

    public Time(int totalSeconds)
    {
       if ( totalSeconds < 0 || totalSeconds >= 86400 )
           throw new IllegalArgumentException(
                  "Time: seconds since midnight "
                  + totalSeconds
                  + " must be >=0 and < 86400.");
    
       // Calculate values to assign to instance variables:
       hour = (byte) (totalSeconds / 3600);
       minute = (byte) (totalSeconds / 60 % 60);
       second = (byte) (totalSeconds % 60);
    }  // constructor(int)
    

    As above, exceptions are usually thrown subject to an if clause which checks whether some undesirable condition is true. If the condition is true, the exception is thrown, thereby automatically halting execution of the rest of the block in which the statement occurs. In the example above, the execution of the entire Time constructor is halted. Thus, the assignment statements after the throw statement are executed only if an exception is NOT thrown, i.e. if the if condition is false.

    When an exception is thrown, the Java Virtual Machine automatically assumes you don't want to execute any code that comes afterward (unless you specify otherwise via a try-catch block). Hence you don't need to -- and shouldn't -- bother with writing statements to halt execution of the rest of the method yourself via System.exit(1) or a return or break statement.

    Look again at the Sun documentation for classes IllegalArgumentException and NumberFormatException, listed under "Exceptions" (rather than under "Classes") on the page for the java.lang package. Observe that both of these exception classes define constructors only, no methods. However, we have called methods for these classes. In TimeTest1A.java, the getMessage method is called. In ShortSequenceTest1.java, the toString method is called.

    Both these methods do not have to be defined in classes IllegalArgumentException and NumberFormatException because they are inherited. Look at the class headings on the pages for IllegalArgumentException and NumberFormatException. Observe that class NumberFormatException extends IllegalArgumentException and that class IllegalArgumentException, in turn, extends RuntimeException. Look also at the class hierarchy diagram at the top of the page for class NumberFormatException:

    java.lang.Object
      |
      +--java.lang.Throwable
            |
            +--java.lang.Exception
                  |
                  +--java.lang.RuntimeException
                        |
                        +--java.lang.IllegalArgumentException
                              |
                              +--java.lang.NumberFormatException
    

    This diagram shows that class NumberFormatException extends IllegalArgumentException, which in turn extends RuntimeException, which in turn extends Exception, which in turn extends Throwable, which in turn extends Object. Another way of saying this is that class NumberFormatException is a subclass of IllegalArgumentException, which in turn is a subclass of RuntimeException, etc. Conversely, class Object is a superclass of class Throwable, which in turn is a superclass of class Exception, etc. The getMessage method is inherited, by IllegalArgumentException and NumberFormatException, all the way up from class Throwable, which also defines a toString method. Classes IllegalArgumentException and NumberFormatException use the version of the toString method defined in class Throwable.

    Now run the application TimeTest3.java again and look at its source code. It defines a main method and another static method, inputTime. Look first at inputTime. It prompts the user to enter a time in either HH:MM:SS format or HH MM SS format (three two-digit integers separated by spaces). If the user does not enter a time in the correct format, the inputTime method says so and prompts the user to enter the time again. And again, if necessary, until the user finally does enter a time in the correct format.

    Instead of a loop, this method uses recurusion, i.e. a call to itself. You will learn more about recursion later in this course.

    If the user types "X" or "x," the program quits. The inputTime method calls System.exit in that case.

    The inputTime method has a try-catch block which catches all runtime exceptions thrown by (1) Integer.parseInt, (2) the Time constructor, or (3) the nextToken method of the StringTokenizer. All these exceptions (NumberFormatException, IllegalArgumentException, and NoSuchElementException) are subclasses of RuntimeException, so they can all be caught by just one catch block which catches exceptions of class RuntimeException.

    Look now at the main method. Observe that it contains a deliberate infinite loop, with a condition always true:

          while ( true )  {
    

    However, this loop does not really make the program run forever, because each iteration of the loop contains a call to inputTime, which allows the user the option of quitting by typing "X" or "x".

    Try running the program TimeTest3.java again, this time with invalid input.

    Observe that when the runtime error messages are printed, the program does NOT halt, but rather continues after the after the exceptions have been caught and the resulting error message has been printed. Exception handling via a try-catch block allows this, giving the programmer the option of whether to make the program halt or not, as desired. The method in which the exception was THROWN still does halt, but the program as a whole may or may not halt, depending on how the exception is handled.



Computer Science 212:  main page