All Categories :
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.
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.
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;
}
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.
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.
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.
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.
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.
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.
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
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().
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.
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 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."
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.
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.
| Class | Description
|
| 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.
|
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.
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.
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.
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 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.
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.
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.
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.
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.
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 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();
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.
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.
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.
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.
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 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.
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:
- java.lang: This package contains classes related to
the basic workings of the Java environment. Such activities include
wrappers for basic data types and strings, classes for managing
threads, a class for mathematical operations, a wide variety of
classes for describing exceptions, the System class, and the Object
base class. The classes in this package appear throughout this
book.
- java.io: Classes for controlling
file and stream operations. The fundamentals of using this package
are introduced in Chapter 4, "Enhancing
the Spreadsheet Applet." The java.io classes for stream operations
are employed in many of the chapter projects.
- java.util: This package
provides a series of utility classes: a class for manipulating
dates; basic data structure classes, such as hash tables and stacks;
a class and interface for writing classes that use the model-view
paradigm; and a tool for parsing Strings. Classes in the java.util
package are used throughout this book.
- java.applet: This package
contains the Applet class that ties your applet to its working
environment, such as a browser. It also has a class that allows
you to more directly exploit the native browser's capabilities.
Classes of the java.applet package are explored in detail in Chapter 6,
"Building a Catalog Applet," and are employed throughout
this book.
- java.awt: The AWT (Abstract
Window Toolkit) package provides simple and advanced tools
used to direct the visual interface of a Java applet. The most
important of the AWT tools is a set of standard controls that
allow you to interact with the applet, such as buttons and text
fields. Because AWT is one of the cornerstones of Java applet
programming, Part II of this book is dedicated to exploring the
package. Furthermore, various techniques for using AWT classes
appear throughout the remainder of this book.
- java.awt.image: This "sub-package"
of AWT provides the underpinnings for using images in Java. Although
it's a complex package, it is also very powerful. This package
is explored in depth in Part III of this book and in Chapter 14,
"Advanced Image Processing."
- java.awt.peer: This package
is used to tie AWT widgets to the underlying platform. Because
you will be more concerned with the AWT classes than with the
inner workings of these peers, this package is not discussed in
the following chapters.
- java.awt.net: The classes
in this package are used to write Java network programs. This
package includes sockets classes for both the client and the server
and classes for referencing URLs. Part IV of this book explores
this package in depth and uses its classes to build a Java-based
HTTP server.
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!

Contact
reference@developer.com with questions or comments.
Copyright 1998
EarthWeb Inc., All rights reserved.
PLEASE READ THE ACCEPTABLE USAGE STATEMENT.
Copyright 1998 Macmillan Computer Publishing. All rights reserved.