This chapter examines the final pieces of the Java programming language: threads, exceptions, and streams. Java uses these structures to create and control simultaneous activities and to perform error handling. These structures also perform interprocess communication among Java-controlled activities.
With threads, a single application in Java can easily run multiple concurrent code execution. This enables an application to do such powerful things as download a file, update a screen, and respond to user input all at the same time without the large amount of overhead incurred by the traditional fork used in languages such as C/C++.
Exceptions are Java's powerful and flexible method for handling errors, replacing the return type error code found in other languages. They are objects in Java and provide the same extensibility as any other object. Exceptions help to enforce Java's goal of being the safest language in which to program.
Streams are Java's way of communicating with the outside world. Streams enable a Java application to communicate with files, networks, devices, and other applications, and also allow for communication between threads within the same application. Streams are based on reading or writing a sequence of bytes from or to an outside source. Because the flow of data is a simple stream of bytes, specific knowledge of where the data comes from or is going to is not needed. This adds to Java's strength as a portable language.
There are several examples in this chapter that demonstrate these programming concepts. Try them yourself; you must see their output to appreciate and understand them. This is especially true because Java is not a procedural language, and it is difficult for most human minds to conceptualize nonsequentially. And you, the Java programmer-to-be, will have a lot more fun with Java when these powerful concepts are utilized.
These three structures are discussed in detail in the following sections. It is important to have a grasp of the uses of threads, exceptions, and streams to have powerful, pleasing Java applications and applets.
Threads enable a single program to run multiple parts of itself at the same time. For example, one part of a program can display an animation on the screen while another part builds the next animation to be displayed.
For advanced programmers, threads are a lightweight version of a process. Threads are similar to processes in that they can be executed independently and simultaneously, but are different in that they do not have all the overhead that a process does.
The fork command is used to create a new process in C or C++. A forked process is an exact copy of the original process, with exact copies of variables, code, and so on. Making this complete copy of all of the parent process makes using the fork resource expensive. When they are running, the child processes are completely independent of the parent process inasmuch as they can modify data without any effect on the parent process.
Threads do not make copies of the entire parent process. Instead, only the code needed is run in parallel. This means that threads can be started quickly because they don't have all of the overhead of a complete process. They do, however, have complete access to all data of the parent process.
Threads can read and/or write data that any other thread can access. This makes interthread communication simpler, but can lead to multiple threads modifying data in an unpredictable manner. Additional programming care is required with threads.
Threads are useful programming tools for two main reasons. First, they enable programs to do multiple things at one time. This is useful for such activities as letting a user do something while something else happens in the background.
Second, threads enable the programmer to concentrate on program functionality without worrying about the implementation of multitasking schemes. A programmer can simply spin off another thread for some background or parallel operation without being concerned about interprocess communications.
To create classes that make use of threads, you can extend the class Thread or implement the interface Runnable. Both yield the same result. By implementing Runnable, existing classes can be converted to threads without having to change the classes on which they are based.
An example of creating a thread by extending class Thread follows:
public class MyMain {
public static void main (String args[]) {
CntThread cntThread; //declare thread
cntThread = new CntThread(); //create thread
cntThread.start(); //start thread running
try {System.in.read();} //wait for keyboard input
catch(java.io.IOException e){}
cntThread.stop(); //stop thread
}
}
class CntThread extends Thread {
public void run() {
int ix = 0;
while (true) {
System.out.println("running, ix = " + ix++); //write count to screen
try {Thread.sleep(1000);} //sleep 1 second
catch(InterruptedException e){}
}
}
}
Note |
To exit the program, press Ctrl+C. |
In this example, a thread is created that will write an incrementing counter to the screen. It will continue to count until the main routine receives a character from the keyboard, at which time the counting thread stops. This means you can press any key on the keyboard followed by the Enter key or press only the Enter key to stop the thread from counting.
This is an excellent example of the concept of threads because it introduces the keywords try and catch (discussed in the "Exceptions" section in this chapter) and also demonstrates the creation and termination of a thread.
The second way to create a thread is by implementing the Runnable interface. Like the previous example, the following code creates a thread that increments a counter until a character is entered from the keyboard:
import java.applet.*;
import java.awt.*;
public class MyApplet extends Applet implements Runnable {
int ix = 0;
Thread mainThread;
CntThread cntThread;
public void start() {
if (mainThread == null) {
mainThread = new Thread(this);
mainThread.start(); //start main thread
}
}
public void run() {
cntThread = new CntThread(this); //create CntThread instance
cntThread.start(); //start cntThread instance
}
public boolean keyDown(Event evt, int key) { //process key press
cntThread.stop(); //stop cntThread instance
return(true);
}
public void paint(Graphics g) {
g.drawString("running, ix = " + ix, 10,20); //write count to screen
}
}
class CntThread implements Runnable {
MyApplet parent;
boolean loop;
Thread cntThread;
public CntThread(MyApplet p) { //constructor for CntThread
parent = p; //save parent instance
}
public void start() {
if (cntThread == null) {
cntThread = new Thread(this); //create counting thread
cntThread.start(); //start counting thread
}
}
public void stop() {
loop = false; //set value to exit main while loop
}
public void run() {
loop = true;
while (loop == true) {
parent.ix++; //incremen t counter
parent.repaint(); //repaint screen
try {Thread.sleep(1000);} //sleep 1 second
catch(InterruptedException e) {}
}
}
}
Before you can view the applet in the applet viewer or your browser, you need to create an HTML document, such as the following:
<HTML>
<HEAD>
<TITLE>Using the Runnable Interface</TITLE>
</HEAD>
<BODY>
<APPLET CODE="MyApplet.class" WIDTH=400 HEIGHT=400>
<EM><B>You need a Java enabled Browser to see this cool applet</B><EM>
</APPLET>
</BODY>
</HTML>
This example also uses try and catch for exceptions. Look at the start and stop sections of the code. Starting and stopping threads are covered in detail later in the "Thread Methods" section of this chapter.
Instances of threads are created using the standard new keyword. Arguments to new can either use the Thread class explicitly, as in
mainThread = new Thread(this);
or specify a class that is a subclass of Thread, like this:
mainThread = new MyThreadClass();
In the first example, the current instance, this, of an object is initialized as a thread. Any object, however, can be passed as an argument to the Thread class.
Note that the Thread class has few constructors. If additional constructor types are needed, creating a subclass of Thread with the needed constructors is quite useful. The second thread-creation example allows for these possibilities.
You can control the execution of a thread in several ways using the stop, start, and destroy methods. The object remains in existence as long as the object is referenced somewhere, even if stop is invoked.
It is not necessary to explicitly destroy the thread object. Java's garbage collector takes care of this detail. If it is necessary to give the garbage-collection process a helping hand, make sure all references are removed to the thread object. The simplest way to do this is to assign the value null to all variables containing thread references, as in the following example:
Thread myThread = new Thread();
myThread.start();
myThread.stop();
myThread = null;
In this example, the thread object is instantiated with new. The thread is then started and stopped. Finally, null is assigned to the variable containing the thread instance. This last step ensures that Java's garbage collector will schedule a resource deallocation for that object.
All Java threads implement four methods: init, run, start, and stop. These are default methods in the Thread class; if a class implements Runnable, these methods must be declared explicitly.
The init method is called the first time a thread is started. It is usually used for initialization of objects, but can be used in any way the programmer sees fit. Here is an example of an init method:
public void init() {
index = 0;
}
This method simply assigns a value of zero to the index variable. Larger, more complex applications might include code that opens databases, creates display objects, or just about anything that should be done only the first time a thread is started.
The start method is called to start a thread execution. This method usually contains the code for actually creating and starting a thread. Here is an example of a start method:
public void start() {
if (mainThread == null) {
new mainThread = Thread(this);
mainThread.start();
}
}
This method checks to see if a thread has not yet been created by testing whether the thread object mainThread is null. If it is not, a new thread object is created using new, and then the thread itself is started by calling the object's start method.
The stop method contains the code needed to stop a thread's execution, often in the form of generating a signal that will be caught by all threads and that causes them to exit. stop also can change an object that causes a calling loop in the run method to exit. Here is an example of a stop method:
public void stop() {
loop = false; {
}
This method simply sets a variable to false. loop is used in a main loop run method for control flow.
The run method is similar in function to main in C or C++. run contains the main body of code to be executed by a thread. Here is an example of a run method:
public void run() {
loop = true;
while (loop == true) {
System.out.println("index = " + index++);
try {Thread.sleep(1000);}
catch(InterruptedException e){}
}
}
The body of this method consists of the initialization of the loop variable and a while loop that will continue executing until loop no longer has a value of true. The body of the loop prints out the value of the index variable to the screen and then sleeps for one second.
This method can be combined with the example from stop to control processing.
Java provides a means for assigning names to threads. Names can consist of any valid Java string. Naming threads makes it convenient to distinguish one thread from another and enables a parent process to query a thread for its name. A thread can be assigned a name at creation time or at any point thereafter. Threads can also be renamed at any time.
To assign a thread a name at creation time, simply use a Thread constructor that accepts a string as an additional argument, like this:
Thread myThread = new Thread(this."My first named thread");
In this example, the thread is instantiated by new and assigned a name of "My first named thread".
You can query a thread for its name using the getName method. getName returns the name associated with a specified thread object, as in the following example:
System.out.println("The name of this thread is " + myThread.getName());
This example prints out the name assigned to the thread object myThread. Using the previous example, this statement would print the following to the screen:
The name of this thread is My first named thread
A thread can also query for its own name in the same way:
System.out.println("My name is " + this.getName());
You can set or change a thread name after creation using the setName method. The parent process, the thread itself, or any other method that has access to the thread object can do this. Following is an example of changing a thread name using setName:
myThread.setName("My newly renamed first thread");
This example changes the name of the thread from "My first named thread" to "My newly renamed first thread".
Multiple threads can access the same object or method because threads are not independent processes with complete copies of data objects. However, there is no guarantee which thread will access an object at a given time, which can lead to unpredictable results. In situations when this is not acceptable, use the Java synchronization logic. The keyword that provides this logic is synchronized.
The synchronized keyword is used to lock an object long enough to execute a block of code. No other thread can make changes to the specified object while the block of code is being executed. Here is an example using synchronized:
public void printIndex() {
syncronized(index) {
index++;
System.out.println("index = " + index);
}
}
In this example, the printIndex method contains a synchronized block of code. In this block, the object index is locked while the block of code making up the body of the synchronized statement is executed. The increment of index and the printing of the value of index are contained inside the block. This ensures that the value of index is printed to the screen without having to worry that some other thread incremented index before it was printed.
synchronized also can be used as a modifier for methods, thus ensuring that only one thread at a time executes the method. The previous example could be rewritten using this construct as
public syncronized void printIndex() {
index++;
System.out.println("index = " + index);
}
Threads are Java's way of running multiple, parallel code segments simultaneously. They are a lightweight process that does not have all the overhead that a normal process has. Threads do not make complete new copies of variables in the way that the C/C++ fork command does, allowing for faster thread startup. It also means that threads have access to each other's data. This can make communication among threads easier, but synchronization of data access may be a problem.
Threads are based on the class Thread and can be implemented by either extending class Thread or using the interface Runnable. Using Runnable, you can add threading to existing classes.
All threads implement the init, start, stop, and run methods. You can explicitly define these in the code, or use the default methods that come with the Thread class. init is called the first time a thread is started, and it is a good place to put initialization code. start is called any time a thread is started. This is usually where thread initialization is done and the thread is actually started. stop is used to stop the execution of a thread and usually contains code that will terminate the main body of the thread. Finally, run is started by start and normally contains the body of the thread code.
The problem of multiple threads modifying the same variable can arise due to the structure of Java, which allows threads access to common variables. Also, the system is in control of scheduling the execution of threads, which may be a different schedule than the programmer had anticipated. You can control this situation by using synchronized. synchronized can be used on an object or on a method, and ensures that only one thread can execute a block of code at a given time.
So far, this book has presented the means by which a programmer can generate code correctly in Java. Correct code is, after all, is a valid programming goal. However, none but the simplest programs is error free; for example, simply not allowing enough space in an array for all needed elements results in an error.
There are problems that are beyond program control, and therefore also beyond the programmer's control. These include problems such as running out of memory. Other nonprogrammatic problems are the network being down or a hardware failure.
There are two main classes of problems in Java: errors and exceptions. Errors are caused by problems in Java itself and are generally of too detailed a nature for the program itself to solve. When an error is encountered, Java typically generates an error message to the screen and aborts the program.
Java handles potentially recoverable errors through exceptions, a special object class that handles virtually all errors in Java. Java continues the idea of making code as reusable and error-free as possible by treating errors as objects. Exception-handling code resides in the java.lang package and is automatically included in all compiled code.
The reason Java treats exceptions as objects is to be able to handle various errors using a standard extensible interface. This manner of treating exceptions confers all the standard advantages that object-oriented design provides, such as reusability and customization.
Exceptions can be handled in Java in several ways: In some cases they can simply be ignored; they can be handled directly by the code in which they occur; or they can be passed on to the code that called the method containing the occurrence of the exception in the hopes that it will be handled there.
If an exception is not handled explicitly anywhere in the code, it is passed on to the Java interpreter. The Java interpreter might handle it in some way or might simply exit. In the case of an applet within a browser, this may result in the browser dying. This is not a desirable activity, so exception handling within a program is generally recommended.
As mentioned previously, some exceptions in Java can be completely ignored. The Java compiler requires others to be handled in one fashion or another to get a clean compile. This latter case is the reason you have seen some exception handling in the examples shown in Chapters 6, "Fundamentals of the Java Language," and 7, "Building Objects," such as in the example of the sleep method. This method can generate an exception that Java requires to be handled explicitly.
You can customize existing exceptions or create new ones. Remember, all exceptions are located in class java.lang.Exception. To customize an existing exception, the program simply overrides the existing methods or rewrites the existing variables. To create a new exception, create a new class that extends java.lang.Exception.
Most Java exception handling is performed using the try, catch, throw, and finally methods. Of course, these methods can be extended if some unusual circumstance requires it.
Java uses the try, catch, and throw keywords to do actual exception handling. They are conceptually similar to a switch statement; think of try like the switch statement in terms of exactly identifying the condition to be tested.
catch is used to specify the action that should be taken for a particular type of exception. It is similar to the case part of a switch statement. There can be several catch statements in a row to deal with each of the exceptions that may be generated in the block specified by the try statement.
Understanding exception handling in Java requires that you learn some new terminology. The first concept you need to grasp is that of throwing an exception, Java's name for causing an exception to be generated. For example, say a method was written to read a file. If the method could not read the file because the file did not exist, this would generate an IOException. In Java terminology, it is said that the method threw an IOException.
Think of it as a horse throwing a shoe: You must stop everything before real damage is done.
The next term to learn in Java exception handling is catch. An exception catch is code that realizes the exception has occurred and deals with it appropriately. In Java terms, you say a thrown exception gets caught.
In the case of the IOException thrown because of the nonexistent file mentioned in the previous section, the catch statement writes an error message to the screen stating that the specified file does not exist. It then allows the user to try entering a different filename if the first was incorrect, or it may exit. In Java terminology, the IOException was caught.
try is the Java exception-handling term that means a Java program is going to try to execute a block of code that might generate (throw) an exception. The try is a way of telling the compiler that some attempt will be made to deal with at least some of the exceptions generated by the block of code.
The finally statement is used to specify the action to take if none of the previous catch statements specifically deals with the situation. It is similar to the default part of a switch statement. finally is the big net that catches everything that falls out of the exception-handling statement.
The following example is the standard structure for Java exception handling, incorporating try, catch, and finally:
try {
statement that generates an exception
}
catch (ExceptionType1 e) {
process exception type 1
}
catch (ExceptionType2 e) {
process exception type 2
}
finally {
process all other exception types
}
try is used to inform Java that a block of code may generate an exception and that some processing of that exception will be done immediately following the try. The syntax of try is
try statement;
or
try {
statement(s)
}
The keyword try begins the try construct and is followed by a statement or block containing the code that might generate an exception. This code could consist of several statements, one or more of which may generate an exception.
If any one statement generates an exception, the remaining statements in the block are skipped and execution continues with the first statement following the try construct, which must be a catch or finally statement. This is an important point to remember. It is an easy way to determine which block of code should be skipped if an error occurs. Here is an example:
public class MyMain {
public static void main (String args[]) {
int[] myArray = new int[10];
try {
System.out.println("Before valid array assignment");
myArray[0] = 1;
System.out.println("Before invalid array assignment");
myArray[100] = 1;
System.out.println("After array exception");
}
}
}
In this example, the array myArray is created with a length of 10. This is followed by a try statement that contains several statements. The first, third, and fifth statements simply write trace messages to the screen. The second statement contains a standard assignment statement that assigns the value 1 to array element 0. The third statement also assigns an array, but attempts to assign a value of 1 to element 100 of the array. Because the array is only 10 in size, this generates an ArrayIndexOutOfBounds exception.
In tracing the execution of the block of code following the try statement, the first three statements are executed normally. The fourth statement, the invalid assignment, will start to execute and then generate an exception, which causes execution to continue at the end of the block, skipping the fifth statement.
A compilation error will result if you attempt to compile this code as it stands because any try statement must be followed immediately by one or more catch or finally statements. No other type of statement is allowed after the end of the try statement and before the first catch or finally statement. (catch statements are explained in the next section.)
catch is used to handle a specific exception that has been generated in a try statement. The syntax is as follows:
catch (ExceptionType ExceptionObj) statement;
or
catch (ExceptionType exceptionObj) {
statement(s)
}
The keyword catch begins the catch construct, followed by a parameter list that contains an exception type and an exception object. This in turn is followed by a statement or block containing the code used to process the exception.
Think of the catch construct as a method that will be called by the Java runtime interpreter if the particular exception type specified in the parameter list is generated. The object specified in the parameter list, exceptionObj, is a variable that contains the exception object generated and is local to the catch block. Within this block it can be manipulated as needed. The object must be assigned to a variable whose scope resides outside the block to use it outside the catch block, as in the following example:
public class MyMain {
public static void main (String args[]) {
int[] myArray = new int[10];
try {
System.out.println("Before valid array assignment");
myArray[0] = 1;
System.out.println("Before invalid array assignment");
myArray[100] = 1;
System.out.println("After array exception");
}
catch(ArrayIndexOutOfBoundsException e) {
System.out.println("An array index error has occured");
}
}
}
This example continues the earlier try example in the "Using try in Exception Handling" section. The required catch statement has been added after the try statement. This catch statement will catch the ArrayIndexOutOfBoundsException specifically. If any other exception occurs, this catch statement is ignored. In this case, an ArrayIndexOutOfBoundsException is generated, so the body of this catch statement will be executed. This body simply generates an error message to the screen. Execution continues normally with the first statement following the catch block that is not a catch or finally block.
Additional catch statements can follow the first catch statement. They must immediately follow the try/catch statement; otherwise a compilation error will occur. When an exception in a try statement is generated, the Java interpreter will treat all the catch statements following the try statement as cases in a switch statement. The first statement that is matched will be executed, and the remainder will be skipped. (An example of this is provided in the introduction to the "Exceptions" section of this chapter.)
Note that Java does not require any processing of the exception at all. Simply having a catch statement for an exception with an empty block is sufficient to avoid program abortion. This has been used in several examples in Chapters 6 and 7 with the sleep method.
sleep is used to put a process to sleep for a specified period of time, during which an InterruptedException could be generated. This could result from some other process signaling this process to continue or quit, for example. In the examples used in this book, catching this type of exception is considered unimportant, and no processing is required. However, because in most cases the compiler generates an error if the exception generated by sleep is not caught, an empty catch statement is used, as in the following:
try {Thread.sleep(timePeriod);}
catch(InterruptedException e);
As you can see, the catch statement simply ends in a semicolon (;), which does nothing. The result is an InterruptedException error that is caught and then totally ignored.
finally is used to handle any exception generated within a try statement. The catch statement discussed previously can handle only one type of exception; the finally statement can handle any type of exception. Following is an example of the syntax:
finally statement;
or
finally {
statement(s)
}
The keyword finally begins the finally construct, followed by a statement or block containing the code used to process the exception.
As with the catch statement, a finally block must follow immediately after a try or other catch statement or a compilation error results. A finally statement acts like a default case in a switch statement if it follows a series of catch statements. Anything that was not explicitly matched by an earlier catch statement is caught by the finally statement. A finally statement cannot be followed by another catch or finally statement or a compilation error will result. Because finally catches everything, additional statements would never be executed in any case. The following code fragment provides an example:
char readFile() {
char ch = '\0';
while (ch == '\0') {
try {
ch = myFile.read();
}
catch (EOFException e) {
system.out.Println("End of file encountered");
break;
}
catch (FileNotFoundException e) {
promptUserForFileName(fileName);
myFile = openFile(fileName);
}
finally() {
system.out.Println("Unexpected error encountered while reading file");
system.Exit(1);
}
}
return(ch);
}
In this example, a while loop is used to read a single character from a file. The first statement in the loop is a try statement that attempts to read a character from a file. It is followed by two catch statements that look for two specific exception types. A finally statement follows that catches all other types of errors. The loop itself is followed by a return of the character read.
Many types of errors can occur when reading a file, including End of file, File not found, Access permission errors, and others. The first catch statement handles End of file exceptions by simply exiting the loop, resulting in the return of a null character. The second catch statement handles File not found errors by prompting the user for a new or correct filename, opening the file, and then allowing the while loop to try again. Lastly, the finally statement catches all other errors. It writes an error message to the screen and then causes the application to exit.
throw is used to cause the code itself to generate an exception. It is then up to calling routines to handle the exception. This is the preferred way of passing error information to calling routines in Java. The syntax follows:
throw(ExceptionObj);
The keyword throw begins the throw construct, followed by a parameter list containing a single item. This item is an exception object, an object that has been declared to be of the desired exception type.
The exception type can be any of those predefined in Java or one custom designed for the job. Most exceptions can be found in java.lang.Exception. To design a new exception, extend the class java.lang.Exception. Here is an example:
public class MyMain {
public static void main (String args[]) {
MalformedURLException e;
e = new MalformedURLException("Are you a novice? Get your URL's right!");
throw(e);
}
}
In this example, an exception object (e) is declared. The object itself is then created with a standard new statement, in which the constructor allows replacing the standard error text for this exception. Finally, the exception object is thrown.
When an exception is thrown using the throw statement, current code execution stops. The exception is passed to the calling routine, and no other code is executed until the exception is caught somewhere. At that time, execution continues where the exception is caught. Keep this in mind when using the throw statement; it is usually a good idea to clean up as much as possible before throwing an exception. It is not a good idea to depend on the calling routines to clean up.
In some cases it may be desirable to catch a standard Java exception and then generate a customized exception by simply including a throw in a catch block, as in the following example:
public class MyMain {
public static void main (String args[]) {
int[] myArray = new int[10];
try {
myArray[100] = 1;
}
catch(ArrayIndexOutOfBoundsException e) {
e = new ArrayIndexOutOfBoundsException("Please insure your array
Âindex is within bounds.");
throw(e);
}
}
}
This is a modified version of the earlier try and catch examples. In this version, a customized error message is generated regarding the array index problem by reassigning e to a new exception object that includes the customized message and then using throw to throw another exception that must be caught at a higher level. If a catch or finally statement followed this catch statement, it would be ignored. However, if this try statement were itself enclosed in a different try statement, the execution would catch the exception. In other words, it is possible to catch a self-generated exception by putting a throw statement inside a try statement. This is rather like playing catch with yourself!
Previously in the section titled "Exceptions," references were made to not only customizing existing exceptions, but creating new exceptions. These can be used to provide customized error codes for a particular application. Java does not provide a lot of numeric error-return codes as are found in other languages, so creating new exceptions falls right in with the Java philosophy.
Class java.lang.Exception is the superclass for all exceptions in Java. Creating your own extensions is simply a matter of extending this class just like extending any other class. For instance, to create a new exception called MyOutOfRangeException, the following implementation could be used:
public class MyOutOfRangeException extends Exception {
public MyOutOfRangeException () {
super();
}
public MyOutOfRangeException (String s) {
super(s);
}
}
In this example, a new class called MyOutOfRangeException has been created by extending the Exception class. It contains two constructors. The first one calls the exception superclass constructor. This creates a new exception, but does not provide a way for the calling routing to customize the message. The second constructor is the same, but enables the calling routine to specify the error message associated with the exception.
To use this exception, simply declare a variable of type MyOutOfRangeException, initialize it with one of the two constructors, and then throw it, as in the following example:
public class VerifyRange {
MyOutOfRangeException e;
public void verifyIntRange (int value, int low, int high)
Âthrows MyOutOfRangeException {
if ((value < low) || (value > high)) {
e = new MyOutOfRangeException("number " + value + " out of range");
throw(e);
}
}
}
In this example, a class called VerifyRange, to verify ranges of objects, has been created. The only method, VerifyIntRange, verifies that a value passed as an argument falls within the range also specified in the method's arguments. If the argument falls outside the range, a MyOutOfRangeException exception is created with new and then thrown with throw.
Notice that the method specifically declares that it throws this exception in the method declaration. Because of this, any calling routines are required to either provide an exception handler or explicitly declare that the method may pass on this exception. This would not be required if the throw portion of the declaration were left off. It is up to the programmer to determine whether an exception is important enough to require handling on the part of the calling routine.
Exceptions are Java's way of performing error handling. They are used in many ways as a replacement for the more standard return codes used by languages such as C and C++. Java itself makes heavy use of exceptions for error conditions, and any programs should be ready to handle them. Using instructions makes a program more robust, which makes for a better program.
Exceptions are handled using a combination of three statement types: try, catch, and finally. try is used to inform the compiler that exception handling will be performed on an associated block of code. catch is used for processing a specific exception generated in the try block of code. Several catch statements can be specified sequentially to handle specific exceptions in a specific order. The finally statement can be used to catch all exceptions not specifically caught by a catch statement.
A program can generate its own exceptions using the throw statement. The exception itself can be either preexisting or newly created. Even a preexisting exception can be customized by specifying a different message when creating the exception object.
If you need a new exception, you can create one by simply extending an existing exception class such as java.lang.Exception. Exceptions are usually simple and can be created using only two constructors. Everything else is done by the superclass that the new exception extended.
Judicious use of exceptions make the difference between a program and a truly user-friendly, good program. Nothing is less user-friendly than an aborted program.
You have probably noticed that the complexity of programming with Java has been slowly building in this book. The next subject is probably the most complex Java concept that must be mastered to complete your knowledge of Java: streams.
Streams in Java provide a way for two or more processes to send information to each other without any of the processes having to know anything about the others. This means that with streams you can write an application or applet that can then pass data to almost any other stream-based application or applet! It is even possible for multiple threads making up a single process to pass data to each other via streams.
At the highest level, streams work by simply sending information from a producer process to a consumer process. A producer process generates data for use by another process. A consumer process makes use of data produced by another process. Information that leaves a producer process is known as an output stream. Information that arrives at a consumer is known as an input stream. The information that makes up a stream is simply a sequence of data blocks sent one at a time from the producer process to the consumer process. The blocks themselves can be almost anything, from simple bytes to complex objects. Only the primitive data types are directly supported in Java, but any object can be sent as a data block by creating new stream classes.
Streams are unidirectional-that is, the data for a particular stream travels only from the producer to the consumer. The consumer cannot send any data back to the producer on this same stream. However, there is nothing to prevent the consumer process from opening a different stream back to the producer process. In fact, a single process can have multiple streams that can be any combination of producers and consumers. These also can be opened between multiple other processes. This is one of the powerful attributes of streams.
Each stream has only a single producer and a single consumer. A single producer stream cannot feed multiple consumer streams, and a single consumer stream cannot be fed by multiple producer streams. This is not to say that a producer process could not feed multiple consumer processes; it's just that each consumer process must be fed by a different and unique stream. The same is true for a single consumer process that is fed by multiple producer processes.
Consumer processes do not have to blindly accept data from a producer, however. They can start and stop the flow of information, read as much or as little of a stream as desired, and in some cases even determine how much data is left in the stream. Consumer processes can also reset the stream to an earlier point. This control makes it sound like the consumer is communicating back to the producer process, but what is actually happening is that the consumer process is communicating with the Java internals that handle the stream.
If a consumer process pauses while receiving data from its end of the stream, the producer will not necessarily pause in sending data. The producer may continue to produce data, and the Java stream-handling code may buffer the data until the consumer process begins accepting data again. This is important to remember: the producer and consumer do not communicate directly with each other over a streams interface. Instead, the producer simply generates a stream of data at one end, which then goes into the Java stream-handling code. From there it is sent to the consumer. The producer and consumer have no knowledge of each other.
Streams have many uses in Java and, in fact, are used quite heavily within Java itself. They are used to read and write information to devices, files, databases, network sockets, other processes-almost anything. All stream interfaces implement a basic set of methods that the programmer can use. These methods are common across all stream types, which makes using a new stream type simple.
There are a few stream methods found in some Java package extensions, but the standard ones are found in the java.io package. This package must be imported in all applications or applets that make use of threads.
There are some extended stream methods that are not always implemented, such as the capability to reset the stream to an earlier point. For these cases, there are other methods discussed in the following sections that you can use to check whether extended methods are supported. This makes it possible for a program to take advantage of these methods if they are available.
So how do you implement a stream? Let's find out!
Input streams are the data streams that are accepted and processed by a consumer process. As mentioned before, stream classes and their associated methods are found in the package java.io. Every application or applet that uses streams must explicitly import this package in the beginning of the code. You do this with a standard import statement:
import java.io.*;
There are many different classes of input streams. Each has its own purpose and can, of course, be extended or customized as needed by the programmer. Table 8.1 is a complete list of Java's input stream classes.
BufferedInputStream | Reads data from the buffer to minimize number of actual reads needed |
ByteArrayInputStream | Reads byte data into an array of bytes |
DataInputStream | Reads data objects from a stream rather than bytes like most other input streams |
FileInputStream | Reads data from a file |
FilterInputStream | Allows a stream to connect to subclasses of itself that can be used to filter the data |
InputStream | Abstracts the superclass to all InputStream classes |
LineNumberInputStream | Reads an input stream that uses line numbers |
PipedInputStream | Reads data from a pipe connected to another process |
PushBackInputStream | Allows pushing a single character back onto input stream |
SequenceInputStream | Treats multiple sequential input streams as single input stream |
StringBufferInputStream | Reads character data into an array of characters (a string) |
The methods associated with input streams are of two types: those
that are guaranteed to work with all input streams (see Table
8.2) and those that are not (see Table 8.3).
read | Reads data from an input stream |
skip | Skips past data in an input stream |
markAvailable | Tests if the mark method is available on this input stream |
close | Closes an input stream |
available | Amount of data available in an input stream |
mark | Marks a point in an input stream to which to return |
reset | Returns to a specified point in an input stream |
The methods not guaranteed to work either may not be available as valid methods or may return inaccurate or inconsistent results because of the uncertain nature of a producer. Some producers send a consistent stream of data; others will not produce data until the intervening Java stream-handling code indicates it is ready. For these reasons, the results can be unpredictable.
The read method is the most basic of the input stream methods. This is the method an application uses to read data from a stream. It has several variations, mainly involving how much data is read, where in the stream the data starts and stops, and to where the data is written. Which read method you use depends on your application's needs.
All read methods are based on a blocking read, meaning that when a read method is called, it will not return until all the data requested has been received or an exception occurs. Recall that in some earlier examples, a read of the keyboard was done. In these cases, the read function did not return until a character was entered on the keyboard. This is an example of a blocking read. If the equivalent of a non-blocking read is needed, methods exist for checking if data is available before a read is executed.
All read methods also throw the IOException exception. This must be handled in an application's code, either by using a try/catch/finally structure or by adding a throws statement to the class declaration. The syntax of read follows:
int read();
int read(byte[] buffer);
int read(byte[] buffer, int offset, int length)
The first form of read simply attempts to read a single byte from the input stream. The byte read is returned by the function itself, cast as an int. The second form reads data into a buffer, an array of bytes. It attempts to read enough data to fill the buffer. The last version also attempts to fill a buffer, but starts at the indicated offset into the stream and will read only length bytes. All cases of read return an integer, which is used to indicate the number of bytes actually read. If there is nothing more to be read, it returns a -1, as in the following:
import java.io.*;
public class MyMain {
public static void main (String args[]) {
FileInputStream s; //declare s to be a file input stream
int rcode; //declare a variable for the read return code
try { //use try to catch file open exceptions
s = new FileInputStream(args[0]); //open the input stream
rcode = 0;
while (rcode != -1) { //loop reading file until entire file is read
try { //use try to catch IO exceptions
rcode = s.read(); //read character from file
System.out.println("ch = " + (char)rcode);//print char to screen
}
catch (IOException e) { //handle IO exceptions
System.out.println("Unknown IO error reading file " + args[0]);
System.exit(2);
}
}
}
catch (FileNotFoundException e) { //handle file not found exception
System.out.println("File " + args[0] + " not found");
System.exit(1);
}
}
}
In this example, read is used to read bytes from a file input stream. The name of the file to read is passed as the first argument on the command line when you run the application. Therefore, if you wanted the program to read from a file called sample.txt, you would invoke the MyMain application using the following:
java MyMain sample.txt
A stream object variable s is declared to be of type FileInputStream. It is then initialized by opening the file input stream using the new operator. This will result in the file actually being opened, so a File not found exception must be caught at this point if the file does not exist.
Next, a while loop is used to continuously read a single byte from the stream until the read method returns -1. The body of the while loop starts out by calling the read method to read a byte. read may return an IOexception, so it is also placed in a try statement. If the read succeeds, the byte is printed to the screen. If not, a catch statement writes an error and the application aborts.
This example shows the basics for handling a simple input stream. It shows how to open an input stream, read data from it, detect the end of it, and set up exception handling for both the open and the read of it.
The skip method is used to bypass a fixed number of bytes of an input stream, thereby enabling the program to move quickly through the stream. It can be used to skip unneeded data or to move to a specific point in the stream. If used with a database input stream, it can be used to skip through a set of fixed-length records to get to the record needed. If used with a file input stream, it can provide some of the same functionality as the C/C++ lseek function.
The skip method works in a forward direction only. It cannot be used to skip backward to an earlier point in an input stream. Some input streams allow this functionality, but they use the mark and reset methods (covered later in their respective sections) to accomplish this.
The skip method throws the same IOException exception that read does. This must be handled in an application's code, either by using a try/catch/finally structure or by adding throws to the class declaration. The syntax of skip follows:
long skip(long num);
The skip method accepts a single argument of type long: the number of bytes to skip in the input stream. Because data streams do not have any fixed size, if more bytes than can be handled in a long must be skipped, use multiple skip statements.
There is a problem with large argument values in the implementation of skip as of this writing (Java 1.0). The long passed as an argument is internally cast to an int because skip is implemented using the read(byte[]) method. The problem with this is that arrays in Java are constrained to be no larger than an int. If skips larger than int are required, they must be implemented as multiple int skips.
The skip method returns a long that is used to indicate the number of bytes actually skipped. If the end of the input stream has already been reached when skip is called, a -1 is returned. The following code fragment provides an example:
public long skipRecords (int num) {
int recordSize = 512; //size of a record in bytes
long rcode = 0;
try {
rcode = s.skip(num*recordSize); //skip num 512 byte records
if (rcode > 0) {
rcode /= recordSize; //compute records skipped
}
}
catch (IOException e) { //handle IO exceptions
System.out.println("Unknown IO error skipping " + num + " records");
}
return(rcode/recordSize);
}
In this example, skip is used in a method that indexes into an input stream by number of fixed 512-byte records. A try statement is used to catch an exception during the skip process. The catch statement prints an error message if an IOException exception is generated. The method returns the number of records skipped, a -1 if the skip was at end of input, or 0 if an exception occurred. (This example is based on s being a stream object that has already been opened as a variable global to this method.)
The close method is used to close an input stream that an application or applet no longer needs. Java closes input streams automatically when an application or applet exits, which is why a close statement has not been used in the preceding stream examples. However, it is good programming practice to close any resources an application no longer needs.
The close method throws the same IOException exception that read and skip do. This must be handled in an application's code, either by using a try/catch/finally structure or by adding throw statement to the class declaration.
public long closeStream () {
try s.close(); //close input stream
catch (IOException e) { //handle IO exceptions
System.out.println("Unknown IO error closing input stream");
}
}
In this example, close is used in a method that encapsulates the try and catch functionality into a single location. A try statement is used to catch an exception during the close process. The catch statement prints an error message if an IOException exception is generated. (As in the skip example used previously, this example is based on s being a stream object that has already been opened as a variable global to this method.)
The available method is used to determine if some amount of data is ready to be read without blocking in the input stream and to test whether enough data is available for processing before some other method such as read is called. However, available does not return valid information for all input streams. Some input streams always return 0, whereas others may return inconsistent numbers. This is due to the unknowns associated with the producer process and the implementation of the underlying Java code that handles the connection between the producer and consumer processes.
In other programming languages, the equivalent functionality of available is used to ensure that a required amount of data is available before issuing a read command. This is done because read often blocks until the read has completed. For applications that must continue doing other processing until all data needed is available, this is the only way to ensure that the application does not hang waiting for data.
In Java, threads can be used to handle this problem. By assigning a specific thread to do reading on an input stream, that thread can block, waiting for the required amount of input while other threads continue with needed processing. The easy availability of threads makes using available largely unnecessary. This is also why the incorrect or inconsistent results returned by available are not considered big problems. The syntax of available follows:
int available();
The available method simply returns an int indicating the number of bytes in the input stream that can currently be read without blocking. The available method does not have any parameters. Note that the return value is an int. Streams can be of unlimited size, so even if more than an int's worth of data is available, only an int's worth is reported. The following code fragment provides an example:
public boolean isRecordReady () {
int recordSize = 512; //size of a record in bytes
boolean rcode = false;
try {
//test if at least 512 bytes are available
if (s.available >= recordSize) {
rcode = true;
}
}
catch (IOException e) { //handle IO exceptions
System.out.println("Unknown IO error checking for record availability");
}
return(rcode);
}
In this example, available is used to determine if an entire fixed-length record of 512 bytes is available in the input stream. A try statement is used to catch an exception during the available test. The catch statement prints an error message if an IOException exception is generated. The method will return true if at least 512 bytes are ready or false if not. (As with skip and close, this example is based on s being a stream object that has already been opened as a variable global to this method.)
The mark method is used to mark a point in an input stream to which it may be necessary to return at a later time. This method is not available for all input stream types. A separate method called markSupported determines if a particular input stream supports this functionality. If mark is supported, you can use the reset method to return to the point indicated by mark.
The mark/reset methods have several limitations that make them useful only for specific applications. When mark is used, the program must specify a maximum amount of data that can go by before the reset method is called. If more than this amount of data goes by, reset will throw an exception. Also, there is no way to specify multiple marks on an input stream. If mark is called again before a reset is done, the mark is simply moved to the new location with the new limit on how much can be read before a reset occurs. The syntax of mark, markSupported, and reset follows:
void mark(int readLimit);
boolean markSupported();
void reset();
The mark method accepts a single parameter of type int that indicates how much data in the input stream can go by before calling the reset method. The markSupported method has no arguments and simply returns a boolean value indicating whether the mark function is available for a given input stream. reset has no calling arguments and no return value. It simply resets the input stream to the point at which mark was called. The following code fragment provides an example:
public static boolean checkForPostScriptHeader (int size) {
byte buffer[];
String txt;
buffer = new byte[size];
if (s.markSupported() == false) {
System.out.println("Input Stream does not support mark");
System.exit(1);
}
try s.read(buffer);
catch (IOException e) {
System.out.println("Unknown IO error reading data stream");
System.exit(2);
}
try s.reset();
catch (IOException e) {
System.out.println("Unknown IO error reseting data stream");
System.exit(3);
}
txt = new String(buffer,size);
if ((txt.indexOf("\n%!") != -1) || (txt.indexOf("%!") == 0))
return(true);
else
return(false);
}
This example is a method that checks a data input stream for a postscript header. If there is one, it will return true; otherwise it will return false. The mark and reset methods are used to return the data stream to its original location so that the calling routines can process the stream as needed.
The method starts by checking whether the mark method is available for this input stream using the markSupported method. If it is not, an application exit is forced. If mark is available, the input stream is marked with the passed-in size. This allows the calling application to specify how much data should be checked for a postscript header. After the input stream is marked, it is read into a buffer. If an IOexception occurs, a message is written to the screen and again the application exits. If the input stream is read successfully, it is then reset back to the location where the method was called. Again it tests for an IOException exception, and an error message and an exit occur if an exception is detected.
After the input stream is reset, the buffer is converted to a string value so that it can use the indexOf method to locate the postscript header. Finally, an if statement uses the indexOf method to locate the header. It is called twice: The first time it checks to see if any line begins with the necessary postscript header characters, and the second time it handles the special case at the beginning of the file where there is no newline character preceding the header. If a header is found, the method exits with a return value of true; otherwise, it exits with a return value of false.
Output streams are the data streams generated by a producer process. As mentioned previously, stream classes and their associated methods are found in the java.io package. Every application or applet that uses streams must explicitly import this package at the beginning of the code with a standard import statement:
import java.io.*;
As with input streams, there are many different classes of output
streams. Each has its own purpose and can, of course, be extended
or customized as needed by the programmer. Table 8.4 is a complete
list of Java's output stream classes.
BufferedOutputStream | Writes data to buffer to minimize the number of actual writes needed |
ByteArrayOutputStream | Writes data to an array of bytes |
DataOutputStream | Writes data objects to a stream rather than bytes like most other output streams |
FileOutputStream | Writes data to a file |
FilterOutputStream | Allows a stream to connect to subclasses of itself that can be used to filter the data |
OutputStream | Abstracts the superclass to all output stream classes |
PipedOutputStream | Writes data to a pipe connected to another process |
PrintStream | Writes formatted data to the user's screen |
Table 8.5 lists the three methods associated with output streams.
Just like any other method in Java, these methods can be extended
and customized as needed by the programmer.
write | Writes data to an output stream |
flush | Forces data to be written to pipe |
close | Closes an output stream |
The write method is the most basic of the output stream methods. This is the method an application uses to write data to a stream. As with the read method, there are several variations associated with it.
All write methods are based on a blocking write, meaning that when a write method is called, it will not return until all the data to be sent has been accepted or an exception occurs. When a call blocks, execution cannot continue until the call completes. This can cause problems if the programmer wants the application to continue if the write takes a significant amount of time. The solution, as with the input stream methods, is to use threads. If you put write into a separate thread, other executions can continue and the thread with write can take as long as it needs to complete the transaction.
All write methods also throw the IOException exception. This must be handled in an application's code, either by using a try/catch/finally structure or by adding throws to the class declaration. The syntax of write follows:
void write(int b);
void write(byte[] buffer);
void write(byte[] buffer, int offset, int length)
The variations on write almost exactly mirror those for read: The first simply writes a single byte to the output stream, and the second writes data to a buffer. The buffer is an array of bytes. The last version also writes a buffer, but will start at the indicated offset into the array and will only write length bytes. All cases of write have a return type of void. The only way to determine if an error occurred is to catch any exceptions that are thrown. Here is an example:
import java.io.*;
public class MyMain {
public static void main (String args[]) {
FileOutputStream s; //declare s to a file output stream
int n,ix; //declare for loop variables
try { //use try to catch file open exceptions
s = new FileOutputStream(args[0]); //open the output stream
for (n=0; n < 20; n++) { //outer for loop
for (ix=0x30; ix < 0x7b; ix++) { //inner for loop
try { //use try to catch IO exceptions
s.write(ix); //write character to output stream
}
catch (IOException e) { //handle IO exceptions
System.out.println("Unknown IO error writing file " + args[0]);
System.exit(2);
}
} &nb sp; //bottom of inner loop
try {
s.write('\n');
} //write newline char to output stream
catch (IOException e) { //handle IO exceptions
System.out.println("Unknown IO error writing file " + args[0]);
System.exit(3);
}
} &nb sp; //bottom of outer loop
}
catch (IOException e) { //handle file open exceptions
System.out.println("Unknown IO error opening file " + args[0]);
System.exit(1);
}
}
}
In this example, write is used to write bytes to a file output stream. The filename itself is passed as the first argument on the command line. A stream object variable s is declared to be of type FileOutputStream. It is then initialized by opening the file output stream using the new operator. This results in either the creation of a new file using the filename specified on the command line or the rewriting of an existing file. This results in the file being opened, so a file IOException exception must be caught at this point if the file is not accessible. Next, two nested while loops are used to write a series of characters to the stream.
The inner loop writes all ASCII characters between 0 and z. The body of this loop starts out by calling the write method to write a single character to the output stream. write may return an IOException exception, so it is placed in a try statement. If write fails, a catch statement writes an error and the application aborts.
The outer loop simply causes the inner loop to be run 20 times. However, after every inner loop completes, the outer loop writes a newline character to the output stream. This makes the resulting output file more readable.
This example shows the basics for handling a simple output stream. It shows how to open an output stream, write data to it, and set up exception handling for both open and write.
The flush method is used to force any data buffered for the output stream to be written to the output device. Some streams may be connected to slow or even hung consumer processes, in which cases write may block for an overly extended period of time, if not forever. In these situations, the flush method can be used to force Java to accept the data. Java may simply buffer the data itself, waiting for the consumer process to begin accepting data again or to die, thus closing the pipe. Remember that if a write is blocked, the only way for an application to force the flush to occur is to call flush using a different thread. This is another example of why putting a stream I/O call into its own thread is a good idea. The syntax of flush follows:
void flush();
The flush method also throws the same IOException exception that write does. This must be handled in an application's code, either by using a try/catch/finally structure or by adding throws to the class declaration, as in the following example:
public void flushStream () {
try {s.flush();} //close output stream
catch (IOException e) { //handle IO exceptions
System.out.println("Unknown IO error flushing output stream");
}
}
In this example, flush is used in a method that encapsulates the try and catch functionality into a single location. A try statement is used to catch any exceptions during the close process. The catch statement will print an error message if an IOException is generated. (This example is based on s being a stream object that has already been opened as a variable global to this method.)
The close method is virtually identical to the close used for input streams. It is used to close an output stream that an application or applet no longer needs. Java will automatically close output streams when an application or applet exits, which is why a close statement has not been used in the preceding stream examples. However, it is good programming practice to close any resources an application no longer needs. The syntax of close follows:
void close();
The close method throws the same IOException exception that write and flush do. This must be handled in an application's code, either by using a try/catch/finally structure or by adding a throw statement to the class declaration. The following is an example:
public void closeStream () {
try {s.close();} //close output stream
catch (IOException e) { //handle IO exceptions
System.out.println("Unknown IO error closing output stream");
}
}
In this example, close is used in a method that encapsulates the try and catch functionality into a single location. A try statement is used to catch an exception during the close process. The catch statement will print an error message if an IOException exception is generated. (This example is based on s being a stream object that has already been opened as a variable global to this method.)
Streams are Java's way of reading and writing data to entities outside an application, such as files, networks, devices, or other processes. All streams in Java consist of a flow of 8-bit bytes. Some stream classes allow the writing of other object types, but internally they simply convert these objects to bytes. Streams can be broken into two main types: input streams and output streams.
Input streams accept data from an external source. Processes that accept input streams are known as consumers. You can use several methods with input streams: read, skip, markSupported, and close work with all input stream types; and available, mark, and reset only work with some input stream types. The read method is used to read data from an input stream. skip is used to skip over information in the input stream to get to data farther on. close simply closes an input stream. The available method can be used to determine if more data is available in an input stream, but the results returned by this method are not always accurate. The markSupported method is used to determine if a particular stream supports the mark/reset methods. These methods are used to mark a particular location in a stream and reset the stream back to that location.
Output streams produce data for an external destination. Processes that produce output streams are known as producers. You can use several methods with output streams: write, flush, and close. The write method is used to write data to an output stream. flush is used to force data to be written that may be buffered. close simply closes an output stream.
By using input and output streams, Java applications and applets can communicate with the rest of the world.
This chapter examines the final pieces of the Java programming language: threads, exceptions, and streams.
The section on threads covers concurrent execution of multiple sections of code. The methods to create, initialize, start, stop, and destroy a thread are also discussed. Threads are a powerful part of the Java programming language, and a programmer who becomes versed in their use can write more powerful, robust, and user-friendly programs.
The section on exceptions covers Java's use of exceptions in place of the more normal return-type error code. Exceptions provide a more robust and extensible way to do error handling. The methods for catching and handling an exception are explained, as well as generation of new, customized exceptions. Because exceptions are simply another object in Java, all the benefits regarding code reuse and customization apply.
Finally, the section on streams covers Java's use of this construct to communicate with the outside world. Streams are a sequence of bytes. By using such a simple data format, Java applications do not have to have specific knowledge of where data is coming from or being written to. This in turn lends itself to simpler, more reusable code. The methods to open, read, write, and close a data stream are covered, in addition to more complex concepts such as rereading a part of a stream. Streams in Java provide a flexible-but above all, standardized-method for external communication.
This concludes the basics of Java programming. With the tool sets covered to this point, you are now ready to create Java applications that will serve your needs. Part IV, "The Java Application Programming Interface," covers the class libraries in Java.