Although it is based on C++, Java
is more of a “pure” object-oriented language.
Both C++ and Java are hybrid languages,
but in Java the designers felt that the hybridization was not as important as it
was in C++. A hybrid language allows multiple programming styles; the reason C++
is hybrid is to support backward compatibility with the C language. Because C++
is a superset of the C language, it includes many of that language’s
undesirable features, which can make some aspects of C++ overly
complicated.
The Java language assumes that you want
to do only object-oriented programming. This means that before you can begin you
must shift your mindset into an object-oriented world (unless it’s already
there). The benefit of this initial effort is the ability to program in a
language that is simpler to learn and to use than many other OOP languages. In
this chapter we’ll see the basic components of a Java program and
we’ll learn that everything in Java is an object, even a Java
program.
Each programming language has its own
means of manipulating data. Sometimes the programmer must be constantly aware of
what type of manipulation is going on. Are you manipulating the object directly,
or are you dealing with some kind of indirect representation (a pointer in C or
C++) that must be treated with a special syntax?
All this is simplified in Java. You treat
everything as an object, so there is a single consistent syntax that you use
everywhere. Although you treat everything as an object, the identifier
you manipulate is actually a “reference” to an
object[21]. You
might imagine this scene as a television (the object) with your remote control
(the reference). As long as you’re holding this reference, you have a
connection to the television, but when someone says “change the
channel” or “lower the volume,” what you’re manipulating
is the reference, which in turn modifies the object. If you want to move around
the room and still control the television, you take the remote/reference with
you, not the television.
Also, the remote control can stand on its
own, with no television. That is, just because you have a reference
doesn’t mean there’s necessarily an object connected to it. So if
you want to hold a word or sentence, you create a String
reference:
String s;
But here you’ve created only
the reference, not an object. If you decided to send a message to s at
this point, you’ll get an error (at run-time) because s isn’t
actually attached to anything (there’s no television). A safer practice,
then, is always to initialize a reference when you create it:
String s = "asdf";
However, this uses a special Java
feature: strings can be initialized with quoted text. Normally, you must use a
more general type of initialization for
objects.
When you create a reference, you want to
connect it with a new object. You do so, in general, with the new
keyword. new says, “Make me a new one of these objects.” So
in the above example, you can say:
String s = new String("asdf");
Not only does this mean “Make me a
new String,” but it also gives information about how to make
the String by supplying an initial character string.
Of course, String is not the only
type that exists. Java comes with a plethora of ready-made types. What’s
more important is that you can create your own types. In fact, that’s the
fundamental activity in Java programming, and it’s what you’ll be
learning about in the rest of this
book.
It’s useful to visualize some
aspects of how things are laid out while the program is running, in particular
how memory is arranged. There are six different places to store
data:
There is a group of types that gets
special treatment; you can think of these as “primitive” types that
you use quite often in your programming. The reason for the special treatment is
that to create an object with new—especially a small, simple
variable—isn’t very efficient because new places objects on
the heap. For these types Java falls back on the approach taken by C and C++.
That is, instead of creating the variable using new, an
“automatic” variable is created that is not a reference. The
variable holds the value, and it’s placed on the stack so it’s much
more efficient.
Java determines the size of each
primitive type. These sizes don’t change from one machine architecture to
another as they do in most languages. This size invariance is one reason Java
programs are so portable.
Primitive type |
Size |
Minimum |
Maximum |
Wrapper type |
---|---|---|---|---|
boolean |
1-bit |
— |
— |
Boolean |
char |
16-bit |
Unicode 0 |
Unicode 216-
1 |
Character |
byte |
8-bit |
-128 |
+127 |
Byte |
short |
16-bit |
-215 |
+215—1 |
Short |
int |
32-bit |
-231 |
+231—1 |
Integer |
long |
64-bit |
-263 |
+263—1 |
Long |
float |
32-bit |
IEEE754 |
IEEE754 |
Float |
double |
64-bit |
IEEE754 |
IEEE754 |
Double |
void |
— |
— |
— |
Void |
All numeric types are signed, so
don’t go looking for unsigned types.
The primitive data types also have
“wrapper” classes for them. That means that if you want to
make a nonprimitive object on the heap to represent that primitive type, you use
the associated wrapper. For example:
char c = 'x'; Character C = new Character(c);
Or you could also use:
Character C = new Character('x');
The reasons for doing this will be shown
in a later chapter.
Java includes two classes for performing
high-precision arithmetic: BigInteger and BigDecimal. Although
these approximately fit into the same category as the “wrapper”
classes, neither one has a primitive analogue.
Both classes have methods that provide
analogues for the operations that you perform on primitive types. That is, you
can do anything with a BigInteger or BigDecimal that you can with
an int or float, it’s just that you must use method calls
instead of operators. Also, since there’s more involved, the operations
will be slower. You’re exchanging speed for accuracy.
BigInteger supports
arbitrary-precision integers. This means that you can accurately represent
integral values of any size without losing any information during
operations.
BigDecimal is for
arbitrary-precision fixed-point numbers; you can use these for accurate monetary
calculations, for example.
Consult your online documentation for
details about the constructors and methods you can call for these two
classes.
Virtually all programming languages
support arrays. Using arrays in C and C++ is perilous because those arrays are
only blocks of memory. If a program accesses the array outside of its memory
block or uses the memory before initialization (common programming errors) there
will be unpredictable results.
One of the primary goals of Java is
safety, so many of the problems that plague programmers in C and C++ are not
repeated in Java. A Java array is guaranteed to be initialized and cannot be
accessed outside of its range. The range checking comes at the price of having a
small amount of memory overhead on each array as well as verifying the index at
run-time, but the assumption is that the safety and increased productivity is
worth the expense.
When you create an array of objects, you
are really creating an array of references, and each of those references is
automatically initialized to a special value with its own keyword:
null. When Java sees
null, it recognizes that the reference in question isn’t pointing
to an object. You must assign an object to each reference before you use
it, and if you try to use a reference that’s still null, the
problem will be reported at run-time. Thus, typical array errors are prevented
in Java.
You can also create an array of
primitives. Again, the compiler guarantees initialization because it zeroes the
memory for that array.
In most programming languages, the
concept of the lifetime of a variable occupies a significant portion of the
programming effort. How long does the variable last? If you are supposed to
destroy it, when should you? Confusion over variable lifetimes can lead to a lot
of bugs, and this section shows how Java greatly simplifies the issue by doing
all the cleanup work for
you.
Most procedural languages have the
concept of scope. This determines both the visibility and lifetime of the
names defined within that scope. In C, C++, and Java, scope is determined by the
placement of curly braces {}. So for example:
{ int x = 12; /* only x available */ { int q = 96; /* both x & q available */ } /* only x available */ /* q “out of scope” */ }
A variable defined within a scope is
available only to the end of that scope.
Indentation makes Java code easier to
read. Since Java is a free-form language, the extra spaces, tabs, and carriage
returns do not affect the resulting program.
Note that you cannot do the
following, even though it is legal in C and C++:
{ int x = 12; { int x = 96; /* illegal */ } }
The compiler will announce that the
variable x has already been defined. Thus the C and C++ ability to
“hide” a variable in a larger scope is not allowed because the Java
designers thought that it led to confusing
programs.
Java objects do not have the same
lifetimes as primitives. When you create a Java object using new, it
hangs around past the end of the scope. Thus if you use:
{ String s = new String("a string"); } /* end of scope */
the reference s vanishes at the
end of the scope. However, the String object that s was pointing
to is still occupying memory. In this bit of code, there is no way to access the
object because the only reference to it is out of scope. In later chapters
you’ll see how the reference to the object can be passed around and
duplicated during the course of a program.
It turns out that because objects created
with new stay around for as long as you want them, a whole slew of C++
programming problems simply vanish in Java. The hardest problems seem to occur
in C++ because you don’t get any help from the language in making sure
that the objects are available when they’re needed. And more important, in
C++ you must make sure that you destroy the objects when you’re done with
them.
That brings up an interesting question.
If Java leaves the objects lying around, what keeps them from filling up memory
and halting your program? This is exactly the kind of problem that would occur
in C++. This is where a bit of magic happens. Java has a garbage
collector, which looks at all the objects that were created with new
and figures out which ones are not being referenced anymore. Then it releases
the memory for those objects, so the memory can be used for new objects. This
means that you never need to worry about reclaiming memory yourself. You simply
create objects, and when you no longer need them they will go away by
themselves. This eliminates a certain class of programming problem: the
so-called “memory leak,” in which a programmer forgets to release
memory.
If everything is an object, what
determines how a particular class of object looks and behaves? Put another way,
what establishes the type of an object? You might expect there to be a
keyword called “type,” and that certainly would have made sense.
Historically, however, most object-oriented languages have used the keyword
class to mean “I’m about to tell you what a new type of
object looks like.” The class keyword (which is so common that it
will not be emboldened throughout this book) is followed by the name of the new
type. For example:
class ATypeName { /* class body goes here */ }
This introduces a new type, so you can
now create an object of this type using new:
ATypeName a = new ATypeName();
In ATypeName, the class body
consists only of a comment (the stars and slashes and what is inside, which will
be discussed later in this chapter), so there is not too much that you can do
with it. In fact, you cannot tell it to do much of anything (that is, you cannot
send it any interesting messages) until you define some methods for
it.
When you define a class (and all you do
in Java is define classes, make objects of those classes, and send messages to
those objects), you can put two types of elements in your class: data members
(sometimes called fields), and member functions (typically called
methods). A data member is an object of any type that you can communicate
with via its reference. It can also be one of the primitive types (which
isn’t a reference). If it is a reference to an object, you must initialize
that reference to connect it to an actual object (using new, as seen
earlier) in a special function called a constructor (described fully in
Chapter 4). If it is a primitive type you can initialize it directly at the
point of definition in the class. (As you’ll see later, references can
also be initialized at the point of definition.)
Each object keeps its own storage for its data members; the data members are not shared among objects. Here is an example of a class with some data members: class DataOnly { int i; float f; boolean b; }
This class doesn’t do
anything, but you can create an object:
DataOnly d = new DataOnly();
You can assign values to the data
members, but you must first know how to refer to a member of an object. This is
accomplished by stating the name of the object reference, followed by a period
(dot), followed by the name of the member inside the object:
objectReference.member
For example:
d.i = 47; d.f = 1.1f; d.b = false;
It is also possible that your object
might contain other objects that contain data you’d like to modify. For
this, you just keep “connecting the dots.” For
example:
myPlane.leftTank.capacity = 100;
The DataOnly class cannot do much
of anything except hold data, because it has no member functions (methods). To
understand how those work, you must first understand arguments and
return values, which will be described shortly.
When a primitive data type is a member of
a class, it is guaranteed to get a default value if you do not initialize
it:
Primitive type |
Default |
---|---|
boolean |
false |
char |
‘\u0000’
(null) |
byte |
(byte)0 |
short |
(short)0 |
int |
0 |
long |
0L |
float |
0.0f |
double |
0.0d |
Note carefully that the default values
are what Java guarantees when the variable is used as a member of a
class. This ensures that member variables of primitive types will always be
initialized (something C++ doesn’t do), reducing a source of bugs.
However, this initial value may not be correct or even legal for the program you
are writing. It’s best to always explicitly initialize your
variables.
This guarantee doesn’t apply to
“local” variables—those that are not fields of a class. Thus,
if within a function definition you have:
int x;
Then x will get some arbitrary
value (as in C and C++); it will not automatically be initialized to zero. You
are responsible for assigning an appropriate value before you use x. If
you forget, Java definitely improves on C++: you get a compile-time error
telling you the variable might not have been initialized. (Many C++ compilers
will warn you about uninitialized variables, but in Java these are
errors.)
Up until now, the term function
has been used to describe a named subroutine. The term that is more commonly
used in Java is method, as in “a way to do something.” If you
want, you can continue thinking in terms of functions. It’s really only a
syntactic difference, but from now on “method” will be used in this
book rather than “function.”
Methods in Java determine the messages an
object can receive. In this section you will learn how simple it is to define a
method.
The fundamental parts of a method are the
name, the arguments, the return type, and the body. Here is the basic
form:
returnType methodName( /* argument list */ ) { /* Method body */ }
The return type is the type of the value
that pops out of the method after you call it. The argument list gives the types
and names for the information you want to pass into the method. The method name
and argument list together uniquely identify the method.
Methods in Java can be created only as
part of a class. A method can be called only for an
object,[22] and
that object must be able to perform that method call. If you try to call the
wrong method for an object, you’ll get an error message at compile-time.
You call a method for an object by naming the object followed by a period (dot),
followed by the name of the method and its argument list, like this:
objectName.methodName(arg1, arg2, arg3). For example, suppose you have a
method f( ) that takes no arguments and returns a value of type
int. Then, if you have an object called a for which
f( ) can be called, you can say this:
int x = a.f();
The type of the return value must be
compatible with the type of x.
This act of calling a method is commonly
referred to as sending a message to an object. In the above example, the
message is f( ) and the object is a. Object-oriented
programming is often summarized as simply “sending messages to
objects.”
The method argument list specifies what
information you pass into the method. As you might guess, this
information—like everything else in Java—takes the form of objects.
So, what you must specify in the argument list are the types of the objects to
pass in and the name to use for each one. As in any situation in Java where you
seem to be handing objects around, you are actually passing
references[23]. The
type of the reference must be correct, however. If the argument is supposed to
be a String, what you pass in must be a string.
Consider a method that takes a
String as its argument. Here is the definition, which must be placed
within a class definition for it to be compiled:
int storage(String s) { return s.length() * 2; }
This method tells you how many bytes are
required to hold the information in a particular String. (Each char
in a String is 16 bits, or two bytes, long, to support Unicode
characters.) The argument is of type String and is called s. Once
s is passed into the method, you can treat it just like any other object.
(You can send messages to it.) Here, the length( ) method is called,
which is one of the methods for Strings; it returns the number of
characters in a string.
You can also see the use of the
return keyword, which does two things. First, it means “leave the
method, I’m done.” Second, if the method produces a value, that
value is placed right after the return statement. In this case, the
return value is produced by evaluating the expression s.length( ) *
2.
You can return any type you want, but if
you don’t want to return anything at all, you do so by indicating that the
method returns void. Here are some examples:
boolean flag() { return true; } float naturalLogBase() { return 2.718f; } void nothing() { return; } void nothing2() {}
When the return type is void, then
the return keyword is used only to exit the method, and is therefore
unnecessary when you reach the end of the method. You can return from a method
at any point, but if you’ve given a non-void return type then the
compiler will force you (with error messages) to return the appropriate type of
value regardless of where you return.
At this point, it can look like a program
is just a bunch of objects with methods that take other objects as arguments and
send messages to those other objects. That is indeed much of what goes on, but
in the following chapter you’ll learn how to do the detailed low-level
work by making decisions within a method. For this chapter, sending messages
will
suffice.
A problem in any programming language is
the control of names. If you use a name in one module of the program, and
another programmer uses the same name in another module, how do you distinguish
one name from another and prevent the two names from “clashing?” In
C this is a particular problem because a program is often an unmanageable sea of
names. C++ classes (on which Java classes are based) nest functions within
classes so they cannot clash with function names nested within other classes.
However, C++ still allowed global data and global functions, so clashing was
still possible. To solve this problem, C++ introduced namespaces using
additional keywords.
Java was able to avoid all of this by
taking a fresh approach. To produce an unambiguous name for a library, the
specifier used is not unlike an Internet domain name. In fact, the Java creators
want you to use your Internet domain name in reverse since those are guaranteed
to be unique. Since my domain name is BruceEckel.com, my utility library
of foibles would be named com.bruceeckel.utility.foibles. After your
reversed domain name, the dots are intended to represent
subdirectories.
In Java 1.0 and Java 1.1 the domain
extensions com, edu, org, net, etc., were
capitalized by convention, so the
library would appear: COM.bruceeckel.utility.foibles. Partway through the
development of Java 2, however, it was discovered that this caused problems, and
so now the entire package name is lowercase.
This mechanism means that all of your
files automatically live in their own namespaces, and each class within a file
must have a unique identifier. So you do not need to learn special language
features to solve this problem—the language takes care of it for
you.
Whenever you want to use a predefined
class in your program, the compiler must know how to locate it. Of course, the
class might already exist in the same source code file that it’s being
called from. In that case, you simply use the class—even if the class
doesn’t get defined until later in the file. Java eliminates the
“forward referencing” problem so you don’t need to think about
it.
What about a class that exists in some
other file? You might think that the compiler should be smart enough to simply
go and find it, but there is a problem. Imagine that you want to use a class of
a particular name, but more than one definition for that class exists
(presumably these are different definitions). Or worse, imagine that
you’re writing a program, and as you’re building it you add a new
class to your library that conflicts with the name of an existing
class.
To solve this problem, you must eliminate
all potential ambiguities. This is accomplished by telling the Java compiler
exactly what classes you want using the import keyword. import
tells the compiler to bring in a package, which is a library of
classes. (In other languages, a library could consist of functions and data as
well as classes, but remember that all code in Java must be written inside a
class.)
Most of the time you’ll be using
components from the standard Java libraries that come with your compiler. With
these, you don’t need to worry about long, reversed domain names; you just
say, for example:
import java.util.ArrayList;
to tell the compiler that you want to use
Java’s ArrayList class. However, util contains a number of
classes and you might want to use several of them without declaring them all
explicitly. This is easily accomplished by using ‘*’ to
indicate a wild card:
import java.util.*;
It is more common to import a collection
of classes in this manner than to import classes
individually.
Ordinarily, when you create a class you
are describing how objects of that class look and how they will behave. You
don’t actually get anything until you create an object of that class with
new, and at that point data storage is created and methods become
available.
But there are two situations in which
this approach is not sufficient. One is if you want to have only one piece of
storage for a particular piece of data, regardless of how many objects are
created, or even if no objects are created. The other is if you need a method
that isn’t associated with any particular object of this class. That is,
you need a method that you can call even if no objects are created. You can
achieve both of these effects with the static keyword. When you say
something is static, it means that data or method is not tied to any
particular object instance of that class. So even if you’ve never created
an object of that class you can call a static method or access a piece of
static data. With ordinary, non-static data and methods you must
create an object and use that object to access the data or method, since
non-static data and methods must know the particular object they are
working with. Of course, since static methods don’t need any
objects to be created before they are used, they cannot directly access
non-static members or methods by simply calling those other members
without referring to a named object (since non-static members and methods
must be tied to a particular object).
Some object-oriented languages use the
terms class data and class methods, meaning that the data and
methods exist only for the class as a whole, and not for any particular objects
of the class. Sometimes the Java literature uses these terms
too.
To make a data member or method
static, you simply place the keyword before the definition. For example,
the following produces a static data member and initializes
it:
class StaticTest { static int i = 47; }
Now even if you make two
StaticTest objects, there will still be only one piece of storage for
StaticTest.i. Both objects will share the same i.
Consider:
StaticTest st1 = new StaticTest(); StaticTest st2 = new StaticTest();
At this point, both st1.i and
st2.i have the same value of 47 since they refer to the same piece of
memory.
There are two ways to refer to a
static variable. As indicated above, you can name it via an object, by
saying, for example, st2.i. You can also refer to it directly through its
class name, something you cannot do with a non-static member. (This is the
preferred way to refer to a static variable since it emphasizes that
variable’s static nature.)
StaticTest.i++;
The ++ operator increments the
variable. At this point, both st1.i and st2.i will have the value
48.
Similar logic applies to static methods.
You can refer to a static method either through an object as you can with any
method, or with the special additional syntax ClassName.method( ).
You define a static method in a similar way:
class StaticFun { static void incr() { StaticTest.i++; } }
You can see that the StaticFun
method incr( ) increments the static data i. You can
call incr( ) in the typical way, through an object:
StaticFun sf = new StaticFun(); sf.incr();
Or, because incr( ) is a
static method, you can call it directly through its class:
StaticFun.incr();
While static, when applied to a
data member, definitely changes the way the data is created (one for each class
vs. the non-static one for each object), when applied to a method
it’s not so dramatic. An important use of static for methods is to
allow you to call that method without creating an object. This is essential, as
we will see, in defining the main( ) method that is the entry point
for running an application.
Like any method, a static method
can create or use named objects of its type, so a static method is often
used as a “shepherd” for a flock of instances of its own
type.
Finally, here’s the
program.[24] It
starts by printing a string, and then the date, using the Date class from
the Java standard library. Note that an additional style of comment is
introduced here: the ‘//’, which is a comment until the end
of the line:
// HelloDate.java import java.util.*; public class HelloDate { public static void main(String[] args) { System.out.println("Hello, it's: "); System.out.println(new Date()); } }
At the beginning of each program file,
you must place the import statement to bring in any extra classes
you’ll need for the code in that file. Note that I say
“extra;” that’s because there’s a certain library of
classes that are automatically brought into every Java file: java.lang.
Start up your Web browser and look at the documentation from Sun. (If you
haven’t downloaded it from java.sun.com or otherwise installed the
Java documentation, do so now). If you look at the list of the packages,
you’ll see all the different class libraries that come with Java. Select
java.lang. This will bring up a list of all the classes that are part of
that library. Since java.lang is implicitly included in every Java code
file, these classes are automatically available. There’s no Date
class listed in java.lang, which means you must import another library to
use that. If you don’t know the library where a particular class is, or if
you want to see all of the classes, you can select “Tree” in the
Java documentation. Now you can find every single class that comes with Java.
Then you can use the browser’s “find” function to find
Date. When you do you’ll see it listed as
java.util.Date, which lets you know that it’s in the util
library and that you must import java.util.* in order to use
Date.
If you go back to the beginning, select
java.lang and then System, you’ll see that the System
class has several fields, and if you select out you’ll discover
that it’s a static PrintStream object. Since it’s
static you don’t need to create anything. The out object is
always there and you can just use it. What you can do with this out
object is determined by the type it is: a PrintStream. Conveniently,
PrintStream is shown in the description as a hyperlink, so if you click
on that you’ll see a list of all the methods you can call for
PrintStream. There are quite a few and these will be covered later in
this book. For now all we’re interested in is println( ),
which in effect means “print what I’m giving you out to the console
and end with a new line.” Thus, in any Java program you write you can say
System.out.println(“things”) whenever you want to print
something to the console.
The name of the class is the same as the
name of the file. When you’re creating a stand-alone program such as this
one, one of the classes in the file must have the same name as the file. (The
compiler complains if you don’t do this.) That class must contain a method
called main( ) with the signature shown:
public static void main(String[] args) {
The public keyword means that the
method is available to the outside world (described in detail in Chapter 5). The
argument to main( ) is an array of String objects. The
args won’t be used in this program, but the Java compiler insists
that they be there because they hold the arguments invoked on the command
line.
The line that prints the date is quite
interesting:
System.out.println(new Date());
Consider the argument: a Date
object is being created just to send its value to println( ). As
soon as this statement is finished, that Date is unnecessary, and the
garbage collector can come along and get it anytime. We don’t need to
worry about cleaning it
up.
To
compile
and run this program, and all the other programs in this book, you must first
have a Java programming environment. There are a number of third-party
development environments, but in this book we will assume that you are using the
JDK from Sun, which is free. If you are using another development system, you
will need to look in the documentation for that system to determine how to
compile and run programs.
Get on the Internet and go to
java.sun.com. There you will find information and links that will lead
you through the process of downloading and installing the JDK for your
particular platform.
Once the JDK is installed, and
you’ve set up your computer’s path information so that it will find
javac and java, download and unpack the
source code for this book (you can find it on the CD ROM that’s bound in
with this book, or at www.BruceEckel.com). This will create a
subdirectory for each chapter in this book. Move to subdirectory c02 and
type:
javac HelloDate.java
This command should produce no response.
If you get any kind of an error message it means you haven’t installed the
JDK properly and you need to investigate those problems.
On the other hand, if you just get your
command prompt back, you can type:
java HelloDate
and you’ll get the message and the
date as output.
This is the process you can use to
compile and run each of the programs in this book. However, you will see that
the source code for this book also has a file called makefile in each
chapter, and this contains “make” commands for automatically
building the files for that chapter. See this book’s Web page at
www.BruceEckel.com for details on how to use the
makefiles.
There are two types of comments in Java.
The first is the traditional C-style comment that was inherited by C++. These
comments begin with a /* and continue, possibly across many lines, until
a */. Note that many programmers will begin each line of a continued
comment with a *, so you’ll often see:
/* This is a comment * that continues * across lines */
Remember, however, that everything inside
the /* and */ is ignored, so there’s no difference in
saying:
/* This is a comment that continues across lines */
The second form of comment comes from
C++. It is the single-line comment, which starts at a // and continues
until the end of the line. This type of comment is convenient and commonly used
because it’s easy. You don’t need to hunt on the keyboard to find
/ and then * (instead, you just press the same key twice), and you
don’t need to close the comment. So you will often see:
// this is a one-line comment
One of the thoughtful parts of the Java
language is that the designers didn’t consider writing code to be the only
important activity—they also thought about documenting it. Possibly the
biggest problem with documenting code has been maintaining that documentation.
If the documentation and the code are separate, it becomes a hassle to change
the documentation every time you change the code. The solution seems simple:
link the code to the documentation. The easiest way to do this is to put
everything in the same file. To complete the picture, however, you need a
special comment syntax to mark special documentation, and a tool to extract
those comments and put them in a useful form. This is what Java has
done.
The tool to extract the comments is
called javadoc. It uses some of the technology from the Java compiler to
look for special comment tags you put in your programs. It not only extracts the
information marked by these tags, but it also pulls out the class name or method
name that adjoins the comment. This way you can get away with the minimal amount
of work to generate decent program documentation.
The output of javadoc is an HTML file
that you can view with your Web browser. This tool allows you to create and
maintain a single source file and automatically generate useful documentation.
Because of javadoc we have a standard for creating documentation, and it’s
easy enough that we can expect or even demand documentation with all Java
libraries.
All of the javadoc commands occur only
within /** comments. The comments end with */ as usual. There are
two primary ways to use javadoc: embed HTML, or use “doc tags.” Doc
tags are commands that start with a ‘@’ and are placed at the
beginning of a comment line. (A leading ‘*’, however, is
ignored.)
There are three “types” of
comment documentation, which correspond to the element the comment precedes:
class, variable, or method. That is, a class comment appears right before the
definition of a class; a variable comment appears right in front of the
definition of a variable, and a method comment appears right in front of the
definition of a method. As a simple example:
/** A class comment */ public class docTest { /** A variable comment */ public int i; /** A method comment */ public void f() {} }
Note that javadoc will process comment
documentation for only public and protected members. Comments for
private and “friendly” members (see Chapter 5) are ignored
and you’ll see no output. (However, you can use the -private flag
to include private members as well.) This makes sense, since only
public and protected members are available outside the file, which
is the client programmer’s perspective. However, all class comments
are included in the output.
The output for the above code is an HTML
file that has the same standard format as all the rest of the Java
documentation, so users will be comfortable with the format and can easily
navigate your classes. It’s worth entering the above code, sending it
through javadoc and viewing the resulting HTML file to see the
results.
Javadoc passes HTML commands through to
the generated HTML document. This allows you full use of HTML; however, the
primary motive is to let you format code, such as:
/** * <pre> * System.out.println(new Date()); * </pre> */
You can also use HTML just as you would
in any other Web document to format the regular text in your
descriptions:
/** * You can <em>even</em> insert a list: * <ol> * <li> Item one * <li> Item two * <li> Item three * </ol> */
Note that within the documentation
comment, asterisks at the beginning of a line are thrown away by javadoc, along
with leading spaces. Javadoc reformats everything so that it conforms to the
standard documentation appearance. Don’t use headings such as
<h1> or <hr> as embedded HTML because javadoc inserts
its own headings and yours will interfere with them.
All three types of comment documentation
(class, variable, and method) can contain @see tags, which allow you to
refer to the documentation in other classes. Javadoc will generate HTML with the
@see tags hyperlinked to the other documentation. The forms
are:
@see classname @see fully-qualified-classname @see fully-qualified-classname#method-name
Each one adds a hyperlinked “See
Also” entry to the generated documentation. Javadoc will not check the
hyperlinks you give it to make sure they are
valid.
Along with embedded HTML and @see
references, class documentation can include tags for version information and
the author’s name. Class documentation can also be used for
interfaces (see Chapter 8).
This is of the form:
@version version-information
in which version-information is
any significant information you see fit to include. When the -version
flag is placed on the javadoc command line, the version information will be
called out specially in the generated HTML documentation.
This is of the form:
@author author-information
in which author-information is,
presumably, your name, but it could also include your email address or any other
appropriate information. When the -author flag is placed on the javadoc
command line, the author information will be called out specially in the
generated HTML documentation.
You can have multiple author tags for a
list of authors, but they must be placed consecutively. All the author
information will be lumped together into a single paragraph in the generated
HTML.
This tag allows you to indicate the
version of this code that began using a particular feature. You’ll see it
appearing in the HTML Java documentation to indicate what version of the JDK is
used.
As well as embedded documentation and
@see references, methods allow documentation tags for parameters, return
values, and exceptions.
This is of the form:
@param parameter-name description
in which parameter-name is the
identifier in the parameter list, and description is text that can
continue on subsequent lines. The description is considered finished when a new
documentation tag is encountered. You can have any number of these, presumably
one for each parameter.
This is of the form:
@return description
in which description gives you the
meaning of the return value. It can continue on subsequent
lines.
Exceptions will be demonstrated in
Chapter 10, but briefly they are objects that can be “thrown” out of
a method if that method fails. Although only one exception object can emerge
when you call a method, a particular method might produce any number of
different types of exceptions, all of which need descriptions. So the form for
the exception tag is:
@throws fully-qualified-class-name description
in which
fully-qualified-class-name gives an unambiguous name of an exception
class that’s defined somewhere, and description (which can continue
on subsequent lines) tells you why this particular type of exception can emerge
from the method call.
This is used to tag features that were
superseded by an improved feature. The deprecated tag is a suggestion that you
no longer use this particular feature, since sometime in the future it is likely
to be removed. A method that is marked @deprecated causes the compiler to
issue a warning if it is
used.
Here is the first Java program again,
this time with documentation comments added:
//: c02:HelloDate.java import java.util.*; /** The first Thinking in Java example program. * Displays a string and today's date. * @author Bruce Eckel * @author http://www.BruceEckel.com * @version 2.0 */ public class HelloDate { /** Sole entry point to class & application * @param args array of string arguments * @return No return value * @exception exceptions No exceptions thrown */ public static void main(String[] args) { System.out.println("Hello, it's: "); System.out.println(new Date()); } } ///:~
The first line of the file uses my own
technique of putting a ‘:’ as a special marker for the
comment line containing the source file name. That line contains the path
information to the file (in this case, c02 indicates Chapter 2) followed
by the file
name[25]. The last
line also finishes with a comment, and this one indicates the end of the source
code listing, which allows it to be automatically extracted from the text of
this book and checked with a
compiler.
The unofficial standard in Java is to
capitalize the first letter of a class name. If the class name consists of
several words, they are run together (that is, you don’t use underscores
to separate the names), and the first letter of each embedded word is
capitalized, such as:
class AllTheColorsOfTheRainbow { // ...
For almost everything else: methods,
fields (member variables), and object reference names, the accepted style is
just as it is for classes except that the first letter of the identifier
is lowercase. For example:
class AllTheColorsOfTheRainbow { int anIntegerRepresentingColors; void changeTheHueOfTheColor(int newHue) { // ... } // ... }
Of course, you should remember that the
user must also type all these long names, and so be merciful.
The Java code you will see in the Sun
libraries also follows the placement of open-and-close curly braces that you see
used in this
book.
In this chapter you have seen enough of
Java programming to understand how to write a simple program, and you have
gotten an overview of the language and some of its basic ideas. However, the
examples so far have all been of the form “do this, then do that, then do
something else.” What if you want the program to make choices, such as
“if the result of doing this is red, do that; if not, then do something
else”? The support in Java for this fundamental programming activity will
be covered in the next
chapter.
[21]
This can be a flashpoint. There are those who say “clearly, it’s a
pointer,” but this presumes an underlying implementation. Also, Java
references are much more akin to C++ references than pointers in their syntax.
In the first edition of this book, I choose to invent a new term,
“handle,” because C++ references and Java references have some
important differences. I was coming out of C++ and did not want to confuse the
C++ programmers whom I assumed would be the largest audience for Java. In the
2nd edition, I decided that “reference” was the more
commonly used term, and that anyone changing from C++ would have a lot more to
cope with than the terminology of references, so they might as well jump in with
both feet. However, there are people who disagree even with the term
“reference.” I read in one book where it was “completely wrong
to say that Java supports pass by reference,” because Java object
identifiers (according to that author) are actually “object
references.” And (he goes on) everything is actually pass by value.
So you’re not passing by reference, you’re “passing an object
reference by value.” One could argue for the precision of such convoluted
explanations, but I think my approach simplifies the understanding of the
concept without hurting anything (well, the language lawyers may claim that
I’m lying to you, but I’ll say that I’m providing an
appropriate abstraction.)
[22]
static methods, which you’ll learn about soon, can be called for
the class, without an object.
[23]
With the usual exception of the aforementioned “special” data types
boolean, char, byte, short, int, long,
float, and double. In general, though, you pass objects, which really
means you pass references to objects.
[24]
Some programming environments will flash programs up on the screen and close
them before you've had a chance to see the results. You can put in the following
bit of code at the end of main( ) to pause the
output:
try { System.in.read(); } catch(Exception e) {}
This will pause the output until
you press “Enter” (or any other key). This code involves concepts
that will not be introduced until much later in the book, so you won’t
understand it until then, but it will do the trick.
[25]
A tool that I created using Python (see www.Python.org) uses this information to
extract the code files, put them in appropriate subdirectories, and create
makefiles.