Web Hosting Tips for Webmasters -
Brewing Java: A Tutorial
Copyright 1995-1998, 2000-2002 Elliotte Rusty Harold
elharo@metalab.unc.edu
Last-modified: 2005/02/19
URL: http://www.cafeaulait.org/javatutorial.html
This tutorial has grown into a book called The Java Developer's Resource,
available now from Prentice Hall. It's
now out of print, but the examples and
exercises from that
book are also online here and may be of use. For more details about the JDR
including the plans for a second edition see the
JDR page.
- June 26, 2004
-
Fix a typo and
make some efforts (incomplete) toward well-formedness
- January 2, 2002
-
Fix a typo and URLs
- February 8, 2001
-
Minor bug fix
- January 6, 2001
-
Some minor changes about = and :=
- May 23, 2000
-
Fixed a few typos.
- April 23, 2000
-
Fixed a few bugs.
- September 7, 1998
-
Fixed a few typos
- June 23, 1998
-
Very minor corrections and updates
- April 8, 1997
-
This tutorial covers Java 1.0. I've also posted several hundred pages of lecture notes
from an Introduction to Java Programming course I'm teaching at Polytechnic University.
These notes cover Java 1.1 extensively, and cover many more topics than
are discussed here.
- March 3, 1997
-
I've fixed a number of typos.
- November 13, 1996
-
I've improved the treatment of recursion, expanded and updated
the installation and getting started instructions, fixed a bug in the Mondrian programs,
and cleaned up some other parts.
- September 20, 1996
-
I fixed a bug in the Arg method of the complex class. Arguments in
all quadrants are now handled properly. I've also fixed a few other minor bugs in various
programs.
- March 26, 1996
-
The RAM config program, typewriter applet, Bozo sort algorithm and
Java doodle applet are finally included.
- March 25, 1996
-
I recently completed my first book which grew out of this tutorial.
It should be out in a few months. This file is
mostly just a rough draft for some sections of that book.
The book will be much improved over the very rough material
you see here. If you don't like this page, you may still
like the book. If you do like this page, you should love
the book.
I'm now left in something of a quandary. How should I best update
this tutorial? On the one hand it would be easiest to just slap the
400 pages I wrote for the book onto the web. However that would make
my publisher very unhappy. On the other hand I don't feel like
duplicating all my work for the book.
What I've decided to do is to try and clean this tutorial up by
fixing the various mistakes, but not to add much new material to it.
For instance the last fix to the ComplexNumber class broke
multiplication. It is now working again. I've also fixed a lot of
other random mistakes. Finally you'll notice that almost all the
unwritten sections have been deleted. At this point if something's
in the table of contents, then it's probably actually here.
Don't despair, though. I'm going to be writing a
series of shorter articles on various topics that I either didn't cover
in the book, or don't think I covered as well as I could have. Planned topics
include
- Why Java is different from C++
- An Introduction to classes through complex numbers
- Linked lists
- Sparse arrays
- Working with Strings
- Java Tokens
- Java Data types
- Bitwise operators
- Calling Native Code
- Inf, NaN and all that: IEEE 754 arithmetic
- Images and the Mandelbrot Set
Furthermore the book includes a substantial number of exercises. However it does not include answers.
I am going to be posting those exercises here along with detailed explanations
and answers. In this way I hope to fill out some more topics than I would have time to
do in a straight tutorial fashion.
Finally I will be putting most of the source code from the book online as well.
This will provide a series of useful examples.
Thus there will be a lot more new tutorial here. However this tutorial is mostly complete.
There's certainly more it could cover, but I just decided I didn't want to
write the same book twice.
- February 7, 1996
-
Fixed some errors in the ComplexNumber class. DivideBy now works and toString()
gives more aesthetic results when the imaginary part of a number is negative.
- January 25, 1996
-
Polished up some of the EventTutor applets. Lesson of the day: You have to add your components to your layout.
- January 24, 1996
-
Finished the Middle Third Applet. Lesson of the day: Casting is not
the same as rounding.
- January 18, 1996
-
Fixed assorted long standing errors.
- January 16, 1996
-
- Substantially improved Complex number class and examples.
- Almost all code and most text is now beta compliant
- Added change history.
This is a Java tutorial in progress. A Java FAQ list is being developed as
simultaneously as time permits.
Yes, I plan to split this file into smaller chunks that are
easier for browsers to digest. However right now this is an
early development version of this material, and ease of
writing and maintenance leads me to want to keep this file
in one piece. When the outline settles down I will break it
up.
It isn't all here yet, but I hope to fill this out quickly.
The chapter on basic Java syntax is reaching completion. I
will update this as time permits. If you find any mistakes,
please do inform me.
Comments you might have on the structure, organization or
contents of this document are appreciated. Although a lot
remains to be fleshed out, the basic structure is as
follows:
Part 1 is a brief introduction to what Java is, why it's
cool and what you need to use it.
Part 2 is a tutorial introduction to Java that just covers
what you need to know to start programming command line applications
in Java. This is an introduction to the basic syntax of the language.
It skims over many details and completely omits little used
features like bit-shift operators. This section is fairly complete.
Part 3 covers the basics of writing applets in Java.
Part 4 introduces you to objects and classes.
Java has caused more excitement
than any development on the Internet since Mosaic.
Everyone, it seems, is talking about it. Unfortunately very few people seem to know anything
about it. This tutorial is designed to change that.
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:
- Inline sounds that play in realtime whenever a user loads a page
- Music that plays in the background on a page
- Cartoon style animations
- Realtime video
- Multiplayer interactive games
However Java is more than just a web browser with special features.
All of these features can be integrated into browsers in other ways.
Although HotJava was the first browser to include inline sound and animation,
Microsoft's Internet Explorer 2.0 and Netscape Navigator 2.0 support these features
in several different ways. What makes Java special?
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
- Simple
-
Java has the bare bones functionality needed to implement its rich feature set.
It does not add lots of syntactic sugar or unnecessary features.
- Object-Oriented
-
Almost everything in Java is either a class, a method or an object. Only the most basic
primitive operations and data types (int, for, while, etc.) are at a sub-object level.
- Platform Independent
-
Java programs are compiled to a byte code format that can be read and run by interpreters
on many platforms including Windows 95, Windows NT, and Solaris 2.3 and later.
- Safe
-
Java code can be executed in an environment that prohibits it from introducing viruses, deleting or modifying files, or otherwise performing data destroying and computer crashing operations.
- High Performance
-
Java can be compiled on the fly with a Just-In-Time compiler (JIT)
to code that rivals C++ in speed.
- Multi-Threaded
-
Java is inherently multi-threaded. A single Java program can have many different things
processing independently and continuously.
As of this writing Java is not a fully developed commercial product.
Versions of Java at varying stages of completion are available
from Sun for Windows 95 and Windows NT for X86, Solaris 2.3 to 2.5,
and MacOS 7.5.
At the present time there are no versions of Java available for MIPS, Alpha or
PowerPC based NT, Windows 3.1, or the Amiga.
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:
Macintosh Installation Instructions
The file you get will be a self-extracting archive called something
like JDK-1_0_2-MacOS.sea.bin. If you use Fetch or Anarchie to
download it will be automatically converted into the self-extracting
JDK-1_0_2-MacOS.sea. Double-click it to extract it and the
double-click the resulting installer JDK-1_0_2-MacOS. It will prompt
you for a location to put it on your hard disk. Put it wherever is
convenient.
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.
The Windows X86 release is a self extracting archive. You
will need about six megabytes of free disk space to install
the JDK. Execute the file by double-clicking on it in the
File Manager or by selecting Run... from the Program
Manager's File menu and typing the path to the file. This
will unpack the archive. The full path is unimportant, but for
simplicity's sake I am going to assume you installed it from
the root of your C: drive. If this is the case the
files will live in C:\java. If you unpacked it somewhere else just replace
C:\ by the full path to the java directory in what
follows.
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:
These two files will be installed in your java directory.
If you do not already have copies of these two files on your
system, (There's a very good chance you do, probably in your
system directory.) copy them into the C:\java\bin directory.
If you do have these two files already, just delete these
extra copies.
If you're on a shared system at a university or an Internet service
provider, there's a good chance Java is already installed. Ask your
local support staff how to access it. Otherwise follow these
instructions.
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:
% set path=($PATH /usr/local/java/bin)
sh:
% PATH=($PATH /usr/local/java/bin); export $PATH
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.
Unix Instructions
Start the Applet Viewer by doing the following:
- Open a command line prompt, and cd to one of the directories in /usr/local/java/demo, for example
% cd /usr/local/java/demo/TicTacToe
- Run the appletviewer on the html file:
% appletviewer example1.html
- Play Tic-Tac-Toe! The algorithm was deliberately broken so it is possible to win.
Macintosh Instructions
- Start the Applet Viewer by double-clicking it.
- Select Open... from the File menu and navigate into the java folder, then the Sample Applets folder, then the TicTacToe folder.
- Select the file example1.html and click on the Open button. Alternately you can drag and drop this file onto the Applet Viewer.
- Play Tic-Tac-Toe! The algorithm was deliberately broken so it is possible to win.
Windows Instructions
Start the Applet Viewer by doing the following:
- Open a DOS window, and cd to one of the directories in C:\JAVA\DEMO, for example
C:< cd C:\JAVA\DEMO\TicTacToe
- Run the appletviewer on the html file:
C:< appletviewer example1.htm
- Play Tic-Tac-Toe! The algorithm was deliberately broken so it is possible to win.
Hot Tip: Getting Rid of that Annoying License Dialog Box
Do you know the annoying dialog box I'm talking about? I
bet you do. It's the one that comes up every time you
launch the applet viewer to make you agree to Sun's license.
Do you want to get rid of it? If so make a directory called
.hotjava in your java/bin directory. You won't see
it again.
Netscape 3.0 will run Java applets on most platforms except
Windows 3.1. Netscape has a Java
Demo Page with links to various applets that will mostly
run. However do not be surprised if an applet fails to work
properly in Netscape.
At least since the first edition of Kernighan and
Ritchie's The C Programming Language it's been customary
to begin programming tutorials and classes
with the "Hello World"
program, a program that prints the string "Hello World" to the
display. Being heavily influenced by Kernighan and Ritchie and not
ones to defy tradition we begin similarly.
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:
- Did you put a semicolon after
System.out.println("Hello World")?
- Did you include the closing bracket?
- Did you type everything exactly as it appears here? In particular did you use the same capitalization? Java is case sensitive.
class is not the same as Class
for example.
- Were you in the same directory as HelloWorld.java when you typed javac HelloWorld.java?
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!
Hello World is very close to the simplest program imaginable. Nonetheless there's quite a lot going on in it. Let's investigate it, line by line.
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.
Exercises
- What happens if you change the name of the source code file, e.g. HelloEarth.java instead of HelloWorld.java?
- What happens if you keep the name of the source code file the same (HelloWorld.java) but change the class's name, e.g. class HelloEarth?
Let's investigate the Hello World program a little more closely. In
Java a source code file is broken up into parts separated by opening
and closing braces, i.e. the { and } characters. Everything between
{ and } is a block and exists more or less independently
of everything outside of the braces.
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.
Comments can appear anywhere in a source file. Comments are
identical to those in C and C++. Everything between /*
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");
}
}
Methods are only half of a Java class. The other half is data. Consider the
following generalization of the HelloWorld program:
// 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.
Our Hello program still isn't very general. We can't change the name we say hello to without
editing and recompiling the source code. This may be fine for the programmers, but
what if the secretaries want their computers to say Hello to them? (I know. This is a little far-fetched
but bear with me. I'm making a point.)
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 but the most trivial computer programs need to make decisions. They need
to test some condition and operate differently based on that condition. This is quite
common in real life. For instance you stick your hand out the window to test
if it's raining. If it is raining then you take an umbrella with you.
If it isn't raining then you don't.
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.
You may have noticed a minor cosmetic bug in the
previous program. A cosmetic bug is one that doesn't crash
the program or system, or produce incorrect results, but
just looks a little annoying. Cosmetic bugs are acceptable
in quick hacks you'll only use once but not in finished
code.
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.
Exercises
- Rework the entire program to use no more than one print method in each block.
- A truly elegant solution to this problem relies on statements that haven't been introduced yet, notably
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?
We'll begin this section by finding a more elegant way to handle multiple command line arguments of an undetermined number.
Toward this end we introduce the concept of a loop. A loop is a section of code that is
executed repeatedly until a stopping condition is met. A typical loop may look like:
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?).
Sidebar: Why Algebra teachers hate Basic and aren't that
fond of C
The statement 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 notable exceptions are Pascal (and the Pascal
derivatives Modula-2, Modula-3 and Oberon), Ada, and Eiffel
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.
Exercises
- What happens now if we don't give the Hello program any command line arguments?
We aren't testing the number of command line arguments anymore so why isn't
an ArrayIndexOutOfBoundsException thrown?
- For math whizzes only: I lied. In certain interpretations of certain number systems the statement i = i + 1 does have a valid solution for i. What is it?
Classes are the single most important feature of Java.
Everything in Java is either a class, a part of a class, or
describes how a class behaves. Although classes will be
covered in great detail in section four, they are so
fundamental to an understanding of Java programs that a
brief introduction is going to be given here.
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.
Like we said, Java source is really just classes. Import
statements are shorthand for many different classes; and
comments aren't really there (as far as the compiler is
concerned) in the first place. However there is one thing
in Java source code that is neither a class nor a member of
a class. That's an interface. We're not going
to say much about interfaces yet since that's a subject for
a more advanced chapter. However we will note that an
interface defines methods that a class implements. In other
words it declares what certain classes do. However
an interface itself does nothing. All the
action at least, happens inside classes.
Java is not just used for the World Wide Web. The next
program demonstrates a classic use of computers that
goes back to the earliest punch card machines. It's reminiscent
of some of the very first useful programs I ever wrote. These were
designed to quickly calculate several dozen numbers to keep
me from having to do them by hand in physics lab. It's also
borrowed directly from Kernighan and Ritchie. // 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.
You may have noticed something a little funny about the
above output. The numbers aren't exactly correct. Zero
degrees Fahrenheit is actually -17.778 degrees Celsius, not
-18 degrees Celsius as this program reports. The problem is
that we used only integers here, not decimal numbers. In
computer-speak decimal numbers are called "floating
point numbers."
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
Exercises
- Have the FahrToCelsius program print a heading above the table.
- Write a similar program to convert Celsius to Fahrenheit.
Java isn't as redundant as perl, but there's still almost always more than one way
to write any given program. The following program produces identical output to the
Fahrenheit to Celsius program in the preceding section. The main difference is the
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 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.
In reality almost nobody writes 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.
Exercises
- Would a float be big enough to count how many grains of wheat the king owes?
- Why isn't there a ** or a // operator?
All the programs we've written to date have been quite simple, well under fifty lines of code
each. As programs grow in size it begins to make sense to break them into parts. Each part
can perform a particular calculation and possibly return a value. This is especially useful when
the calculation needs to be repeated at several different places in the program.
It also helps to define a clearer picture of the flow of the program, much like an outline shows the flow of a book.
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.
Java supports recursive methods, i.e. even if you're already inside methodA() you can call
methodA().
The easiest way I can think of to explain recursion is to look at a
simple acronym, GNU. The GNU project, among other things, is trying
to produce free versions of the Unix operating system and many Unix
tools, such as lex, yacc, and cc. One minor problem with this effort
is that the name Unix is trademarked so the GNU project can't use
it. Hence, instead of Unix, we have GNU, where GNU stands for
"Gnu's Not Unix." The definition of GNU refers to itself; that is,
it's recursive. So what is GNU? One level deeper it's "(Gnu's Not
Unix)'s Not Unix." One level deeper still, it becomes "((Gnu's Not
Unix)'s Not Unix)'s Not Unix." And so on, ad infinitum. It's like
standing between a pair of mirrors. The images just fade off into
the distance with no clear end in sight. In computer programming
recursion is achieved by allowing a method to call itself.
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. (The factorial is a special case of the
gamma function for non-negative integers. Although the factorial
function is only defined for non-negative integers, the gamma
function is defined for all real numbers. It is possible to show
that the gamma function is infinite for negative
integers.) Java doesn't support infinite values for
longs, though, so return the warning value -1 instead. (Java does
support infinite values for floats and doubles.) Here's a better
recursive factorial method:
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.
(The article may be on PCWeek's web site somewhere, but
regrettably that site, while it looks pretty, is lacking some basic
navigation aids. I was unable to locate the article, either directly
or through their search engine. If anyone finds the URL let me know.)
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.
Exercises
- Find the non-recursive equivalent to the Ram Config algorithm.
In non-trivial computing problems you often need to store lists of
items. Often these items can be specified sequentially and referred
to by their position in the list. Sometimes this ordering is
natural as in a list of the first ten people to arrive at a sale.
The first person would be item one in the list, the second person to
arrive would be item two, and so on. Other times the ordering
doesn't really mean anything such as in the ram configuration
problem of the previous chapter where having a 4 MB SIMM in slot A
and an 8 MB SIMM in slot B was effectively the same as an 8 MB SIMM
in slot A and a 4 MB SIMM in slot B. However it's still convenient
to be able to assign each item a unique number and enumerate all the
items in a list by counting out the numbers.
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:
In this case we're showing an integer array named I
with five elements; i.e. the type of the array is int
and the array has dimension 5.
There are three steps to creating an array, declaring it, allocating
it and initializing it. Declaring Arrays Like other
variables in Java, an array must have a specific type like byte,
int, String or double. Only variables of the appropriate type can
be stored in an array. You cannot have an array that will store
both ints and Strings, for instance.
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.
Allocating Arrays
Declaring an array merely says what it is. It
does not create the array.
To actually create the array (or any other object) use the
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.
Initializing Arrays
Individual elements of the array are referenced by the array name
and by an integer which represents their position in the array. The
numbers we use to identify them are called subscripts or
indexes into the array. Subscripts are consecutive
integers beginning with 0. Thus the array 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 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:
- Watch the fenceposts! Since array subscripts begin at zero we need 101 elements if
we want to include the square of 100.
- Although
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.
One way to avoid fencepost errors is to use the array's built-in length member. This
tells you the number of components in the array. In the example above, squares.length equals
101. Thus the loop could have been written like this:
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. Shortcuts
It may seem to be a lot of work to set up arrays, particularly if
you're used to a more array friendly language like Fortran.
Fortunately Java has several shorthands for declaring, dimensioning
and strong values in arrays.
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};
We've already seen one example of an array. Main methods in an
application store the command line arguments in an array of strings
called 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, Carl
Sagan's Contact and John
Updike's Roger's Version. More realistically
we might wish to test the randomness of a random number generator.
If a random number generator is truly random, all digits should
occur with equal frequency over a sufficiently long period of time.
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.
The most common kind of multidimensional array is a two-dimensional array.
If you think of a one-dimensional array as a column of values you can think
of a two-dimensional array as a table of values like so:
|
|
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
|
3
|
4
|
5
|
6
|
r3
|
3
|
4
|
5
|
6
|
7
|
We need to use two numbers to identify a position in a two-dimensional array.
These are the element's row and column positions. For instance if the above array
is called J then J[0][0] is 0, J[0][1] is 1, J[0][2] is 2, J[0][3] is 3, J[1][0] is 1,
and so on.
Here's how the elements in a four by five array called M are referred to:
M[0][0]
| M[0][1]
| M[0][2]
| M[0][3]
| M[0][4]
|
M[1][0]
| M[1][1]
| M[1][2]
| M[1][3]
| M[1][4]
|
M[2][0]
| M[2][1]
| M[2][2]
| M[2][3]
| M[2][4]
|
M[3][0]
| M[3][1]
| M[3][2]
| M[3][3]
| M[3][4]
|
Declaring, Allocating and Initializing Two Dimensional Arrays
Two dimensional arrays are declared, allocated and initialized much like
one dimensional arrays. However we have to specify two dimensions
rather than one, and we typically use two nested for loops to
fill the array.
The array examples above are filled with the sum of their row and column
indices. Here's some code that would create and fill such an array:
class FillArray {
public static void main (String args[]) {
int[][] M;
M = new int[4][5];
for (int row=0; row < 4; row++) {
for (int col=0; col < 5; col++) {
M[row][col] = row+col;
}
}
}
}
Of course the algorithm you would use to fill the array depends completely
on the use to which the array is to be put. Here is a program which calculates
the identity matrix for a given dimension. The identity matrix of dimension
N is a square matrix which contains ones along the diagonal and zeros in all
other positions.
class IDMatrix {
public static void main (String args[]) {
double[][] ID;
ID = new double[4][4];
for (int row=0; row < 4; row++) {
for (int col=0; col < 4; col++) {
if (row != col) {
ID[row][col]=0.0;
}
else {
ID[row][col] = 1.0;
}
}
}
}
}
In two-dimensional arrays ArrayIndexOutOfBounds errors occur
whenever you exceed the maximum column index or row index. Unlike
two-dimensional C arrays, two-dimensional Java arrays are not just
one-dimensional arrays indexed in a funny way.
Exercises
- Write a program to generate the HTML for the above tables.
You don't have to stop with two dimensional arrays. Java lets you
have arrays of three, four or more dimensions. However chances are
pretty good that if you need more than three dimensions in an array,
you're probably using the wrong data structure. Even three
dimensional arrays are exceptionally rare outside of scientific and
engineering applications.
The syntax for three dimensional arrays is a direct extension of
that for two-dimensional arrays. Here's a program that declares,
allocates and initializes a three-dimensional array:
class Fill3DArray {
public static void main (String args[]) {
int[][][] M;
M = new int[4][5][3];
for (int row=0; row < 4; row++) {
for (int col=0; col < 5; col++) {
for (int ver=0; ver < 3; ver++) {
M[row][col][ver] = row+col+ver;
}
}
}
}
}
We need three nested for loops here to handle the extra
dimension.
The syntax for still higher dimensions is similar. Just add another
pair pf brackets and another dimension.
Like C Java does not have true multidimensional arrays. Java fakes
multidimensional arrays using arrays of arrays. This means that it
is possible to have unbalanced arrays. An unbalanced
array is a multidimensional array where the dimension isn't the same
for all rows. IN most applications this is a horrible idea and
should be avoided.
One common task is searching an array for a specified value.
Sometimes the value may be known in advance. Other times you may
want to know the largest or smallest element.
Unless you have some special knowledge of the contents of the array
(for instance, that it is sorted) the quickest algorithm for
searching an array is straight-forward linear search. Use a
for loop to look at every element of the array until
you find the element you want. Here's a simple method that prints
the largest and smallest elements of an array:
static void printLargestAndSmallestElements (int[] n) {
int max = n[0];
int min = n[0];
for (int i=1; i < n.length; i++) {
if (max < n[i]) {
max = n[i];
}
if (min > n[i]) {
min = n[i];
}
}
System.out.println("Maximum: " + max);
System.out.println("Minimum: " + min);
return;
}
If you're going to search an array many times, you may want to sort the array,
before searching it. We'll discuss sorting algorithms in the next section.
All sorting algorithms rely on two fundamental operations, comparison and swapping.
Comparison is straight-forward. Swapping is a little more complex. Consider
the following problem. We want to swap the value of a and b.
Most people propose something like this as the solution:
class Swap1 {
public static void main(String args[]) {
int a = 1;
int b = 2;
System.out.println("a = "+a);
System.out.println("b = "+b);
// swap a and b
a = b;
b = a;
System.out.println("a = "+a);
System.out.println("b = "+b);
}
}
This produces the following output:
a = 1
b = 2
a = 2
b = 2
That isn't what you expected! The problem is that we lost track
of the value 1 when we put the value of b into a. To correct
this we need to introduce a third variable, temp, to hold the
original value of a.
class Swap2 {
public static void main(String args[]) {
int a = 1;
int b = 2;
int temp;
System.out.println("a = "+a);
System.out.println("b = "+b);
// swap a and b
temp = a;
a = b;
b = temp;
System.out.println("a = "+a);
System.out.println("b = "+b);
}
}
This code produces the output we expect:
a = 1
b = 2
a = 2
b = 1
Bubble Sort
Now that we've learned how to properly swap the values of two
variables, let's proceed to sorting. There are
many different sorting algorithms. One of the
simplest and the most popular algorithms is referred to as
bubble sort. The idea of bubble sort is to start at
the top of the array. We compare each element to the next
element. If its greater than that element then we swap the two.
We pass through the array as many times as necessary to sort
it. The smallest value bubbles up to the top of the array while
the largest value sinks to the bottom. (You could equally well
call it a sink sort, but then nobody would know what you were
talking about.) Here's the code:
import java.util.*;
class BubbleSort {
public static void main(String args[]) {
int[] n;
n = new int[10];
Random myRand = new Random();
// initialize the array
for (int i = 0; i < 10; i++) {
n[i] = myRand.nextInt();
}
// print the array's initial order
System.out.println("Before sorting:");
for (int i = 0; i < 10; i++) {
System.out.println("n["+i+"] = " + n[i]);
}
boolean sorted = false;
// sort the array
while (!sorted) {
sorted = true;
for (int i=0; i < 9; i++) {
if (n[i] > n[i+1]) {
int temp = n[i];
n[i] = n[i+1];
n[i+1] = temp;
sorted = false;
}
}
}
// print the sorted array
System.out.println();
System.out.println("After sorting:");
for (int i = 0; i < 10; i++) {
System.out.println("n["+i+"] = " + n[i]);
}
}
}
In this case we have sorted the array in ascending order,
smallest element first. It would be easy to change this to
sort in descending order.
Do you remember this 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]);
}
}
Do you remember what happened when you ran the program
without giving it any command line arguments? The run |