People are excited about Java because of what it lets them do. Java was the first way to include inline sound and animation in a web page. Java also lets users interact with a web page. Instead of just reading it and perhaps filling out a form, users can now play games, calculate spreadsheets, chat in realtime, get continuously updated data and much, much more.
Here are just a few of the many things Java can do for a web page:
Java is a programming language for distributed applications. It doesn't just allow you to add new types of content to your pages like Netscape and Internet Explorer do. Rather it lets you add both the content and the code necessary to interact with that content. You no longer need to wait for the next release of a browser that supports your preferred image format or special game protocol. With Java you send browsers both the content and the program necessary to view this content at the same time!
Let's think about what this means for a minute. Previously you had to wait for all the companies that make the web browsers your readers use to update their browsers before you could use a new content type. Then you had to hope that all your readers actually did update their browsers. Java compatibility is a feature that any browser can implement and by so doing implement every feature!
For instance let's say you want to use EPS files on your Web site. Previously you had to wait until at least one web browser implemented EPS support. Now you don't wait. Instead you can write your own code to view EPS files and send it to any client that requests your page at the same time they request the EPS file.
Or suppose you want people to be able to search your electronic card catalog. However the card catalog database exists on a mainframe system that doesn't speak HTTP. Before Java you could hope that some browser implemented your proprietary card catalog protocol; (fat chance) or you could try to program some intermediate cgi-bin on a UNIX box that can speak HTTP and talk to the card catalog, not an easy task. With Java when a client wants to talk to your card catalog you can send them the code they need to do so. You don't have to try to force things through an httpd server on port 80 that were never meant to go through it.
If that were all Java was,
it would still be more interesting than a <marquee> or <frame>
tag in some new browser beta. But there's a lot more. Java is platform independent.
A Java program can run equally well on any architecture that has a Java enabled
browser. With the release of Netscape Navigator 2.0 that includes Windows 95,
Windows NT, the MacOS, Sun Solaris, Sun OS 4.1.3, SGI IRIX, OSF/1, HP-UX with
more to come. But wait. There's more!
Java isn't just for web sites. Java is a programming language that lets you do almost anything you can do with a traditional programming langauge like Fortran or C++. However Java has learned from the mistakes of its predecessors. It is considerably cleaner and easier to use than those languages.
As a language Java is
Natural Intelligence has its own Java environment for the Mac called Roaster. Borland is also working a Java development environment to be released in the first half of 1996. Various third-party efforts are under way to port Java to other platforms including the Amiga, Windows 3.1, OS/2 and others.
The basic Java environment consists of a web browser that can play Java applets, a Java compiler to turn to Java source code into byte code, and a Java interpreter to run Java programs. These are the three key components of a Java environment. You'll also need a text editor like Brief or BBEdit. Other tools like a debugger, a visual development environment, documentation and a class browser are also nice but aren't absolutely necessary.
Note that it isn't necessary to get all three of these from the same source. For instance Netscape is committed to providing a Java-enabled web browser. However it will only provide a Java compiler with the next version of its server products.
Sun has made the Java Developers Kit available for its supported platforms. It includes an applet viewer that will let you view and test your applets. The JDK also includes the javac compiler, the java interpreter, the javaprof profiler, the javah header file generator (for integrating C into your Java code), the Java debugger and limited documentation. However most of the documentation for the API and the class library is on Sun's web site.
You can ftp the programs from the following sites:
It may be helpful to make aliases of the Applet Viewer, the Java Compiler and the Java Runner and put them on your desktop for ease of dragging and dropping later, especially if you have a large monitor.
You will need to add C:\java\bin directory to your PATH environment variable
In addition to the java files, the archive includes two common DLL's:
The Unix release is a compressed tar file. You will need about nine megabytes of disk space to uncompress and untar the JDK. Double that would be very helpful. You do this with the commands:
% uncompress JDK-1_0_2-solaris2-sparc.tar.Z
% tar xvf JDK-1_0_2-solaris2-sparc.tar
The exact file name may be
a little different if youıre retrieving the release for a different platform such
as Irix or if the version is different. You can untar it in your home directory,
or, if you have root privileges, in some convenient place like /usr/local where
all users can have access to the files. However root privileges are not necessary
to install or run Java. Untarring the file creates all necessary directories and
sub-directories. The exact path is unimportant, but for simplicity's sake this
book assumes itıs installed it in /usr/local. If a sysop already installed it,
this is probably where it lives. (Under Solaris it's also possible the sysop put
it into /opt.) If this is the case the files live in /usr/local/java. If you unpacked
it somewhere else, just replace /usr/local by the full path to the java directory
in what follows. If you installed it in your home directory, you can use ~/java
and ~/hotjava instead of a full path.
You now need to add /usr/local/java/bin directory to your PATH environment variable. You use one of the following commands depending on your shell.
csh, tcsh:You should also add these lines to the end of your .profile and .cshrc files so you won't have to do this every time you login. Now you're ready to run some applets.% set path=($PATH /usr/local/java/bin)sh:% PATH=($PATH /usr/local/java/bin); export $PATH
% cd /usr/local/java/demo/TicTacToe
% appletviewer example1.html
C:< cd C:\JAVA\DEMO\TicTacToe
C:< appletviewer example1.htm
The following is the Hello World Application as written in Java. Type it into a text file or copy it out of your web browser, and save it as a file named HelloWorld.java.
class HelloWorld {
public static void main (String args[]) {
System.out.println("Hello World!");
}
}
To compile this program make
sure you're in the same directory HelloWorld.java is in and type javac HelloWorld.java
at the command prompt. Hello World is very close to the simplest program
imaginable. Although it doesn't teach very much from a programming standpoint,
it gives you a chance to learn the mechanics of writing and compiling code. If
you're like me your first effort won't compile, especially if you typed it in
from scratch rather than copying and pasting. Here are a few common mistakes:
System.out.println("Hello World")?
class is not the same as Class
for example.
Once your program has compiled successfully, the compiler places the executable output in a file called HelloWorld.class in the same directory as the source code file. You can then run the program by typing java HelloWorld at the command prompt. As you probably guessed the program responds by printing Hello World! on your screen.
Congratulations! You've just written your first Java program!
For now the initial class
statement may be thought of as defining the program name, in this case HelloWorld.
The compiler actually got the name for the class file from the class HelloWorld
statement in the source code, not from the name of the source code file. If
there is more than one class in a file, then the Java compiler will store each
one in a separate .class file. For reasons we'll see later it's advisable to
give the source code file the same name as the main class in the file plus the
.java extension.
The initial class
statement is actually quite a bit more than that since this "program"
can be called not just from the command line but also by other parts of the
same or different programs. We'll see more in the section on classes and methods
below.
The HelloWorld class contains
one method, the main method. As in C the main method is where an
application begins executing. The method is declared public meaning
that the method can be called from anywhere. It is declared static
meaning that all instances of this class share this one method. (If that last
sentence was about as intelligible as Linear B, don't worry. We'll come back
to it later.) It is declared void which means, as in C, that this
method does not return a value. Finally we pass any command line arguments to
the method in an array of Strings called args. In this simple program
there aren't any command line arguments though.
Finally when the main method
is called it does exactly one thing: print "Hello World" to the standard
output, generally a terminal monitor or console window of some sort. This is
accomplished by the System.out.println method. To be more precise
this is accomplished by calling the println() method of the static
out field belonging to the System class; but for now
we'll just treat this as one method.
One final note: unlike
the printf function in C the System.out.println method
does append a newline at the end of its output. There's no need to
include a \n at the end of each string to break a line.
Blocks are important both syntactically and logically. Without the braces the code wouldn't compile. The compiler would have trouble figuring out where one method or class ended and the next one began. Similarly it would be very difficult for someone else reading your code to understand what was going on. For that matter it would be very difficult for you, yourself to understand what was going on. The braces are used to group related statements together. In the broadest sense everything between matching braces is executed as one statement (though depending not necessarily everything inside the braces is executed every time).
Blocks can be hierarchical. One block can contain one or more subsidiary blocks. In this case we have one outer block that defines the HelloWorld class. Within the HelloWorld block we have a method block called "main".
In this tutorial we help to identify different blocks with indentation. Every time we enter a new block we indent our source code by two spaces. When we leave a block we deindent by two spaces. This is a common convention in many programming languages. However it is not part of the language. The code would produce identical output if we didn't indent it. In fact I'm sure you'll find a few examples here where I haven't followed convention precisely. Indentation makes the code easier to read and understand, but it does not change its meaning.
/* and */ is ignored by the compiler and everything
on a line after two consecutive slashes is also thrown away. Therefore the following
program is, as far as the compiler is concerned, identical to the first one:
// This is the Hello World program in Java
class HelloWorld {
public static void main (String args[]) {
/* Now let's print the line Hello World */
System.out.println("Hello World");
}
}
// This is the Hello Rusty program in Java
class HelloRusty {
public static void main (String args[]) {
// You may feel free to replace "Rusty" with your own name
String name = "Rusty";
/* Now let's say hello */
System.out.print("Hello ");
System.out.println(name);
}
}
Here, rather than saying hello
to a rather generic world, we allow Java to say hello to a specific individual.
We do this by creating a String variable called "name" and
storing the value "Rusty" in it. (You may, of course, have replaced
Rusty with your own name.) Then we print out "Hello ". Notice that we've
switched here from System.out.println method to the similar System.out.print
method. System.out.print is just like System.out.println
except that it doesn't break the line after it's finished. Therefore when we reach
the next line of code, the cursor is still located on the same line as the word
"Hello" and we're ready to print out the name.
What we need is a way to change the name at runtime rather than at compile time. (Runtime is when we type java HelloRusty. Compile time is when we type javac HelloRusty.java). To do this we'll make use of command-line arguments. They allow us to type something like Java Hello Gloria and have the program respond with "Hello Gloria". Here's the code:
// This is the Hello program in Java
class Hello {
public static void main (String args[]) {
/* Now let's say hello */
System.out.print("Hello ");
System.out.println(args[0]);
}
}
Compile this program in the
javahtml directory as usual and then type java Hello Gloria.
This isn't very hard is
it? In fact we've even gotten rid of the name variable from the HelloRusty program.
We're using args[0] instead. args is what is known
as an array. An array stores a series of values. The values can be
Strings as in this example, numbers, objects or any other kind of Java data
type.
args is a
special array that holds the command line arguments. args[0] holds
the first command line argument. args[1] holds the second command
line argument, args[2] holds the third command line argument and
so on.
At this point almost everyone reading this is probably saying "Whoa, that can't be right." However why you're saying depends on your background.
If you've never programmed before or if you've programmed only in Pascal or Fortran, you're probably wondering why the first element of the array is at position 0, the second at position 1, the third at position 2 instead of the clearly more sensible element 1 being the first element in the array, element 2 being the second and so on. All I can tell you is that this is a holdover from C where this convention almost made sense.
On the other hand if you're
used to C you're probably upset because args[0] is the first command
line argument instead of the command name. The problem is that in Java it's
not always clear what the command name is. For instance in the above example
is it java or Hello? On some systems where Java runs there may not even be a
command line, the Mac for example.
Now you should experiment with this program a little. What happens if instead of typing java Hello Gloria you type java Hello Gloria and Beth? What if you leave out the name entirely, i.e. java Hello?
That was interesting wasn't it? You should have seen something very close to
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException at Hello.main(C:\javahtml\Hello.java:7)
What happened was that
since we didn't give Hello any command line arguments there wasn't anything
in args[0]. Therefore Java kicked back this not too friendly error
message about an "ArrayIndexOutOfBoundsException." That's a mouthful.
We'll see one way to fix it in the next section.
All programming languages
have some form of an if statement that allows you to test conditions.
In the previous code we should have tested whether there actually were command
line arguments before we tried to use them.
All arrays have lengths
and we can access that length by referencing the variable arrayname.length.
(Experienced Java programmers will note that this means that the array is an
object which contains a public member variable called length.) We test the length
of the args array as follows:
// This is the Hello program in Java
class Hello {
public static void main (String args[]) {
/* Now let's say hello */
System.out.print("Hello ");
if (args.length > 0) {
System.out.println(args[0]);
}
}
}
Compile and run this program
and toss different inputs at it. You should note that there's no longer an ArrayIndexOutOfBoundsException
if you don't give it any command line arguments at all.
What we did was wrap the
System.out.println(args[0]) statement in a conditional test, if
(args.length > 0) { }. The code inside the braces, System.out.println(args[0]),
now gets executed if and only if the length of the args array is greater than
zero. In Java numerical greater than and lesser than tests are done with the
> and < characters respectively. We can test for a number being less than
or equal to and greater than or equal to with <= and >= respectively.
Testing for equality is a little trickier. We would expect to test if two numbers were equal by using the = sign. However we've already used the = sign to set the value of a variable. Therefore we need a new symbol to test for equality. Java borrows C's double equals sign, ==, to test for equality.
It's not uncommon for even experienced programmers to write == when they mean = or vice versa. In fact this is a very common cause of errors in C programs. Fortunately in Java, you are not allowed to use == and = in the same places. Therefore the compiler can catch your mistake and make you fix it before you run the program.
All conditional statements
in Java require boolean values, and that's what the ==, <, >, <=, and
>= operators all return. A boolean is a value that is either true or false.
Unlike in C booleans are not the same as ints, and ints and booleans cannot
be cast back and forth. If you need to set a boolean variable in a Java program,
you have to use the constants true and false. false
is not 0 and true is not non-zero as in C. Boolean values are no
more integers than are strings.
Experienced programmers
may note that there was an alternative method to deal with the ArrayIndexOutOFBoundsException
involving try and catch statements. We'll return to
that soon.
The cosmetic bug here was
that if we didn't include any command line arguments, although the program didn't
crash, it still didn't say Hello. The problem was that we only used System.out.print
and not System.out.println. There was never any end of line character.
It was like we typed in what we wanted to say, but never hit the return key.
We could fix this by putting
a System.out.println(""); line at the end of the main
method, but then we'd have one too many end-of-lines if the user did type in
a name. We could add an additional if statement like so:
// This is the Hello program in Java
class Hello {
public static void main (String args[]) {
/* Now let's say hello */
System.out.print("Hello ");
if (args.length > 0) {
System.out.println(args[0]);
}
if (args.length <= 0) {
System.out.println("whoever you are");
}
}
}
This corrects the bug, but
the code is hard to read and maintain. It's very easy to miss a possible case.
For instance we might well have tested to see if args.length were
less than zero and left out the more important case that args.length
equals zero. What we need is an else statement that will catch any
result other than the one we hope for, and luckily Java provides exactly that.
Here's the right solution:
// This is the Hello program in Java
class Hello {
public static void main (String args[]) {
/* Now let's say hello */
System.out.print("Hello ");
if (args.length > 0) {
System.out.println(args[0]);
}
else {
System.out.println("whoever you are");
}
}
}
Now that Hello at least doesn't
crash with an ArrayIndexOutOfBoundsException we're still not done. java Hello
works and Java Hello Rusty works, but if we type java Hello Elliotte
Rusty Harold, Java still only prints Hello Elliotte. Let's fix
that.
We're not just limited
to two cases though. We can combine an else and an if
to make an else if and use this to test a whole range of mutually
exclusive possibilities. For instance here's a version of the Hello program
that handles up to four names on the command line:
// This is the Hello program in Java
class Hello {
public static void main (String args[]) {
/* Now let's say hello */
System.out.print("Hello ");
if (args.length == 0) {
System.out.print("whoever you are");
}
else if (args.length == 1) {
System.out.println(args[0]);
}
else if (args.length == 2) {
System.out.print(args[0]);
System.out.print(" ");
System.out.print(args[1]);
}
else if (args.length == 3) {
System.out.print(args[0]);
System.out.print(" ");
System.out.print(args[1]);
System.out.print(" ");
System.out.print(args[2]);
}
else if (args.length == 4) {
System.out.print(args[0]);
System.out.print(" ");
System.out.print(args[1]);
System.out.print(" ");
System.out.print(args[2]);
System.out.print(" ");
System.out.print(args[3]);
}
else {
System.out.print(args[0]);
System.out.print(" ");
System.out.print(args[1]);
System.out.print(" ");
System.out.print(args[2]);
System.out.print(" ");
System.out.print(args[3]);
System.out.print(" and all the rest!");
}
System.out.println();
}
}
You can see that this gets
mighty complicated mighty quickly. Once again no experienced Java programmer would
write code like this. One of the things that makes this solution so unwieldy is
that I've used a different print statement for every single variable. However
Java makes it very easy to print multiple items at once. Instead of including
just one thing in the print method's arguments we put multiple items in there
separated by + signs. These items can include variables like args[0]
and constant strings like " and all the rest!". For example
the last else block could have been written as
else {
System.out.print(args[0] + " " + args[1] + " " + args[2] + " " + args[3] + " and all the rest!");
}
This syntax is simpler to
read and write but would still be unwieldy once the number of command line arguments
grew past ten or so. In the next section we'll see how to handle over two billion
command line arguments in a much simpler fashion.
for. However there is a more elegant and space efficient solution
that accomplishes everything we did above, does not use the + operator, and
uses only if's and a single else. No else
if's are needed. Can you find it?
while there's more data {
Read a Line of Data
Do Something with the Data
}
This isn't working code but
it does give you an idea of a very typical loop. We have a test condition (Is
there more data?) and something we want to do with if the condition is met. (Read
a Line of Data and Do Something with the Data.)
There are many different
kinds of loops in Java including while, for, and do
while loops. They differ primarily in the stopping conditions used.
For loops
typically iterate a fixed number of times and then exit. While
loops iterate continuously until a particular condition is met. You usually
do not know in advance how many times a while loop will loop.
In this case we want to
write a loop that will print each of the command line arguments in succession,
starting with the first one. We don't know in advance how many arguments there
will be, but we can easily find this out before the loop starts using the args.length.
Therefore we will write this with a for loop. Here's the code:
// This is the Hello program in Java
class Hello {
public static void main (String args[]) {
int i;
/* Now let's say hello */
System.out.print("Hello ");
for (i=0; i < args.length; i = i+1) {
System.out.print(args[i]);
System.out.print(" ");
}
System.out.println();
}
}
We begin the code by declaring
our variables. In this case we have exactly one variable, the integer i.
Then we begin the program by saying "Hello" just like before.
Next comes the for
loop. The loop begins by initializing the counter variable i to
be zero. This happens exactly once at the beginning of the loop. Programming
tradition that dates back to Fortran insists that loop indices be named i,
j, k, l, m and n
in that order. This is purely a convention and not a feature of the Java language.
However anyone who reads your code will expect you to follow this convention.
If you choose to violate the convention, try to give your loop variables mnemonic
names like counter or loop_index.
Next is the test condition.
In this case we test that i is less than the number of arguments.
When i becomes equal to the number of arguments, (args.length)
we exit the loop and go to the first statement after the loop's closing brace.
You might think that we should test for i being less than or equal
to the number of arguments; but remember that we began counting at zero, not
one.
Finally we have the increment
step, i=i+1. This is executed at the end of each iteration of the
loop. Without this we'd continue to loop forever since i would
always be less than args.length. (unless, of course, args.length
were less than or equal to zero. When would this happen?).
i=i+1
drives algebra teachers up the wall. It's an invalid assertion. There isn't a
number in the world for which the statement i=i+1 is true. In fact if you subtract
i from both sides of that equation you get the patently false statement that 0 = 1.
The trick here is that the symbol = does not imply equality. That is reserved
for the double equals sign, ==. In almost all programming languages including
Java a single equals sign is the assignment operator.
The one notable exception is Pascal (and the Pascal derivatives Modula-2, Modula-3 and Oberon) where = does in fact mean equality and where := is the assignment operator. Math teachers are very fond of their equal sign and don't like to see it abused. This is one reason why Pascal is still the most popular language for teaching programming, especially in schools where the Computer Science department is composed mainly of math professors.
Needless to say math professors hate languages like Basic where, depending on context, = can mean either assignment or equality.
All the action in Java programs takes place inside class blocks, in this case the HelloWorld class. In Java almost everything of interest is either a class itself or belongs to a class. Methods are defined inside the classes they belong to. This may be a little confusing to C++ programmers who are used to defining all but the simplest methods outside the class block, but this approach is really more sensible. C++ takes the road it does primarily out of desire to be compatible with C, not out of good object-oriented design. Both syntactically and logically everything in Java happens inside a class.
Even basic data primitives like integers often need to be incorporated into classes before you can do many useful things with them. The class is the fundamental unit of Java programs, not source code files like in C. For instance consider the following Java program:
class HelloWorld {
public static void main (String args[]) {
System.out.println("Hello World");
}
}
class GoodbyeWorld {
public static void main (String args[]) {
System.out.println("Goodbye Cruel World!");
}
}
Save this code in a single
file called hellogoodbye.java in your javahtml directory, and compile it with
the command javac hellogoodbye.java. Then list the contents of the
directory. You will see that the compiler has produced two separate class files,
HelloWorld.class and GoodbyeWorld.class.
The second class is a completely independent program. Type java GoodbyeWorld and then type java HelloWorld. These programs run and execute independently of each other although they exist in the same source code file. Off the top of my head I can't think of why you might want two separate programs in the same file, but if you do the capability is there.
It's more likely that you'll want more than one class in the same file. In fact you'll see source code files with many classes and methods.
In fact there are a few statements that can, at least at first glance, appear outside a class. Import statements appear at the start of a file outside of any classes. However the compiler replaces them with the contents of the imported file which consists of, you guessed it, more classes.
// Print a Fahrenheit to Celsius table
class FahrToCelsius {
public static void main (String args[]) {
int fahr, celsius;
int lower, upper, step;
lower = 0; // lower limit of temperature table
upper = 300; // upper limit of temperature table
step = 20; // step size
fahr = lower;
while (fahr <= upper) { // while loop begins here
celsius = 5 * (fahr-32) / 9;
System.out.print(fahr);
System.out.print(" ");
System.out.println(celsius);
fahr = fahr + step;
} // while loop ends here
} // main ends here
} //FahrToCelsius ends here
This program calculates the
Celsius equivalent of Fahrenheit temperatures between zero and three hundred degrees.
The first two lines of the main method declare the variables we'll
use. That is they specify the names and the types. For now we use only integers.
In Java an int can have a value between -2,147,483,648 to 2,147,483,647.
More types will be forthcoming.
Then we initialize the
variables using statements like "lower = 0". This sets
lower's initial value to 0. When used this way the equals sign
is called the assignment operator.
After establishing the
initial values for all our variables we go into the loop which does the main
work of our program. At the beginning of each iteration of the loop (fahr
<= upper) checks to see if the value of fahr is in fact
less than or equal to the current value of upper. If it is then
the computer executes the statements in the loop block (everything between "while
loop begins here" and "while loop ends here".) Loops in Java
are marked off by matching pairs of braces and may be nested.
celsius = 5 * (fahr-32)
/ 9; actually calculates the Celsius temperature given the fahrenheit
temperature. The arithmetic operators here do exactly what you'd expect. * means
multiplication. - is subtraction. / is division; and +, though not used in here,
is addition. Precedence follows normal algebraic conventions, and can be rearranged
through parentheses.
Java contains an almost complete set of arithmetic operators. Like C it is missing an exponentiation operator. For exponentiation you need to use the pow methods in the java.lang.Math package.
Printing output is very
similar to what you've seen before. We use System.out.print(fahr)
to print the fahrenheit value, then System.out.print(" ")
to print a one-character string containing a space, and finally System.out.println(celsius);
the Celsius value.
Finally we increment the
value of fahr by step to move on to the next value
in the table.
Floating point numbers can represent a broader range of values than integers. For example you can write very large numbers like the speed of light (2.998E8 meters per second) and very small numbers like Plank's constant (6.63E-27 ) using the same number of digits. On the other hand you lose some precision that you probably didn't need for such large and small numbers anyway.
Some languages have a third kind of number called a fixed point number. This number has a set precision, for instance two decimal places, and is often useful in monetary calculations. Java has no fixed point data type.
Using floating point numbers
is no harder than using integers. We can make our Fahrenheit to Celsius program
more accurate merely by changing all our int variables into double
variables.
// Print a more accurate Fahrenheit to Celsius table
class FahrToCelsius {
public static void main (String args[]) {
double fahr, celsius;
double lower, upper, step;
lower = 0.0; // lower limit of temperature table
upper = 300.0; // upper limit of temperature table
step = 20.0; // step size
fahr = lower;
while (fahr <= upper) { // while loop begins here
celsius = 5.0 * (fahr-32.0) / 9.0;
System.out.print(fahr);
System.out.print(" ");
System.out.println(celsius);
fahr = fahr + step;
} // while loop ends here
} // main ends here
} //FahrToCelsius ends here
I've made one further change
to the Fahrenheit to Celsius program. All the integer constants in the program
like 5 and 9 have become 5.0 and 9.0
and so on. If a constant number includes a decimal point, then the compiler assumes
it's a double precision floating point number. If it doesn't then the compiler
assumes it's an integer. However when two numbers of different types such as integer
and floating point are involved in a calculation on the right hand side of
an equation, the compiler promotes the number of the weaker type
to the stronger type before doing the calculation.
What makes one type of
number stronger than another? It's the ability to represent a broader spectrum
of numbers. Since a byte can only represent 256 numbers it's weaker than a short
which can stand for 65,535 different numbers including all the numbers a byte
can represent. Similarly an int is stronger than a short.
Floating point numbers are stronger than any integer type and doubles are the
strongest type of all.
Therefore we could have left all the small constants as integers and the program output would have been unchanged. However it is customary to put in decimal points to remind yourself and anyone else who may be unlucky enough to have to read your code, exactly what is going on.
This applies to calculations that take place on the right hand side of an equal sign. The left hand side of the equals sign is a different story. In fact it's so different that programmers have given the different sides of the equals sign special names. The left-hand side is called an lvalue while the right hand side is called an rvalue.
An rvalue is a calculated
result and as specified above it takes on the strongest type of any number involved
in the calculation. On the other hand the lvalue has a type that must be defined
before it is used. That's what all those float fahr, celsius; statements
are doing. Once the type of an lvalue is defined it never changes. Thus if we
declare fahr to be an int, then on the left hand side
of an equals sign fahr will always be an int, never
a float or a double or a long.
If you've been following along you may notice a problem here. What if the type on the left doesn't match the type on the right? e.g. what happens with code like the following?
class FloatToInt {
public static void main (String args[]) {
int myInteger;
myInteger = 9.7;
} // main ends here
} //FloatToInt ends here
Two things can happen. If,
as above, we're trying to move a number into a weaker type of variable, the compiler
generates an error. On the other hand if we're trying to move a weaker type into
a stronger type then the compiler converts it to the stronger type. For instance
the following code is legal:
class IntToFloat {
public static void main (String args[]) {
float myFloat;
int myInteger;
myInteger = 9;
myFloat = myInteger;
System.out.println(myFloat);
} // main ends here
} //IntToFloat ends here
for loop instead
of a while loop.
// Print a Fahrenheit to Celsius table
class FahrToCelsius {
public static void main (String args[]) {
int fahr, celsius;
int lower, upper, step;
lower = 0; // lower limit of temperature table
upper = 300; // upper limit of temperature table
step = 20; // step size
for (fahr=lower; fahr <= upper; fahr = fahr + step) {
celsius = 5 * (fahr-32) / 9;
System.out.println(fahr + " " + celsius);
} // for loop ends here
} // main ends here
}
The only difference between
this program and the previous one is that here we've used a for loop
instead of a while loop. The for loop has identical
syntax to C's for loops. i.e. for (initialization; test; increment)
The initialization, in this case setting the variable fahr
equal to the lower limit, happens the first time the loop is entered and only
the first time. Then the first time and every time after that when control reaches
the top of the loop a test is made. In our example the test is whether the variable
fahr is less than or equal to the upper limit. If it is we execute
the code in the loop one more time. If not we begin executing the code that follows
the loop. Finally when the at the end of each loop the increment step is made.
In this case we increase fahr by step.
If that's unclear let's look at a simpler example:
//Count to ten
class CountToTen {
public static void main (String args[]) {
int i;
for (i=1; i <=10; i = i + 1) {
System.out.println(i);
}
System.out.println("All done!");
}
}
This program prints out the
numbers from one to ten. It begins by setting the variable i to 1.
Then it checks to see if one is in fact less than or equal to ten. Since one is
less than ten, the program prints it. Finally it adds one to i and
starts over. i is now 2. The program checks to see if 2 is less than
10. It is! so the program prints "2" and adds one to i
again. i is now three. Once again the code checks to see that 3 is
less than or equal to 10. This is getting rather boring rather quickly. Fortunately
computers don't get bored, and very soon computer has counted to the point where
i is now ten. The computer prints "10" and adds one to
ten. Now i is eleven. Eleven is not less than or equal to ten so
the computer does not print it. Rather it moves to the next statement after the
end of the for loop, System.out.println("All done!);. The computer
prints "All Done!" and the program ends.
For loops do not always work this smoothly. For instance consider the following program:
//Count to ten??
class BuggyCountToTen {
public static void main (String args[]) {
int i;
for (i=1; i <=10; i = i - 1) {
System.out.println(i);
}
System.out.println("All done!");
}
}
This program counts backwards.
There's nothing fundamentally wrong with a program counting backwards, but we're
testing for i being bigger than ten. Since i is never
going to be bigger than ten in this program, the program never stops. (That's
not completely true. If you have a very fast machine or you wait long enough somewhere
below negative two billion i will suddenly become a very large positive
number and the program will halt. This happens because of vagaries in computer
arithmetic we'll discuss later. Nonetheless this is still almost certainly a bug.
If you don't want to wait for that to happen just type Control-C to
abort the program. This sort of behavior is referred to as an infinite loop and
is a more common programming error than you might think.
for loops like we did in the previous sections. They would almost
certainly use the increment or decrement operators instead. There are two of these,
++ and -- and they work like this.
//Count to ten
class CountToTen {
public static void main (String args[]) {
int i;
for (i=1; i <=10; i++) {
System.out.println(i);
}
System.out.println("All done!");
}
}
//Count to ten??
class BuggyCountToTen {
public static void main (String args[]) {
int i;
for (i=1; i <=10; i--) {
System.out.println(i);
}
System.out.println("All done!");
}
}
When we write i++
we're using shorthand for i = i + 1. When we say i--
we're using shorthand for i = i - 1. Adding and subtracting one from
a number are such common operations that these special increment and
decrement operators have been added to the language. They also allow
the compiler to be smarter about certain optimizations on some CPU architectures,
but mainly they make code easier to write and read.
However what do you do
when you want to increment not by one but by two? or three? or fifteen? We could
of course write i = i + 15 but this happens frequently enough that
there's another short hand for the general add and assign operation, +=.
We would normally write this as i += 15. Thus if we wanted to count
from 0 to 20 by two's we'd write:
class CountToTwentyByTwos {
public static void main (String args[]) {
int i;
for (i=0; i <=20; i += 2) {
System.out.println(i);
}
System.out.println("All done!");
} //main ends here
}
As you might guess there is
a corresponding -= operator. If we wanted to count down from twenty
to zero by twos we could write:
class CountToZeroByTwos {
public static void main (String args[]) {
int i;
for (i=20; i >= 0; i -= 2) {
System.out.println(i);
}
System.out.println("All done!");
}
}
You should note that we also
had to change the initialization and test components of the for loop
to count down instead of up.
There are also *=
and /= operators that multiply and divide by their right hand sides
before assigning. In practice you almost never see these because of the speed
at which variables making use of them go to either zero or Inf. If you don't
believe me consider the following cautionary tale.
Many centuries ago in India a very smart man is said to have invented the game of chess. This game greatly amused the king, and he became so enthralled with it that he offered to reward the inventor with anything he wished, up to half his kingdom and his daughter's hand in marriage.
Now the inventor of the game of chess was quite intelligent and not a little greedy. Not being satisfied with merely half the kingdom, he asked the king for the following gift:
"Mighty King," he said, "I am but a humble man and would not know what to do with half of your kingdom. Let us merely calculate my prize as follows. Put onto the first square of the chessboard a single grain of wheat. Then onto the second square of the chessboard two grains, and onto the third square of the chessboard twice two grains, and so on until we have covered the board with wheat."
Upon hearing this the king was greatly pleased for he felt he had gotten off rather cheaply. He rapidly agreed to the inventor's prize. He called for a bag of wheat to be brought to him, and when it arrived he began counting out wheat. However he soon used up the first bag and was not yet halfway across the board. He called for a second, and a third, and more and more, until finally he was forced to admit defeat and hand over his entire kingdom for lack of sufficient wheat with which to pay the inventor.
How much wheat did the king need? Let's try to calculate it. Although we won't use physical wheat, we will soon find ourselves in the same dire straits as the king. Remember that a chessboard has 64 squares.
class CountWheat {
public static void main (String args[]) {
int i, j, k;
j = 1;
k = 0;
for (i=1; i <= 64; i++) {
k += j;
System.out.println(k);
j *= 2;
}
System.out.println("All done!");
}
}
We can improve our results
slightly (but only slightly) by changing our ints to longs like so:
class CountWheat {
public static void main (String args[]) {
long i, j, k;
j = 1;
k = 0;
for (i=1; i <= 64; i++) {
k += j;
System.out.println(k);
j *= 2;
}
System.out.println("All done!");
}
}
A long is an integer type
variable that can hold up to 9,223,372,036,854,775,807. However even that isn't
enough to count how much wheat the king owed. Let's try using a double instead,
the largest type of all.
class CountWheat {
public static void main (String args[]) {
int i;
double j, k;
j = 1.0;
k = 0.0;
for (i=1; i <= 64; i++) {
k += j;
System.out.println(k);
j *= 2;
}
System.out.println("All done!");
}
}
A double can hold a number
as large as 1.79769313486231570e+308. That turns out to be large enough to count
the king's debt which comes to 1.84467e+019 grains of wheat, give or take a few
billion grains. That's a lot of wheat.
Each calculation part of a program is called a method. Methods are logically the same as C's functions, Pascal's procedures and functions, and Fortran's functions and subroutines.
The above programs have
already used a number of methods although these were all methods provided by
the system. When we wrote System.out.println("Hello World!");
in the first program we were using the System.out.println() method.
(To be more precise we were using the println() method of the out
member of the System class, but we'll talk more about that in Chapter
4.) The System.out.println() method actually requires quite a lot
of code, but it is all stored for us in the System libraries. Thus rather than
including that code every time we need to print, we just call the System.out.println()
method.
You can write and call your own methods too. Let's look at a simple example. Java has no built-in factorial method so we'll write one. The following is a simple program that requests a number from the user and then calculates the factorial of that number.
We'll use two methods in the program, one that checks to see if the user has in fact entered a valid positive integer, and another that calculates the factorial. However we'll start by writing the main method of the program:
class Factorial {
public static void main(String args[]) {
int n;
while ((n = getNextInteger()) >= 0) {
System.out.println(factorial(n));
}
} // main ends here
}
Among other things this code
demonstrates that methods make it possible to design the flow of a program without
getting bogged down in the details. We've simply named two methods, getNextInteger()
and factorial() without worrying about their exact implementations. We can add
the rest of the code in smaller, easier-to-understand pieces. Let's write the
factorial method first.
long factorial (long n) {
int i;
long result=1;
for (i=1; i <= n; i++) {
result *= i;
}
return result;
} // factorial ends here
We could have included this
code in our main method, but the algorithm is much easier to understand by breaking
the code into smaller, more manageable pieces. It's also easier to test and debug.
We can write a simple program that lets us test the factorial method before we
worry ourselves with the much harder problem of accepting and validating user
input. Here's the test program:
class FactorialTest {
public static void main(String args[]) {
int n;
int i;
long result;
for (i=1; i <=10; i++) {
result = factorial(i);
System.out.println(result);
}
} // main ends here
static long factorial (int n) {
int i;
long result=1;
for (i=1; i <= n; i++) {
result *= i;
}
return result;
} // factorial ends here
}
C++ programmers should take
note that both methods are defined inside the class definition. Once
again we see that in Java everything belongs to a class.
Let's take a closer look at the syntax of a method:
static long factorial (int n) {
int i;
long result=1;
for (i=1; i <= n; i++) {
result *= i;
}
return result;
}
Methods begin with a declaration.
This can include three to five parts. First is an optional access specifier
which can be public, private or protected. A public method can be called from
pretty much anywhere. A private method can only be used within the class where
it is defined. A protected method can be used anywhere within the package in which
it is defined. Methods that aren't specifically declared public or private are
protected by default.
Next we decide whether the method is or is not static. Static methods have only one instance per class rather than one instance per object. All objects of the same class share a single copy of a static method. By default methods are not static.
Next we specify the return type. This is the value that will be sent back to the calling method when all calculations inside the method are finished. If the return type is int, for example, we can use the method anywhere we use a constant integer. If the return type is void then no value will be returned.
Next is the name of the method.
Then there are parentheses. Inside the parentheses we give names and types to the arguments of the method. A method may have no arguments or it may have one or several. These arguments can be used inside the method just like local variables.
Finally the rest of the method is enclosed in braces to make it a single block. The part inside braces is just like the main methods we've been exploring till now. There are variable declarations, some code, and finally something new, a return statement. The return statement sends a value back to the calling method. The type of this value must match the declared type of the method.
In general any line that looks like Text(arg1, arg2) or text(arg1) or text() is a method call. The compiler is responsible for distinguishing between parentheses that mean method calls and parentheses that serve as grouping operators in mathematical expressions like (3 + 7) * 2. The compiler does a very good job of this and you may safely assume that anything that isn't clearly an arithmetical expression is a method call.
Methods break up a program into logically separate algorithms and calculations. In still larger programs it's necessary to break up the data as well. The data can be separated into different classes and the methods attached to the classes they operate on. This is the heart of object oriented programming and the subject of Chapter 4 below.
You probably already see one problem with recursion. Where does it all end? In fact when you write recursive methods you have to be careful to include stopping conditions. Although Java doesn't put any particular limits on the depth to which you can expand a recursion, it is very possible to have a run-away recursion eat up all the memory in your computer.
Let's look at an example. n! (pronounced "En-factorial") is defined as n times n-1 times n-2 times n-3 ... times 2 times 1 where n is a positive integer. 0! is defined as 1. As you see n! = n time (n-1)!. This lends itself to recursive calculation, as in the following method:
public static long factorial (int n) {
if (n == 0) {
return 1;
}
else {
return n*factorial(n-1);
}
}
Something to think about:
What happens if a negative integer is passed into the factorial method? For example
suppose you ask for factorial(-1). Then you get the following chain of calls:
-1 * -2 * -3 * -4 * .... If you're lucky your program may unexpectedly pop into
the positive numbers and count down to zero. If you're not, your program will
crash with a StackOutOfMemoryError. Stopping conditions are very important. In
this case you should check to see if you've been passed a negative integer; and,
if you have been, return infinity. (
public static long factorial (int n) {
if (n < 0) {
return -1;
}
else if (n == 0) {
return 1;
}
else {
return n*factorial(n-1);
}
}
It can be proven mathematically
that all recursive algorithms have non-recursive counterparts. For instance the
factorial method could have been written non-recursively like this:
public static long factorial (int n) {
long result = 1;
for (int i = 1; i <= n; i++) {
result *= i;
}
return result;
}
The non-recursive equivalent
in this problem is straight-forward, but sometimes the non-recursive counterpart
to a recursive algorithm isn't at all obvious. To see that one always exists,
note that at the machine level of the computer, there's no such thing as recursion
and that everything consists of values on a stack. Therefore even if you can't
find a simpler way to rewrite the algorithm without recursion, you can always
use your own stack instead of the Java stack.
Here's an example of a recursive program for which I have not been able to find a simple, non-recursive equivalent method. The goal is to find all possible RAM configurations for a PC, given the size of the memory chips it will accept and the number of slots it has available. We are not concerned with how the RAM is arranged inside the PC, only with the total quantity of installed RAM. For some computers this can easily be calculated by hand. For instance Apple's PowerBook 5300 series comes with 8 megabytes of RAM soldered onto the logic board and one empty slot that can hold chips of 8, 16, 32 or 56 MB capacity. Therefore the possible Ram configurations are 8, 16, 24, 40 and 64 MB. However as the number of available slots and the number of available chip sizes increases this becomes a much more difficult problem. The following is a recursive program to calculate and print all possible RAM configurations:
import java.util.Hashtable;
import java.util.Enumeration;
public class RamConfig {
static int[] sizes = {0, 8, 16, 32, 64};
static Hashtable configs = new Hashtable(64);
static int slots[] = new int[4];
public static void main (String args[]) {
System.out.println("Available DIMM sizes are: ");
for (int i=0; i < sizes.length; i++) System.out.println(sizes[i]);
fillSlots(slots.length - 1);
System.out.println("Ram configs are: ");
for (Enumeration e = configs.elements(); e.hasMoreElements(); ) {
System.out.println(e.nextElement());
}
}
private static void fillSlots(int n) {
int total;
for (int i=0; i < sizes.length; i++) {
slots[n] = sizes[i];
if (n == 0) {
total = 0;
for (int j = 0; j < slots.length; j++) {
total += slots[j];
}
configs.put(Integer.toString(total), new Integer(total));
}
else {
fillSlots(n - 1);
}
}
}
}
Recursive methods are also
useful for benchmarking. In particular, deep recursion tests the speed with which
a language can make method calls. This is important because modern applications
have a tendency to spend much of their time calling various API functions. PCWeek
uses a benchmark they invented called Tak which performs 63,609 recursive method
calls per pass. The algorithm is simple: If y is greater than or equal to x, Tak(x,
y, z) is z. This is the nonrecursive stopping condition. Otherwise, if y is less
than x, Tak(x, y, z) is Tak(Tak(x-1, y, z), Tak(y-1, z, x), Tak(z-1, x, y)). The
Tak benchmark calculates Tak(18, 12, 6) between 100 and 10000 times and reports
the number of passes per second. For more information about the Tak benchmark
see Peter Coffee's article, "Tak test stands the test of time" on p. 91 of the
9-30-1996 PCWeek. (Below is my variation of this benchmark. There are overloaded integer and floating point versions of the Tak method. Integer is used by default. If the -f flag is given on the command line, the floating point method is used. The number of passes to make may also be entered from the command line. If it is not, 1000 passes are made. The Java Date class is used to time that part of the test where the benchmarking is done.
import java.util.Date;
public class Tak {
public static void main(String[] args) {
boolean useFloat = false;
int numpasses;
for (int i = 0; i < args.length; i++) {
if (args[i].startsWith("-f")) useFloat = true;
}
try {
numpasses = Integer.parseInt(args[args.length-1]);
}
catch (Exception e) {
numpasses = 1000;
}
Date d1, d2;
if (useFloat) {
d1 = new Date();
for (int i = 0; i < numpasses; i++) {
Tak(18.0f, 12.0f, 6.0f);
}
d2 = new Date();
}
else {
d1 = new Date();
for (int i = 0; i < numpasses; i++) {
Tak(18, 12, 6);
}
d2 = new Date();
}
long TimeRequired = d2.getTime() - d1.getTime();
double numseconds = TimeRequired/1000.0;
System.out.println("Completed " + numpasses + " passes in "
+ numseconds + " seconds" );
System.out.println(numpasses/numseconds + " calls per second");
}
public static int Tak(int x, int y, int z) {
if (y >= x) return z;
else return Tak(Tak(x-1, y, z), Tak(y-1, z, x), Tak(z-1, x, y));
}
public static float Tak(float x, float y, float z) {
if (y >= x) return z;
else return Tak(Tak(x-1.0f, y, z), Tak(y-1.0f, z, x), Tak(z-1.0f, x, y));
}
}
If you'd like to try this
out right away, you can use the applet interface to the Tak benchmark at http://metalab.unc.edu/javafaq/newsletter/TakApplet.html.
My Powerbook 5300 achieved speeds between 3.5 and 5 passes per second on this test. Sun's Mac VM was about 10% faster on this test than Natural Intelligence's. The heavily loaded Sparcstation at metalab.unc.edu (load average 4+) achieved a little more than 3 passes per second. Given the various external factors affecting machine performance, these are hardly scientific measurements. I'd be curious to hear what your results are.
There are many ways to store lists of items including linked lists, sets, hashtables, binary trees and arrays. Which one you choose depends on the requirements of your application and the nature of your data. Java provides classes for many of these ways to store data and we'll explore them in detail in the chapter on the Java Class Library.
Arrays are probably the oldest and still the most generally effective means of storing groups of variables. An array is a group of variables that share the same name and are ordered sequentially from zero to one less than the number of variables in the array. The number of variables that can be stored in an array is called the array's dimension. Each variable in the array is called an element of the array.
An array can be visualized as a column of data like so:
|
|
I with five elements; i.e. the type of the
array is int and the array has dimension 5.
Like all other variables
in Java an array must be declared. When you declare an array variable you suffix
the type with [] to indicate that this variable is an array. Here
are some examples:
int[] k;
float[] yt;
String[] names;
In other words you declare
an array like you'd declare any other variable except you append brackets to the
end of the variable type.
new operator. When we create an array we need
to tell the compiler how many elements will be stored in it. Here's how we'd create
the variables declared above:
k = new int[3];
yt = new float[7];
names = new String[50];
The numbers in the brackets
specify the dimension of the array; i.e. how many slots it has to hold
values. With the dimensions above k can hold three ints, yt
can hold seven floats and names can hold fifty Strings. Therefore this step is
sometimes called dimensioning the array. More commonly this is called
allocating the array since this step actually sets aside the memory
in RAM that the array requires.
This is also our first
look at the new operator. new is a reserved
word in java that is used to allocate not just an array, but also all kinds
of objects. Java arrays are full-fledged objects with all that implies. For
now the main thing it implies is that we have to allocate them with new.
k above has elements k[0], k[1],
and k[2]. Since we started counting at zero there is no k[3],
and trying to access it will generate an ArrayIndexOutOfBoundsException.
You can use array elements wherever you'd use a similarly typed variable that wasn't part of an array.
Here's how we'd store values in the arrays we've been working with:
k[0] = 2;
k[1] = 5;
k[2] = -2;
yt[6] = 7.5f;
names[4] = "Fred";
This step is called initializing
the array or, more precisely, initializing the elements of the array. Sometimes
the phrase "initializing the array" would be reserved for when we initialize
all the elements of the array.
For even medium sized arrays,
it's unwieldy to specify each element individually. It is often helpful to use
for loops to initialize the array. For instance here is is a loop
that fills an array with the squares of the numbers from 0 to 100.
float[] squares = new float[101];
for (int i=0, i <= 100; i++) {
squares[i] = i*i;
}
Two things you should note
about this code fragment:
i
is an int it becomes a float when it is stored in squares, since we've declared
squares to be an array of floats.
float[] squares = new float[101];
for (int i=0, i < squares.length; i++) {
squares[i] = i*i;
}
Note that the <= changed
to a < to make this work.
We can declare and allocate an array at the same time like this:
int[] k = new int[3];
float[] yt = new float[7];
String[] names = new String[50];
We can even declare, allocate,
and initialize an array at the same time providing a list of the initial values
inside brackets like so:
int[] k = {1, 2, 3};
float[] yt = {0.0f, 1.2f, 3.4f, -9.87f, 65.4f, 0.0f, 567.9f};
args.
As our second example let's
consider a class that counts the occurrences of the digits 0-9 in decimal expansion
of the number pi, for example. This is an issue of some interest both to pure
number theorists and to theologians. See, for example,
We will do this by creating
an array of ten longs called ndigit. The zeroth element of ndigit
will track the number of zeroes in the input stream; the first element of ndigit
will track the numbers of 1's and so on. We'll test Java's random number generator
and see if it produces apparently random numbers.
import java.util.*;
class RandomTest {
public static void main (String args[]) {
int[] ndigits = new int[10];
double x;
int n;
Random myRandom = new Random();
// Initialize the array
for (int i = 0; i < 10; i++) {
ndigits[i] = 0;
}
// Test the random number generator a whole lot
for (long i=0; i < 100000; i++) {
// generate a new random number between 0 and 9
x = myRandom.nextDouble() * 10.0;
n = (int) x;
//count the digits in the random number
ndigits[n]++;
}
// Print the results
for (int i = 0; i <= 9; i++) {
System.out.println(i+": " + ndigits[i]);
}
}
}
We've got three for
loops in this code, one to initialize the array, one to perform the desired calculation,
and a final one to print out the results. This is quite common in code that uses
arrays.
| c0 |
c1 |
c2 |
c3 |
|
| r0 |
0 |
1 |
2 |
3 |
| r1 |
1 |
2 |
3 |
4 |
| r2 |
2 |
3 |
4 |
5 |
| r3 |
3 |
4 |
5 |
6 |
| r4 |
4 |
5 |
6 |
7 |
Here we have an array with five rows and four columns. It has twenty total elements. However we say it has dimension four by five, not dimension twenty. This array is not the same as a five by four array like this one:
| c0 |
c1 |
c2 |
c3 |
c4 |
|
| r0 |
0 |
1 |
2 |
3 |
4 |
| r1 |
1 |
2 |
3 |
4 |
5 |
| r2 |
2 |