|
|||||
![]() The
size of the boolean
type
is not explicitly defined; it is
only specified
to be
able to take the literal
values true
or
false.
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.
High-precision
numbers
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.
106
Thinking
in Java
![]() Arrays
in Java
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.
Arrays
will be covered in detail in
later chapters.
You
never need to
destroy
an object
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.
Chapter
2: Everything is an Object
107
![]() Scoping
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.
108
Thinking
in Java
![]() Scope
of objects
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.
Chapter
2: Everything is an Object
109
![]() Creating
new
data
types: class
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.
Fields
and methods
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.)
110
Thinking
in Java
![]() 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.
Default
values for primitive
members
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:
Chapter
2: Everything is an Object
111
![]() 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.)
Methods,
arguments,
and
return values
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
112
Thinking
in Java
![]() 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,2 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."
2
static
methods,
which you'll learn about soon, can be
called for
the class, without an
object.
Chapter
2: Everything is an Object
113
![]() The
argument list
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 references3.
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:
3
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.
114
Thinking
in Java
![]() 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.
Building
a Java program
There
are several other issues
you must understand before
seeing your
first
Java program.
Name
visibility
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
Chapter
2: Everything is an Object
115
![]() 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.
Using
other components
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.)
116
Thinking
in Java
![]() 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.
The
static
keyword
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).
Chapter
2: Everything is an Object
117
![]() 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++; }
}
118
Thinking
in Java
![]() 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.
Your
first Java program
Finally,
here's the program.4 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
4
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.
Chapter
2: Everything is an Object
119
![]() 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
120
Thinking
in Java
![]() 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.
Compiling
and running
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
Chapter
2: Everything is an Object
121
![]() source
code for this book
(you can find it on the CD
ROM that's bound in
with
this book, or at ).
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
for
details on how to use
the
makefiles.
Comments
and embedded
documentation
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
*/
122
Thinking
in Java
![]() 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
Comment
documentation
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
Chapter
2: Everything is an Object
123
![]() have
a standard for creating
documentation, and it's easy
enough that we
can
expect or even demand
documentation with all Java
libraries.
Syntax
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.
124
Thinking
in Java
![]() Embedded
HTML
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
types of comment documentation--class,
variable, and
method--can
support
embedded HTML.
@see:
referring to other
classes
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
Chapter
2: Everything is an Object
125
![]() @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.
Class
documentation tags
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).
@version
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.
@author
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.
126
Thinking
in Java
![]() @since
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.
Variable
documentation tags
Variable
documentation can include
only embedded HTML and
@see
references.
Method
documentation tags
As
well as embedded documentation
and @see
references,
methods allow
documentation
tags for parameters, return
values, and
exceptions.
@param
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.
@return
This
is of the form:
@return
description
in
which description
gives
you the meaning of the
return value. It can
continue
on subsequent lines.
@throws
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
Chapter
2: Everything is an Object
127
![]() 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.
@deprecated
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.
Documentation
example
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
*
@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());
}
128
Thinking
in Java
![]() }
///:~
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
name5.
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.
Coding
style
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.
5
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.
Chapter
2: Everything is an Object
129
![]() Summary
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.
Exercises
Solutions
to selected exercises can be
found in the electronic
document The
Thinking in Java
Annotated
Solution Guide, available
for a small fee from
.
1.
Following
the HelloDate.java
example
in this chapter, create
a
"hello,
world" program that simply
prints out that statement.
You
need
only a single method in your
class (the "main" one
that gets
executed
when the program starts).
Remember to make it static
and
to include the argument
list, even though you
don't use the
argument
list. Compile the program
with javac
and
run it using
java.
If you are using a different
development environment
than
the
JDK, learn how to compile
and run programs in
that
environment.
2.
Find
the code fragments involving
ATypeName
and
turn them
into
a program that compiles and
runs.
3.
Turn
the DataOnly
code
fragments into a program
that compiles
and
runs.
4.
Modify
Exercise 3 so that the
values of the data in
DataOnly
are
assigned
to and printed in main(
).
5.
Write
a program that includes and
calls the storage(
) method
defined
as a code fragment in this
chapter.
6.
Turn
the StaticFun
code
fragments into a working
program.
130
Thinking
in Java
![]() 7.
Write
a program that prints three
arguments taken from
the
command
line. To do this, you'll
need to index into the
command-
line
array of Strings.
8.
Turn
the AllTheColorsOfTheRainbow
example
into a program
that
compiles and runs.
9.
Find
the code for the
second version of HelloDate.java,
which is
the
simple comment documentation
example. Execute javadoc
on
the file and view
the results with your
Web browser.
10.
Turn
docTest
into
a file that compiles and
then run it through
javadoc.
Verify the resulting
documentation with your
Web
browser.
11.
Add
an HTML list of items to the
documentation in Exercise
10.
12.
Take
the program in Exercise 1
and add comment
documentation
to
it. Extract this comment
documentation into an HTML
file
using
javadoc
and
view it with your Web
browser.
Chapter
2: Everything is an Object
131
![]() 3:
Controlling
Program
Flow
Like
a sentient creature, a program
must manipulate its
world
and make choices during
execution.
In
Java you manipulate objects
and data using operators,
and you make
choices
with execution control
statements. Java was
inherited from C++,
so
most of these statements and
operators will be familiar to C
and C++
programmers.
Java has also added
some improvements and
simplifications.
If
you find yourself
floundering a bit in this
chapter, make sure you
go
through
the multimedia CD ROM bound
into this book: Thinking
in C:
Foundations
for Java and C++. It contains
audio lectures,
slides,
exercises,
and solutions specifically
designed to bring you up to
speed
with
the C syntax necessary to
learn Java.
Using
Java operators
An
operator takes one or more
arguments and produces a new
value. The
arguments
are in a different form than
ordinary method calls, but
the
effect
is the same. You should be
reasonably comfortable with
the general
concept
of operators from your
previous programming
experience.
Addition
(+),
subtraction and unary minus
(-),
multiplication (*),
division
(/),
and assignment (=)
all work much the
same in any
programming
language.
All
operators produce a value
from their operands. In
addition, an
operator
can change the value of an
operand. This is called a
side
effect.
The
most common use for
operators that modify their
operands is to
generate
the side effect, but
you should keep in mind
that the value
produced
is available for your use
just as in operators without
side effects.
133
![]() Almost
all operators work only
with primitives. The
exceptions are `=',
`=='
and `!=',
which work with all
objects (and are a point of
confusion for
objects).
In addition, the String
class
supports `+'
and `+='.
Precedence
Operator
precedence defines how an
expression evaluates when
several
operators
are present. Java has
specific rules that
determine the order
of
evaluation.
The easiest one to remember
is that multiplication
and
division
happen before addition and
subtraction. Programmers
often
forget
the other precedence rules,
so you should use
parentheses to make
the
order of evaluation explicit.
For example:
A
= X + Y - 2/2 + Z;
has
a very different meaning
from the same statement
with a particular
grouping
of parentheses:
A
= X + (Y - 2)/(2 + Z);
Assignment
Assignment
is performed with the
operator =. It means "take
the value of
the
right-hand side (often
called the rvalue)
and copy it into the
left-hand
side
(often called the lvalue).
An rvalue is any constant,
variable or
expression
that can produce a value,
but an lvalue must be a
distinct,
named
variable. (That is, there
must be a physical space to
store a value.)
For
instance, you can assign a
constant value to a variable
(A
= 4;),
but
you
cannot assign anything to
constant value--it cannot be an
lvalue. (You
can't
say 4
= A;.)
Assignment
of primitives is quite straightforward.
Since the primitive
holds
the actual value and
not a reference to an object,
when you assign
primitives
you copy the contents
from one place to another.
For example,
if
you say A
= B for
primitives, then the
contents of B
are
copied into A.
If
you
then go on to modify A,
B is
naturally unaffected by this
modification.
As
a programmer, this is what
you've come to expect for
most situations.
When
you assign objects, however,
things change. Whenever
you
manipulate
an object, what you're
manipulating is the reference, so
when
you
assign "from one object to
another" you're actually
copying a
134
Thinking
in Java
![]() reference
from one place to another.
This means that if you
say C
= D for
objects,
you end up with both
C and
D pointing
to the object that,
originally,
only D
pointed
to. The following example
will demonstrate
this.
Here's
the example:
//:
c03:Assignment.java
//
Assignment with objects is a bit tricky.
class
Number {
int
i;
}
public
class Assignment {
public
static void main(String[]
args)
{
Number
n1 = new Number();
Number
n2 = new Number();
n1.i
= 9;
n2.i
= 47;
System.out.println("1:
n1.i: "
+
n1.i +
",
n2.i: " + n2.i);
n1
= n2;
System.out.println("2:
n1.i: "
+
n1.i +
",
n2.i: " + n2.i);
n1.i
= 27;
System.out.println("3:
n1.i: "
+
n1.i +
",
n2.i: " + n2.i);
}
}
///:~
The
Number
class
is simple, and two instances
of it (n1
and
n2)
are
created
within main(
).
The i
value
within each Number
is
given a
different
value, and then n2 is
assigned to n1,
and n1
is
changed. In many
programming
languages you would expect
n1 and
n2 to
be independent
at
all times, but because
you've assigned a reference
here's the output
you'll
see:
1:
n1.i: 9, n2.i: 47
2:
n1.i: 47, n2.i: 47
3:
n1.i: 27, n2.i: 27
Chapter
3: Controlling Program Flow
135
![]() Changing
the n1
object
appears to change the
n2 object
as well! This is
because
both n1
and
n2 contain
the same reference, which is
pointing to
the
same object. (The original
reference that was in
n1 that
pointed to the
object
holding a value of 9 was
overwritten during the
assignment and
effectively
lost; its object will be
cleaned up by the garbage
collector.)
This
phenomenon is often called
aliasing
and
it's a fundamental way
that
Java
works with objects. But
what if you don't want
aliasing to occur in
this
case? You could forego
the assignment and
say:
n1.i
= n2.i;
This
retains the two separate
objects instead of tossing
one and tying n1
and
n2 to
the same object, but
you'll soon realize that
manipulating the
fields
within objects is messy and
goes against good
object-oriented
design
principles. This is a nontrivial
topic, so it is left for
Appendix A,
which
is devoted to aliasing. In the
meantime, you should keep in
mind
that
assignment for objects can
add surprises.
Aliasing
during method calls
Aliasing
will also occur when
you pass an object into a
method:
//:
c03:PassObject.java
//
Passing objects to methods may not be what
//
you're used to.
class
Letter {
char
c;
}
public
class PassObject {
static
void f(Letter y) {
y.c
= 'z';
}
public
static void main(String[] args) {
Letter
x = new Letter();
x.c
= 'a';
System.out.println("1:
x.c: " + x.c);
f(x);
System.out.println("2:
x.c: " + x.c);
}
136
Thinking
in Java
![]() }
///:~
In
many programming languages,
the method f(
) would
appear to be
making
a copy of its argument
Letter y
inside
the scope of the
method.
But
once again a reference is
being passed so the
line
y.c
= 'z';
is
actually changing the object
outside of f(
).
The output shows
this:
1:
x.c: a
2:
x.c: z
Aliasing
and its solution is a
complex issue and, although
you must wait
until
Appendix A for all the
answers, you should be aware
of it at this
point
so you can watch for
pitfalls.
Mathematical
operators
The
basic mathematical operators
are the same as the
ones available in
most
programming languages: addition
(+),
subtraction (-),
division (/),
multiplication
(*)
and modulus (%,
which produces the remainder
from
integer
division). Integer division
truncates, rather than
rounds, the
result.
Java
also uses a shorthand
notation to perform an operation
and an
assignment
at the same time. This is
denoted by an operator followed
by
an
equal sign, and is
consistent with all the
operators in the
language
(whenever
it makes sense). For
example, to add 4 to the
variable x
and
assign
the result to x,
use: x
+= 4.
This
example shows the use of
the mathematical
operators:
//:
c03:MathOps.java
//
Demonstrates the mathematical
operators.
import
java.util.*;
public
class MathOps {
//
Create a shorthand to save typing:
static
void prt(String s) {
System.out.println(s);
}
//
shorthand to print a string and an int:
Chapter
3: Controlling Program Flow
137
![]() static
void pInt(String s, int i) {
prt(s
+ " = " + i);
}
//
shorthand to print a string and a float:
static
void pFlt(String s, float f) {
prt(s
+ " = " + f);
}
public
static void main(String[] args) {
//
Create a random number generator,
//
seeds with current time by default:
Random
rand = new Random();
int
i, j, k;
//
'%' limits maximum value to 99:
j
= rand.nextInt() % 100;
k
= rand.nextInt() % 100;
pInt("j",j);
pInt("k",k);
i
= j + k; pInt("j + k", i);
i
= j - k; pInt("j - k", i);
i
= k / j; pInt("k / j", i);
i
= k * j; pInt("k * j", i);
i
= k % j; pInt("k % j", i);
j
%= k; pInt("j %= k", j);
//
Floating-point number tests:
float
u,v,w; // applies to doubles, too
v
= rand.nextFloat();
w
= rand.nextFloat();
pFlt("v",
v); pFlt("w", w);
u
= v + w; pFlt("v + w", u);
u
= v - w; pFlt("v - w", u);
u
= v * w; pFlt("v * w", u);
u
= v / w; pFlt("v / w", u);
//
the following also works for
//
char, byte, short, int, long,
//
and double:
u
+= v; pFlt("u += v", u);
u
-= v; pFlt("u -= v", u);
u
*= v; pFlt("u *= v", u);
u
/= v; pFlt("u /= v", u);
}
}
///:~
138
Thinking
in Java
![]() The
first thing you will
see are some shorthand
methods for printing:
the
prt(
) method
prints a String,
the pInt(
) prints
a String
followed
by an
int
and
the pFlt(
) prints
a String
followed
by a float.
Of course, they all
ultimately
end up using System.out.println(
).
To
generate numbers, the
program first creates a
Random
object.
Because
no arguments are passed
during creation, Java uses
the current
time
as a seed for the random
number generator. The
program generates
a
number of different types of
random numbers with the
Random
object
simply
by calling different methods:
nextInt(
),
nextLong(
),
nextFloat(
) or
nextDouble(
).
The
modulus operator, when used
with the result of the
random number
generator,
limits the result to an
upper bound of the operand
minus one
(99
in this case).
Unary
minus and plus
operators
The
unary minus (-) and
unary plus (+) are
the same operators as
binary
minus
and plus. The compiler
figures out which use is
intended by the
way
you write the expression.
For instance, the
statement
x
= -a;
has
an obvious meaning. The
compiler is able to figure
out:
x
= a * -b;
but
the reader might get
confused, so it is clearer to
say:
x
= a * (-b);
The
unary minus produces the
negative of the value. Unary
plus provides
symmetry
with unary minus, although
it doesn't have any
effect.
Auto
increment and decrement
Java,
like C, is full of shortcuts.
Shortcuts can make code
much easier to
type,
and either easier or harder
to read.
Two
of the nicer shortcuts are
the increment and decrement
operators
(often
referred to as the auto-increment
and auto-decrement
operators).
The
decrement operator is --
and
means "decrease by one
unit." The
Chapter
3: Controlling Program Flow
139
![]() increment
operator is ++
and
means "increase by one
unit." If a
is
an int,
for
example, the expression
++a is
equivalent to (a
= a + 1).
Increment
and
decrement operators produce
the value of the variable as
a result.
There
are two versions of each
type of operator, often
called the prefix
and
postfix
versions. Pre-increment means
the ++
operator
appears before
the
variable or expression, and
post-increment means the
++ operator
appears
after the variable or
expression. Similarly, pre-decrement
means
the
-- operator
appears before the variable
or expression, and
post-
decrement
means the --
operator
appears after the variable
or expression.
For
pre-increment and pre-decrement,
(i.e., ++a
or
--a),
the operation is
performed
and the value is produced.
For post-increment and
post-
decrement
(i.e. a++
or
a--),
the value is produced, then
the operation is
performed.
As an example:
//:
c03:AutoInc.java
//
Demonstrates the ++ and -- operators.
public
class AutoInc {
public
static void main(String[] args) {
int
i = 1;
prt("i
: " + i);
prt("++i
: " + ++i); // Pre-increment
prt("i++
: " + i++); // Post-increment
prt("i
: " + i);
prt("--i
: " + --i); // Pre-decrement
prt("i--
: " + i--); // Post-decrement
prt("i
: " + i);
}
static
void prt(String s) {
System.out.println(s);
}
}
///:~
The
output for this program
is:
i:
1
++i
:2
i++
:2
i:
3
--i
:2
140
Thinking
in Java
![]() i--
: 2
i:1
You
can see that for
the prefix form you
get the value after
the operation
has
been performed, but with
the postfix form you
get the value before
the
operation
is performed. These are the
only operators (other than
those
involving
assignment) that have side
effects. (That is, they
change the
operand
rather than using just
its value.)
The
increment operator is one
explanation for the name
C++, implying
"one
step beyond C." In an early
Java speech, Bill Joy
(one of the
creators),
said that "Java=C++--" (C
plus plus minus minus),
suggesting
that
Java is C++ with the
unnecessary hard parts
removed and therefore
a
much
simpler language. As you
progress in this book you'll
see that many
parts
are simpler, and yet
Java isn't that
much
easier than C++.
Relational
operators
Relational
operators generate a boolean
result.
They evaluate the
relationship
between the values of the
operands. A relational
expression
produces
true if
the relationship is true,
and false
if
the relationship is
untrue.
The relational operators are
less than (<), greater
than (>), less
than
or equal to (<=), greater
than or equal to (>=),
equivalent (==) and
not
equivalent (!=). Equivalence
and nonequivalence works
with all built-
in
data types, but the
other comparisons won't work
with type boolean.
Testing
object equivalence
The
relational operators ==
and
!= also
work with all objects,
but their
meaning
often confuses the
first-time Java programmer.
Here's an
example:
//:
c03:Equivalence.java
public
class Equivalence {
public
static void main(String[] args) {
Integer
n1 = new Integer(47);
Integer
n2 = new Integer(47);
System.out.println(n1
== n2);
System.out.println(n1
!= n2);
}
Chapter
3: Controlling Program Flow
141
![]() }
///:~
The
expression System.out.println(n1
== n2) will
print the result of
the
boolean
comparison
within it. Surely the
output should be true
and
then
false,
since both Integer
objects
are the same. But
while the
contents
of
the objects are the
same, the references are
not the same
and
the
operators ==
and
!= compare
object references. So the
output is
actually
false
and
then true.
Naturally, this surprises
people at first.
What
if you want to compare the
actual contents of an object
for
equivalence?
You must use the
special method equals(
) that
exists for
all
objects (not primitives,
which work fine with
== and
!=).
Here's how
it's
used:
//:
c03:EqualsMethod.java
public
class EqualsMethod {
public
static void main(String[] args) {
Integer
n1 = new Integer(47);
Integer
n2 = new Integer(47);
System.out.println(n1.equals(n2));
}
}
///:~
The
result will be true,
as you would expect. Ah,
but it's not as simple
as
that.
If you create your own
class, like this:
//:
c03:EqualsMethod2.java
class
Value {
int
i;
}
public
class EqualsMethod2 {
public
static void main(String[] args) {
Value
v1 = new Value();
Value
v2 = new Value();
v1.i
= v2.i = 100;
System.out.println(v1.equals(v2));
}
}
///:~
142
Thinking
in Java
![]() you're
back to square one: the
result is false.
This is because the
default
behavior
of equals(
) is
to compare references. So unless
you override
equals(
) in
your new class you
won't get the desired
behavior.
Unfortunately,
you won't learn about
overriding until Chapter 7,
but being
aware
of the way equals(
) behaves
might save you some
grief in the
meantime.
Most
of the Java library classes
implement equals(
) so
that it compares
the
contents of objects instead of
their references.
Logical
operators
The
logical operators AND (&&), OR
(||) and NOT (!)
produce a boolean
value
of true
or
false
based
on the logical relationship of
its arguments.
This
example uses the relational
and logical
operators:
//:
c03:Bool.java
//
Relational and logical operators.
import
java.util.*;
public
class Bool {
public
static void main(String[] args) {
Random
rand = new Random();
int
i = rand.nextInt() % 100;
int
j = rand.nextInt() % 100;
prt("i
= " + i);
prt("j
= " + j);
prt("i
> j is " + (i > j));
prt("i
< j is " + (i < j));
prt("i
>= j is " + (i >= j));
prt("i
<= j is " + (i <= j));
prt("i
== j is " + (i == j));
prt("i
!= j is " + (i != j));
//
Treating an int as a boolean is
//
not legal Java
//!
prt("i && j is " + (i && j));
//!
prt("i || j is " + (i || j));
//!
prt("!i is " + !i);
prt("(i
< 10) && (j < 10) is "
Chapter
3: Controlling Program Flow
143
![]() +
((i < 10) && (j < 10)) );
prt("(i
< 10) || (j < 10) is "
+
((i < 10) || (j < 10)) );
}
static
void prt(String s) {
System.out.println(s);
}
}
///:~
You
can apply AND, OR, or
NOT to boolean
values
only. You can't use
a
non-boolean
as
if it were a boolean
in
a logical expression as you
can in
C
and C++. You can
see the failed attempts at
doing this commented
out
with
a //!
comment
marker. The subsequent
expressions, however,
produce
boolean
values
using relational comparisons,
then use logical
operations
on the results.
One
output listing looked like
this:
i
= 85
j=4
i
> j is true
i
< j is false
i
>= j is true
i
<= j is false
i
== j is false
i
!= j is true
(i
< 10) && (j < 10) is false
(i
< 10) || (j < 10) is true
Note
that a boolean
value
is automatically converted to an
appropriate
text
form if it's used where a
String
is
expected.
You
can replace the definition
for int
in
the above program with
any other
primitive
data type except boolean.
Be aware, however, that
the
comparison
of floating-point numbers is very
strict. A number that is
the
tiniest
fraction different from
another number is still "not
equal." A
number
that is the tiniest bit
above zero is still
nonzero.
Short-circuiting
When
dealing with logical
operators you run into a
phenomenon called
"short
circuiting." This means that
the expression will be
evaluated only
144
Thinking
in Java
![]() until
the
truth or falsehood of the
entire expression can be
unambiguously
determined.
As a result, all the parts
of a logical expression might
not be
evaluated.
Here's an example that
demonstrates short-circuiting:
//:
c03:ShortCircuit.java
//
Demonstrates short-circuiting
behavior.
//
with logical operators.
public
class ShortCircuit {
static
boolean test1(int val) {
System.out.println("test1("
+ val + ")");
System.out.println("result:
" + (val < 1));
return
val < 1;
}
static
boolean test2(int val) {
System.out.println("test2("
+ val + ")");
System.out.println("result:
" + (val < 2));
return
val < 2;
}
static
boolean test3(int val) {
System.out.println("test3("
+ val + ")");
System.out.println("result:
" + (val < 3));
return
val < 3;
}
public
static void main(String[] args) {
if(test1(0)
&& test2(2) && test3(2))
System.out.println("expression
is true");
else
System.out.println("expression
is false");
}
}
///:~
Each
test performs a comparison
against the argument and
returns true
or
false. It also prints
information to show you that
it's being called.
The
tests
are used in the
expression:
if(test1(0)
&& test2(2) && test3(2))
You
might naturally think that
all three tests would be
executed, but the
output
shows otherwise:
test1(0)
Chapter
3: Controlling Program Flow
145
![]() result:
true
test2(2)
result:
false
expression
is false
The
first test produced a
true result,
so the expression
evaluation
continues.
However, the second test
produced a false
result.
Since this
means
that the whole expression
must be false,
why continue
evaluating
the
rest of the expression? It
could be expensive. The
reason for short-
circuiting,
in fact, is precisely that;
you can get a potential
performance
increase
if all the parts of a
logical expression do not
need to be evaluated.
Bitwise
operators
The
bitwise operators allow you
to manipulate individual bits in
an
integral
primitive data type. Bitwise
operators perform boolean
algebra on
the
corresponding bits in the
two arguments to produce the
result.
The
bitwise operators come from
C's low-level orientation;
you were often
manipulating
hardware directly and had to
set the bits in
hardware
registers.
Java was originally designed
to be embedded in TV set-top
boxes,
so this low-level orientation
still made sense. However,
you
probably
won't use the bitwise
operators much.
The
bitwise AND operator
(&)
produces a one in the output
bit if both
input
bits are one; otherwise it
produces a zero. The bitwise
OR operator
(|)
produces a one in the output
bit if either input bit is a
one and
produces
a zero only if both input
bits are zero. The
bitwise EXCLUSIVE
OR,
or XOR (^),
produces a one in the output
bit if one or the other
input
bit
is a one, but not both.
The bitwise NOT (~,
also called the ones
complement
operator)
is a unary operator; it takes
only one argument.
(All
other bitwise operators are
binary operators.) Bitwise
NOT produces
the
opposite of the input bit--a
one if the input bit is
zero, a zero if the
input
bit is one.
The
bitwise operators and
logical operators use the
same characters, so it
is
helpful to have a mnemonic
device to help you remember
the meanings:
since
bits are "small," there is
only one character in the
bitwise operators.
146
Thinking
in Java
![]() Bitwise
operators can be combined
with the =
sign
to unite the
operation
and
assignment: &=,
|= and
^= are
all legitimate. (Since
~ is
a unary
operator
it cannot be combined with
the =
sign.)
The
boolean
type
is treated as a one-bit value so it is
somewhat different.
You
can perform a bitwise AND,
OR and XOR, but you
can't perform a
bitwise
NOT (presumably to prevent
confusion with the logical
NOT). For
booleans
the bitwise operators have
the same effect as the
logical
operators
except that they do not
short circuit. Also, bitwise
operations on
booleans
include an XOR logical
operator that is not
included under the
list
of "logical" operators. You're
prevented from using
booleans
in shift
expressions,
which is described
next.
Shift
operators
The
shift operators also
manipulate bits. They can be
used solely with
primitive,
integral types. The
left-shift operator (<<)
produces the
operand
to the left of the operator
shifted to the left by the
number of bits
specified
after the operator
(inserting zeroes at the
lower-order bits).
The
signed
right-shift operator (>>)
produces the operand to the
left of the
operator
shifted to the right by the
number of bits specified
after the
operator.
The signed right shift
>>
uses
sign
extension: if the
value is
positive,
zeroes are inserted at the
higher-order bits; if the
value is
negative,
ones are inserted at the
higher-order bits. Java has
also added
the
unsigned right shift
>>>,
which
uses zero
extension: regardless
of the
sign,
zeroes are inserted at the
higher-order bits. This
operator does not
exist
in C or C++.
If
you shift a char,
byte,
or
short,
it will be promoted to int
before
the
shift
takes place, and the
result will be an int.
Only the five low-order
bits
of
the right-hand side will be
used. This prevents you
from shifting more
than
the number of bits in an
int.
If you're operating on a long,
you'll get
a
long
result.
Only the six low-order
bits of the right-hand side
will be
used
so you can't shift more
than the number of bits in a
long.
Shifts
can be combined with the
equal sign (<<=
or
>>=
or
>>>=).
The
lvalue
is replaced by the lvalue
shifted by the rvalue. There
is a problem,
however,
with the unsigned right
shift combined with
assignment. If you
use
it with byte
or
short you
don't get the correct
results. Instead,
these
are
promoted to int
and
right shifted, but then
truncated as they are
Chapter
3: Controlling Program Flow
147
![]() assigned
back into their variables,
so you get -1
in
those cases. The
following
example demonstrates
this:
//:
c03:URShift.java
//
Test of unsigned right shift.
public
class URShift {
public
static void main(String[] args) {
int
i = -1;
i
>>>= 10;
System.out.println(i);
long
l = -1;
l
>>>= 10;
System.out.println(l);
short
s = -1;
s
>>>= 10;
System.out.println(s);
byte
b = -1;
b
>>>= 10;
System.out.println(b);
b
= -1;
System.out.println(b>>>10);
}
}
///:~
In
the last line, the
resulting value is not
assigned back into b,
but is
printed
directly and so the correct
behavior occurs.
Here's
an example that demonstrates
the use of all the
operators involving
bits:
//:
c03:BitManipulation.java
//
Using the bitwise operators.
import
java.util.*;
public
class BitManipulation {
public
static void main(String[] args) {
Random
rand = new Random();
int
i = rand.nextInt();
int
j = rand.nextInt();
pBinInt("-1",
-1);
pBinInt("+1",
+1);
148
Thinking
in Java
![]() int
maxpos = 2147483647;
pBinInt("maxpos",
maxpos);
int
maxneg = -2147483648;
pBinInt("maxneg",
maxneg);
pBinInt("i",
i);
pBinInt("~i",
~i);
pBinInt("-i",
-i);
pBinInt("j",
j);
pBinInt("i
& j", i & j);
pBinInt("i
| j", i | j);
pBinInt("i
^ j", i ^ j);
pBinInt("i
<< 5", i << 5);
pBinInt("i
>> 5", i >> 5);
pBinInt("(~i)
>> 5", (~i) >> 5);
pBinInt("i
>>> 5", i >>> 5);
pBinInt("(~i)
>>> 5", (~i) >>> 5);
long
l = rand.nextLong();
long
m = rand.nextLong();
pBinLong("-1L",
-1L);
pBinLong("+1L",
+1L);
long
ll = 9223372036854775807L;
pBinLong("maxpos",
ll);
long
lln = -9223372036854775808L;
pBinLong("maxneg",
lln);
pBinLong("l",
l);
pBinLong("~l",
~l);
pBinLong("-l",
-l);
pBinLong("m",
m);
pBinLong("l
& m", l & m);
pBinLong("l
| m", l | m);
pBinLong("l
^ m", l ^ m);
pBinLong("l
<< 5", l << 5);
pBinLong("l
>> 5", l >> 5);
pBinLong("(~l)
>> 5", (~l) >> 5);
pBinLong("l
>>> 5", l >>> 5);
pBinLong("(~l)
>>> 5", (~l) >>> 5);
}
static
void pBinInt(String s, int i) {
System.out.println(
s
+ ", int: " + i + ", binary: ");
Chapter
3: Controlling Program Flow
149
![]() System.out.print("
");
for(int
j = 31; j >=0; j--)
if(((1
<< j) & i) != 0)
System.out.print("1");
else
System.out.print("0");
System.out.println();
}
static
void pBinLong(String s, long l) {
System.out.println(
s
+ ", long: " + l + ", binary: ");
System.out.print("
");
for(int
i = 63; i >=0; i--)
if(((1L
<< i) & l) != 0)
System.out.print("1");
else
System.out.print("0");
System.out.println();
}
}
///:~
The
two methods at the end,
pBinInt( )
and
pBinLong( )
take
an int
or
a
long,
respectively, and print it
out in binary format along
with a
descriptive
string. You can ignore
the implementation of these
for now.
You'll
note the use of System.out.print(
) instead
of
System.out.println(
).
The print(
) method
does not emit a new
line,
so
it allows you to output a
line in pieces.
As
well as demonstrating the
effect of all the bitwise
operators for int
and
long,
this example also shows
the minimum, maximum, +1 and
-1 values
for
int and
long
so
you can see what
they look like. Note
that the high
bit
represents
the sign: 0 means positive
and 1 means negative. The
output
for
the int
portion
looks like this:
-1,
int: -1, binary:
11111111111111111111111111111111
+1,
int: 1, binary:
00000000000000000000000000000001
maxpos,
int: 2147483647, binary:
01111111111111111111111111111111
150
Thinking
in Java
![]() maxneg,
int: -2147483648, binary:
10000000000000000000000000000000
i,
int: 59081716, binary:
00000011100001011000001111110100
~i,
int: -59081717, binary:
11111100011110100111110000001011
-i,
int: -59081716, binary:
11111100011110100111110000001100
j,
int: 198850956, binary:
00001011110110100011100110001100
i
& j, int: 58720644, binary:
00000011100000000000000110000100
i
| j, int: 199212028, binary:
00001011110111111011101111111100
i
^ j, int: 140491384, binary:
00001000010111111011101001111000
i
<< 5, int: 1890614912, binary:
01110000101100000111111010000000
i
>> 5, int: 1846303, binary:
00000000000111000010110000011111
(~i)
>> 5, int: -1846304, binary:
11111111111000111101001111100000
i
>>> 5, int: 1846303, binary:
00000000000111000010110000011111
(~i)
>>> 5, int: 132371424, binary:
00000111111000111101001111100000
The
binary representation of the
numbers is referred to as signed
two's
complement.
Ternary
if-else operator
This
operator is unusual because it
has three operands. It is
truly an
operator
because it produces a value,
unlike the ordinary if-else
statement
that
you'll see in the next
section of this chapter. The
expression is of the
form:
boolean-exp
? value0 : value1
If
boolean-exp
evaluates
to true,
value0
is
evaluated and its
result
becomes
the value produced by the
operator. If boolean-exp
is
false,
Chapter
3: Controlling Program Flow
151
![]() value1
is evaluated
and its result becomes
the value produced by
the
operator.
Of
course, you could use an
ordinary if-else
statement
(described later),
but
the ternary operator is much
terser. Although C (where
this operator
originated)
prides itself on being a
terse language, and the
ternary
operator
might have been introduced
partly for efficiency, you
should be
somewhat
wary of using it on an everyday
basis--it's easy to
produce
unreadable
code.
The
conditional operator can be
used for its side
effects or for the value
it
produces,
but in general you want
the value since that's
what makes the
operator
distinct from the if-else.
Here's an example:
static
int ternary(int i) {
return
i < 10 ? i * 100 : i * 10;
}
You
can see that this
code is more compact than
what you'd need to
write
without
the ternary operator:
static
int alternative(int i) {
if
(i < 10)
return
i * 100;
else
return
i * 10;
}
The
second form is easier to
understand, and doesn't
require a lot more
typing.
So be sure to ponder your
reasons when choosing the
ternary
operator.
The
comma operator
The
comma is used in C and C++
not only as a separator in
function
argument
lists, but also as an
operator for sequential
evaluation. The sole
place
that the comma operator
is
used in Java is in for
loops,
which will
be
described later in this
chapter.
152
Thinking
in Java
![]() String
operator
+
There's
one special usage of an
operator in Java: the
+ operator
can be
used
to concatenate strings, as you've
already seen. It seems a
natural use
of
the +
even
though it doesn't fit with
the traditional way that
+ is
used.
This
capability seemed like a
good idea in C++, so
operator
overloading
was
added to C++ to allow the
C++ programmer to add
meanings to
almost
any operator. Unfortunately,
operator overloading combined
with
some
of the other restrictions in
C++ turns out to be a fairly
complicated
feature
for programmers to design
into their classes. Although
operator
overloading
would have been much
simpler to implement in Java
than it
was
in C++, this feature was
still considered too
complex, so Java
programmers
cannot implement their own
overloaded operators as
C++
programmers
can.
The
use of the String
+ has
some interesting behavior. If an
expression
begins
with a String,
then all operands that
follow must be Strings
(remember
that the compiler will
turn a quoted sequence of
characters
into
a String):
int
x = 0, y = 1, z = 2;
String
sString = "x, y, z ";
System.out.println(sString
+ x + y + z);
Here,
the Java compiler will
convert x,
y,
and z
into
their String
representations
instead of adding them
together first. And if you
say:
System.out.println(x
+ sString);
Java
will turn x
into
a String.
Common
pitfalls when using
operators
One
of the pitfalls when using
operators is trying to get
away without
parentheses
when you are even
the least bit uncertain
about how an
expression
will evaluate. This is still
true in Java.
An
extremely common error in C
and C++ looks like
this:
while(x
= y) {
Chapter
3: Controlling Program Flow
153
![]() //
....
}
The
programmer was trying to
test for equivalence
(==)
rather than do an
assignment.
In C and C++ the result of
this assignment will always
be
true
if
y is
nonzero, and you'll probably
get an infinite loop. In
Java, the
result
of this expression is not a
boolean,
and
the compiler expects
a
boolean
and
won't convert from an
int,
so it will conveniently give
you a
compile-time
error and catch the
problem before you ever
try to run the
program.
So the pitfall never happens
in Java. (The only time
you won't
get
a compile-time error is when
x and
y are
boolean,
in which case x
=
y
is
a legal expression, and in
the above case, probably an
error.)
A
similar problem in C and C++
is using bitwise AND and OR
instead of
the
logical versions. Bitwise
AND and OR use one of
the characters (&
or
|)
while logical AND and OR
use two (&&
and
||).
Just as with =
and
==,
it's
easy to type just one
character instead of two. In
Java, the compiler
again
prevents this because it
won't let you cavalierly
use one type
where
it
doesn't belong.
Casting
operators
The
word cast
is
used in the sense of
"casting into a mold." Java
will
automatically
change one type of data
into another when
appropriate. For
instance,
if you assign an integral
value to a floating-point variable,
the
compiler
will automatically convert
the int
to
a float.
Casting allows you
to
make this type conversion
explicit, or to force it when it
wouldn't
normally
happen.
To
perform a cast, put the
desired data type (including
all modifiers)
inside
parentheses to the left of
any value. Here's an
example:
void
casts() {
int
i = 200;
long
l = (long)i;
long
l2 = (long)200;
}
As
you can see, it's
possible to perform a cast on a
numeric value as well
as
on a variable. In both casts
shown here, however, the
cast is
superfluous,
since the compiler will
automatically promote an int
value
to
154
Thinking
in Java
![]() a
long
when
necessary. However, you are
allowed to use
superfluous
casts
in to make a point or to make
your code more clear. In
other
situations,
a cast may be essential just
to get the code to
compile.
In
C and C++, casting can
cause some headaches. In
Java, casting is
safe,
with
the exception that when
you perform a so-called
narrowing
conversion
(that
is, when you go from a
data type that can
hold more
information
to one that doesn't hold as
much) you run the
risk of losing
information.
Here the compiler forces
you to do a cast, in effect
saying
"this
can be a dangerous thing to
do--if you want me to do it
anyway you
must
make the cast explicit."
With a widening
conversion an explicit
cast
is
not needed because the
new type will more
than hold the
information
from
the old type so that no
information is ever
lost.
Java
allows you to cast any
primitive type to any other
primitive type,
except
for boolean,
which
doesn't allow any casting at
all. Class types do
not
allow casting. To convert
one to the other there
must be special
methods.
(String
is
a special case, and you'll
find out later in this
book
that
objects can be cast within a
family
of
types; an Oak
can
be cast to a
Tree
and
vice-versa, but not to a
foreign type such as a
Rock.)
Literals
Ordinarily
when you insert a literal
value into a program the
compiler
knows
exactly what type to make
it. Sometimes, however, the
type is
ambiguous.
When this happens you
must guide the compiler by
adding
some
extra information in the
form of characters associated
with the
literal
value. The following code
shows these
characters:
//:
c03:Literals.java
class
Literals {
char
c = 0xffff; // max char hex value
byte
b = 0x7f; // max byte hex value
short
s = 0x7fff; // max short hex value
int
i1 = 0x2f; // Hexadecimal
(lowercase)
int
i2 = 0X2F; // Hexadecimal
(uppercase)
int
i3 = 0177; // Octal (leading zero)
//
Hex and Oct also work with long.
long
n1 = 200L; // long suffix
long
n2 = 200l; // long suffix
Chapter
3: Controlling Program Flow
155
![]() long
n3 = 200;
//!
long l6(200); // not allowed
float
f1 = 1;
float
f2 = 1F; // float suffix
float
f3 = 1f; // float suffix
float
f4 = 1e-45f; // 10 to the power
float
f5 = 1e+9f; // float suffix
double
d1 = 1d; // double suffix
double
d2 = 1D; // double suffix
double
d3 = 47e47d; // 10 to the power
}
///:~
Hexadecimal
(base 16), which works
with all the integral
data types, is
denoted
by a leading 0x
or
0X followed
by 0--9 and a--f either in
upper
or
lowercase. If you try to
initialize a variable with a
value bigger than it
can
hold (regardless of the
numerical form of the
value), the compiler
will
give
you an error message. Notice
in the above code the
maximum
possible
hexadecimal values for
char,
byte,
and
short.
If you exceed
these,
the compiler will
automatically make the value
an int
and
tell you
that
you need a narrowing cast
for the assignment. You'll
know you've
stepped
over the line.
Octal
(base 8) is denoted by a leading
zero in the number and
digits from
0-7.
There is no literal representation
for binary numbers in C, C++
or
Java.
A
trailing character after a
literal value establishes
its type. Upper or
lowercase
L means
long,
upper or lowercase F
means
float
and
upper or
lowercase
D means
double.
Exponents
use a notation that I've
always found rather
dismaying: 1.39
e-
47f.
In science and engineering,
`e' refers to the base of
natural
logarithms,
approximately 2.718. (A more
precise double
value
is
available
in Java as Math.E.)
This is used in exponentiation
expressions
such
as 1.39 x e-47, which means 1.39 x
2.718-47.
However, when FORTRAN
was
invented they decided that
e would
naturally mean "ten to
the
power,"
which is an odd decision
because FORTRAN was designed
for
science
and engineering and one
would think its designers
would be
156
Thinking
in Java
![]() sensitive
about introducing such an
ambiguity.1 At
any rate, this
custom
was
followed in C, C++ and now
Java. So if you're used to
thinking in
terms
of e
as
the base of natural
logarithms, you must do a
mental
translation
when you see an expression
such as 1.39
e-47f in Java;
it
means
1.39 x 10-47.
Note
that you don't need to
use the trailing character
when the compiler
can
figure out the appropriate
type. With
long
n3 = 200;
there's
no ambiguity, so an L
after
the 200 would be
superfluous.
However,
with
float
f4 = 1e-47f; // 10 to the power
the
compiler normally takes
exponential numbers as doubles, so
without
the
trailing f
it
will give you an error
telling you that you
must use a cast
to
convert double
to
float.
Promotion
You'll
discover that if you perform
any mathematical or bitwise
operations
on
primitive data types that
are smaller than an
int (that
is, char,
byte,
or
short),
those values will be
promoted to int
before
performing the
operations,
and the resulting value
will be of type int.
So if you want to
assign
back into the smaller
type, you must use a
cast. (And, since
you're
assigning
back into a smaller type,
you might be losing
information.) In
general,
the largest data type in an
expression is the one that
determines
1
John Kirkham
writes, "I started computing in 1962
using FORTRAN II on an IBM
1620.
At
that time, and throughout the 1960s
and into the 1970s, FORTRAN
was an all
uppercase
language. This probably
started because many of the
early input devices
were
old
teletype units that used 5 bit Baudot
code, which had no lowercase
capability. The `E'
in
the exponential notation was
also always upper case
and was never confused
with the
natural
logarithm base `e', which is always
lowercase. The `E' simply
stood for
exponential,
which
was for the base of the number system
used--usually 10. At the time octal was
also
widely
used by programmers. Although I never saw
it used, if I had seen an
octal number
in
exponential notation I would
have considered it to be base 8.
The first time I remember
seeing
an exponential using a lowercase `e'
was in the late 1970s and I
also found it
confusing.
The problem arose as
lowercase crept into
FORTRAN, not at its beginning.
We
actually
had functions to use if you
really wanted to use the
natural logarithm base, but
they
were all uppercase."
Chapter
3: Controlling Program Flow
157
![]() the
size of the result of that
expression; if you multiply a
float
and
a
double,
the result will be double;
if you add an int
and
a long,
the
result
will be long.
Java
has no "sizeof"
In
C and C++, the sizeof( )
operator
satisfies a specific need: it
tells you
the
number of bytes allocated
for data items. The
most compelling need
for
sizeof( )
in
C and C++ is portability.
Different data types might
be
different
sizes on different machines, so
the programmer must find
out
how
big those types are
when performing operations
that are sensitive to
size.
For example, one computer
might store integers in 32
bits, whereas
another
might store integers as 16
bits. Programs could store
larger values
in
integers on the first
machine. As you might
imagine, portability is a
huge
headache for C and C++
programmers.
Java
does not need a sizeof( )
operator
for this purpose because
all the
data
types are the same
size on all machines. You do
not need to think
about
portability on this level--it is
designed into the
language.
Precedence
revisited
Upon
hearing me complain about
the complexity of
remembering
operator
precedence during one of my
seminars, a student suggested
a
mnemonic
that is simultaneously a commentary:
"Ulcer Addicts Really
Like
C A lot."
Mnemonic
Operator
type
Operators
Ulcer
Unary
+
- ++--
Addicts
Arithmetic
(and shift)
*
/ % + - << >>
Really
Relational
>
< >= <= == !=
Like
Logical
(and bitwise)
&&
|| & | ^
C
Conditional
(ternary)
A>B?X:Y
A
Lot
Assignment
=
(and
compound
assignment
like *=)
Of
course, with the shift
and bitwise operators
distributed around
the
table
it is not a perfect mnemonic,
but for non-bit operations
it works.
158
Thinking
in Java
![]() A
compendium of operators
The
following example shows
which primitive data types
can be used with
particular
operators. Basically, it is the
same example repeated over
and
over,
but using different
primitive data types. The
file will compile
without
error because the lines
that would cause errors
are commented
out
with a //!.
//:
c03:AllOps.java
//
Tests all the operators on all the
//
primitive data types to show which
//
ones are accepted by the Java compiler.
class
AllOps {
//
To accept the results of a boolean test:
void
f(boolean b) {}
void
boolTest(boolean x, boolean y) {
//
Arithmetic operators:
//!
x = x * y;
//!
x = x / y;
//!
x = x % y;
//!
x = x + y;
//!
x = x - y;
//!
x++;
//!
x--;
//!
x = +y;
//!
x = -y;
//
Relational and logical:
//!
f(x > y);
//!
f(x >= y);
//!
f(x < y);
//!
f(x <= y);
f(x
== y);
f(x
!= y);
f(!y);
x
= x && y;
x
= x || y;
//
Bitwise operators:
//!
x = ~y;
x
= x & y;
x
= x | y;
Chapter
3: Controlling Program Flow
159
![]() x
= x ^ y;
//!
x = x << 1;
//!
x = x >> 1;
//!
x = x >>> 1;
//
Compound assignment:
//!
x += y;
//!
x -= y;
//!
x *= y;
//!
x /= y;
//!
x %= y;
//!
x <<= 1;
//!
x >>= 1;
//!
x >>>= 1;
x
&= y;
x
^= y;
x
|= y;
//
Casting:
//!
char c = (char)x;
//!
byte B = (byte)x;
//!
short s = (short)x;
//!
int i = (int)x;
//!
long l = (long)x;
//!
float f = (float)x;
//!
double d = (double)x;
}
void
charTest(char x, char y) {
//
Arithmetic operators:
x
= (char)(x * y);
x
= (char)(x / y);
x
= (char)(x % y);
x
= (char)(x + y);
x
= (char)(x - y);
x++;
x--;
x
= (char)+y;
x
= (char)-y;
//
Relational and logical:
f(x
> y);
f(x
>= y);
f(x
< y);
f(x
<= y);
160
Thinking
in Java
![]() f(x
== y);
f(x
!= y);
//!
f(!x);
//!
f(x && y);
//!
f(x || y);
//
Bitwise operators:
x=
(char)~y;
x
= (char)(x & y);
x
= (char)(x | y);
x
= (char)(x ^ y);
x
= (char)(x << 1);
x
= (char)(x >> 1);
x
= (char)(x >>> 1);
//
Compound assignment:
x
+= y;
x
-= y;
x
*= y;
x
/= y;
x
%= y;
x
<<= 1;
x
>>= 1;
x
>>>= 1;
x
&= y;
x
^= y;
x
|= y;
//
Casting:
//!
boolean b = (boolean)x;
byte
B = (byte)x;
short
s = (short)x;
int
i = (int)x;
long
l = (long)x;
float
f = (float)x;
double
d = (double)x;
}
void
byteTest(byte x, byte y) {
//
Arithmetic operators:
x
= (byte)(x* y);
x
= (byte)(x / y);
x
= (byte)(x % y);
x
= (byte)(x + y);
x
= (byte)(x - y);
Chapter
3: Controlling Program Flow
161
![]() x++;
x--;
x
= (byte)+ y;
x
= (byte)- y;
//
Relational and logical:
f(x
> y);
f(x
>= y);
f(x
< y);
f(x
<= y);
f(x
== y);
f(x
!= y);
//!
f(!x);
//!
f(x && y);
//!
f(x || y);
//
Bitwise operators:
x
= (byte)~y;
x
= (byte)(x & y);
x
= (byte)(x | y);
x
= (byte)(x ^ y);
x
= (byte)(x << 1);
x
= (byte)(x >> 1);
x
= (byte)(x >>> 1);
//
Compound assignment:
x
+= y;
x
-= y;
x
*= y;
x
/= y;
x
%= y;
x
<<= 1;
x
>>= 1;
x
>>>= 1;
x
&= y;
x
^= y;
x
|= y;
//
Casting:
//!
boolean b = (boolean)x;
char
c = (char)x;
short
s = (short)x;
int
i = (int)x;
long
l = (long)x;
float
f = (float)x;
162
Thinking
in Java
![]() double
d = (double)x;
}
void
shortTest(short x, short y) {
//
Arithmetic operators:
x
= (short)(x * y);
x
= (short)(x / y);
x
= (short)(x % y);
x
= (short)(x + y);
x
= (short)(x - y);
x++;
x--;
x
= (short)+y;
x
= (short)-y;
//
Relational and logical:
f(x
> y);
f(x
>= y);
f(x
< y);
f(x
<= y);
f(x
== y);
f(x
!= y);
//!
f(!x);
//!
f(x && y);
//!
f(x || y);
//
Bitwise operators:
x
= (short)~y;
x
= (short)(x & y);
x
= (short)(x | y);
x
= (short)(x ^ y);
x
= (short)(x << 1);
x
= (short)(x >> 1);
x
= (short)(x >>> 1);
//
Compound assignment:
x
+= y;
x
-= y;
x
*= y;
x
/= y;
x
%= y;
x
<<= 1;
x
>>= 1;
x
>>>= 1;
x
&= y;
Chapter
3: Controlling Program Flow
163
Table of Contents:
|
|||||