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.
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.
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
‘+=’.
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 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 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
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 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); } } ///:~
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.
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: 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); } } ///:~
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).
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.
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 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 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 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.
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); } } ///:~
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)); } } ///:~
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.
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 " + ((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.
When dealing with
logical
operators you run into a phenomenon called “short circuiting.” This
means that the expression will be evaluated only 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) 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.
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.
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.
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 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); 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: "); 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 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
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, 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 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.
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);
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) { // .... }
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.
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 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.)
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 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 sensitive about introducing such an
ambiguity.[25] 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.
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 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.
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.
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.
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; 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); 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); 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; 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; x ^= y; x |= y; // Casting: //! boolean b = (boolean)x; char c = (char)x; byte B = (byte)x; int i = (int)x; long l = (long)x; float f = (float)x; double d = (double)x; } void intTest(int x, int 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(!x); //! f(x && y); //! f(x || y); // Bitwise operators: x = ~y; x = x & y; x = x | y; 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: //! boolean b = (boolean)x; char c = (char)x; byte B = (byte)x; short s = (short)x; long l = (long)x; float f = (float)x; double d = (double)x; } void longTest(long x, long 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(!x); //! f(x && y); //! f(x || y); // Bitwise operators: x = ~y; x = x & y; x = x | y; 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: //! boolean b = (boolean)x; char c = (char)x; byte B = (byte)x; short s = (short)x; int i = (int)x; float f = (float)x; double d = (double)x; } void floatTest(float x, float 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(!x); //! f(x && y); //! f(x || y); // Bitwise operators: //! x = ~y; //! x = x & y; //! x = x | y; //! 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: //! boolean b = (boolean)x; char c = (char)x; byte B = (byte)x; short s = (short)x; int i = (int)x; long l = (long)x; double d = (double)x; } void doubleTest(double x, double 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(!x); //! f(x && y); //! f(x || y); // Bitwise operators: //! x = ~y; //! x = x & y; //! x = x | y; //! 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: //! boolean b = (boolean)x; char c = (char)x; byte B = (byte)x; short s = (short)x; int i = (int)x; long l = (long)x; float f = (float)x; } } ///:~
Note that
boolean is quite limited. You can assign to it the
values true and false, and you can test it for truth or falsehood,
but you cannot add booleans or perform any other type of operation on
them.
In char, byte, and
short you can see the effect of promotion with the
arithmetic operators. Each arithmetic operation on any of those types results in
an int result, which must be explicitly cast back to the original type (a
narrowing conversion that might lose information) to assign back to that type.
With int values, however, you do not need to cast, because everything is
already an int. Don’t be lulled into thinking everything is safe,
though. If you multiply two ints that are big enough, you’ll
overflow the result. The following example demonstrates
this:
//: c03:Overflow.java // Surprise! Java lets you overflow. public class Overflow { public static void main(String[] args) { int big = 0x7fffffff; // max int value prt("big = " + big); int bigger = big * 4; prt("bigger = " + bigger); } static void prt(String s) { System.out.println(s); } } ///:~
The output of this is:
big = 2147483647 bigger = -4
and you get no errors or warnings from
the compiler, and no exceptions at run-time. Java is good, but it’s not
that good.
Compound assignments do not
require casts for char, byte, or short, even though they
are performing promotions that have the same results as the direct arithmetic
operations. On the other hand, the lack of the cast certainly simplifies the
code.
You can see that, with the exception of
boolean, any primitive type can be cast to any
other primitive type. Again, you must be aware of the effect of a narrowing
conversion when casting to a smaller type, otherwise you
might unknowingly lose information during the
cast.
Java uses all of C’s execution
control statements, so if you’ve programmed with C or C++ then most of
what you see will be familiar. Most procedural programming languages have some
kind of control statements, and there is often overlap among languages. In Java,
the keywords include if-else, while, do-while, for,
and a selection statement called switch. Java does not, however, support
the much-maligned goto (which can still be the most expedient way to
solve certain types of problems). You can still do a goto-like jump, but it is
much more constrained than a typical
goto.
All conditional statements use the truth
or falsehood of a conditional expression to determine the execution path. An
example of a conditional expression is A == B. This uses the conditional
operator == to see if the value of A is equivalent to the value of
B. The expression returns true or false. Any of the
relational operators you’ve seen earlier in this chapter can be used to
produce a conditional statement. Note that Java doesn’t allow you to use a
number as a boolean, even though it’s allowed in C and C++ (where
truth is nonzero and falsehood is zero). If you want to use a non-boolean
in a boolean test, such as if(a), you must first convert it to a
boolean value using a conditional expression, such as if(a !=
0).
The if-else statement is probably
the most basic way to control program flow. The else is optional, so you
can use if in two forms:
if(Boolean-expression) statement
or
if(Boolean-expression) statement else statement
The conditional must produce a
boolean result. The statement means either a simple statement
terminated by a semicolon or a compound statement, which is a group of simple
statements enclosed in braces. Any time the word “statement”
is used, it always implies that the statement can be simple or compound.
As an example of if-else, here is
a test( ) method that will tell you whether a guess is above, below,
or equivalent to a target number:
//: c03:IfElse.java public class IfElse { static int test(int testval, int target) { int result = 0; if(testval > target) result = +1; else if(testval < target) result = -1; else result = 0; // Match return result; } public static void main(String[] args) { System.out.println(test(10, 5)); System.out.println(test(5, 10)); System.out.println(test(5, 5)); } } ///:~
It is conventional to indent the body of a control flow statement so the reader might easily determine where it begins and ends.
The return keyword has two
purposes: it specifies what value a method will return (if it doesn’t have
a void return value) and it causes that value to be returned immediately.
The test( ) method above can be rewritten to take advantage of
this:
//: c03:IfElse2.java public class IfElse2 { static int test(int testval, int target) { int result = 0; if(testval > target) return +1; else if(testval < target) return -1; else return 0; // Match } public static void main(String[] args) { System.out.println(test(10, 5)); System.out.println(test(5, 10)); System.out.println(test(5, 5)); } } ///:~
while, do-while and
for control looping and are sometimes classified as iteration
statements. A statement repeats until the controlling
Boolean-expression evaluates to false. The form for a while
loop is
while(Boolean-expression) statement
The Boolean-expression is
evaluated once at the beginning of the loop and again before each further
iteration of the statement.
Here’s a simple example that
generates random numbers until a particular condition is met:
//: c03:WhileTest.java // Demonstrates the while loop. public class WhileTest { public static void main(String[] args) { double r = 0; while(r < 0.99d) { r = Math.random(); System.out.println(r); } } } ///:~
This uses the static method
random( ) in the Math library, which generates a double
value between 0 and 1. (It includes 0, but not 1.) The conditional
expression for the while says “keep doing this loop until the
number is 0.99 or greater.” Each time you run this program you’ll
get a different-sized list of
numbers.
The form for do-while
is
do statement while(Boolean-expression);
The sole difference between while
and do-while is that the statement of the do-while always executes
at least once, even if the expression evaluates to false the first time. In a
while, if the conditional is false the first time the statement never
executes. In practice, do-while is less common than
while.
A for loop performs initialization
before the first iteration. Then it performs conditional testing and, at the end
of each iteration, some form of “stepping.” The form of the
for loop is:
for(initialization; Boolean-expression; step) statement
Any of the expressions
initialization, Boolean-expression or step can be empty.
The expression is tested before each iteration, and as soon as it evaluates to
false execution will continue at the line following the for
statement. At the end of each loop, the step executes.
for loops are usually used for
“counting” tasks:
//: c03:ListCharacters.java // Demonstrates "for" loop by listing // all the ASCII characters. public class ListCharacters { public static void main(String[] args) { for( char c = 0; c < 128; c++) if (c != 26 ) // ANSI Clear screen System.out.println( "value: " + (int)c + " character: " + c); } } ///:~
Note that the variable c is
defined at the point where it is used, inside the control expression of the
for loop, rather than at the beginning of the block denoted by the open
curly brace. The scope of c is the expression controlled by the
for.
Traditional procedural languages like C
require that all variables be defined at the beginning of
a block so when the compiler creates a block it can allocate space for those
variables. In Java and C++ you can spread your variable declarations throughout
the block, defining them at the point that you need them. This allows a more
natural coding style and makes code easier to understand.
You can define multiple variables within
a for statement, but they must be of the same type:
for(int i = 0, j = 1; i < 10 && j != 11; i++, j++) /* body of for loop */;
The int definition in the for
statement covers both i and j. The ability to define
variables in the control expression is limited to the for loop. You
cannot use this approach with any of the other selection or iteration
statements.
Earlier in this chapter I stated that the
comma operator (not the
comma separator, which is used to separate definitions and function
arguments) has only one use in Java: in the control expression of a for
loop. In both the initialization and step portions of the control expression you
can have a number of statements separated by commas, and those statements will
be evaluated sequentially. The previous bit of code uses this ability.
Here’s another example:
//: c03:CommaOperator.java public class CommaOperator { public static void main(String[] args) { for(int i = 1, j = i + 10; i < 5; i++, j = i * 2) { System.out.println("i= " + i + " j= " + j); } } } ///:~
Here’s the output:
i= 1 j= 11 i= 2 j= 4 i= 3 j= 6 i= 4 j= 8
You can see that in both the
initialization and step portions the statements are evaluated in sequential
order. Also, the initialization portion can have any number of definitions of
one type.
Inside the body of any of the iteration
statements you can also control the flow of the loop by using break and
continue. break quits the loop without executing the rest of the
statements in the loop. continue stops the execution of the current
iteration and goes back to the beginning of the loop to begin the next
iteration.
This program shows examples of
break and continue within for and while
loops:
//: c03:BreakAndContinue.java // Demonstrates break and continue keywords. public class BreakAndContinue { public static void main(String[] args) { for(int i = 0; i < 100; i++) { if(i == 74) break; // Out of for loop if(i % 9 != 0) continue; // Next iteration System.out.println(i); } int i = 0; // An "infinite loop": while(true) { i++; int j = i * 27; if(j == 1269) break; // Out of loop if(i % 10 != 0) continue; // Top of loop System.out.println(i); } } } ///:~
In the for loop the value of
i never gets to 100 because the break statement breaks out of the
loop when i is 74. Normally, you’d use a break like this
only if you didn’t know when the terminating condition was going to occur.
The continue statement causes execution to go back to the top of the
iteration loop (thus incrementing i) whenever i is not evenly
divisible by 9. When it is, the value is printed.
The second portion shows an
“infinite loop” that would, in theory, continue forever. However,
inside the loop there is a break statement that will break out of the
loop. In addition, you’ll see that the continue moves back to the
top of the loop without completing the remainder. (Thus printing happens in the
second loop only when the value of i is divisible by 10.) The output
is:
0 9 18 27 36 45 54 63 72 10 20 30 40
The value 0 is printed because 0 % 9
produces 0.
A second form of the infinite loop is
for(;;). The compiler treats both while(true) and for(;;)
in the same way so whichever one you use is a matter of programming
taste.
The goto
keyword has been present in programming languages from the beginning.
Indeed, goto was the genesis of program control in assembly language:
“if condition A, then jump here, otherwise jump there.” If you read
the assembly code that is ultimately generated by virtually any compiler,
you’ll see that program control contains many jumps. However, a goto
is a jump at the source-code level, and that’s what brought it into
disrepute. If a program will always jump from one point to another, isn’t
there some way to reorganize the code so the flow of control is not so jumpy?
goto fell into true disfavor with the publication of the famous
“Goto considered harmful” paper by Edsger Dijkstra, and since then
goto-bashing has been a popular sport, with advocates of the cast-out keyword
scurrying for cover.
As is typical in situations like this,
the middle ground is the most fruitful. The problem is not the use of
goto, but the overuse of goto—in rare situations goto
is actually the best way to structure control flow.
Although goto is a reserved word
in Java, it is not used in the language; Java has no goto. However, it
does have something that looks a bit like a jump tied in with the break
and continue keywords. It’s not a jump but rather a way to break
out of an iteration statement. The reason it’s
often thrown in with discussions of goto is because it uses the same
mechanism: a label.
label1:
The only place a label is useful
in Java is right before an iteration statement. And that means right
before—it does no good to put any other statement between the label and
the iteration. And the sole reason to put a label before an iteration is if
you’re going to nest another iteration or a switch inside it. That’s
because the break and
continue keywords will normally interrupt only the
current loop, but when used with a label they’ll interrupt the loops up to
where the label exists:
label1: outer-iteration { inner-iteration { //... break; // 1 //... continue; // 2 //... continue label1; // 3 //... break label1; // 4 } }
In case 1, the break breaks out of
the inner iteration and you end up in the outer iteration. In case 2, the
continue moves back to the beginning of the inner iteration. But in case
3, the continue label1 breaks out of the inner iteration and the
outer iteration, all the way back to label1. Then it does in fact
continue the iteration, but starting at the outer iteration. In case 4, the
break label1 also breaks all the way out to label1, but it does
not re-enter the iteration. It actually does break out of both
iterations.
Here is an example using for
loops:
//: c03:LabeledFor.java // Java’s "labeled for" loop. public class LabeledFor { public static void main(String[] args) { int i = 0; outer: // Can't have statements here for(; true ;) { // infinite loop inner: // Can't have statements here for(; i < 10; i++) { prt("i = " + i); if(i == 2) { prt("continue"); continue; } if(i == 3) { prt("break"); i++; // Otherwise i never // gets incremented. break; } if(i == 7) { prt("continue outer"); i++; // Otherwise i never // gets incremented. continue outer; } if(i == 8) { prt("break outer"); break outer; } for(int k = 0; k < 5; k++) { if(k == 3) { prt("continue inner"); continue inner; } } } } // Can't break or continue // to labels here } static void prt(String s) { System.out.println(s); } } ///:~
This uses the prt( ) method
that has been defined in the other examples.
Note that break breaks out of the
for loop, and that the increment-expression doesn’t occur until the
end of the pass through the for loop. Since break skips the
increment expression, the increment is performed directly in the case of i ==
3. The continue outer statement in the case of i == 7 also
goes to the top of the loop and also skips the increment, so it too is
incremented directly.
Here is the output:
i = 0 continue inner i = 1 continue inner i = 2 continue i = 3 break i = 4 continue inner i = 5 continue inner i = 6 continue inner i = 7 continue outer i = 8 break outer
If not for the break outer
statement, there would be no way to get out of the outer loop from within an
inner loop, since break by itself can break out of only the innermost
loop. (The same is true for continue.)
Of course, in the cases where breaking
out of a loop will also exit the method, you can simply use a
return.
Here is a demonstration of labeled
break and continue statements with while
loops:
//: c03:LabeledWhile.java // Java's "labeled while" loop. public class LabeledWhile { public static void main(String[] args) { int i = 0; outer: while(true) { prt("Outer while loop"); while(true) { i++; prt("i = " + i); if(i == 1) { prt("continue"); continue; } if(i == 3) { prt("continue outer"); continue outer; } if(i == 5) { prt("break"); break; } if(i == 7) { prt("break outer"); break outer; } } } } static void prt(String s) { System.out.println(s); } } ///:~
The same rules hold true for
while:
The output of this
method makes it clear:
Outer while loop i = 1 continue i = 2 i = 3 continue outer Outer while loop i = 4 i = 5 break Outer while loop i = 6 i = 7 break outer
It’s important to remember that the
only reason to use labels in Java is when you have nested loops and you
want to break or continue through more than one nested
level.
In Dijkstra’s “goto
considered harmful” paper, what he specifically objected to was the
labels, not the goto. He observed that the number of bugs seems to increase with
the number of labels in a program. Labels and gotos make programs difficult to
analyze statically, since it introduces cycles in the program execution graph.
Note that Java labels don’t suffer from this problem, since they are
constrained in their placement and can’t be used to transfer control in an
ad hoc manner. It’s also interesting to note that this is a case where a
language feature is made more useful by restricting the power of the
statement.
The switch is sometimes classified
as a selection statement. The switch statement selects from among
pieces of code based on the value of an integral expression. Its form
is:
switch(integral-selector) { case integral-value1 : statement; break; case integral-value2 : statement; break; case integral-value3 : statement; break; case integral-value4 : statement; break; case integral-value5 : statement; break; // ... default: statement; }
Integral-selector is an expression
that produces an integral value. The switch compares the result of
integral-selector to each integral-value. If it finds a match, the
corresponding statement (simple or compound) executes. If no match
occurs, the default statement
executes.
You will notice in the above definition
that each case ends with a break, which
causes execution to jump to the end of the switch body. This is the
conventional way to build a switch statement, but the break is
optional. If it is missing, the code for the following case statements execute
until a break is encountered. Although you don’t usually want this
kind of behavior, it can be useful to an experienced programmer. Note the last
statement, following the default, doesn’t have a break
because the execution just falls through to where the break would have
taken it anyway. You could put a break at the end of the default
statement with no harm if you considered it important for style’s
sake.
The switch statement is a clean
way to implement multi-way selection (i.e., selecting from among a number of
different execution paths), but it requires a selector that evaluates to an
integral value such as int or char. If you want to use, for
example, a string or a floating-point number as a selector, it won’t work
in a switch statement. For non-integral types, you must use a series of
if statements.
Here’s an example that creates
letters randomly and determines whether they’re vowels or
consonants:
//: c03:VowelsAndConsonants.java // Demonstrates the switch statement. public class VowelsAndConsonants { public static void main(String[] args) { for(int i = 0; i < 100; i++) { char c = (char)(Math.random() * 26 + 'a'); System.out.print(c + ": "); switch(c) { case 'a': case 'e': case 'i': case 'o': case 'u': System.out.println("vowel"); break; case 'y': case 'w': System.out.println( "Sometimes a vowel"); break; default: System.out.println("consonant"); } } } } ///:~
Since Math.random( )
generates a value between 0 and 1, you need only multiply it by the upper bound
of the range of numbers you want to produce (26 for the letters in the alphabet)
and add an offset to establish the lower bound.
Although it appears you’re
switching on a character here, the switch statement is actually using the
integral value of the character. The singly-quoted characters in the case
statements also produce integral values that are used for
comparison.
Notice how the cases can be
“stacked” on top of each other to provide multiple matches for a
particular piece of code. You should also be aware that it’s essential to
put the break statement at the end of a particular case, otherwise
control will simply drop through and continue processing on the next
case.
The statement: char c = (char)(Math.random() * 26 + 'a');
deserves a closer look.
Math.random( ) produces a double, so the value 26 is
converted to a double to perform the multiplication, which also produces
a double. This means that ‘a’ must be converted to a
double to perform the addition. The double result is turned back
into a char with a cast.
What does the cast to char do?
That is, if you have the value 29.7 and you cast it to a char, is the
resulting value 30 or 29? The answer to this can be seen in this
example:
//: c03:CastingNumbers.java // What happens when you cast a float // or double to an integral value? public class CastingNumbers { public static void main(String[] args) { double above = 0.7, below = 0.4; System.out.println("above: " + above); System.out.println("below: " + below); System.out.println( "(int)above: " + (int)above); System.out.println( "(int)below: " + (int)below); System.out.println( "(char)('a' + above): " + (char)('a' + above)); System.out.println( "(char)('a' + below): " + (char)('a' + below)); } } ///:~
The output is:
above: 0.7 below: 0.4 (int)above: 0 (int)below: 0 (char)('a' + above): a (char)('a' + below): a
A second question concerns
Math.random( ). Does
it produce a value from zero to one, inclusive or exclusive of the value
‘1’? In math lingo, is it (0,1), or [0,1], or (0,1] or [0,1)? (The
square bracket means “includes” whereas the parenthesis means
“doesn’t include.”) Again, a test program might provide the
answer:
//: c03:RandomBounds.java // Does Math.random() produce 0.0 and 1.0? public class RandomBounds { static void usage() { System.out.println("Usage: \n\t" + "RandomBounds lower\n\t" + "RandomBounds upper"); System.exit(1); } public static void main(String[] args) { if(args.length != 1) usage(); if(args[0].equals("lower")) { while(Math.random() != 0.0) ; // Keep trying System.out.println("Produced 0.0!"); } else if(args[0].equals("upper")) { while(Math.random() != 1.0) ; // Keep trying System.out.println("Produced 1.0!"); } else usage(); } } ///:~
To run the program, you type a command
line of either:
java RandomBounds lower
or
java RandomBounds upper
In both cases you are forced to break out
of the program manually, so it would appear that
Math.random( ) never produces either 0.0 or 1.0. But this is where
such an experiment can be deceiving. If you
consider[26] that
there are about 262 different double fractions between 0 and 1, the
likelihood of reaching any one value experimentally might exceed the lifetime of
one computer, or even one experimenter. It turns out that 0.0 is included
in the output of Math.random( ). Or, in math lingo, it is
[0,1).
This chapter concludes the study of
fundamental features that appear in most programming languages: calculation,
operator precedence, type casting, and selection and iteration. Now you’re
ready to begin taking steps that move you closer to the world of object-oriented
programming. The next chapter will cover the important issues of initialization
and cleanup of objects, followed in the subsequent chapter by the essential
concept of implementation
hiding.
Solutions to selected exercises
can be found in the electronic document The Thinking in Java Annotated
Solution Guide, available for a small fee from
www.BruceEckel.com.
[25]
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.”
[26]
Chuck Allison writes: The total number of numbers in a floating-point number
system is
2(M-m+1)b^(p-1) +
1
where b is the base (usually
2), p is the precision (digits in the mantissa), M is the largest
exponent, and m is the smallest exponent. IEEE 754
uses:
M = 1023, m = -1022, p = 53, b =
2
so the total number of numbers
is
2(1023+1022+1)2^52
=
2((2^10-1) + (2^10-1))2^52
=
(2^10-1)2^54
= 2^64 -
2^54
Half of these numbers
(corresponding to exponents in the range [-1022, 0]) are less than 1 in
magnitude (both positive and negative), so 1/4 of that expression, or 2^62 -
2^52 + 1 (approximately 2^62) is in the range [0,1). See my paper at
http://www.freshsources.com/1995006a.htm (last of text).