Now that you have seen some examples of Java programming, this section provides an overview of Java. Previous examples show that Java can be used to create both standalone applications and applets that can be embedded in an HTML document. As you learn the features of Java, you'll notice that Java has everything you need from a general-purpose programming language. Although Java has been associated mostly with the creation of applets to be embedded in Web pages, many people have come to realize that Java is also ideal for developing general-purpose applications. Nowadays, Java is also extensively used for server-side programs such as Java servlets that work with Web servers.
One of the reasons for Java's popularity is the portability of Java code. The compiled Java code runs on any system for which a Java interpreter and the Java libraries are available. It's not trivial to build the Java libraries, but once the Java interpreter and libraries are available on an operating system, you can run most Java applications unchanged on that system.
Java is an object-oriented language. So much so that Java does not allow any standalone procedures at all; all data and procedures must be inside an object. In fact, the entire Java application is an object as well.
The basic concepts of object-based programming are the same as those of other object-oriented programming languages such as Smalltalk or C++. There are three underlying concepts:
Data abstraction
Inheritance
Polymorphism
To understand data abstraction, consider the file input/output (I/O) routines in the C run-time library. These routines allow you to view the file as a stream of bytes and allow you to perform various operations on this stream by calling the file I/O routines. For example, you can call fopen to open a file, fclose to close it, fgetc to read a character from it, and fputc to write a character to it. This abstract model of a file is implemented by defining a data type named FILE to hold all relevant information about a file. The C constructs struct and typedef are used to define FILE. You can think of this definition of FILE, together with the functions that operate on it, as a new data type just like C's int or char.
To use the FILE data type in C, you do not have to know the data structure that defines it. In fact, the underlying data structure of FILE can vary from one system to another. Yet, the C file I/O routines work in the same manner on all systems. This is possible because you never access the members of the FILE data structure directly. Instead you rely on functions and macros that essentially hide the inner details of FILE. This is known as data hiding.
Data abstraction is the process of defining a data type, often called an abstract data type (ADT), together with the principle of data hiding. The definition of an ADT involves specifying the internal representation of the ADT's data as well as the functions to be used by others to manipulate the ADT. Data hiding is used to ensure that the the internal structure of the ADT can be altered without any fear of breaking the programs that call the functions that are provided for operations on that ADT. Thus, C's FILE data type is an example of an ADT (see Figure 26-2).
In object-oriented languages, you create an object from an ADT. Essentially, an ADT is a collection of variables together with the procedures necessary to operate on those variables. The variables represent the information contained in the object while the procedures define the operations that can be performed on that object. You can think of the ADT as a template from which specific instances of objects can be created as needed. The term class is often used for this template, consequently class is synonymous with an ADT. In fact, Java provides the class keyword precisely for the purpose of defining an ADT-the template from which objects are created. The ADT is a template for objects in the sense that creating an object involves setting aside a block of memory for the variables of that object.
In Java, the procedures that operate on an object are known as methods. This term comes from the object-oriented language Smalltalk. The methods define the behavior of an object. Another common term of object-oriented programming also originated in Smalltalk-the idea of sending messages to an object causing it to perform an operation by invoking one of the methods. In Java, you do this is by calling the appropriate method of the object.
In Java, you use the class keyword to define the blueprint of an object. That means a class defines the data and methods that constitute a type of object. Then, you can create instances of the object by using the new keyword. For example, you might create an instance of a Circle class as follows:
Circle c1 = new Circle(100.0, 60.0, 50.0);
This example invokes the Circle class constructor that accepts three floating-point numbers as argument (presumably, the coordinates of the center and the radius of the circle). You'll see a complete example of classes in the 'Geometric Shapes in Java' section, later in this chapter.
Data abstraction does not cover an important characteristic of objects. Real-world objects do not exist in isolation. Each object is related to one or more other objects. In fact, you can often describe a new kind of object by pointing out how the new object's characteristics and behavior differ from those of a class of objects that already exists. This is what you do when you describe an object with a sentence such as: B is just like A, except that B has ... and B does ... . Here you are defining objects of type B in terms of those of type A.
This notion of defining a new object in terms of an old one is an integral part of object-oriented programming. The term inheritance is used for this concept because you can think of one class of objects as inheriting the data and behavior from another class. Inheritance imposes a hierarchical relationship among classes where a child class inherits from its parent. In Java terminology, the parent class is known as the superclass; the child is the subclass. In a Java program, you use the extends keyword to indicate that one class is a subclass of another.
Insider Insight |
Unlike C++, Java does not support multiple inheritance (multiple inheritance allows a class to inherit from multiple superclasses). Java allows a class to have only one superclass. In Java, all classes inherit from a single root class named Object. (The Smalltalk programming language also provides a class hierarchy of this type). Here the term root class refers to a class that does not have any superclass. |
In a literal sense, polymorphism means the quality of having more than one form. In the context of object-oriented programming, polymorphism refers to the fact that a single operation can have different behavior in different objects. In other words, different objects react differently to the same message. For example, consider the operation of addition. For two numbers, addition should generate the sum. In a programming language that supports object-oriented programming, you should be able to express the operation of addition by an operator, say, +. When this is possible, you can use the expression x+y to denote the sum of x and y, for many different types of x and y: integers, floating-point numbers, and, even strings (for strings the + operation means the concatenation of two strings).
Similarly, suppose a number of geometrical shapes all support a method, draw. Each object reacts to this method by displaying its shape on a display screen. Obviously, the actual mechanism for displaying the object differs from one shape to another, but all shapes perform this task in response to the same method.
Polymorphism helps by allowing you to simplify the syntax of performing the same operation on a collection of objects. For example, by exploiting polymorphism, you can compute the area of each geometrical shape in an array of shapes with a simple loop like this:
// Assume "shapes" is an array of shapes (rectangles, circles, // etc.) and "computeArea" is a function that computes the // area of a shape. for(int i=0; i < shapes.length; i++) { double area = shapes[i].computeArea(); System.out.println("Area = "+area); }
This is possible because regardless of the exact geometrical shape, each object supports the computeArea method and computes the area in a way appropriate for that shape.
For a concrete illustration of object-oriented programming in Java, consider the following example. Suppose that you want to write a computer program that handles geometric shapes such as rectangles and circles. The program should be able to draw any shape and compute its area.
The first step in implementing the geometric shapes in Java is to define the classes. I'll start with an abstract class that simply defines the behavior of the shape classes by defining the common methods. The following listing shows the abstract Shape class, which you should save in the Shape.java file:
//--------------------------------------------------------------- // File: Shape.java // // Abstract class for Shape objects abstract public class Shape { abstract public void draw(); abstract public double computeArea(); }
As this listing shows, you use the abstract keyword to indicate that a class cannot be instantiated because it does not implement all the methods.
Caution |
If you know C++, you'll notice some superficial similarities between C++ and Java class definitions. Note, however, that the similarities are really superficial. As an experienced C++ programmer, I would say that you'll be better off if you approach Java as a brand-new object-oriented language rather than as a mutation of C++. You can still use your knowledge of C syntax, but when it comes to the object-oriented features of Java, you should not try to extrapolate from your knowledge of C++. |
After you define the abstract Shape class, you can create concrete versions of Shape classes such as a Circle and Rectangle. Here is a simple implementation of the Circle class that you should save in a file named Circle.java:
//--------------------------------------------------------------- // File: Circle.java // // Circle class public class Circle extends Shape { double x, y; double radius; public Circle(double x, double y, double r) { this.x = x; this.y = y; this.radius = r; } public double computeArea() { return Math.PI * radius * radius; } public void draw() { System.out.println("Circle of radius "+radius+ " at ("+x+", "+y+")"); } }
As this definition of the Circle class shows, this shape class implements the computeArea and draw methods that were declared as abstract in the Shape class. The Circle class also defines three double variables-x, y, and radius-that denote the coordinates of the center and the radius of the circle. Such variables are referred to as instance variables of the class.
The extends keyword in the Circle class definition indicates that Circle is a subclass of the Shape class. The public qualifier specifies that the Circle class is accessible from other packages (in Java, a package is a collection of classes).
Following the pattern of the Circle class, I defined the Rectangle class as shown in the following listing:
//--------------------------------------------------------------- // File: Rectangle.java // // Rectangle class public class Rectangle extends Shape { double x1, y1; double x2, y2; public Rectangle(double x1, double y1, double x2, double y2) { this.x1 = x1; this.y1 = y1; this.x2 = x2; this.y2 = y2; } public double computeArea() { return Math.abs((x1-x2)*(y1-y2)); } public void draw() { System.out.println("Rectangle with corners "+ "("+x1+", "+y1+") and "+ "("+x2+", "+y2+")"); } }
Just as the Circle class is saved in the Circle.java file, the Rectangle class should be saved in the Rectangle.java file-at least that's how Sun's Java 2 SDK expects the class definitions.
Insider Insight |
The computeArea method of the Rectangle class calls the Math.abs method to get the absolute value of an argument. Although this looks like the invocation of an object's method, there is no object named Math; instead Math is the name of a class. The abs method is a static method in the Math class. You can invoke a static method without having to create an instance of the class. |
Now that I have defined the Circle and Rectangle classes, it's time to test them with a simple program. In Java, the program itself must be another class, and for a standalone Java program, the class must include a public static void method called main (this is akin to the main function in a C program).
The following listing shows the MakeShape class (in a file named MakeShape.java) that includes a main method to test the geometric shapes:
//--------------------------------------------------------------- // File: MakeShape.java // // Java application to try out various shapes public class MakeShape { public static void main(String args[]) { Shape shapes[] = new Shape[2]; shapes[0] = new Circle(100.0, 100.0, 50.0); shapes[1] = new Rectangle(80., 40., 120., 60.); System.out.println("\n"+shapes.length+" shapes\n"); for(int i=0; i < shapes.length; i++) { double area = shapes[i].computeArea(); System.out.println("Shape #"+i); shapes[i].draw(); System.out.println("Area = "+area); System.out.println("-----------"); } } }
The main method creates an array of Shape objects and initializes the array with different shapes. Then, the computeArea and draw methods of each shape are invoked.
Insider Insight |
You use the new keyword to create instances of objects in Java. There is no corresponding delete or free keyword to get rid of objects when you no longer need them. Instead, Java supports a technique called garbage collection to automatically destroy objects that are no longer needed (Java keeps track of all references to an object and removes the object when there are no more references to it). That means you never have to worry about freeing memory in Java. Because memory management is a source of many errors in C and C++ programs, Java's support for garbage collection helps eliminate a major source of potential errors in your applications. |
In this example, the MakeShape class is the Java application. To compile the program, type the following command:
javac MakeShape.java
This step takes care of compiling the MakeShape.java file and all the related classes (the Shape.java, Circle.java, and Rectangle.java files). In other words, the Java compiler (javac) acts a bit like the make utility in UNIX-javac determines the dependencies among classes and compiles all necessary classes.
To run the MakeShape program, use the Java interpreter, as follows:
java MakeShape
The following listing shows the result of running the MakeShape program in a terminal window in Red Hat Linux:
2 shapes Shape #0 Circle of radius 50.0 at (100.0, 100.0) Area = 7853.981633974483 ----------- Shape #1 Rectangle with corners (80.0, 40.0) and (120.0, 60.0) Area = 800.0 -----------
A Java program consists of one or more classes. For standalone applications, you must have a class with a public void static main method. Applets require only a subclass of the Applet class.
The Java 2 SDK expects the source files to have the same name as the class, but with a .java extension (thus, a class named Circle is defined in the file Circle.java). However, other Java development environments may have a different convention for naming files.
Within each source file, the parts of the program are laid out in the following manner:
The file starts with some comments that describe the purpose of the class and provide other pertinent information such as the name of author and revision dates. Java supports both C and C++-style comments. As in ANSI C, comments may start with a /* and end after a */ pair. Or, you may simply begin a line of comment with a pair of slashes (//). A special type of comment, known as doc comment, begins with a /** and ends with */. The doc comments can be extracted by the javadoc utility program to create online documentation for classes.
One or more import statements that allow you to use abbreviated class names. For example, if you use the java.applet.Applet class, you can refer to that class by the short name Applet provided you include the following import statement in the source file:
import java.applet.*;
Note that the import statement does not really bring in any extra code into a class; it's simply a way to save typing so you can refer to a class by a shorter name (for example, Applet instead of java.applet.Applet).
The class definition that includes instance variables and methods. All variables and methods must be inside the class definition.
Java supports the standard C data types of char, short, int, long, float, and double. Additionally, Java introduces the byte and boolean types. Unlike C, however, Java also specifies the exact size of all primitive data types (in C, the size of the int type varies from one system to another). Table 26-2 summarizes Java's primitive types.
Type |
Description |
---|---|
boolean |
A 1-bit value that contains true or false. A boolean value is not an integer. |
byte |
An 8-bit signed integer with values between -128 and 127 |
char |
A 16-bit unsigned integer value representing a Unicode character (Unicode is a character encoding system designed to support storage and processing of written texts of many diverse languages). You can initialize a char variable with the notation \uxxxx, where xxxx is a sequence of hexadecimal digits (for example, char c = \u00ff;). |
double |
A 64-bit, double-precision floating-point value in the IEEE 754 format (a standard format that expresses a floating-point number in binary format) |
float |
A 32-bit, single-precision floating-point value in IEEE 754 format |
int |
A 32-bit signed integer with values between -2,147,483,648 and 2,147,483,647. |
long |
A 64-bit signed integer with values between -9,223,372,036,854,775,808 and 9,223,372,036,854,775,807. |
short |
A 16-bit signed integer with values between -32,768 and 32,767. |
Although most of the primitive types should be familiar to C programmers, the following C-style usage are not allowed in Java:
There is no unsigned keyword in Java; all integer values are signed.
You cannot write short int or long int (these combinations are often used in C programs).
The nonprimitive data types in Java are objects and arrays. All nonprimitive types are handled by reference-that means when you pass a nonprimitive type to a method, the address of the object is passed to the method. The primitive types, however, are passed by value-that means a copy of the data is passed to the method.
Java includes a built-in String type, and the compiler treats a String almost like a primitive type. Java's String class is a part of the java.lang package, which the Java compiler uses by default.
The Java compiler automatically converts text within double quotes ("...") into a String. Also, the + operator concatenates String objects. For example, you can write code such as the following:
String welcome = "Welcome to " + "Java."; int numchars = welcome.length(); System.out.println("There are "+numchars+ " characters in: "+welcome);
When executed, this code prints the following:
There are 16 characters in: Welcome to Java.
The String class supports a number of other methods that provide the functionality of the C's string manipulation functions (that are defined in the C header file <string.h>). Java includes two related classes for handling text strings:
String to hold a string of characters that cannot be individually manipulated (in other words, you cannot insert or replace characters in the string).
StringBuffer to represent a string of characters that may be manipulated as necessary. You can append, insert, and replace characters in a StringBuffer object.
Table 26-3 summarizes the methods of the String class.
Method |
Description |
---|---|
char charAt(int index); |
Returns character at a specified index |
int compareTo(String s); |
Compares this String with another String s |
String concat(String s); |
Concatenates this String with another String |
String copyValueOf(char[] ca); |
Returns a String equivalent to the specified array of characters. This is a static method of the String class. |
boolean endsWith (String suffix); |
Returns true if String ends with suffix |
boolean equals(String s); |
Returns true if this String matches another String s (case-sensitive comparison) |
boolean equalsIgnoreCase s (String s); |
Returns true if this String matches another String regardless of case (case-insensitive comparison) |
byte[] getBytes(); |
Copies characters from a String into a new byte array (uses the default character-encoding of the system) |
byte[] getBytes(String encodingName); |
Copies characters from a String into a new byte array according to the character encoding specified by a name such as US-ASCII, ISO-8859-1, UTF-8, UTF-16, and so on |
void getChars(int srcBegin, int srcEnd, char[] dest, int destBegin); |
Copies characters from a String into a char array |
int hashcode(); |
Returns hash code for the String |
int indexOf(int c); |
Returns index of first occurrence of a character |
int indexOf(String s); |
Returns index of first occurrence of String s in the current String |
String intern(); |
Returns a unique instance of String from a global shared pool of String objects |
int lastIndexOf(int c); |
Returns index of the last occurrence of a character |
int lastIndexOf(String s); |
Returns index of last occurrence of String s in the current String |
int length(); |
Returns the length of the String (the number of characters in the string) |
boolean regionMatches (int offset, String s, String s int soffset, int length); |
Returns true if a region of the String matches the specified region of another |
String replace(char old, char new); |
Replaces all occurrences of old character with new |
boolean startsWith (String prefix); |
Returns true if String starts with specified prefix |
String substring(int begin, int end); |
Returns a substring of this String |
char[] toCharArray(); |
Returns a character array from the String |
String toLowerCase(); |
Converts String to lowercase |
String toString(); |
Returns another String with the same contents |
String toUpperCase(); |
Converts String to uppercase |
String trim(); |
Removes any leading and trailing white space from String |
String valueOf(type obj); |
Returns the String representation of an object's value (type may be any object of primitive type such as boolean, char, int, long, float, and double). This is a static method of the String class. |
Insider Insight |
The String class is defined with a final keyword. That means you cannot subclass String. The Java compiler can optimize a final class so that the class can be used as efficiently as possible. To declare a class as final, use the following syntax: public final class ClassName extends SuperClassName { // Body of class } |
An array is an ordered collection of one or more elements. Java supports arrays of all primitive as well as nonprimitive data types. As in C, you can declare an array variable by appending square brackets to the variable name, as follows:
byte buffer[] = new byte[256]; // Create an array for 256 byte variables. Shape shapes[] = new Shape[10]; // Create an array to hold 10 Shape objects.
Exceptions refer to unusual conditions in a program. They can be outright errors that cause the program to fail or conditions that can lead to errors. Typically, you can always detect when certain types of errors are about to occur. For instance, when indexing an array with the [] operator, you could detect if the index is beyond the range of valid values. Although you could check for such an error, it's tedious to check for errors whenever you index into an array. Java's exception-handling mechanism allows you to place the exception-handling code in one place and avoid having to check for errors all over your program.
The following code fragment illustrates the syntax of try, catch, and finally blocks in Java:
try { // Code that may generate exceptions of type AnException and // AnotherException } catch(AnException e1) { // Handle exception of type AnException } catch(AnotherException e2) { // Handle exception of type AnotherException } finally { // This block of code is always executed after exiting the try block // regardless of how the try block exits. }
Java does not support multiple inheritance, so each class can have only one superclass. However, sometimes you may want to inherit other types of behavior. For example, you may want to support a specific set of methods in a class (this is akin to saying that your class can do x, y, and z). Java provides the interface keyword that allows a class to implement a specific set of capabilities.
An interface looks like an abstract class declaration. For example, the run method is used by the Thread class (see the 'Threads' section) to start a thread of execution in a Java program. The Runnable interface specifies this capability through the following interface declaration:
public interface Runnable { public abstract void run(); }
If you implement all the methods of an interface, you can declare this fact by using the implements keyword in the class definition. For example, if a class defines the run method (which constitutes the Runnable interface), the class definition specifies this fact as follows:
import java.applet.*; public class MyApplet extends Applet implements Runnable { // Must include the run() method public void run() { // This can be the body of a thread. } // Other methods of the class // ... }
Java includes built-in support for threads. A thread refers to a single flow of control-a single sequence of Java byte codes being executed by the Java Virtual Machine. Just as an operating system can execute multiple processes (by allocating short intervals of processor time to each process in turn), the Java Virtual Machine can execute multiple threads within a single application. You can think of threads as 'multitasking within a single process.'
The best way to understand threads is to go through a simple example. The following listing shows the DigitalClock applet that uses a thread to display the current date and time:
//--------------------------------------------------------------- // File: DigitalClock.java // // Displays a digital clock using a thread. import java.util.*; import java.awt.*; import java.applet.*; public class DigitalClock extends Applet implements Runnable { private volatile Thread clockThread; int interval = 1000; // The run method is required by the Runnable interface. public void run() { Thread curThread = Thread.currentThread(); while(curThread == clockThread) { try { curThread.sleep(interval); } catch(InterruptedException e) {} // Repaint the clock. repaint(); } } public void start() { // Create and start the clock thread. clockThread = new Thread(this); clockThread.start(); } public void stop() { // Get rid of the clock thread. clockThread = null; } // The paint method displays the date and time. public void paint(Graphics gc) { Font helv = new Font("Helvetica", Font.ITALIC, 16); gc.setFont(helv); gc.drawString(new Date().toString(), 4, 20); } }
Save this listing in the DigitalClock.java file and compile the applet with the following command:
javac DigitalClock.java
Next, create an HTML document, DigitalClock.html, and embed the applet as follows:
<html> <body> <applet width=200 height=24 code=DigitalClock> A Java clock applet. </applet> </body> </html>
Now, type the following command to run the applet using the appletviewer:
appletviewer DigitalClock.html
Figure 26-3 shows the resulting appletviewer window with the output from the Clock applet.
As the applet runs, it should update the clock display every second.
After you have seen the DigitalClock applet in action, you should go through its listing to see how a thread works. When going through the DigitalClock applet's code, you should note the following:
To create a thread, you need an instance of a class that implements the Runnable interface. Typically, you would implement the Runnable interface in your applet by providing a run method. Then, you can create the Thread object by calling its constructor as follows:
myThread = Thread(this);
The Java Virtual Machine (JVM) calls the start method of the applet when the applet should run and the JVM calls the stop method when the applet should stop running. Thus, the applet's start method is a good place to create the Thread object.
After you create the Thread object, you must call that Thread object's start method (note that this is different from the start method of the applet) to start running the thread.
When started, the Thread executes the body of the run method. Thus, the run method should include code to perform whatever task you want the thread to perform.
When you want to stop the thread, set the Thread's reference to null, as follows:
myThread = null;
The Java garbage collection mechanism would clean up the Thread at an appropriate time.