Skip to main content.

Web Based Programming Tutorials

Homepage | Forum - Join the forum to discuss anything related to programming! | Programming Resources

Developing Professional Java Applets

Chapter 2 -- Object-Oriented Development in Java

Chapter 2

Object-Oriented Development in Java


CONTENTS


Now that you're familiar with the general architecture of Java and its basic programming constructs, you're ready to look at Java's tools for object-oriented development. This chapter first looks at the simple, yet powerful, object-oriented features of the Java programming language. You then see how you can organize Java classes into libraries by using the package mechanism. Finally, you're introduced to the suite of class libraries provided with Java, known as the Java Developer's Kit (JDK) or the Java API.

Introduction to Java Classes and Objects

The starting place of object-oriented programming in Java is classes, which provide templates for specifying the state and behavior of an object at runtime. An object is said to be an instance of a class. For a given class and runtime session, there can be multiple object instances.

Basic Structure of a Class

A class generally consists of variables and methods. The following is a rudimentary example of a class called FirstClass:

class FirstClass {
   int firstVariable = 0; // Initially set to 0
   // Set the value of the variable...
   public void setValue(int newValue) {
      firstVariable = newValue;
   }
   // Get the variable value...
   public int getValue() {
      return firstVariable;
   }
}

This class consists of a single variable, called firstVariable, which is initially set to 0. The class has two methods. The setValue() method is used to set firstVariable to the specified value. The getValue() method returns the current value of firstVariable.

The variable firstVariable is called an instance variable. This type of variable is defined for an instance of an object. It differs from a local variable, which is defined inside a block of code or a method. Suppose that a new method is defined to get half the value of firstVariable:

public int getHalf() {
      int half;  // A local variable
      half = firstVariable / 2;
      return half;
}

The variable half is a local variable; it will not exist after the getHalf() method is finished. However, the firstVariable instance variable hangs around until the object instance is destroyed.

Note that the getHalf() local variable isn't really needed. This method could have gotten the same results in a single return statement:

public int getHalf() {
      return firstVariable / 2;
}

Creating an Object Instance

After defining a class, you can use it as a runtime object. Use the new operator to create an instance of a class. You can use the following code to create an object instance of the FirstClass class:

FirstClass firstObject = new FirstClass();

The new operator does a couple of things. First, it allocates memory for the object. Recall that Java has automatic memory management; consequently, you don't have to explicitly allocate memory. The new operator also initializes instance variables. In this sample class, firstVariable is set to an initial value of zero. However, the explicit initialization to zero is not necessary; Java automatically sets instance variables to initial values such as zero or null.

The new operator also calls a constructor. A constructor is a method called when an object is instantiated. You can use a constructor to provide additional initialization. Constructors are discussed in greater detail in the "Constructors" section of this chapter.

Using Methods

A method has two parts: a definition and a body. The definition must have at least three components: a return value, a name, and a parameter list. These three components define the signature of a method. As you will see in the next section, signatures are important because you can use a method name multiple times in a class.

The return value of a method can be a primitive data type (such as int), a class name (such as String), or void if there is no return value. A method has zero or more parameters. If there are no parameters, the method consists of empty parentheses; otherwise, the parameters can be primitive data types or classes, separated by commas.

It's easy to call object methods. The following code instantiates a FirstClass object and manipulates its internal variable values:

      FirstClass firstObject;  // Object not
instantiated yet
      firstObject = new FirstClass(); // Object
instantiated!
      // See what the default value of the object is...
      int val = firstObject.getValue();
      System.out.println("Value of firstObject = " +
val);
      // Set it to a new value...
      firstObject.setValue(1000);
      System.out.println("Value of firstObject = " +
         firstObject.getValue());
      System.out.println("Half Value of firstObject = "
+
         firstObject.getHalf());

Recall that System.out.println prints a string to standard output. In this example, the first Print statement outputs a value of 0, which was the initialized value of firstVariable. The code then sets that variable to a value of 1000 by using the setValue() method. When its value prints again, a value of 1000 is output. Finally, the code calls the getHalf() method to print half the instance variable's value, namely 500.

Overloading Methods

As mentioned earlier, a method has a signature. This is important because Java provides a mechanism for repeatedly using the same method name; this mechanism is called overloading. An overloaded method has the same name but different parameters. To illustrate overloading, a class is created that defines two instance variables. This class, called SecondClass, illustrates overloading by providing two methods of the same name to set the instance variables:

class SecondClass {
   int var1 = 1;
   int var2 = 2;
   // Set only the first variable
   public void setVar(int newVal1) {
      var1 = newVal1;
   }
   // Overloaded! Set both variables...
   public void setVar(int newVal1,int newVal2) {
      var1 = newVal1;
      var2 = newVal2;
   }
   // Get variable values...
   public int getVar1() {
      return var1;
   }
   public int getVar2() {
      return var2;
   }
}

The version of setVar() with one parameter sets the value of variable 1; the setVar() version with two parameters sets both variable values. The following code shows how you can use this class:

SecondClass secondObject = new SecondClass();
      System.out.println("Var1=" +
secondObject.getVar1()
         + " Var2= " + secondObject.getVar2());
      secondObject.setVar(1000);
      System.out.println("Var1=" +
secondObject.getVar1()
         + " Var2= " + secondObject.getVar2());
      secondObject.setVar(secondObject.getVar1()/2,
         secondObject.getVar1());
      System.out.println("Var1=" +
secondObject.getVar1()
         + " Var2= " + secondObject.getVar2());

After you call the first print method, the output is 1 and 2, the initialized values of the two variables. The code then uses the first version of setVar() to set the first variable to a value of 1000. The second print call prints the values 1000 and 2. Finally, the other setVar() method is called to set both variables. The first variable is set to half its existing value, and the second variable is set to half the existing value. Parameters resolve fully before passing to the called method. This means that half of the existing first variable value of 500 passes to the first parameter, but the full value of 1000 passes to the second parameter. The consequent printing displays 500 and 1000 for SecondClass object variables var1 and var2, respectively.

Periodically, you might hear method invocation called "message passing" or "sending a message." This is object-oriented "lingo" for talking about how two objects communicate. The reason that message passing is more than a buzzword is because of what goes on behind the scenes when a method is invoked. Because a method can be overloaded (and overridden, as you'll see in the section on "Method Overriding"), an invocation of an object actually results in the receiving object performing a lookup to see which method should be called. Consequently, it is more correct to call the procedure a "message passing" because the method invocation is not a direct call (such as calling a function in C) but a request for action.

Constructors

As stated earlier, constructors are a special type of method called when an object is initialized; their syntax is similar to that of methods, except they don't return values. The name of a constructor is the same as its class. A constructor is automatically called when an object is instantiated.

Now, rework the SecondClass class to illustrate constructors. First, add a default constructor:

class SecondClass {
   int var1;
   int var2;
   // Main constructor...
   public SecondClass() {
      var1 = 1;
      var2 = 2;
   }
   // ... OTHER METHODS GO HERE...
}

Rather than setting the values of the instance variables in their declaration, set them in the constructor. This constructor is called if you create an instance of SecondClass, as follows:

SecondClass secondObject = new SecondClass();

As with methods, you can overload constructors. Here is a constructor that defines the variable var1 at creation:

// Another constructor...
   public SecondClass(int var1) {
      this.var1 = var1;
      var2 = 2;
   }

To instantiate an object with this constructor, use the following:

SecondClass secondObject = new SecondClass(100);

This call creates an object with var1 set to 100; var2 is set to 2.

You may have noticed the curious way var1 is set in the new constructor.

this.var1 = var1;

In this code, this refers to the current instance of the object, so use this to differentiate the var1 of the object from that of the parameter. Because local variables and parameter variables are first in scope, they will be used unless the this keyword is used. Effectively, this appears in front of every call to an object's variable, so this is the var2 initialization of the constructor:

this.var2 = 2;

To round out this example, you can use a third constructor to set both the variable's initial values:

// Define both values at initialization
   public SecondClass(int var1,int var2) {
      this.var1 = var1;
      this.var2 = var2;
   }

You can call this constructor with the following:

SecondClass secondObject = new SecondClass(100,200);

This call creates an object with var1 set to 100; var2 is set to 200.

What happens if there is no constructor, as in the earlier examples? In this case, the constructor of the class's superclass is called. This brings up the subject of inheritance in Java.

Inheritance in Java

One of the distinguishing features of object-oriented languages is inheritance, a mechanism you can use to create a new class by extending the definition of another class. It gives you a powerful way to increase the reusability of your code. You can take an old class and use it to create a new class by defining the differences between your new class and the old one. The old extended class is the superclass; the new extended class is the subclass. In Java, the process of extending one class to create a new class is called subclassing.

Subclassing

Java uses the extends keyword to indicate the creation of a new class as a subclass of an existing class. To illustrate subclassing, the following code creates a simple class that keeps a record of people's first and last names. It has a default constructor that simply initializes the two names, a constructor for storing String parameters, and a list() method that dumps the current values to standard output.

class SimpleRecord {
   String firstName;
   String lastName;
   // Default constructor
   public SimpleRecord() {
      firstName = "";
      lastName = "";
   }   // Constructor...
   public SimpleRecord(String firstName, String
lastName) {
      this.firstName = firstName;
      this.lastName = lastName;
   }
   // List the elements of the record...
   public void list() {
      System.out.println("First Name: " + firstName);
      System.out.println("Last Name: " + lastName);
   }
}

To instantiate the class with a name and dump its contents, use the following:

SimpleRecord simple = new
SimpleRecord("Thomas","Jefferson");
simple.list();

You can use the extends operator to derive from the SimpleRecord class a new class with additional address information:

class AddressRecord extends SimpleRecord {
   String address;
   // Default constructor
   public AddressRecord() {
      firstName = "";
      lastName = "";
      address = "";
   }
   // Constructor...
   public AddressRecord(String firstName, String
lastName,
    String address) {
      this.firstName = firstName;
      this.lastName = lastName;
      this.address = address;
   }
}

Notice how this new AddressRecord class inherits the firstName and lastName variables from the SimpleRecord class. It also inherits the list() method. Therefore, if you call

record = new AddressRecord("Thomas","Jefferson",
         "Monticello");
record.list();

you create a fully initialized AddressRecord object. However, the list() method prints only the name variables because this call ultimately results in calling the SimpleRecord list() method. The reason for this is because you haven't explicitly defined a list() method in the AddressRecord. However, you can address this limitation through method overriding.

Method Overriding

Use method overriding when you need a subclass to replace a method of its superclass. Recall that each method has a signature. When you override a method, you are defining a new method that replaces the method in the superclass that has the same signature. Whenever you call a method, Java looks for a method of that signature in the class definition of the object. If Java doesn't find the method, it looks for a matching signature in the definition of the superclass. Java continues to look for the matching method until it reaches the topmost superclass.

To add a list() method to the AddressRecord class that displays the address, all you need to do is add this code to the class definition:

// List the elements of the record...
   public void list() {
      System.out.println("First Name: " + firstName);
      System.out.println("Last Name: " + lastName);
      System.out.println("Address: " + address);
   }

The list() method call from the previous example will now print the following results:

First Name: Thomas
Last Name: Jefferson
Address: Monticello

Calling Superclass Methods

You may have noticed that the AddressRecord list() method duplicates some code of the SimpleRecord list() method. Namely, it duplicates the following:

System.out.println("Last Name: " + lastName);
System.out.println("Address: " + address);

You can eliminate this redundancy. Recall that one of the points of inheritance is that a subclass takes an existing class and extends its definition. Because the list() method of SimpleRecord already has some of the behavior of the AddressRecord class, you should be able to use the SimpleRecord list() method as part of the base behavior of the AddressRecord class.

Use the super keyword to refer to a superclass. The super keyword is appropriate for the current problem being discussed because calling the superclass of AddressRecord has some of the behavior the list() method needs. Consequently, the list() method of AddressRecord is modified to call the SimpleRecord list() method before specifying additional behavior:

public void list() {
      super.list();
      System.out.println("Address: " + address);
   }

This code now prints the full name and address, as mentioned in the preceding section's definition of list().

Calling Superclass Constructors

You can use the super keyword to call the constructor of the superclass. The only difference from the previous use of super is that you call it without any method definition. For example, the two constructors of AddressRecord are modified to call the SimpleRecord constructor before performing its own initialization:

// Default constructor
   public AddressRecord() {
      super();
      address = "";
   }
   // Constructor...
   public AddressRecord(String firstName, String
lastName,
    String address) {
      super(firstName,lastName);
      this.address = address;
   }

You can call the default constructor (no parameters) or another constructor with super as long as the constructor with the matching signature exists.

You can use the this keyword in a similar manner to call methods within your class. For example, suppose that you need a constructor that just adds an address to the AddressRecord class, with the other fields set to empty strings. You can do this by using the following code:

public AddressRecord(String address) {
      this();
      this.address = address;
   }

This code calls the default constructor of the AddressRecord class, which in turn calls the SimpleRecord default constructor. At this point, all the fields are set to empty strings. The code then assigns the specified String to the address field, and you're ready to go.

You might wonder what the superclass of SimpleRecord is; the answer is in the next section.

Important Core Classes

Now that you have seen the fundamentals of classes in Java, it's time to see how some of the basic classes in Java work. Because these classes are used throughout Java, you should be familiar with them.

The Object Base Class

The Object class is the base class of all classes in Java-the ultimate superclass of all classes. Because of its primary nature, the methods of the Object class are present in all other classes, although custom methods often override them.

The Object class has a variety of interesting methods. Each Object has its own String representation. This may be a system-generated name or something generated by the class. For example, a String object's representation is the string itself. You get an Object's String representation in two ways:

Object o = new Object();
System.out.println("Object = " + o);
System.out.println("Object = " + o.toString());

In the second line of the preceding example, the Object itself is provided as part of the printout. The Object's toString() method is actually called when an Object is provided as part of the print statement. Consequently, the second and third lines are functionally equivalent. The toString() method should be overridden if you want a custom String representation; it is overriden throughout the Java class libraries.

The finalize() method is the closest thing Java objects have to a destroy method. Recall that Java has automatic memory management so you don't have to explicitly delete objects. The garbage collector removes unreferenced objects from memory. When an object is about to get "garbage collected," its finalize() method is called. This can be overridden if you need to do some custom cleanup, such as closing a file or terminating a connection. However, because garbage collection doesn't happen at predictable times, you need to use the finalize() method carefully. You can also call the method manually, as in right before removing a reference.

Each class in Java has a class descriptor. The Class class represents this descriptor, which you access by using the getClass() method of the Object class. You cannot modify the returned class descriptor. However, you can use it to get all kinds of useful information. For example, the getSuperclass() method returns the class descriptor of a class's superclass. For example, to get the superclass of a String, you can call the following:

String s = "A string";
System.out.println("String superclass = " +
         s.getClass().getSuperclass());

The getName() method of Class returns the name of the class.

You may notice the wait() and notify() methods of the Object class. These are complex methods related to threaded processing, which is discussed in detail in Chapter 8, "Adding Threads to Applets."

String Classes

As stated in Chapter 1, string literals are implemented as objects of the String class. You can create strings in a variety of ways. First, you can assign a String to a literal, as has been shown. You can also use String arithmetic to concatenate one String to another. For example, the second string in

String s1 = "A string";
String s2 = s1 + " with some additional text";

is set to "A string with some additional text". You can also use other operators, such as +=, in String arithmetic. String concatenation also works with primitive data types. For example, this is a valid operation:

String s = "This is number " + 6;

Although you cannot change String objects after creating them, you can apply methods to them to yield new String objects. For example, you can use the substring() method to return a String starting at a specific index. You can also apply other operations. The length() method returns the length of the String. For example, the following methods applied on the above String objects print with some additional text:

System.out.println(s2.substring(s1.length()));

A series of overloaded valueOf() methods is particularly useful. These methods take a primitive data type and return a String representation. The following code sets the String variable s to 100.

int i = 100;
String s = String.valueOf(i);

Notice that the preceding example didn't use any instance of the String class to invoke the valueOf() method. This is because valueOf() is a class method, which means that the method is global to the class, not to a specific instance of the class. Class methods (sometimes called static methods) are discussed in the section "Class Methods and Class Variables" later, but keep this syntax in mind when you see it.

The equals() method returns a boolean indicating whether two String objects have identical internal strings. The call

s2.equals(s);

using the String variables from the previous example returns false. The String class has many methods; you'll see many of these used in the examples and chapter projects of this book.

Another type of string class is StringBuffer. Unlike the String class, you can modify a StringBuffer object after you've created it. You typically construct a StringBuffer object with a String as the lone parameter, although other variations are possible. After construction, you can use operations like append() to modify its state. Using the two String variables again in an example, the following code creates the same String value as variable s2, except with a period concatenated to the end:

StringBuffer sb = new StringBuffer(s2);
sb.append('.');
System.out.println(sb);

The StringBuffer class has some of the same methods as String, and you usually use it in close conjunction with String objects.

Type Wrappers

As pointed out in Chapter 1, "The Java Development Environment," Java had some important design decisions to make regarding primitive data types like integers and floating point numbers. On one hand, designers of Java wanted it to be "objects all the way down," while on the other hand, it needed good performance. In other object-oriented languages, such as Smalltalk, everything is an object, including a number. To perform a mathematical operation would then actually be an application of a method. However, this purity comes at the expense of performance; methods calls are relatively expensive compared to, say, an addition of two integers.

Java uses a middle-ground approach. You can work with the primitive data types directly so that

int x = 2 + x2;

does not result in an object method call. However, if you want to use numbers or other data types of objects, you can use instances of a special set of classes called type wrappers. Each of the primitive data types has a type wrapper class, as shown in Table 2.1.

Table 2.1. Type wrapper classes.

ClassDescription
Boolean Object wrapper for the boolean data type
Character Object wrapper for the char data type
Double Object wrapper for double data type
Float Wrapper for float data type
Integer Wraps integer data type
Long Type wrapper for long data type
Number A superclass that defines methods of numeric type wrappers

You can create wrappers in a variety of ways depending on the data type. You can create instances of the Integer class in two ways:

Integer I1 = new Integer(6);
Integer I2 = new Integer("6");

Once created, you can apply a suite of methods. You can use some of these to convert the internal value to a new data type. For example, the preceding code returns a double value for the integer 6:

double dbl = I1.doubleValue();

As with the String class, you can employ class methods to perform operations on primitive data types without creating an instance of a class. For example, the following code converts a string to an integer:

int i = Integer.parseInt("6");

If the String cannot be converted to a number, a NumberFormatException object is thrown. Exceptions are discussed later in the section "Introduction to Exception Handling."

Finally, the type wrappers provide public variables that give information about such things as the upper and lower bounds of a data type. To get the maximum value a double primitive type can have, you may call the following:

System.out.println("Max double value: " + Double.MAX_VALUE);

The MAX_VALUE public variable is a class variable of the Double class and is declared as static as in class methods. You learn more about these types of variables in the section "Class Methods and Class Variables."

Note
Now that you have seen some of Java's core classes, you can gain some further insight into Java arrays; as stated earlier, arrays are first-class objects. Because Object is the base class, this implies that arrays are subclasses of the Object class. Actually there is a class called Array, which is a direct subclass of Object. For each primitive type and class, there is a subclass of Array. Thus, there is a String[] and int[] class. Inheritance relationships are also maintained in this Array hierarchy. Turning back to the first inheritance example, the AddressRecord[] class is a subclass of SimpleRecord[].
Because arrays are subclasses of Object, this means that you can apply Object methods to it. For example, this is legal:
String s[] = new String[4];
System.out.println("S Superclass: " + s.getClass().getSuperclass());
Therefore, you can assign arrays to Objects. These are all valid calls:
String s[] = new String[4];
Object oArray[] = s;
int numbers[] = new int[4];
System.out.println(numbers);
Object o = numbers;
However, you cannot explicitly subclass an array.

More about Classes

It is time to go back to the mechanics of using Java classes. However, with the basics and a description of some core classes behind you, you're ready for some more advanced class constructs.

Access Modifiers

Java uses access control modifiers to specify the level of visibility a method or variable has to other classes. Java has four levels of access: public, private, protected, and package. The first three are straightforward and may be familiar; the package access level is a little more involved.

The public modifier indicates that you can access a variable by any class or method. Constructors are usually public, as in the AddressRecord example from earlier:

class AddressRecord extends SimpleRecord {
   // Default constructor
   public AddressRecord() {
     //...
   }
}

You can also declare a variable as public. For example, you can extend the AddressRecord class to have two public/FONT> variables to indicate whether the address is for the Internet or is physical:

class AddressRecord extends SimpleRecord {
   public int INTERNET_ADDRESS = 0;
   public int PHYSICAL_ADDRESS = 1;
   // ... Constructors and methods...
   }
}

You can use the following code to print its values:

AddressRecord a = new AddressRecord();
      System.out.println("Physical Address = " +
         a.PHYSICAL_ADDRESS);  // which is 1

You use the protected accessor to restrict access only to subclasses of the protected class. This accessor allows you to design classes so that you can specify methods only of use to subclasses. For example, you can make the name variables of SimpleRecord protected. This restricts their use to a subclass, such as AddressRecord. This is how the variable portion of SimpleRecord is defined:

class SimpleRecord {
   protected String firstName;
   protected String lastName;
   // ... Constructors and methods...
}

The AddressRecord class can continue to use these variables, but outside classes cannot access them.

The private accessor indicates that a variable or method is not available to any other class except the one in which it appears. For example, recall that the list() method of SimpleRecord is called by the AddressRecord subclass. This can happen because access to the method is allowed. However, if the method is declared as private,

private void list()

then AddressRecord will not be able to access the method. A compilation error will therefore arise when you try to compile the AddressRecord class.

The last and default form of access, package, does not directly correspond to an accessor keyword. Packages, as discussed in the section of the same name, are a way of creating libraries of classes. If you do not specify an access control modifier, a method or variable is accessible to all classes in the package. The package level of access is a way of saying that the method or variable is accessible to all classes that are "friends" with, or the same package as, the class they're contained in.

Class Methods and Class Variables

Sometimes you may need to have information that is global to a class and shared by every instance of that class. One reason you might want to do this is for values that do not change, as in defining mathematical constants, like pi. Or you may want to define a version number for a class. Finally, if your class is providing a service that is used throughout an applet, you may want to make its data global to the class so that objects can share the information.

Class methods and class variables are employed to define data that is local to a class and not an object. For this reason, class variables are different from instance variables. Class methods and class variables are declared by using the static keyword, which sometimes results in them being referred to as static methods and static variables.

From the ever present AddressRecord example, you can make the two public address flags into class variables by adding the static keyword:

class AddressRecord extends SimpleRecord {
   public static int INTERNET_ADDRESS = 0;
   public static int PHYSICAL_ADDRESS = 1;
   // ... Constructors and methods...
   }
}

This is more efficient than the previous use that just declared the addresses as public. By adding the static keyword, you have indicated that these flags are global to the class and not the particular instance.

Class methods are even more interesting. In Part III of this book, you will create a class that keeps track of images that have been loaded in memory. This class information is kept across invocations of applets, so some of this class information doesn't need to be tied to a particular instance. In fact, there is only one instance of the class, and it is invoked by the class itself! The class does this through a private constructor. Here are some of the code highlights:

public class MediaLoader {
   // Cache is hashtable...

   static Hashtable cache = new Hashtable(cacheSize);

   // The loader is static and so can be created only once
   private static MediaLoader loader = new
MediaLoader();

   // Private internal constructor
   private MediaLoader() {
      // ... various initilization goes on here...
   }

   // Return reference to this MediaLoader object
   public static MediaLoader getMediaLoader() {
      return loader;
   }
   // ... internal methods..
}

A lot of interesting things are going on here. A cache, which is used to store images, is declared as a class variable. One of the neat things about Java is the flexibility with which you can structure your code. Note how this initialization is not within a method but is part of the class definition; remember that because the cache variable is static, it is a class variable and thus is not tied to an object instance.

The next step is even more unusual. The loader class initializes itself! The constructor is private and not public, so no other object can create an instance of the loader. Instead, the class calls the private constructor and stores the instantiated loader object into a private class variable. In effect, the class is saying, "Make only one instance of this class, and only I know it!"

How do other objects use this MediaLoader object? They do so by calling the last method listed, getMediaLoader(). It is a class method and so is not tied to any instance. The job of the method is to return a reference to the private instance of the object. The other objects can then use this reference to call the public methods of the loader class. These methods will not be declared as static.

To keep things simple, suppose that the loader has a public method called getImage(), with no parameters. Another object calls the method as follows:

MediaLoader ref = MediaLoader.getMediaLoader(); // Get the reference!
ref.getImage(); // Call the method!

Note how you call the class method getMediaLoader() by prefacing it with the name of the class. Because class methods are global to the class, you do not need an instance of the class.

The preceding call also could have been accomplished in one line of code:

MediaLoader.getMediaLoader().getImage();

This method-chaining technique prevents you from creating short-lived variables; you can use the object returned from one method call as the object to be used in the next method call.

While class methods are extremely powerful, they have some restrictions. In particular, they can work only on class variables. If you think about it, the reason for this is obvious: They cannot work on instance variables because there may not be an instance for them to work with!

The various techniques you have seen in the MediaLoader class are used throughout the Java Developer's Kit (JDK). You sometimes see classes that you cannot figure out how to use. In this situation, look for methods named something like getReference or getDefault; these may return a reference to an object instance of that class. You would then employ them in a fashion similar to the MediaLoader class.

The final Modifier

You use the final modifier to indicate that something cannot be changed or overridden. If the final modifier is applied to a variable, it is effectively made into a constant. In the AddressRecord example, the address flags are set to their best form by adding the final modifier:

class AddressRecord extends SimpleRecord {
   public static final int INTERNET_ADDRESS = 0;
   public static final int PHYSICAL_ADDRESS = 1;
   // ... Constructors and methods...
   }
}

Without the final modifier, other classes could change the value of the variable by calling something like

AddressRecord.INTERNET_ADDRESS = 6;

With the final modifier attached to INTERNET_ADDRESS, however, this line of code would generate a compiler error. This is because final variables cannot be modified.

If a method is declared as final, it cannot be subclassed. Because private methods cannot be subclassed, they are effectively final. Note, however, that the converse does not hold. It is useful to declare methods as final whenever it is appropriate. This allows the Java compiler to make some optimizations, thus improving the performance of your code. If the compiler knows that a method cannot be subclassed, it can do such things as "in-lining" the method wherever it is called. Because a final method cannot be subclassed, runtime lookups for matching method signatures as part of the subclassing mechanism are not necessary.

To declare a method as final, place the modifier as follows:

public final void list()

You can also declare classes as final. This means that the class cannot be subclassed. The main reason you may want to declare a class as final is related to security. In the JDK, many classes are final. The reason for this is clear if you think about it. For example, suppose that you could subclass the System class! It would then be relatively easy to subvert the security of a client. Final classes may also be subject to some compiler optimizations, thus providing an additional reason for declaring a class as final.

Note
The term accessor methods refers to methods used to set and retrieve a variable. This is the preferred way of constructing a class rather than declaring everything as public. In the SimpleRecord class, the preferred way to set or retrieve the firstName variable of an object is to create methods like
public void setFirstName(String firstName) {
      this.firstName = firstName;
   }
   public String getFirstName() {
      return firstName;
   }
where the variable firstName is not declared as public. While writing a set or get method for every accessible variable may seem tedious, it is better than declaring everything public. If you make instance variables public, you're compromising the principle of encapsulation. By exposing public instance variables to objects that use your class, you're making the outside world aware of how your class works. You may then lose the freedom to modify the inner workings of your class. For example, if you want to rename or delete a public instance variable, you may not be able to do so because all kinds of classes are using this variable! This very bad practice departs from all the good principles of object-oriented programming. So while it may seem painful now to write large numbers of set or get methods, you're saving yourself time down the road. If you have a good editor, it won't take that much time anyway.
The term accessor class is sometimes used to refer to classes that do nothing more than hold data reachable through accessor methods; the classes have no behavior per se. For C programmers, you will often want to write an accessor class where you would normally create a structure.

The null Keyword and More about Garbage Collection

The null keyword indicates that something has no instance. By default, an uninitialized class variable is set to null. Sometimes you might want to do this explicitly-simply for code readability-if your variable will not be initialized for a while.

The biggest use of null occurs when you no longer need an object. For example, suppose that you create a local variable in a long and involved method that refers to an instance of the Integer class. You use the Integer object for a while, and then you want to indicate that it is no longer needed. You can do this with the null keyword:

Integer I = new Integer(6);
// ... do something with the object...
I = null;

You can achieve the same result in other ways. You can enclose the variable in a block statement; when the variable goes out of scope, the object will no longer be referenced. If you reuse the variable for another object reference, the old object will no longer be referenced:

Integer I = new Integer(6);
// ... do something with the object...
I = new Integer(9);  // Old Integer reference is
gone...

Why would you want to set something to null? Recall that Java's memory management is through a garbage collector. This garbage collector removes objects from memory that are no longer referenced. If your variable was the only reference to the object, then after the reference is removed, the object is a candidate for garbage collection. Remember that the garbage collector is a low-priority thread, so the object may not be cleaned up immediately. If you are really tight on memory, you can call the garbage collector explicitly via the System class:

System.gc();

Like all methods in the System class, gc() is a class method, so you don't have to create any instance to use it. The kind of situations in which you might want to call the garbage collector explicitly is after involved operations that consume a large number of objects and system resources. Do not call the collector in a loop, however. The gc() operation runs the full collector, so the collector's execution will take a moment or so.

Scoping Rules

When a variable is referenced inside a method, Java first looks in an enclosing block for a declaration. If a declaration is not found, Java travels up the nested blocks until it reaches the method definition. If the variable is found anywhere inside a method, the variable has priority over a similarly named instance or class variable. This is why you often see code like the following:

public SimpleRecord(String firstName, String lastName) {
      this.firstName = firstName;
      this.lastName = lastName;
}

The this keyword is used here to differentiate the instance variable from the local variable.

If the referenced variable is not found inside the method, Java searches the class. If the reference variable still is not found, Java travels up the class hierarchy (until the Object class is reached), inspecting the superclasses for the variable. You have seen how this works in the "Inheritance in Java" section.

Casting Rules

Use casting when you need to convert an object or primitive of one type to another type. For example, you may want to convert a double to an int, or a subclass to its superclass. Casting allows you to do this, although the rules of casting can be complicated. The key thing to remember about casting is that it does not affect the object or value being cast. However, the receiver of the cast constitutes a new object or a new type.

Casting primitive data types occurs quite often, such as when you are reading in a stream of data. How the cast occurs depends on the precision of the types involved in the cast. Precision relates to how much information the type can contain; for example, a floating point type such as double or float has more precision than a simple number like int. Likewise, double has more precision than float because it is 64-bit as opposed to 32-bit. Whenever you transfer data from a less precise type to a more precise type, explicit casting is not required:

int i = 3;
double pi = i + .14159;

On the other hand, transferring data from a more precise type to a less precise type requires casting. This is because data may be lost in the casting. Java forces you to explicitly cast because it wants you to be aware of the possible danger of such a conversion:

double pi = 3.14159;
int i = (int)pi; // This is set to 3 (you lost the
.14159!)

Casting between objects is a little more complicated. To illustrate casting, look at the Hashtable class of the JDK. This class takes instances of the Object class and places them into a hash table via a method called put(), where a String is used as a key. To retrieve them from the table, you invoke get(), which takes a String key and returns the corresponding Object.

Recall that Object is a superclass of all classes; every class is a subclass of Object. Suppose that you have a class called MyClass and a String that will be used as a key, placed in a variable key. You can place it in a Hashtable object, indicated by the variable hash, as follows:

MyClass MyObject;
hash.put(key,MyObject);

Because you want to keep your object, MyObject, as an instance of MyClass, don't cast it when you call put(). This is acceptable because MyClass is a subclass of Object.

Suppose that you want to actually store MyObject as a proper Object. In other words, you want to cast a subclass to a superclass. You would then cast it as follows:

hash.put(key,(Object)MyObject);

By doing this, however, you lose the functionality of MyObject.

Suppose that you store the MyObject in the original example, without casting. When you retrieve the object, it is returned as an Object. You can convert the Object returned by get() to the original MyObject by casting:

MyClass MyObject;
hash.put(key,MyObject);
// ... do something...
MyClass MyObject2 = (MyClass)hash.get(key);  // Get back original MyObject

In this case, you're using casting to convert a superclass to a subclass. You can then use
the variable MyObject2 as an instance of MyClass. It will be identical to the original MyObject variable.

If you structure the code in the following way, you will have problems:

MyClass MyObject;
hash.put(key,(Object)MyObject);
// ... do something...
MyClass myObject2 = (MyClass)hash.get(key);  // Don't
do this!

Problems will occur because MyObject was stored in this case as an Object. When you retrieve it, even after casting, it is not really an instance of MyObject any more because that data was lost. If you try to use a MyObject method or variable after this, you get a runtime exception.

You cannot cast indiscriminately. If you try to convert two sibling classes (they are not derived from each other), you get a compilation error:

AddressRecord a = new AddressRecord("Thomas","Jefferson","Monticello");
String s = (String)a; // THIS WILL NOT COMPILE!

You cannot cast primitive data types to objects or vice versa. However, you can effectively perform these conversions by using the type wrapper classes discussed earlier in this chapter.

Other Keywords

You use the instanceof operator to test if a class is an instance of another. A subclass will be an instance of its superclass, but not vice versa. For example, the first three of the following print statements will display true. (Recall that AddressRecord is a subclass of SimpleRecord.) Only the last test will be false.

AddressRecord a = new AddressRecord();
System.out.println((a instanceof SimpleRecord));  // true
System.out.println((a instanceof AddressRecord));  // true
SimpleRecord b = new SimpleRecord();
System.out.println((b instanceof SimpleRecord));  // true
System.out.println((b instanceof AddressRecord));  // false

Another important modifier is synchronized. You will see this modifier in code throughout this book and in the JDK. The synchronized modifier is related to coordinating thread processing; it is discussed in depth in Part III of the book.

Introduction to Exception Handling

One of the great challenges for programmers is how to handle runtime errors in a graceful and efficient manner. In traditional programming, developers manage problems by passing success or failure codes in return statements. The calling functions then check the return code in an if...else statement. If the function succeeds, one chain of action is called; otherwise, another course of action is taken.

There are a couple of problems with this approach. First, this approach results in bloated code. To have to put an if...else check around every function call increases the size of your code by several factors. Even worse, this traditional approach does not enforce strong error checking. Because of sloppiness or overconfidence, programmers often ignore return codes. If there is a problem in an unchecked function call, however, the program may end up reaching a dangerous state that eventually culminates in an abnormal termination. The ensuing search for the cause of the error would probably prove to be quite painful because the source of the problem is not readily apparent.

Fortunately, Java's strongly enforced implementation of exception handling makes it easier to track down errors. In fact, many errors are caught by the compiler instead of at runtime. If the problem does happen at runtime, however, a stack trace makes the difficulty easy to spot. Furthermore, Java's error-handling mechanism does not result in the kind of code bloat typical of traditional programming. Java's way of handling errors is through a programming construct called an exception handler.

Structure of an Exception Handler

An exception handler is often called a try-catch block because of its structure. It is typified by a block of code called a try block, which is where you attempt to execute your normal course of action. The code marches right through the block if there is no problem. If there is an error, however, Java or the called method may generate an object that may indicate the problem. This object is called an exception object and is passed off to the runtime system in search of a way to handle the error. The act of passing an exception object to the runtime system is called throwing an exception.

The job of the catch block of an exception handler is to catch any exception objects thrown from within the try block. It can then perform any cleanup or message notification as a consequence of the error.

A simple example can illustrate the basics of writing an exception handler. Suppose that you need to divide two numbers. Division, of course, can be the cause of a frequent problem: divide-by-zero errors. However, the following code handles this problem in a graceful manner:

int z = 0;
try {
    z = x/y;
    System.out.println("Z = " + z);
}
catch (Exception e) {
    System.out.println("Divide by zero error.");
}

If the variable y in this example is not 0, the division goes fine, and the result is printed. However, if the variable is 0, an exception is thrown. The Java runtime system will then try to find an exception handler to manage the error. Fortunately, Java will not have to look very far. The catch clause in the exception handler catches the thrown exception. This handler prints the fact that there is a divide-by-zero error to standard output.

An exception handler has an optional block placed at the end of the code called the finally block, which provides code to be executed regardless of whether an exception occurs.

int z = 0;
try {
    z = x/y;
    System.out.println("Z = " + z);
}
catch (Exception e) {
    System.out.println("Divide by zero error.");
}
finally {
    System.out.println("Finally: Z = " + z);
}

Regardless of what happens in the division operation, the last print statement is invoked.

When to Catch Exceptions

When should you catch an exception? Sometimes exceptions are generated as a result of a normal runtime violation, such as a division error or an array out of bounds. However, methods can also explicitly throw exceptions. A method can declare that it can throw an exception via the throws clause.

For example, it was stated earlier that the Integer class has a method, called parseInt(), that takes a String and converts it to an int data type. When the String cannot be converted to a number, however, the parseInt() method throws a NumberFormatException object.

The following code tries to convert some Strings to integers and prints their values:

String s = "100";
String s2 = "Not a number";
try {
      System.out.println("The first number is: " +
         Integer.parseInt(s));
      System.out.println("The second number is: " +
         Integer.parseInt(s2));
}
catch (NumberFormatException e) {
      System.out.println("Cannot convert String to
number!");
}

The first conversion succeeds, but the second fails. The NumberFormatException generated is caught by the exception handler, and an error message is printed. The calling code knows that parseInt() throws this kind of exception because of how the method is declared:

public static int parseInt(String s) throws NumberFormatException

The throws section of the declaration indicates that the method may throw a certain type of exception if an operation cannot be performed. Whenever you use a method that has a throws clause, you should probably catch the type of exception the method throws. Depending on the type of exception thrown, an exception handler may or may not be required for a successful compile. Categories of exceptions are discussed in more detail in the next chapter.

To throw an exception, you use the throw keyword. The throw statement passes a throwable object that follows it to the runtime environment, which then begins trying to find an exception handler to catch the object. The method that throws the exception is no longer executed after the throw statement is invoked.

Exceptions, like everything else in Java, are objects. Consequently, you can generate an exception object by instantiating it with the new operator. You would then use the throw keyword to throw the object. Because the parseInt() method throws a NumberFormatException, it's likely that there is a line of code in the method similar to the following:

throw new NumberFormatException();

Exception Handlers and Exception Classes

As indicated in the previous section, exceptions have different types, or more specifically, classes. For example, NumberFormatException refers to a specific class. When an exception is generated, the Java runtime system looks for an appropriate handler to catch the exception. The details of this process are explained in the next chapter, but remember that whether or not a handler is appropriate has to do with the class of the exception.

To clarify this, consider a couple of exception classes. The ArithmeticException class represents errors caused by illegal arithmetic operations, usually divided by zero. Objects of class ArrayIndexOutOfBoundsException are thrown when an invalid array index is accessed. The Exception class is used to represent the general class of exceptions; the other exception classes that have been discussed are actually subclasses of Exception.

The following example shows how these exceptions are used:

try {
      int x = Integer.parseInt(s);
      int y =Integer.parseInt(s2);
      int index =Integer.parseInt(s3);
\      int a[] = new int[5];
      a[index] = x/y;
      System.out.println("Everything worked!");
}
catch (ArithmeticException e) {
      System.out.println("Arithmetic exception");
}
catch (ArrayIndexOutOfBoundsException e) {
      System.out.println("Array index out of bounds
exception");
}
catch (NumberFormatException e) {
      System.out.println("Cannot convert String to
number!");
}
catch (Exception e) {
      System.out.println("Generic exception");
}

The first part of the code takes three input Strings and tries to convert them into integers; if the conversion fails, a NumberFormatException object is thrown. However, the third catch clause will catch the thrown object. If the clause were not there, the last catch clause would catch the problem because NumberFormatException is a subclass of Exception. The general rule is this: Java searches the catch clauses in the order they are declared, looking for a handler that either matches the class of the exception thrown or is a superclass of it. The runtime system will use the first handler that matches. If no appropriate handler is found, Java will go up the runtime stack and look at the next method. Java looks for an appropriate handler in the same way. This process repeats until the top of the stack is reached or until an appropriate handler is found. If there is no appropriate handler, Java dumps the stack trace to standard output. Depending on the nature of the class being thrown, the program may terminate abnormally.

After the Strings are converted to numbers, the program tries to divide the numbers. If the program divides by zero, an ArithmeticException object is thrown. The object is caught by the ArithmeticException catch clause, although the Exception clause also would have handled it.

Finally, the code assigns the divided number to an array index. If the index is illegal, an ArrayIndexOutOfBoundsException is thrown. However, there is also an appropriate handler for it, so the code finishes gracefully.

Nested Exceptions

You can also nest exceptions. To illustrate this, look at the following rework of the preceding example:

int y = 0;
int z = 0;
int z = 0;
int index = 0;
int a[] = new int[5];
try {
      x = Integer.parseInt(s);
      y =Integer.parseInt(s2);
      index =Integer.parseInt(s3);
      a[index] = x/y;
      System.out.println("Everything worked!");
}
    catch (ArithmeticException e) {
      System.out.println("Arithmetic exception. Try to assign Zero");
      try {
         a[index] = x/z;  // Try another division to
the index
      }
      catch (ArithmeticException e2) {
         System.out.println("Another Arithmetic exception");
      }
}
catch (ArrayIndexOutOfBoundsException e) {
      System.out.println("Array index out of bounds
exception");
}
catch (NumberFormatException e) {
      System.out.println("Cannot convert String to number!");
}
catch (Exception e) {
      System.out.println("Generic exception");
}

If the int variable y evaluates to 0, an ArithmeticException is thrown. When it is caught, the code attempts to perform yet another division. Unfortunately, this is also a divide-by-zero
error. Fortunately, this is caught in a nested ArithmeticException catch block. If there were no divide-by-zero problem but the index were illegal, the outer ArrayIndexOutOfBoundsException catch clause would catch the exception.

You learn more about exception handling in the next chapter. You see how Java organizes its exception classes, what methods to use, and how to use them. The following chapter also discusses how to write your own exception classes within the framework established by Java.

Organizing Projects in Java

You've seen a wide variety of constructs that Java uses to help you develop classes. These are useful for quickly getting an applet up and running. But what happens if you are working on a large-scale project that requires a carefully designed and organized class hierarchy? How will Java help you manage all of the classes required in such an environment?

Fortunately, Java provides a variety of constructs that help you develop large-scale software as well as small applets. These constructs range from those that help you lay out your design to those that help you pull the pieces together. With these techniques in hand, you can use Java to design applets that can meet your current requirements and can be used as a foundation for future development.

Abstract Methods

Suppose that you're defining a class that manages files on your hard disk. You can define most of the high-level methods, such as how to list files and their attributes; however, you can't define low-level behavior, such as how you actually get the file attributes. Because this is platform dependent, you want to leave this behavior as undefined so it can be implemented for practical purposes.

In Java, you use abstract methods and abstract classes to define a template for a class that is well defined except for a few methods. A class is abstract if one or more methods are defined by the abstract keyword. The following case shows how you might structure the discussed file manager as an abstract class:

abstract class FileManager {
   // Abstract class that enumerates files...
   public abstract String enumerateFiles(String file);
   // Practical implementation. List all files
   // using abstract enumerateFiles...
   public void dir() {
     // ... call enumerateFiles...
   }
   // ... other methods
}

In this example, the only abstract method is enumerateFiles(), which provides the low-level implementation of getting a file attribute. However, this is enough to make the class abstract. All the other methods are well defined, so if you create a class that provides a practical implementation of enumerateFiles(), the class will be ready to use. As long as a class is abstract, though, it cannot be instantiated.

You need to subclass the FileManager class to create a class that can be implemented. Here is one way to do it:

class MyFileManager extends FileManager {
   // Enumerate all files...
   public String enumerateFiles(String file) {
      // ... do the platform specific operation...
   }
}

This class uses all the methods of FileManager and is ready to be instantiated.

There are a couple of restrictions in creating abstract methods. Constructors and private or static methods cannot be declared as abstract. (If you think about the semantics of these, it should be clear why they cannot be abstract.) You also cannot replace a superclass method with an abstract method.

Interfaces

If you have a class that is nothing but abstract methods, it's better not to use the abstract keyword. Java provides a technique called interfaces, which you can use to define a template of behavior. Like abstract methods, interfaces cannot be instantiated. Interfaces differ from abstract methods, however, in that no interface methods can have a body. Interfaces are strictly templates.

By their nature, interfaces are a way of defining a design. It specifies the kinds of behaviors that something needs to have. Classes that implement the interface are said to provide the implementation of the design.

Interfaces are also Java's way of dealing with the limitations of single inheritance. Unlike C++, Java avoids multiple inheritance because of all the problems related to it, such as name ambiguity. Interfaces are a way of adding behavior to a class without compromising its fundamental behavior. However, a class can implement multiple interfaces and so simulate multiple inheritance.

The following example shows how interfaces are used. Suppose that you're working for a printer company. You have a well-defined and tested Printer class and a Document class that represents what is to be printed. However, your company wants to move into the fax and copier markets, so a multifunction printer will be developed.

You define two interfaces for copiers and fax machines as follows:

interface Copier {
  public void copyDocument(Document d);
}

interface Fax {
  public void transmitDocument(Document d);
}

These interfaces define the behavior that a copier and fax machine should have, respectively. You can also add this behavior to the Printer class to get the desired multifunction printer. You can implement these interfaces in a class that is a subclass of Printer:

class MultiFunctionPrinter extends Printer
 implements Copier, Fax {
  public void copyDocument(Document d) {
   // Practical implementation of Copier...
  }
  public void transmitDocument(Document d) {
   // Practical implementation of Fax...
  }
}

You now have a multifunction Printer class!

It should be noted that interfaces can have variables but cannot have modifiers.

Packages

Packages are a mechanism for grouping classes and interfaces. Every class or interface has a package. You can explicitly name the package in which it appears by using the package statement:

package myPackage;

If no package is specified, a default package is used (usually represented by the current directory).

Java has a hierarchical naming convention for naming packages, known as a dot notation. The top level of the notation represents the producer of the source file, such as "java." After that, you would name subhierarchies. For example, the JDK has a package called "lang" that is used to house classes related to the basic mechanics of programming in Java. The String class is in the "lang" package. You can therefore refer to the class from anywhere in your source code as java.lang.String. This dot structure is reflected in the directory organization of the source code files that make up the package. Thus, the String.java file will be located off a java/lang subdirectory.

Given this, the package declaration for the String class would be

package java.lang;

The package declaration must be the first non-comment, non-white space line in the file.

The import statement allows you to use a class defined in another package or source file. You can use the import statement in several ways. You can define the full reference to the class, as in the following:

import java.lang.String;

You can also bring in all classes in a package by specifying a wildcard:

import java.lang.*;

Anything imported is thereafter treated as part of the current package for the purposes of compilation.

You can also reference a class in the code by using the full package reference:

java.lang.String s = "A string";

Note
The Java runtime system looks for classes by the path specified by the CLASSPATH environment variable. You can specify multiple directories in the path. For example:
CLASSPATH=\java\classes;.
This path looks first in a Java directory for the class files and then in the current directory.
If the runtime system cannot find the class requested, it may look in the current directory, even if it was not specified in CLASSPATH.

A final note about compilation units: A compilation unit is a source code file that has at most one public class or interface. The file must be the same name as the public declaration, followed by the .java suffix. So if your source code declares a public class SimpleRecord, the file it appears in must be named SimpleRecord.java. While this convention may seem annoying at first, it will prove to be helpful because your files will give a quick listing of your public classes. The convention also helps you write more modular and easier-to-maintain code.

The Java Developer's Kit

You have now seen all of the fundamentals of Java programming. However, one last thing needs to be discussed. The Java Developer's Kit (JDK) is a series of class libraries organized as packages that give you a set of tools for creating software based on Java. The JDK consists of over 150 classes that provide functionality ranging from manipulating Strings to working with network sockets. A large number of these classes are used and explained throughout this book. Many of them are subject to extended tutorials, while others are explained in the course of describing a chapter project. A quick overview of the JDK will get you ready for the in-depth coverage that follows.

Eight packages make up the JDK. These packages are represented by the "java." notation mentioned earlier:

Summary

This first part of this book has introduced you to the fundamentals of Java. You're now ready to dive head first into the world of Java programming. Wear your safety belt! Each chapter that follows will have an involved project that explores a feature of Java in depth. Some of these projects are quite involved and touch on many of the classes in the JDK. By the end of Chapter 4, you will have been presented with almost every major aspect of writing an applet in Java (and more!). You're encouraged to explore these projects on your computer. Modify the code to give extra functionality or to add print statements to help you understand what is going on. The emphasis of this book is learning by example, so there will be no shortage of code to look at!