| Thinking in Java source ref: work2.html |
Like a sentient creature, a program must manipulate its world and make
choices during execution.
In Java you manipulate 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: Foundations for Java. It contains audio
lectures, slides, exercises, and solutions specifically designed to bring you up to speed
with the fundamentals 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. 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 the
value.) For instance, you can assign a constant value to a variable:
a = 4;
but you cannot assign anything to constant valueit cannot be an lvalue. (You
cant 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 youve
come to expect for most situations. .
When you assign objects, however, things change. Whenever you manipulate an object,
what youre manipulating is the reference, so when you assign from one object
to another, youre 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. Heres an example that demonstrates this
behavior: .
//: c03:Assignment.java
// Assignment with objects is a bit tricky.
import com.bruceeckel.simpletest.*;
class Number {
int i;
}
public class Assignment {
static Test monitor = new Test();
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);
monitor.expect(new String[] {
"1: n1.i: 9, n2.i: 47",
"2: n1.i: 47, n2.i: 47",
"3: n1.i: 27, n2.i: 27"
});
}
} ///:~
First, notice that something new has been added. The line:
import com.bruceeckel.simpletest.*;
imports the simpletest library that has been created to test the
code in this book, and is explained in Chapter 15. At the beginning of the Assignment
class, you see the line:
static Test monitor = new Test();
This creates an instance of the simpletest class Test, called monitor.
Finally, at the end of main( ), you see the statement:
monitor.expect(new String[] {
"1: n1.i: 9, n2.i: 47",
"2: n1.i: 47, n2.i: 47",
"3: n1.i: 27, n2.i: 27"
});
This is the expected output of the program, expressed as an array of String
objects. When the program is run, it not only prints out the output, but it compares it to
this array to verify that the array is correct. Thus, when you see a program in this book
that uses simpletest, you will also see an expect( ) call that will
show you what the output of the program is. This way, you see validated output from the
program.
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 youve assigned a reference, youll see
the output in the expect( ) statement. 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 its a fundamental way that Java works with objects. But what if you dont
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 youll 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.
import com.bruceeckel.simpletest.*;
class Letter {
char c;
}
public class PassObject {
static Test monitor = new Test();
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);
monitor.expect(new String[] {
"1: x.c: a",
"2: x.c: z"
});
}
} ///:~
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 in the expect( )
statement shows this. .
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 com.bruceeckel.simpletest.*;
import java.util.*;
public class MathOps {
static Test monitor = new Test();
// Shorthand to print a string and an int:
static void printInt(String s, int i) {
System.out.println(s + " = " + i);
}
// Shorthand to print a string and a float:
static void printFloat(String s, float f) {
System.out.println(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;
// Choose value from 1 to 100:
j = rand.nextInt(100) + 1;
k = rand.nextInt(100) + 1;
printInt("j", j); printInt("k", k);
i = j + k; printInt("j + k", i);
i = j - k; printInt("j - k", i);
i = k / j; printInt("k / j", i);
i = k * j; printInt("k * j", i);
i = k % j; printInt("k % j", i);
j %= k; printInt("j %= k", j);
// Floating-point number tests:
float u,v,w; // applies to doubles, too
v = rand.nextFloat();
w = rand.nextFloat();
printFloat("v", v); printFloat("w", w);
u = v + w; printFloat("v + w", u);
u = v - w; printFloat("v - w", u);
u = v * w; printFloat("v * w", u);
u = v / w; printFloat("v / w", u);
// the following also works for
// char, byte, short, int, long,
// and double:
u += v; printFloat("u += v", u);
u -= v; printFloat("u -= v", u);
u *= v; printFloat("u *= v", u);
u /= v; printFloat("u /= v", u);
monitor.expect(new String[] {
"%% j = -?\\d+",
"%% k = -?\\d+",
"%% j \\+ k = -?\\d+",
"%% j - k = -?\\d+",
"%% k / j = -?\\d+",
"%% k \\* j = -?\\d+",
"%% k % j = -?\\d+",
"%% j %= k = -?\\d+",
"%% v = -?\\d+\\.\\d+(E-?\\d)?",
"%% w = -?\\d+\\.\\d+(E-?\\d)?",
"%% v \\+ w = -?\\d+\\.\\d+(E-?\\d)??",
"%% v - w = -?\\d+\\.\\d+(E-?\\d)??",
"%% v \\* w = -?\\d+\\.\\d+(E-?\\d)??",
"%% v / w = -?\\d+\\.\\d+(E-?\\d)??",
"%% u \\+= v = -?\\d+\\.\\d+(E-?\\d)??",
"%% u -= v = -?\\d+\\.\\d+(E-?\\d)??",
"%% u \\*= v = -?\\d+\\.\\d+(E-?\\d)??",
"%% u /= v = -?\\d+\\.\\d+(E-?\\d)??"
});
}
} ///:~
The first thing you will see are some shorthand methods for printing: the printInt( )
prints a String followed by an int and the printFloat( ) prints
a String followed by a float. .
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 the methods: nextInt( ) and
nextFloat( ) (you can also call nextLong( ) 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 1 (99 in this case). .
Since random numbers are used to generate the output for this program, the expect( )
statement cant just show literal output as it did before, since the output will vary
from one run to the next. To solve this problem, regular expressions, a new feature
introduced in Java JDK 1.4 (but an old feature in languages like Perl and Python) will be
used inside the expect( ) statement. Although coverage of this intensely
powerful tool doesnt occur until Chapter 12, to understand these statements
youll need an introduction to regular expressions. Here, youll learn just
enough to read the expect( ) statements, but if you want a full description,
look up java.util.regex.Pattern in the downloadable JDK documentation. .
A regular expression is a way to describe strings in general terms, so that you can
say: If a string has these things in it, then it matches what Im looking
for. For example, to say that a number might or might not be preceded by a minus
sign, you put in the minus sign followed by a question mark, like this: .
-?
To describe an integer, you say that its one or more digits. In regular
expressions, a digit is \d, but in a Java String you have to
escape the backslash by putting in a second backslash: \\d.
To indicate one or more of the preceding expression in regular expressions,
you use the +. So to say possibly a minus sign, followed by one
or more digits, you write: .
-?\\d+
Which you can see in the first lines of the expect( ) statement in the
preceding code.
One thing that is not part of the regular expression syntax is the %%
(note the space included for readability) at the beginning of the lines in the expect( )
statement. This is a flag used by simpletest to indicate that the rest of the line
is a regular expression. So you wont see it in normal regular expressions, only in simpletest
expect( ) statements. .
Any other characters that are not special characters to regular expression searches are
treated as exact matches. So in the first line:
%% j = -?\\d+
The j = is matched exactly. However, in the third line, the +
in j + k must be escaped because it is a special regular expression character,
as is *. The rest of the lines should be understandable from this
introduction. Later in the book, when additional features of regular expressions are used
inside expect( ) statements, they will be explained. .
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);
Unary minus inverts the sign on the data. Unary plus provides symmetry with unary
minus, although it doesnt 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 not only modify the variable, but also 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.
import com.bruceeckel.simpletest.*;
public class AutoInc {
static Test monitor = new Test();
public static void main(String[] args) {
int i = 1;
System.out.println("i : " + i);
System.out.println("++i : " + ++i); // Pre-increment
System.out.println("i++ : " + i++); // Post-increment
System.out.println("i : " + i);
System.out.println("--i : " + --i); // Pre-decrement
System.out.println("i-- : " + i--); // Post-decrement
System.out.println("i : " + i);
monitor.expect(new String[] {
"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 Java
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, youll see that many parts are simpler, and
yet Java isnt 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 work with all
built-in data types, but the other comparisons wont work with type boolean. .
The relational operators == and !=
also work with all objects, but their meaning often confuses the first-time Java
programmer. Heres an example: .
//: c03:Equivalence.java
import com.bruceeckel.simpletest.*;
public class Equivalence {
static Test monitor = new Test();
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);
monitor.expect(new String[] {
"false",
"true"
});
}
} ///:~
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 !=). Heres how its used: .
//: c03:EqualsMethod.java
import com.bruceeckel.simpletest.*;
public class EqualsMethod {
static Test monitor = new Test();
public static void main(String[] args) {
Integer n1 = new Integer(47);
Integer n2 = new Integer(47);
System.out.println(n1.equals(n2));
monitor.expect(new String[] {
"true"
});
}
} ///:~
The result will be true, as you would expect. Ah, but its not as simple as
that. If you create your own class, like this: .
//: c03:EqualsMethod2.java
import com.bruceeckel.simpletest.*;
class Value {
int i;
}
public class EqualsMethod2 {
static Test monitor = new Test();
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));
monitor.expect(new String[] {
"false"
});
}
} ///:~
youre 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 wont get the desired behavior. Unfortunately, you wont
learn about overriding until Chapter 7 and about the proper way to define equals( )
until Chapter 11, 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. .
Each of the logical operators AND
(&&), OR (||) and NOT (!) produces 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 com.bruceeckel.simpletest.*;
import java.util.*;
public class Bool {
static Test monitor = new Test();
public static void main(String[] args) {
Random rand = new Random();
int i = rand.nextInt(100);
int j = rand.nextInt(100);
System.out.println("i = " + i);
System.out.println("j = " + j);
System.out.println("i > j is " + (i > j));
System.out.println("i < j is " + (i < j));
System.out.println("i >= j is " + (i >= j));
System.out.println("i <= j is " + (i <= j));
System.out.println("i == j is " + (i == j));
System.out.println("i != j is " + (i != j));
// Treating an int as a boolean is not legal Java:
//! System.out.println("i && j is " + (i && j));
//! System.out.println("i || j is " + (i || j));
//! System.out.println("!i is " + !i);
System.out.println("(i < 10) && (j < 10) is "
+ ((i < 10) && (j < 10)) );
System.out.println("(i < 10) || (j < 10) is "
+ ((i < 10) || (j < 10)) );
monitor.expect(new String[] {
"%% i = -?\\d+",
"%% j = -?\\d+",
"%% i > j is (true|false)",
"%% i < j is (true|false)",
"%% i >= j is (true|false)",
"%% i <= j is (true|false)",
"%% i == j is (true|false)",
"%% i != j is (true|false)",
"%% \\(i < 10\\) && \\(j < 10\\) is (true|false)",
"%% \\(i < 10\\) \\|\\| \\(j < 10\\) is (true|false)"
});
}
} ///:~
In the regular expressions in the expect( ) statement, parentheses have the
effect of grouping an expression, and the vertical bar | means OR. So:
(true|false)
Means that this part of the string may be either true or false.
Because these characters are special in regular expressions, they must be escaped with a
\\ if you want them to appear as ordinary characters in the expression.
.
You can apply AND, OR, or NOT to boolean values only. You cant 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. .
Note that a boolean value is automatically converted to an appropriate text form
if its used where a String is expected. .
You can replace the definition for int in the preceding 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, the latter parts of a
logical expression might not be evaluated. Heres an example that demonstrates
short-circuiting:
//: c03:ShortCircuit.java
// Demonstrates short-circuiting behavior.
// with logical operators.
import com.bruceeckel.simpletest.*;
public class ShortCircuit {
static Test monitor = new Test();
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");
monitor.expect(new String[] {
"test1(0)",
"result: true",
"test2(2)",
"result: false",
"expression is false"
});
}
} ///:~
Each test performs a comparison against the argument and returns true or false. It also
prints information to show you that its 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. 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 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 Cs low-level orientation,
where you often manipulate hardware directly and must 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 wont 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 bita 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: because 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 cant 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. Youre prevented
from using booleans in shift expressions, which are 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 youre operating on a long,
youll get a long result. Only the six low-order bits of the right-hand side
will be used, so you cant 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 dont 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.
import com.bruceeckel.simpletest.*;
public class URShift {
static Test monitor = new Test();
public static void main(String[] args) {
int i = -1;
System.out.println(i >>>= 10);
long l = -1;
System.out.println(l >>>= 10);
short s = -1;
System.out.println(s >>>= 10);
byte b = -1;
System.out.println(b >>>= 10);
b = -1;
System.out.println(b>>>10);
monitor.expect(new String[] {
"4194303",
"18014398509481983",
"-1",
"-1",
"4194303"
});
}
} ///:~
In the last shift, the resulting value is not assigned back into b, but is
printed directly, so the correct behavior occurs. .
Heres an example that demonstrates the use of all the operators involving bits:
//: c03:BitManipulation.java
// Using the bitwise operators.
import com.bruceeckel.simpletest.*;
import java.util.*;
public class BitManipulation {
static Test monitor = new Test();
public static void main(String[] args) {
Random rand = new Random();
int i = rand.nextInt();
int j = rand.nextInt();
printBinaryInt("-1", -1);
printBinaryInt("+1", +1);
int maxpos = 2147483647;
printBinaryInt("maxpos", maxpos);
int maxneg = -2147483648;
printBinaryInt("maxneg", maxneg);
printBinaryInt("i", i);
printBinaryInt("~i", ~i);
printBinaryInt("-i", -i);
printBinaryInt("j", j);
printBinaryInt("i & j", i & j);
printBinaryInt("i | j", i | j);
printBinaryInt("i ^ j", i ^ j);
printBinaryInt("i << 5", i << 5);
printBinaryInt("i >> 5", i >> 5);
printBinaryInt("(~i) >> 5", (~i) >> 5);
printBinaryInt("i >>> 5", i >>> 5);
printBinaryInt("(~i) >>> 5", (~i) >>> 5);
long l = rand.nextLong();
long m = rand.nextLong();
printBinaryLong("-1L", -1L);
printBinaryLong("+1L", +1L);
long ll = 9223372036854775807L;
printBinaryLong("maxpos", ll);
long lln = -9223372036854775808L;
printBinaryLong("maxneg", lln);
printBinaryLong("l", l);
printBinaryLong("~l", ~l);
printBinaryLong("-l", -l);
printBinaryLong("m", m);
printBinaryLong("l & m", l & m);
printBinaryLong("l | m", l | m);
printBinaryLong("l ^ m", l ^ m);
printBinaryLong("l << 5", l << 5);
printBinaryLong("l >> 5", l >> 5);
printBinaryLong("(~l) >> 5", (~l) >> 5);
printBinaryLong("l >>> 5", l >>> 5);
printBinaryLong("(~l) >>> 5", (~l) >>> 5);
monitor.expect("BitManipulation.out");
}
static void printBinaryInt(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 printBinaryLong(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, printBinaryInt( ) and printBinaryLong( ),
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. .
Youll note the use of System.out.print( ) instead of System.out.println( ).
The print( ) method does not emit a newline, so it allows you to output a line
in pieces. .
In this case, the expect( ) statement takes a file name, from which it
reads the expected lines (which may or may not include regular expressions). This is
useful in situations where the output is too long or inappropriate to include in the book.
The files ending with .out are part of the code distribution, available for
download from www.BruceEckel.com, so you can open the file and look at it to see
what the output should be (or simply run the program yourself). .
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
The binary representation of the numbers is referred to as signed twos complement. .
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 youll 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
basisits 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 thats what makes the operator distinct from
the if-else. Heres 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 youd 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 doesnt require a lot more typing. So
be sure to ponder your reasons when choosing the ternary operatorits generally
warranted when youre setting a variable to one of two values. .
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. .
Theres one special usage of an operator in Java: the +
operator can be used to concatenate strings, as youve already
seen. It seems a natural use of the + even though it doesnt 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 like 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. .
One of the pitfalls when using operators
is attempting to leave out the 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 clearly 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 youll probably get an infinite loop. In Java, the result of this
expression is not a boolean, but the compiler expects a boolean and
wont 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 wont get a compile-time error is when x and y are boolean, in which case x = y
is a legal expression, and in the preceding example, 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 ==,
its easy to type just one character instead of two. In Java, the compiler again
prevents this, because it wont let you cavalierly use one type where it doesnt
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
wouldnt normally happen. .
To perform a cast, put the desired data type (including all modifiers) inside
parentheses to the left of any value. Heres an example:
void casts() {
int i = 200;
long l = (long)i;
long l2 = (long)200;
}
As you can see, its 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 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 doesnt 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 doif 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 doesnt 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 youll 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
public 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 (but can be confusing)
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
or a-f either in uppercase 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 preceding 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. Youll know youve 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. Uppercase or lowercase L means
long, upper or lowercase F
means float and uppercase or
lowercase D means double. .
Exponents use a notation that Ive 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.[17]
At any rate, this custom was followed in C, C++ and now Java. So if youre 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 dont need to use the trailing character when the compiler can
figure out the appropriate type. With .
long n3 = 200;
theres 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. .
Youll 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
youre 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 levelit 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.
| 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.
public 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 produces 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. Dont be lulled into thinking everything is safe, though. If you multiply
two ints that are big enough, youll overflow the result. The following
example demonstrates this: .
//: c03:Overflow.java
// Surprise! Java lets you overflow.
import com.bruceeckel.simpletest.*;
public class Overflow {
static Test monitor = new Test();
public static void main(String[] args) {
int big = 0x7fffffff; // max int value
System.out.println("big = " + big);
int bigger = big * 4;
System.out.println("bigger = " + bigger);
monitor.expect(new String[] {
"big = 2147483647",
"bigger = -4"
});
}
} ///:~
You get no errors or warnings from the compiler, and no exceptions at run time. Java is
good, but its 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 Cs execution control statements, so if youve 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 youve seen earlier in this chapter can be used to produce a
conditional statement. Note that Java doesnt allow you to use a number as a boolean,
even though its 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 by 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 is 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
import com.bruceeckel.simpletest.*;
public class IfElse {
static Test monitor = new Test();
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));
monitor.expect(new String[] {
"1",
"-1",
"0"
});
}
} ///:~
It is conventional to indent the body of a control flow statement so the reader can
easily determine where it begins and ends.
The return keyword has two purposes: It specifies what value a method will
return (if it doesnt have a void return value) and it causes that value to be
returned immediately. The preceding test( ) method can be rewritten to take
advantage of this: .
//: c03:IfElse2.java
import com.bruceeckel.simpletest.*;
public class IfElse2 {
static Test monitor = new Test();
static int test(int testval, int target) {
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));
monitor.expect(new String[] {
"1",
"-1",
"0"
});
}
} ///:~
Theres no need for else, because the method will not continue after
executing a return. .
Looping is controlled by while, do-while and for,
which 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. .
Heres a simple example that generates random numbers until a particular condition
is met:
//: c03:WhileTest.java
// Demonstrates the while loop.
import com.bruceeckel.simpletest.*;
public class WhileTest {
static Test monitor = new Test();
public static void main(String[] args) {
double r = 0;
while(r < 0.99d) {
r = Math.random();
System.out.println(r);
monitor.expect(new String[] {
"%% \\d\\.\\d+E?-?\\d*"
}, Test.AT_LEAST);
}
}
} ///:~
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, youll get a
different-sized list of numbers. .
In the expect( ) statement, you see the Test.AT_LEAST flag following
the expected list of strings. The expect( ) statement can include several
different flags to modify its behavior; this one says that expect( ) should
see at least the lines shown, but others may also appear (which it ignores). Here, it says
you should see at least one double value. .
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 lowercase ASCII letters.
import com.bruceeckel.simpletest.*;
public class ListCharacters {
static Test monitor = new Test();
public static void main(String[] args) {
for(int i = 0; i < 128; i++)
if(Character.isLowerCase((char)i))
System.out.println("value: " + i +
" character: " + (char)i);
monitor.expect(new String[] {
"value: 97 character: a",
"value: 98 character: b",
"value: 99 character: c",
"value: 100 character: d",
"value: 101 character: e",
"value: 102 character: f",
"value: 103 character: g",
"value: 104 character: h",
"value: 105 character: i",
"value: 106 character: j",
"value: 107 character: k",
"value: 108 character: l",
"value: 109 character: m",
"value: 110 character: n",
"value: 111 character: o",
"value: 112 character: p",
"value: 113 character: q",
"value: 114 character: r",
"value: 115 character: s",
"value: 116 character: t",
"value: 117 character: u",
"value: 118 character: v",
"value: 119 character: w",
"value: 120 character: x",
"value: 121 character: y",
"value: 122 character: z"
});
}
} ///:~
Note that the variable i 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 i is the expression controlled by the
for. .
This program also uses the java.lang.Character wrapper class, which
not only wraps the primitive char type in an object, but also provides other
utilities. Here, the static isLowerCase( ) method is used to detect
whether the character in question is a lower-case letter. .
Traditional procedural languages like C require that all variables be defined at the
beginning of a block so that 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 method 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.
Heres another example:
//: c03:CommaOperator.java
import com.bruceeckel.simpletest.*;
public class CommaOperator {
static Test monitor = new Test();
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);
}
monitor.expect(new String[] {
"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. .
You can also control the flow of the loop inside the body of any of the iteration
statements 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.
import com.bruceeckel.simpletest.*;
public class BreakAndContinue {
static Test monitor = new Test();
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);
}
monitor.expect(new String[] {
"0",
"9",
"18",
"27",
"36",
"45",
"54",
"63",
"72",
"10",
"20",
"30",
"40"
});
}
} ///:~
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, youd use a break
like this only if you didnt 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, youll 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.) In the output, 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,
youll see that program control contains many jumps (the Java compiler produces its
own assembly code, but this code is run by the Java Virtual Machine rather
than directly on a hardware CPU).
A goto is a jump at the source-code level, and thats what brought it into
disrepute. If a program will always jump from one point to another, isnt 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. Its not a jump but rather a way
to break out of an iteration statement. The reason its often thrown in with
discussions of goto is because it uses the same mechanism: a label. .
A label is an identifier followed by a colon, like this:
label1:
The only place a label is useful in Java is right before an iteration statement.
And that means right beforeit 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
youre going to nest another iteration or a switch inside it. Thats because the
break and continue keywords
will normally interrupt only the current loop, but when used with a label, theyll
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 reenter 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.
import com.bruceeckel.simpletest.*;
public class LabeledFor {
static Test monitor = new Test();
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++) {
System.out.println("i = " + i);
if(i == 2) {
System.out.println("continue");
continue;
}
if(i == 3) {
System.out.println("break");
i++; // Otherwise i never
// gets incremented.
break;
}
if(i == 7) {
System.out.println("continue outer");
i++; // Otherwise i never
// gets incremented.
continue outer;
}
if(i == 8) {
System.out.println("break outer");
break outer;
}
for(int k = 0; k < 5; k++) {
if(k == 3) {
System.out.println("continue inner");
continue inner;
}
}
}
}
// Can't break or continue to labels here
monitor.expect(new String[] {
"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"
});
}
} ///:~
Note that break breaks out of the for loop, and that the
increment-expression doesnt 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. .
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.
import com.bruceeckel.simpletest.*;
public class LabeledWhile {
static Test monitor = new Test();
public static void main(String[] args) {
int i = 0;
outer:
while(true) {
System.out.println("Outer while loop");
while(true) {
i++;
System.out.println("i = " + i);
if(i == 1) {
System.out.println("continue");
continue;
}
if(i == 3) {
System.out.println("continue outer");
continue outer;
}
if(i == 5) {
System.out.println("break");
break;
}
if(i == 7) {
System.out.println("break outer");
break outer;
}
}
}
monitor.expect(new String[] {
"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"
});
}
} ///:~
The same rules hold true for while: .
Its 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 Dijkstras 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 dont suffer from this problem, since they are constrained in their
placement and cant be used to transfer control in an ad hoc manner. Its 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 preceding 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 dont usually want this kind of behavior, it can be
useful to an experienced programmer. Note that the last statement, following the default,
doesnt 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 styles sake. .
The switch statement is a clean way to implement multiway 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 wont work in
a switch statement. For non-integral types, you must use a series of if
statements. .
Heres an example that creates letters randomly and determines whether
theyre vowels or consonants: .
//: c03:VowelsAndConsonants.java
// Demonstrates the switch statement.
import com.bruceeckel.simpletest.*;
public class VowelsAndConsonants {
static Test monitor = new Test();
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");
}
monitor.expect(new String[] {
"%% [aeiou]: vowel|[yw]: Sometimes a vowel|" +
"[^aeiouyw]: consonant"
}, Test.AT_LEAST);
}
}
} ///:~
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 youre switching on a character here, the switch
statement is actually using the integral value of the character. The single-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 its
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. .
In the regular expression in this expect( ) statement, the |
is used to indicate three different possibilities. The [] encloses a
set of characters in a regular expression, so the first part says one of
a, e, i, o, or u, followed by a colon and the word vowel. The second
possibility indicates either y or w and: Sometimes a vowel. The set in the
third possibility begins with a ^, which means not any of the
characters in this set, so it indicates anything other than a vowel will match. .
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?
import com.bruceeckel.simpletest.*;
public class CastingNumbers {
static Test monitor = new Test();
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));
monitor.expect(new String[] {
"above: 0.7",
"below: 0.4",
"(int)above: 0",
"(int)below: 0",
"(char)('a' + above): a",
"(char)('a' + below): a"
});
}
} ///:~
So the answer is that casting from a float or double
to an integral value always truncates the number. .
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 doesnt
include.) Again, a test program might provide the answer: .
//: c03:RandomBounds.java
// Does Math.random() produce 0.0 and 1.0?
// {RunByHand}
public class RandomBounds {
static void usage() {
System.out.println("Usage: \n\t" +
"RandomBounds lower\n\tRandomBounds 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[18]
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 youre 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.
[17] 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 usedusually 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.
[18] 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).
|
|
|