Chapter 1. Java in Review
I can hear the groans from here—a review on Java? Don’t worry, I won’t bore you with all of the gory syntax details or concepts of the Java language that you can easily pick up in other books. Instead, I will present a conceptual review that focuses on some various important issues that are often overlooked or underemphasized. The study of these issues will not only give you a better understanding of the Java language, but prepare you for what’s covered in the rest of the book. You should think of this chapter as a roving spotlight, highlighting various issues of Java that are worthy of mention; even the intermediate and advanced programmer will benefit from the study of these issues.
Core Concepts
To understand the advanced concepts of the Java language, there are a few core concepts that you must have firmly in mind. Without these concepts, much of this book will not make a lot of sense. The concepts of pointers in Java, its class hierarchy, and RTTI (runtime type identification) are three of the most important on this list.
Constant Problems with Pointers
Java and C++ use a very analogous syntax to symbolize their instructions to the computer’s CPU. In fact, there are probably more similarities between these languages than the two entrenched camps of supporters would like to admit. One difference between Java and C++ that is often mentioned, though, is that Java does not use pointers.
Pointers in C++ were a constant source of problems and were determined to be the programming equivalent of evil incarnate. There was, and is to this day, a large group of applications in C++ that suffer from the effects of this particular wrong. Therefore, Sun decided to leave them out of Java—at least that’s the theory.
In reality, Java uses what C++ calls references . In fact, other than primitives, all variables in Java are references. References and pointers are very similar; both contain the memory location of a particular object. Pointers, however, allow you to do arithmetic whereas references do not. The fact that Java uses so many references introduces some difficulties that novice and proficient Java developers often get burned by. The code shown in Example 1-1 demonstrates one of these difficulties.
package oreilly.hcj.review; public class PointersAndReferences { public static void someMethod(Vector source) { Vector target = source; target.add("Swing"); } }
Here, you simply copy the passed-in source
vector
and add a new element to the copy (named target
);
at least that is how it appears. Actually, something quite different
happens. When you set target
equal to
source
in the code, you copy a reference to the
target
vector, and not the contents of the vector
itself. Since both variables now point to the same vector, this
function actually adds an element to the source
vector that was passed into the method; this was almost certainly not
the desired effect! We will discuss how to prevent this problem in
Chapter 2.
Since you can change the contents of an incoming collection, Java actually does have pointers—that is, Java references embody the same computer science principles that pointers do. So, when someone tells you that Java doesn’t have pointers, you can correct them by saying, “Java has pointers, it just doesn’t have pointer arithmetic.”
Also, Java’s use of references
isn’t a bad thing. In fact, the references are
actually necessary to the Java language. Without them, you would have
to pass everything by value. This would entail copying every single
object each time an object was passed to a method. If the object were
a String
, this copying probably
wouldn’t be a big deal. However, if the object is a
large array or set, the copy could take a long time. Therefore,
passing everything by value would make Java code run extremely
slowly. Furthermore, some objects simply don’t make
sense to copy at all. If you have a GUI panel and wish to pass the
GUI panel to another component that needs to refer to it, you
certainly don’t want two copies of the panel
floating around in memory. Instead, you want to send a reference to
the existing panel. All of these issues point out the need for
references in Java.
Everything Is a Class and Object Is God
In Java, all constructed
types are regarded as objects. In
fact, the only items in Java that are not objects are the primitive
types, such as int
and boolean
;
and even these primitive types can be treated as objects under some
circumstances, such as when you use reflection.
I would expect that most Java programmers would find this trivial
since they already know that every nonprimitive in Java is an object.
The point that I am really trying to make, though, is that every
constructed type in Java descends from the class
java.lang.Object
. Even if you
don’t declare a class as extending
java.lang.Object
, you still get this behavior.
Example 1-2 shows a class that explicitly extends
java.lang.Object
, while Example 1-3 is a class that has no explicit extension.
In Example 1-2, SomeClass
makes
its object hierarchy clear; its superclass is
Object
.
Example 1-3, while visibly different, is actually
equivalent to Example 1-2. The Java compiler will
automatically assign the superclass of Object
to
this version of SomeClass
.
In fact, even a class that implements interfaces instead of extending
another class extends java.lang.Object
. As a
result, these two declarations are identical:
public class SomeClass extends Object implements Serializable { } public class SomeClass implements Serializable { }
So, unlike C++, you cannot create a class that does not have a
superclass. Object
is the superclass to all
classes in the entire language. In the example code for this book,
you will find the class
oreilly.hcj.review.ObjectIsGod
, which demonstrates
this concept:
> ant -Dexample=oreilly.hcj.review.ObjectIsGod run_example run_example: [java] class oreilly.hcj.review.ObjectIsGod$SomeClass --|> class java.lang.Object [java] class oreilly.hcj.review.ObjectIsGod$SomeOtherClass --|> class java.lang.Object
There are three main reasons why java.lang.Object
is always at the top of the inheritance tree:
For Java to know the type of objects it must load, and to enforce type safety, it has to have some base type with which it can refer to all objects.
Java currently lacks the concept of the parameterized type (also known as a template). This means that all the objects that are be stored in collection classes ultimately have to descend from one class.
The fact that all objects have
java.lang.Object
as a superclass gives you supreme power when it comes to using reflection to turbocharge your code. Without this sort of inheritance, many techniques in reflection would be impractical. (We will discuss reflection in detail in Chapter 9.)
RTTI
Runtime type
identification
, or RTTI, is an
extremely powerful tool. With RTTI, objects become very friendly and
readily tell you what they are, how they are used, and more. In Java,
RTTI is built right into the core of the virtual machine.
You’ve almost certainly used RTTI, even if you
don’t realize it; it’s all over the
place. For example, consider that every object in Java can tell you
what type it is through the getClass(
)
method. Whenever you invoke
getClass( )
, you use RTTI. Example 1-4 shows the getclass( )
method in action.
package oreilly.hcj.review; public class RTTIDemo { public final static void basicRTTIDemo ( ) { Float y = new Float(15.0); String name = "Fred"; System.out.println(y.getClass( )); System.out.println(name.getClass( )); } }
This method will, quite politely, print the following:
>ant -Dexample=oreilly.hcj.review.RTTIDemo run_example run_example: [java] class java.lang.Float [java] class java.lang.String
Since the getClass( )
method is a method defined
by the class java.lang.Object
, it is guaranteed to
be there for all objects.
Java also uses RTTI to protect a programmer from her own errors, as Example 1-5 demonstrates.
package oreilly.hcj.review; public class RTTIDemo { public static class A { } public static class B extends A { } public static class C { } public final static void castingWithRTTI ( ) { A a = null; A a1 = new A( ); B b = new B( ); C c = new C( ); a = (A)b; // no problem b = (B)a; // still no problem, casting back to what it was created as. a = a1; // Same type so no problem Object d = (Object)c; // no problem because of implicit inheritance c = (C)d; // casting back b = (A)a1; // compiler error: a1 is not a B. b = (B)c; // compiler error: c is not a B. } }
This code shows how to create an object of a subclass, cast it to its base class, and then cast it back to the subclass. RTTI works in the background to ensure that your casting is always legal; in other words, an object can safely be cast only to its own type, or to a type that it is inherited from. In the sample code for the book, Example 1-5 is replicated with the bad casts commented out. If you uncomment these lines, you will see that the program won’t even compile, and you will get errors such as those shown here:
>ant -Dexample=oreilly/hcj/review/RTTIDemo.java compile_example compile_example: [javac] Compiling 1 source file to C:\dev\hcj\bin [javac] C:\dev\hcj\src\oreilly\hcj\review\RTTIDemo.java:54: incompatible types [javac] found : oreilly.hcj.review.RTTIDemo.A [javac] required: oreilly.hcj.review.RTTIDemo.B [javac] b = (A)a1; // compiler error: a1 is not a B. [javac] ^ [javac] C:\dev\hcj\src\oreilly\hcj\review\RTTIDemo.java:55: inconvertible types [javac] found : oreilly.hcj.review.RTTIDemo.C [javac] required: oreilly.hcj.review.RTTIDemo.B [javac] b = (B)c; // compiler error: c is not a B. [javac] ^ [javac] 2 errors
RTTI is useful for preventing common errors in programming. It also has many other uses; we will get to the juicy details of exploiting RTTI in Chapter 7 and Chapter 8.
Syntax Issues
When compared to languages such as C++, Java has a very simple syntax. However, there are some points of syntax that should be covered, even for the intermediate and advanced Java programmer.
Abbreviated if
One of the things that is not well understood about the
if
statement is that it abbreviates
evaluations in order from left to right. For example, consider the
following code:
package oreilly.hcj.review; public class SyntaxIssues { public static void containsNested(final List list, final Object target) { Iterator iter = list.iterator( ); for (Set inner = null; iter.hasNext( ); inner = (Set)iter.next( )) {if (inner != null) {
if (inner.contains(target)) {
// do code.
}
}
} } }
In this code, the method is passed a list of sets to determine
whether the targeted element is in one of the nested sets. Since a
list can contain null
s, the method wisely checks
for null
before dereferencing
inner
. As long as inner
isn’t null
, the method checks to
see whether the set contains target
. This code
works, but the deep nesting is not necessary. You can write the code
in another way:
package oreilly.hcj.review; public class SyntaxIssues { public static void containsNested2(final List list, final Object target) { Iterator iter = list.iterator( ); for (Set inner = null; iter.hasNext( ); inner = (Set)iter.next( )) {if ((inner != null) && (inner.contains(target))) {
// do code.
}
} } }
In this version, the method checks for null
and
containment on the same line. This version of the method is in no
danger of throwing NullPointerExceptions
because
the evaluation of the if
statement is abbreviated
at runtime. While evaluating an if
statement, the
evaluations are run from left to right. Once the evaluation reaches a
definitive condition that cannot be altered by any other evaluation,
the remaining evaluations are skipped.
Ternary Expressions
Ternary expressions look a little strange at first. However, they can be useful in making code read better. For example, consider the following code:
package oreilly.hcj.review; public class SyntaxIssues { public int someMethod(final Point p) { if (p == null) { return 0; } else { return p.x + p.y; } } }
Although this method runs properly, it is unnecessarily wordy. The same thing could have been accomplished using a much simpler ternary expression:
package oreilly.hcj.review;
public class SyntaxIssues {
public int someElegantMethod(final Point p) {
return p == null ? 0 : p.x + p.y;
}
}
The emphasized line is not as cryptic as it may appear at first. It
is merely a large evaluation expression. If the given clause
evaluates as true
, then the value of the entire
statement is in the ?
clause. However, if the
condition evaluates as false
, then the value of
the entire statement is in the : clause. The
result of the evaluation is returned by the return
statement.
Ternary statements are very useful for performing evaluations such as this. However, you have to be aware of a few gotchas. For example, the following code will not work:
p == null ? System.out.println("p is null!")
: return p.x + p.y;
return 0;
In this statement, the user wants to make an
if
-then
clause that would print
a message if the point passed to the method was
null
or return the value if the point was not
null
. Basically, he is going for a less wordy
version of the following code:
package oreilly.hcj.review; public class SyntaxIssues { public static int someMethod(final Point p) { if (p == null) { System.out.println("p is null!") } else { return p.x + p.y; } } return 0; }
The problem is that a ternary expression is not
an if
-then
clause. It is an
evaluation clause. Both of the clauses in the ternary expression must
evaluate to something. The statement System.out.println("p
is null!")
evaluates to void
because the
return of the println( )
method is
void
. Additionally, the statement return
p.x + p.y
is pure syntactical nonsense because
return
is a keyword that doesn’t
evaluate to anything.
Although ternary expressions are useful for shortening what would be
an otherwise long and drawn-out piece of code, they can be cryptic to
read if they are abused. I recommend that you use ternary expressions
only for very small evaluations. If your evaluation is complex, you
are better off going with the
if
-then
structure.
Leveraging for Loops
The for
loop is one of the most elegant and underused looping structures in
the Java language. Most developers use only the basic concept of a
for
loop to iterate through a predefined series of
numbers:
for (int idx = 0; idx < args.length; idx++) { // . . . do something. }
To the average reader, this code should be completely boring.
However, to many developers, this is the pinnacle of
for
loop usage. But there are many more things
that you can do with a for
loop to make your code
more elegant while dealing with some annoying programming issues.
Although the techniques in this section are not necessarily required,
they do make your code look a lot nicer. However, if there is one
concept that you should take from this section, it’s
that for
loops can do much more than simply cycle
through numbers.
for loop fundamentals
One recurring problem when dealing with collections is the need to
iterate through the collection and keep track of an index at the same
time. For example, you may need to copy all of the
x
coordinates from a List
of
Point
objects into an array of
int
s. The following code represents a first
attempt at implementing this method:
package oreilly.hcj.review; import java.awt.Point; import java.util.Iterator; import java.util.LinkedList; import java.util.List; public class SyntaxIssues { public static int[] extractXCoords(final List points) { int[] results = new int[points.size( )]; Point element = null; int idx = 0; Iterator iter = points.iterator( ); while (iter.hasNext( )) { element = (Point)iter.next( ); results[idx] = element.x; idx++; } return results; } }
Although this piece of code will work, it isn’t very elegant. The code would look much neater written in the following way:
package oreilly.hcj.review;
public class SyntaxIssues {
public static int[] extractXCoords2(final List points) {
int[] results = new int[points.size( )];
Point element = null;
Iterator iter = points.iterator( );
for (int idx = 0; iter.hasNext( ); idx++) {
element = (Point)iter.next( );
results[idx] = element.x;
}
return results;
}
}
This second example is much more elegant than the first. However, the
important thing to note about the rewritten method is that the exit
clause to the for
loop has nothing to do with
idx
.
You can use the for
loop structure in this manner
because each of the statements inside the for
loop
are completely independent of each other. This is a fact that seems
to escape many Java developers. The point is that the
for
loop has the following grammar:
for (Allocation Statement; Expression; Iteration Statement)
The allocation statement in the for
loop is
executed when the loop is set up. At each iteration of the loop, the
expression in the middle is evaluated and, if the expression is
true
, the iteration statement is run. You can
stick any statement or expression in the for
loop
regardless of whether they are using the same variables. For example,
the following is perfectly legal:
package oreilly.hcj.review; public class ForLoops { public static void forWeird( ) { boolean exit = false; int idx = 0; for (System.setProperty("user.sanity", "minimal"); exit == false; System.out.println(System.currentTimeMillis( ))) { // do some code. idx++; if (idx == 10) { exit = true; } } } }
When the loop is initialized, the system property
user.sanity
will be set to
minimal
. At each iteration of the loop, the
current time in milliseconds will be printed as long as
exit
is false
. This is a great
demonstration of what you can do with for
loops.
Now that you know just what is possible with for
loops, you can leverage them even further to do things that are a bit
more mainstream.
Collection iteration with for
Iterating through collections offers you
another chance to have a bit of fun with the for
loop. Let’s begin with typical code used to iterate
through a collection of objects:
package oreilly.hcj.review; public class ForLoops { public static void forLong( ) { Properties props = System.getProperties( ); Iterator iter = props.keySet( ) .iterator( ); String key = null; while (iter.hasNext( )) { key = (String)iter.next( ); System.out.println(key + "=" + System.getProperty(key)); } } }
This snippet will iterate through all of the system properties and
dump them to System.out
. It works fine but it is
very wordy and can be slightly problematic. For example, try the
following bug on for size:
package oreilly.hcj.review; public class ForLoops { public static void propsDump(final Set customKeys) { Properties props = System.getProperties( ); Iterator iter = props.keySet( ) .iterator( ); String key = null; System.out.println("All Properties:"); while (iter.hasNext( )) { key = (String)iter.next( ); System.out.println(key + "=" + System.getProperty(key)); } System.out.println("Custom Properties:"); iter = customKeys.iterator( ); while (iter.hasNext( )) { System.out.println(key + "=" + System.getProperty(key)); } } } }
If you didn’t see the bug in this piece of code
instantly, then you are in trouble. The problem is that this code
generates a logic error. In the second iteration,
key
= (String)iter.next( )
wasn’t called. However, since key
is still in scope from the previous loop, the code compiles and runs.
Trivial errors such as this can consume tons of hours in complex
applications.
It would be great if you could scope the key
variable and the iter
variable to the
for
loop where they are used. You can do this and
make your program more elegant with the following code:
package oreilly.hcj.review; public class ForLoops { public static void forShort( ) { Properties props = System.getProperties( ); for (Iterator iter = props.keySet( ).iterator( ); iter.hasNext( );) { String key = (String)iter.next( ); System.out.println(key + "=" + System.getProperty(key)); } } }
In this example, the ability of for
was leveraged
to isolate your iteration variable. In this example,
key
is defined only within the
for
loop, and any attempt to access it outside the
for
loop will generate a compiler error.
Additionally, the iterator itself is scoped to the
for
loop, which guards against a user that forgets
to set up his iterator. Finally, the increment portion of the
for
loop has no value at all, which is completely
legal.
When looking at forShort( )
, you may be inclined
to object that key
is being allocated in a loop
and that loop allocation is bad. In fact, in this example, the
compiler notices that as well and optimizes out the variable to be
allocated once while keeping it scoped to the for
loop.
Abrupt Flow Control
Two of the more poorly understood
pieces of Java syntax are the break
and
continue
keywords. Both are used to control flow
within loops or other complex statements. They can be very useful in
some situations, but you need to be aware of their limitations.
break
The break
keyword is probably familiar to most
Java programers who have a flair for using switch
statements. An example of a common usage is shown here:
package oreilly.hcj.review; public class SyntaxIssues { public static String decode(final int input) { String decoded = null; switch (input) { case 1: decoded = "Option 1"; break; case 2: case 3: decoded = "Option 2"; break; default: return "Option 3"; } return decoded; } } }
The break
keyword tells the virtual machine to
exit the switch
statement. In this
switch
statement, if the user inputs a
1
, the result will be "Option
1
“. If the user inputs a 2
or a
3
, the result will be "Option
2
“. Finally, if she inputs anything else, the result will
be "Option
4
“. The
break
statement lets certain cases, such as
2
, fall through, but breaks out when it is done
with the relevant logic. Without the break, you would always get
"Option 4
" as the result of the method because all
lines inside the switch
statement would run.
The break
statement is a great way to make
decisions such as those in the previous example. However, every now
and then you get silly people that do something like the following:
package oreilly.hcj.review;
public class SyntaxIssues {
public static String decode(final int input) {
String decoded = null;
switch (input) {
case 1:
decoded = "Option 1";
break;
case 2:
case 3:
decoded = "Option 2";
break;
default:
return "Option 3";
break; // <= someone doesn't get it!
}
return decoded;
}
}
}
In this code, the final break
statement is
completely unnecessary, and also unreachable. The code will exit the
switch
statement regardless of whether the break
was there. If you find it hard to believe people actually write code
like this, the templates to generate switch
statements that come with Eclipse do exactly this. However, this is a
rather minor, if somewhat silly, problem. Some problems with
break
are a lot more serious. Consider the
following code:
package oreilly.hcj.review; public class SyntaxIssues { public static void matrixMeth(final Point[][] values) { for (int x = 0; x < values[0].length; x++) { for (int y = 0; y < values.length; y++) { if ((values[x][y].x < 0) || (values[x][y].y < 0)) { break; // exit to the System.err line. } // do something with the value } } System.err.println("Invalid Point in Matrix"); // cleanup processing resources } } }
In this piece of code, the developer wants the nested
for
loops to exit if a bad
Point
is detected in the matrix. However, the
break
statement doesn’t do this.
It merely breaks out of its current loop. The
outer loop still runs, and the method processes bad data with
potentially disastrous results.
When you use break
in your code, you should be
aware of these limitations and plan accordingly. The
break
keyword can be extremely useful for quick
and abrupt termination of complex loops but its limitations can bite
you if you aren’t careful.
continue
The continue
keyword is similar to
break
in that it abruptly changes the flow control
of a method. However, whereas break
exits a loop
completely, continue
merely skips back to the loop
clause itself:
package oreilly.hcj.review; public class SyntaxIssues { public static void continueFunc( ) { for (int idx = 0; idx < 1000; idx++) { // . . . do some complex code. if (idx == 555) { break; } // . . . processing code } for (int idx = 0; idx < 1000; idx++) { // . . . do some complex code. if (idx == 555) { continue; } // . . . processing code. } } }
In the first loop, the processing code is executed only 554 times.
When the value of idx
is 555
,
the break
statement is hit and the loop exits.
However, in the second loop the processing code is executed 999
times. In this case, when idx
is
555
, the continue
statement is
hit, and the remainder of the logic inside the for
loop is skipped. Subsequently, the loop then continues on normally
with 556; therefore, the processing code is skipped only for element
555.
Although this example is trivial, the use of the
continue
statement can be a significant asset to
your development when the code inside the loop is very complex. With
many such possible “skip”
conditions, your code can become deeply nested. The
continue
statement simplifies things dramatically.
Without the continue
statement, you would have to
enclose the entire remainder of the for
loop
inside an if
statement.
Labels
Labels are one of those obscure pieces of Java
syntax that you see only once in a blue moon. They are a way to mark
a statement with an identifier to be used by a
break
or continue
statement.
The following code declares labels on two different lines in a program:
LOGLINE:
System.err.println("Invalid Point in Matrix");LINE:
for (int x = 0; x < values[0].length; x++) { ... }
The label is declared by simply prefacing the line with a legal variable name and then a colon. As usual with Java, any leading whitespace on the line will be ignored. However, keep in mind that you cannot declare a label unless it is the first statement on a line. The compiler would spit out all sorts of parsing errors if you tried something like the following:
System.err.println("Invalid Point in Matrix"); LOGLINE2:
Once you have declared the label, you can use it in a
break
or a continue
statement.
The following code shows how you can use break
to
repair a faulty matrix:
package oreilly.hcj.review;
public class SyntaxIssues {
public static void matrixMeth2(final Point[][] values) {
RESTART: {
for (int x = 0; x < values[0].length; x++) {
for (int y = 0; y < values.length; y++) {
if ((values[x][y].x < 0) || (values[x][y].y < 0)) {
values[x][y].x = Math.max(values[x][y].x, 0);
values[x][y].y = Math.max(values[x][y].y, 0);
break RESTART; // Try to process again!
}
// do something with the value
}
}
}
// continue processing
}
}
In this version, if an error condition is detected, the method will
try to repair the error condition and then start to process the
matrix all over again. This is accomplished by the break
RESTART;
statement. This statement tells the virtual
machine to immediately transfer control to the statement labeled
RESTART
. This will work only if the
break
statement is nested inside the labeled
statement. Otherwise, the compiler will just laugh at you
sardonically and tell you that the label RESTART
isn’t declared.
Using labels with continue
is slightly different.
continue
must be nested within the code block of
any label to which it refers. Additionally, a
continue
can transfer control only to a label set
on a looping construct such as a for
,
while
, or do
statement. For
clarification on this rule, look at the following code:
package oreilly.hcj.review; public class SyntaxIssues { public static void matrixMeth3(final Point[][] values) {LINE:
{ for (int x = 0; x < values[0].length; x++) {COLUMN:
for (int y = 0; y < values.length; y++) { if ((values[x][y].x < 0) || (values[x][y].y < 0)) {continue LINE
; // skip the rest of the line; } // do something with the value } } } LOGLINE: System.err.println("Invalid Point in Matrix"); // continue processingPASS_TWO:
for (int x = 0; x < values[0].length; x++) { // do some code. } } }
In this example, instead of exiting to the log line, the programmer
wants to simply skip the rest of the problematic line in the matrix.
Using a continue
statement without a label would
have merely transferred control to the inner for
loop. However, by telling the continue
statement
to continue at the LINE
label, you can exit the
inner for
loop and skip the rest of the line
properly. However, you could not use LOGLINE
as a
continue target because the statement after the label
isn’t a loop construct, such as a
for
, while
, or
do
statement. You also couldn’t
use PASS_TWO
as a target for the
continue
statement because although the label
marks a looping statement, the continue
statement
is not nested in that looping statement. Furthermore, it would be
impossible to break or continue to a label outside of the method.
Although labels can be useful in controlling the flow in a program, they can lead to a lot of confusion in development due to their idiosyncrasies. I recommend that you not use them as a rule; however, you should now be able to unwind the spaghetti code of another developer that did use them.
assert
The
assert
keyword is a new addition to Java that
was introduced in JDK 1.4. It was a long overdue addition to the
language, as it provides for error checking that C++ and other
languages already have. However, since its introduction, I
haven’t seen it used nearly as much as it should be.
The reason for this is a basic lack of understanding as to how
assert
works. The assert
keyword has two forms. The first form of assertion uses the following
grammar:
assert Boolean_Expression;
In this form, Expression
is replaced by an
expression that evaluates to a boolean
result. If
the result is false
, then an
AssertionError
will be thrown by the compiler:
package oreilly.hcj.review;
public class Assertions {
protected static void helperParseArgs (final String[] args) {
assert (args != null);
// . . . code
}
}
In this example, the programmer wants to make sure that the user of
the helperParseArgs( )
method
didn’t send a null
for the
arguments
parameter. To accomplish this, he uses
the assert
keyword. If the user passes a
null
, then the virtual machine will throw an
AssertionError
with no description, but with a
stack trace to help the errant user figure out what he did wrong. If
the developer of the method wants to provide an error message, he
could have used the second form of assert
:
package oreilly.hcj.review;
public class Assertions {
protected static void helperParseArgs (final String[] args) {
assert (args != null) : "args cannot be null.";
// . . . code
}
}
In this case, if the assertion is thrown, the second expression is
evaluated and the result of that evaluation is used for the detail
message of the AssertionError
. Keep in mind that
you can use any expression that evaluates to anything other than
void
as the second expression. For example, the
following would be illegal:
package oreilly.hcj.review;
public class Assertions {
protected static void helperParseArgs (final String[] args) {
assert (args != null) : String s = "args cannot be null.";
// . . . code
}
}
An attempt to do something like this would be rejected by the
compiler since the evaluation of the expression is
void
. Similarly, any method that returns
void
is off limits as well.
Assertions versus exceptions
One of the most common questions asked about assertions is why you should bother using them when you can perform these checks and indicate errors with exceptions.
First of all, the code doing the check may be far more complicated
than a simple check for null
. In fact, the body of
assertion expressions can easily be as much work for the processor as
the body of the method itself. If you use exceptions, then these
checks will be performed every time the method is called. In a
production system, this can add significant overhead.
On the other hand, with assertions, the virtual machine can be set to ignore the assertions. As a result, their processing time is near zero. In development mode, during testing, or during diagnosis of some nasty problem, you can turn on assertions and figure out the problems. Once the software is stable, you can turn off assertions and gain a performance boost. As a bonus, assertions offer a shorthand that makes your code look more elegant.
To assert or not to assert
Deciding whether to use assertions or exceptions is a decision that you have to take on a case-by-case basis. Here are some tips that will help you make the right decisions:
Don’t use assertions to validate parameters of public functions. These functions should throw
NullPointerException
,IllegalArgumentException
, and other relevant exceptions instead. Since public functions will be used by other programmers, you should make sure that they get the right errors if they mess up.Use assertions to check preconditions and postconditions on parameters of protected and private access methods.
Don’t use assertions to check for software user errors. If you expect the user of your web-based online sales system to enter a 10-digit credit card number and she enters only 9 digits, don’t use an assert. Instead, throw
IllegalArgumentException
. If you useassert
, as soon as someone turns off assertions on your servlet container, the checking logic in your system would go away.Use assertions to check parameters and variables for conditions that shouldn’t happen. For example, consider the following event handler method:
package oreilly.hcj.review; public class Assertions { public void mouseClicked(final MouseEvent event) { Object source = event.getSource( ); assert(source != null); int hc = source.hashCode( ); // . . . do code using source } }
The virtual machine should never pass
null
as a source for a mouse event. However, just to be sure, the user asserts it. The granularity of assertions that you should use here is something of a judgment call. If you assert everything, you will have to write a lot more code; however, your code will be rock solid.Use assertions to check for invalid code branches. For example, consider the following GUI class:
package oreilly.hcj.review; public class Assertions { package oreilly.hcj.review; import java.awt.event.ActionEvent; import javax.swing.JButton; public class SomeGuiPanel { private JButton cancelBtn; private JButton noBtn; private JButton yesBtn; public void actionPerformed(final ActionEvent event) { Object source = event.getSource( ); assert (source != null); if (source == yesBtn) { // . . . do code } else if (source == noBtn) { // . . . do code } else if (source == cancelBtn) { // . . . do code } else {
assert false : "Invalid Source " + source.toString( );
} } }In this class, the action handler expects the user to click on one of three buttons. The method doesn’t expect there to be any other buttons to which the class is listening. However, if a developer added a new button but forgot to change the handler, there would be an extra button to deal with. On the final
else
clause, the method checks to make sure there aren’t any other buttons by usingassert
. If the emphasized line is hit, the program will generate an assertion exception. In this case, usingassert
is much more elegant than usingthrow
.Tip
Some may object that the program should always throw an irrecoverable error at this point regardless of whether assertions are enabled. This is a valid point. However, in a production environment, that is not always a good idea. The accountants using your software wouldn’t know what an
AssertionError
is. Instead, when the program crashes, they are likely to give you an informative bug report such as, “It crashed while I was making my finance report.” Its better to use assertions so the program can at least limp along if something weird happens. Let the developers and QA people deal with the assertion errors.Don’t use an assertion to do any work. Assertions are developer-level errors and shouldn’t be used to repair state in the program or perform complex logging. Also, don’t forget that if a user runs the program without assertions, the code will be gone. If that code was critical to the functioning of the program, you could be in deep trouble.
Don’t bother internationalizing assertion error messages. Again, since assertions are developer-level issues, internationalizing them would be a waste of time.
Use assertions to check post conditions. If you create a method and expect that it will never to return
null
to the user, you might write something like the following:package oreilly.hcj.review; public class SyntaxIssues { protected Object someHelperMethod( ) { Object result = null; // . . . do some code that sets result.
assert (result != null); // check post condition.
return result; } }In this code, the method checks the return value against a post-condition before returning the value. If the value is still
null
after all of the work, the assertion will toss an error.
Now that you have a good idea of when to use
assert
and when not to, apply what you have
learned on an actual class:
package oreilly.hcj.review; import java.awt.event.MouseEvent; import java.util.Arrays; import java.util.Iterator; import java.util.List; public class Assertions { public static void main(final String[] args) {helperParseArgs(args);
Iterator iter = System.getProperties( ) .keySet( ) .iterator( ); for (String key = null; iter.hasNext( ); key = (String)iter.next( )) { assert (key != null); System.out.println(key + "=" + System.getProperty(key)); } } protected static void helperParseArgs(final String[] args) {assert (args != null);
assert (!Arrays.asList(args)
.contains(null));
// -- List arglist = Arrays.asList(args); Iterator iter = arglist.iterator( ); for (String argument = null; iter.hasNext( ); argument = (String)iter.next( )) { if (argument.startsWith("-D")) {if (argument.length( ) < 3) {
int idx = arglist.indexOf(argument);
throw new IllegalArgumentException("Argument" + idx
+ " is not a legal property argument.");
}
int valueIdx = argument.indexOf('='); System.setProperty(argument.substring(2, valueIdx), argument.substring(valueIdx + 1));assert (System.getProperty(
argument.substring(2, valueIdx))
.equals(argument.substring(valueIdx + 1)));
} } } }
In this code, you have a method that parses the arguments to the
program and looks for system properties to set. If it encounters an
argument such as -Dmy.property=5
on the command
line, it will set the system property my.property
to the value 5
.
The method that does the actual work is a protected helper method
that checks the arguments to the method for validity using
assertions. You should also note that the second check to the method
takes quite a bit of work to accomplish since the list has to go
through and see whether there are any null
s in the
argument list. At production time, however, this work will be removed
by turning off assertions. Inside the for
loop,
the method doesn’t use an assertion if the user
gives a bad property name because this is an error made by the user
of the program and not by the developers of the program.
Finally, the last assert
statement checks that the
system property was actually set properly. Notice that this
assert
does the exact same string manipulation
work as the original setting of the system property. Some
performance-minded developers may catch on to this fact and get the
bright idea to store the key and value at each iteration of the loop,
as shown here:
package oreilly.hcj.review; public class Assertions { for (String argument = null; iter.hasNext( ); argument = (String)iter.next( )) { if (argument.startsWith("-D")) { if (argument.length( ) < 3) { int idx = arglist.indexOf(argument); throw new IllegalArgumentException("Argument" + idx + " is not a legal property argument."); } int valueIdx = argument.indexOf('=');String key = argument.substring(2, valueIdx);
String value = argument.substring(valueIdx + 1);
System.setProperty(key, value); assert (System.getProperty(key).equals(value)); } }
Although this will work, it is a bit wasteful. The extra allocations
of the key
and value
variables
are needed only if assertions are enabled. If assertions are
disabled, then these variables will be used only once. Whether to use
variables to store parts of an assertion is a judgment call. If the
code that is creating the variable is very long, a variable may be
the best bet. If the code is simple, you should just save yourself
the extra allocations at production time.
Assertions and deployment
If
you are developing an application using assertions, you should be
aware that by default, assertions are not enabled in the virtual
machine. To enable all assertions, you need to pass the argument
-ea
to your virtual machine. If you need to enable
assertions on system classes, use the switch -esa
.
Additionally, there are facilities for enabling assertions on
individual packages. You should consult your virtual machine
documentation for information on how to do this.
When developing a product, you should always have assertions enabled, as they will help you catch bugs. Also, your QA testers should run a program with assertions enabled since it is their job to figure out how to break your code.
In a production release, the situation is a bit foggier. In the first days of a production product release, you may want to make sure the project runs with assertions enabled. Although you will loose some performance and incur a few crashes, you will be able to detect many errors caught by your assertions. When to turn off the assertions in a production environment is a judgment call that the project manager has to make. There will come a time when you think you have used the assertions enough to catch most of the errors, and performance and stability become the overriding need; after all, assertions do crash your program. At that point, it is time to update your shortcuts and batch files to turn off assertions. When releasing new features, you should employ a similar operating strategy. This will help you catch unexpected bugs.
Unfortunately, there is no way to turn assertions on and off in a running system; however, you can easily find out whether they are on by using the little assertion trick shown here:
boolean ASSERTIONS = false; assert ASSERTIONS = true;
The assert expression here evaluates to true
and
will not throw an assertion error at any time. If assertions are
enabled, then the code will run and ASSERTIONS
will be set to true
. If assertions are disabled,
then the code won’t be executed and
ASSERTIONS
will remain
false
.
Chaining Constructors
One of my personal pet-peeves is duplicated code. Unfortunately, you see a lot of it, especially in the creation of GUI widgets. The problem results from the way these widgets are built. Consider the button class shown in Example 1-6.
package oreilly.hcj.review; public class UnchainedConstructors extends JButton implements ActionListener { public UnchainedConstructors(final String text) { setText(text); String tooltip = new String("A button to show " + text); setToolTipText(tooltip); } public UnchainedConstructors(final String text, final String tooltip) { setText(text); setToolTipText(tooltip); } public UnchainedConstructors(final String text, final String tooltip, final ActionListener listener) { setText(text); setToolTipText(tooltip); addActionListener(listener); } }
Although this code will work, it is wasteful and prone to errors. For example, if you change a line in the constructor that accepts three parameters and forget to make the same change in the other constructors, you would have a button that builds correctly under some circumstances, but fails to build correctly under others. This would lead to a debugging session that wouldn’t be much fun. The symptom of the problem is in the thought, “If I change this in one constructor, I have to remember to change it in the others.” Whenever you hear yourself thinking similar thoughts, you should realize that something is wrong. All of your code dependencies should be in the code, and not in your head. Inevitably, you will forget some of these dependencies during your project. Also, even if you have a photographic memory and perfect recall, the junior programmer working for you won’t be privy to these details and could unintentionally bust the code you had so well thought out. To avoid having to remember these things, you can rewrite the class as shown in Example 1-7.
package oreilly.hcj.review; public class ChainedConstructors extends JButton implements ActionListener { public ChainedConstructors(final String text) { this(text, new String("A button to show " + text), null); } public ChainedConstructors(final String text, final String tooltip) { this(text, tooltip, null); } public ChainedConstructors(final String text, final String tooltip, final ActionListener listener) { setText(text); setToolTipText(tooltip); if (listener != null) { addActionListener(listener); } }
This code is much better; it chains the constructors so that each
delegates to the constructor that takes all parameters. This
constructor is referred to as the primary
constructor
. By using this technique, the code
for the other constructors is limited only to what they do
differently. This technique relieves you of having to remember to
change each constructor if the common code is modified. You chain the
constructors via the this( )
method. This special
method allows you to call one constructor from another. It is
analogous to using the super( )
method to
construct base classes. The only remaining problem is that you have
some rather ugly code as a result of chaining your constructors:
this(text, new String("A button to show " + text), null);
This code results from the fact that, just like super(
)
, you have to use the this( )
method as
the first line in the constructor. I have yet to understand why this
limitation is in Java, but it does indeed exist. As a result, some
rather annoying problems are introduced. In this case, you were
merely building a String
object in the call to
this( )
. However, there are other cases where you
will need to do a lot more work before calling the primary
constructor. The secret to doing this without the ugliness is via
helper methods:
package oreilly.hcj.review;
public class ChainedConstructors extends JButton
implements ActionListener {
public ChainedConstructors(final String text, final boolean showDate) {
this(text, buildDateToolTip(text, showDate)
, null);
}
private static String buildDateToolTip(final String text,
final boolean showDate) {
final StringBuffer buf = new StringBuffer(250);
buf.append("A panel to show ");
buf.append(text);
if (showDate) {
buf.append(" created on ");
DateFormat df = DateFormat.getInstance( );
buf.append(df.format(Calendar.getInstance( ).getTime( )));
}
return buf.toString( );
}
}
Here, you create a new static method that manipulates the input to
the constructor and returns the tool tip text. Then you use the
resulting text as the input to the tooltip
parameter of the primary constructor. This constructor helper
technique makes your code much cleaner and allows you to chain
constructors even when there is complex logic within the
constructors. The resulting code is much more stable and easier to
maintain.
When using helper methods, it is important that you keep in mind one
restriction: the this
pointer is not available to
the virtual machine at the time of a this( )
or
super( )
call. The result of this restriction is
that you can’t use any instance-scoped methods on
the object being constructed. For example, the following code would
be illegal:
public class ChainedConstructors extends JButton implements ActionListener { public ChainedConstructors(final Class dataType) { this(buildClassText(dataType)
, dataType.getName( ),this
); } protected String buildClassText(final Class dataType) { return dataType.getName( ).substring(dataType.getName( ) .lastIndexOf('.') + 1); } }
In Java, a call such as buildClassText( )
is
identical to the call this.buildClassText( )
. The
this
reference is assumed unless you explicitly
add some other kind of reference, such as an object or a class. In
this case, the this
pointer is not available yet,
so you can’t call instance-scoped methods on this
object. In fact, even though the class
ChainedConstructors
implements
ActionListener
, you can’t even
use the this
pointer to pass the object as a
listener to itself. If you uncomment the above constructor in the
code and try to compile it, you will get the following results:
>ant -Dexample=oreilly/hcj/review/ChainedConstructors.java compile_example compile_example: [javac] Compiling 1 source file to C:\dev\hcj\bin[javac] C:\dev\hcj\src\oreilly\hcj\review\ChainedConstructors.java:43: cannot reference this before supertype constructor has been called
[javac] this(buildClassText(dataType), dataType.getName( ), this); [javac] ^[javac] C:\dev\hcj\src\oreilly\hcj\review\ChainedConstructors.java:43: cannot reference this before supertype constructor has been called
[javac] this(buildClassText(dataType), dataType.getName( ), this); [javac] ^ [javac] 2 errors
In short, the this
pointer is off-limits to all
constructor helper methods. Therefore, if you want to declare a
helper method, you must make sure that the method is declared
static
. Since a static method
doesn’t have a this
pointer and
doesn’t need one, the call will work properly.
However, keep in mind that instance methods on objects already in
scope are completely permissible, as shown here:
public ChainedConstructors(final Color color) {
this(color.toString( )
, "", null);
}
In this example, the instance method toString( )
on the color
parameter passed to you is legal
because it is already in scope.
While using chained constructors, you will come across situations in
which you do not want to expose the primary constructor to the user
of the class. The solution to this problem is to merely make the
visibility of the primary constructor protected
.
That way, you get the best of both worlds. The constructors will be
chained, and you won’t reveal any more about the
class than you want to. Although it is unusual to see a class with
constructors that have various visibilities, it is quite
legal.
Initialization
Initialization is the process of assigning values to variables prior to the execution of the constructor for a class. Initialization is used in many places, and is often poorly understood by developers. Many developers think of initialization as one topic, when, in fact, there are six types of initialization that you can use in your code. The simplest type of initialization is shown here:
package oreilly.hcj.review;
public class InitalizerDemo {
public String description = "An initialized member";
}
The variable description
in this class is assigned
the value of "An
initialized
member
" just prior to construction of each instance of the
object. Therefore, even if the constructor does not explicitly set
description
, it will be guaranteed to contain the
value set in the initializer.
This type of initialization is important for solidifying code because it allows you to preset instance attributes to default values before the constructor is called. In this manner, constructors can ignore these attributes if they wish. Although this type of initialization is useful, there are times when initializations need to be a bit more complex:
package oreilly.hcj.review;
public class InitalizerDemo {
public long timestamp = System.currentTimeMillis( );
}
In this initializer, you can call a method on the line of the
initialization. This ability allows you to perform more complex
initializations that would be impossible without a method. However,
keep in mind that methods used in initializers are under the same
restrictions as the helper methods for chained constructors that we
discussed earlier—they should be static
and
can’t use the this
pointer.
In fact, you don’t even need to define a helper method if you don’t want to; an initializer can be a method itself:
package oreilly.hcj.review; public class InitalizerDemo {private String xmlClasspath;
{
final StringBuffer buf = new StringBuffer(500);
final String classPath = System.getProperty("java.class.path");
StringTokenizer tok = new StringTokenizer(classPath,
System.getProperty("path.separator"));
buf.append("<classpath>\n");
while (tok.hasMoreTokens( )) {
buf.append(" <pathelement location=\"");
buf.append(tok.nextToken( ));
buf.append("\"/>\n");
}
buf.append("</classpath>");
xmlClasspath = buf.toString( );
}
}
In this code, you translate the classpath into an XML structure upon
initialization of the object. This code will work fine, as you can
see by running the
oreilly.hcj.review.InitializerDemo
class in the
example code:
>ant -Dexample=oreilly.hcj.review.InitializerDemo run_example run_example: [java] ------Dumping Contents----------- [java] --------------------------------- [java] Initializer Demo [java] x86 [java] C:\Documents and Settings\Robert [java] An initialized member [java] <classpath> [java] <pathelement location="c:\j2sdk\\lib\tools.jar"/> [java] <pathelement location="C:\j2sdk\addons\jakarta-ant-1.5\bin\\..\lib\xml-apis.jar"/> [java] <pathelement location="C:\j2sdk\addons\jakarta-ant-1.5\bin\\..\lib\xercesImpl.jar"/> [java] <pathelement location="C:\j2sdk\addons\jakarta-ant-1.5\bin\\..\lib\optional.jar"/> [java] <pathelement location="C:\j2sdk\addons\jakarta-ant-1.5\bin\\..\lib\ant.jar"/> [java] </classpath> [java] ---------------------------------
Although this initialization technique is useful, you shouldn’t use it routinely. Instead, it should be saved for special circumstances, as it makes code a little cryptic to read. Use it only when you really need a special multiline initialization.
Instance attributes aren’t the only kind of
attributes in a class. Attributes declared with the
static
keyword are class-scoped. Class-scoped
attributes can be initialized in a manner similar to instance-scoped
attributes. Here is a simple case:
package oreilly.hcj.review;
public class InitalizerDemo {
public static final String NAME = "Initializer Demo";
}
This code works exactly like the instance initializer, except that it
is called when the class is loaded into the virtual machine by the
ClassLoader
. Another difference is that this
attribute is final
, so it cannot be changed after
it is initialized.
Tip
Although I show final
class-scoped attributes in
the examples, keep in mind that the behavior of class-scoped
attributes that are not final
is identical to that
of final
class-scoped attributes.
Like instance initializers, you can call methods in static variable initialization:
package oreilly.hcj.review;
public class InitalizerDemo {
public static final String ARCH = System.getProperty("os.arch")
;
}
You can also defer an initialization of a static variable to do something more complex:
package oreilly.hcj.review;
public class InitalizerDemo {
public static final String USER_HOME;
}
In this code, you have created a constant without giving the constant
a value. Perhaps the value you want requires more complex code than a
simple assignment. This is where the special
static{}
method comes in. The
ClassLoader
automatically invokes all
static{}
initializer methods when the class is
loaded into the virtual machine. It is in this method only that you
can initialize final
static variables that are not
initialized on their declaration line. You can use this method in a
similar manner as the method-based instance initializers:
package oreilly.hcj.review; public class InitalizerDemo { public static final String USER_HOME;static {
USER_HOME = System.getProperty("user.home");
}
}
The variable USER_HOME
will be filled with the
string version of the user’s home directory from the
system properties. The value will then be final
for the duration of the life of the virtual machine.
While using the static{}
method, it is important
to keep order in mind. Since static initializers are called in order
of their declaration, you can get into a bit of trouble if you
aren’t careful:
public static class Values {
public final static String VALUE_ONE = "Blue";
static {
System.out.println("static{} method for One");
System.out.println(VALUE_ONE);
System.out.println(VALUE_TWO); // <= compiler error
}
public final static String VALUE_TWO = "Red";
}
In this code, the compiler cannot find VALUE_TWO
because it has not been declared at the time when the
static{}
method was run. Since initializers are
processed in order, you get a bug called an illegal forward
reference
. Get rid of the bug with a coding
standard that declares all static variables first, and then declares
static methods. This will keep you out of trouble with your compiler
(although not necessarily with your programming logic).
While order is important, be aware that you cannot depend on the order of static initialization. Any methods that depend on ordering are likely to end up with some strange results. To make things a bit clearer, let’s intentionally build an example of this problem. Example 1-8 shows the constructed bug.
package oreilly.hcj.review; public class StaticOrderDemo { public StaticOrderDemo( ) { } public static final void main(final String[] args) { } public static class Ranges { public static final String[] RANGE_BLUE = { "Sky", "Navy" }; public static final String[] RANGE_RED = { "Light", "Dark" }; static { System.out.println("static{} method for Ranges"); System.out.println(Arrays.asList(RANGE_BLUE)); System.out.println(Values.VALUE_SPECIFIER); System.out.println(Arrays.asList(RANGE_RED)); } } public static class Values { public static final String VALUE = "Blue"; public static final String VALUE_SPECIFIER; static { System.out.println("static{} method for Values"); System.out.println(VALUE); System.out.println(Ranges.RANGE_BLUE); VALUE_SPECIFIER = Ranges.RANGE_BLUE[1]; } } }
When the example is run, the static nested class
Ranges
is initialized first. As a result, you will
see the following output:
>ant -Dexample=oreilly.hcj.review.StaticOrderDemo run_example run_example: [java] static{} method for Values [java] Blue [java] static{} method for Ranges [java] [Sky, Navy] [java] null [java] [Light, Dark] [java] [Ljava.lang.String;@1e3118a [java] Class oreilly.hcj.review.StaticOrderDemo$Values Loaded
Note especially the null
. Since
Values.VALUE_SPECIFIER
had not been initialized
when it was used in the Ranges
initializer, its
value was null
at the time of the
System.out.println( )
. Only microseconds later,
when Values
finishes initializing, it is no longer
null
. This kind of bug is very difficult to find
because of its very transient life. The moral of this story is that
you can never depend on the initialization order of static members.
Tip
Although this example was specifically constructed for this book, I actually encountered this bug in production code several times. The first time resulted in an overnight programming frenzy to try to find and correct the bug.
To summarize, there are many kinds of initializers, which are all useful for setting values prior to object construction, but beware of the little gotchas with static initializers. In Example 1-9, all of the possible initializers are shown in one class.
package oreilly.hcj.review; public class InitalizerDemo { /** Simple static initialization. */ public static final String NAME = "Initializer Demo"; /** Initialized static on one line. */ public static final String ARCH = System.getProperty("os.arch"); /** Static method based initialization. */ public static final String USER_HOME; static { USER_HOME = System.getProperty("user.home"); } /** Simple instance member initialization. */ public String description = "An initialized member"; /** Method call instance member initialization. */ public long timestamp = System.currentTimeMillis( ); /** Complex instance member initialization. */ private String xmlClasspath; { final StringBuffer buf = new StringBuffer(500); final String classPath = System.getProperty("java.class.path"); StringTokenizer tok = new StringTokenizer(classPath, System.getProperty("path.separator")); buf.append("<classpath>\n"); while (tok.hasMoreTokens( )) { buf.append(" <pathelement location=\""); buf.append(tok.nextToken( )); buf.append("\"/>\n"); } buf.append("</classpath>\n"); xmlClasspath = buf.toString( ); } }
One final thing to remember about all initializers is that you cannot
throw any exceptions other than subclasses of
RuntimeException
within the initializer. All other
exceptions will cause all sorts of compilation errors. Therefore, if
you want to use methods that throw other types of exceptions, you
will have to wrap up any possible exceptions in a
try
and catch
block and then
throw a RuntimeException
; of course,
you’re caught up on chained exceptions by now, so
this is no problem. With this arsenal of initializers at your
command, you should be ready to tackle setting defaults for any type
of class you can think of.
Access Issues
When you look through the many Java books
that are available, they all talk about access restrictions. The
words private
, protected
, and
public
are some of the first keywords that a
newbie Java programmer learns. However, most of these books discuss
access restrictions only with regards to the impact of restrictions
on the code.
By now, you should know what a private
method is
and the difference between private
and
protected
. Therefore, I won’t
bother rehashing this familiar territory. Instead, I would like to
take your understanding of access restrictions to another level.
Instead of focusing on what they do, I will focus on which to use in
various situations.
Preferred Restrictions
While writing Java programs, many programmers fall into a definable
pattern. All attributes are private
, all interface
methods are public
, and all helper methods are
private
. Unfortunately, this causes a ton of
problems in the real world. Consider the following common GUI code:
package oreilly.hcj.review; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.*; public class SomeDialogApp extends JDialog implements ActionListener { private JButton okBtn = null; private JButton someBtn = null; // . . . etc. public SomeDialogApp( ) { setJMenuBar(buildMenu( )); buildContents( ); } public void actionPerformed(final ActionEvent event) { Object source = event.getSource( ); if (source == this.okBtn) { handleOKBtn( ); } else if (source == this.someBtn) { handleSomeBtn( ); } // . . . etc. } private void buildContents( ) { this.okBtn = new JButton("OK"); this.okBtn.addActionListener(this); this.someBtn = new JButton("Something"); this.someBtn.addActionListener(this); // . . . etc. } private JMenuBar buildMenu( ) { JMenuBar result = new JMenuBar( ); // . . . add items and menus return result; } private void handleOKBtn( ) { // handler code } private void handleSomeBtn( ) { // handler code } }
In this code, the attributes are all private
.
There are also four private
helper methods:
handleSomeBtn( )
, handleOKBtn(
)
, buildContents( )
, and
buildMenu( )
. Everything in this class is okay
until someone wants to modify the class. For example, what if I only
want to change the functionality of the handleOKbtn(
)
method and reuse the rest of the class? In this case, I
would basically have to reimplement the entire dialog. Accessing the
button instance itself is impossible, so I wouldn’t
be able to rebuild the actionPerfomed( )
method.
Furthermore, since the helper method is private, I
can’t access that either. Time to reinvent the
wheel.
On the other hand, if all those helper methods were
protected
instead of private
, I
would be able to simply redefine the meaning of the
handleOKBtn( )
helper method and reuse the entire
class.
When developing classes that others will use, you can never be sure
what they will want to do to the class. But if your goal is to
promote reuse, making the helper methods protected
allows the users to extend the class. Also, since
protected
blocks users attempting to use the
method directly, you won’t be giving up any security
on that front. On the other hand, those inheriting from your class
will have access to these helper methods. However, the general
assumption you should be making is that people extending the class
generally know what they are doing. Even if they
don’t, the worst they can do is break their derived
class.
Tip
I came hard up against this problem recently when trying to extend
the Introspector
class to perform some
functionality. I merely wanted to redefine one method. However,
because that method was private
, and
Introspector
is implicitly
final
(which we will discuss in the next chapter),
I couldn’t do it.
In the end, you are better off using private
for
attributes—public
for public methods, and
protected
for helper methods. This gives your
class maximum reusability without breaking encapsulation.
Friends Allow Unrestricted Access
Most access permissions are clearly laid out in the class file. However, many Java programmers don’t understand the access permissions as they are related to instances of the same class. I call these instances friend instances.
In real life, friends don’t allow unrestricted access. You can rarely borrow a friend’s car without asking. Java is much more friendly and trusting. In Java, instances of the same class are friends and give unrestricted access to all of their properties and methods. This unlimited access is a serious hazard that must be carefully avoided while making solid code. The class in Example 1-10 shows how friend instance access works.
package oreilly.hcj.review; public class FriendAccess { private int value; public FriendAccess(final int value) { setValue(value); } public void setValue(final int value) { this.value = value; } public int getValue( ) { return value; } public void someMethod(final FriendAccess obj) { if ((obj.value == 5) && (this.value == 5)) { obj.value = 25; // <= Ouch, this works and bypasses the setter. } } }
In someMethod
, different instances of the same
class have complete access to each other. This can become a problem
when you modify the class a little bit. Hack on the class until you
end up with this variation, which demonstrates the dangers of friend
access:
package oreilly.hcj.review; public class FriendAccess { private int value; public FriendAccess(final int value) { setValue(value); } public void setValue(final int value) {if (value > 10) {
throw new IllegalArgumentException( );
}
this.value = value; } public int getValue( ) { return value; } public void someMethod(final FriendAccess obj) { if ((obj.value == 5) && (this.value == 5)) { obj.value = 25; } } }
In this variation, the range-checking code that is emphasized in the
setValue( )
method was added. Now, if you look
back at someMethod( )
, you see that the method
body sets the property value
of the instance
passed and completely bypasses the setter. The problem is that you
set value
to something that the setter would have
rejected as illegal. However, since you have access to the variables
directly, you can make the change and get away with it. Later, when
some other class uses the object, expecting its value to be less than
or equal to 10, your code will explode. Fortunately, you can easily
block this with another edit to the code:
package oreilly.hcj.review;
public class FriendAccess {
private int value;
public FriendAccess(final int value) {
setValue(value);
}
public void setValue(final int value) {
if (value > 10) {
throw new IllegalArgumentException( );
}
this.value = value;
}
public int getValue( ) {
return value;
}
public void someMethod(final FriendAccess obj) {
if ((obj.value == 5) && (this.value == 5)) {
obj.setValue(25); // <= IllegalArgumentException
}
}
}
If you write your class this way, you generate a lot of overhead due
to the call to the setter. However, you also reap the benefits of
having your setters work and your debugging time massively reduced.
The bug in the above class should take any competent programmer about
10 seconds to fix once he sees the
RuntimeException
.
Directly setting member variables of a different instance is a problem waiting to happen and should be avoided. The compiler won’t stop you from doing it—then again, a gun won’t stop you from shooting yourself in the foot either.
In fact, I wouldn’t even advise setting properties directly in the same instance. Properties that use setters and getters are special little creatures with their own needs. Many of the setters perform logic checks or do other tasks that you may miss if you set them directly in other utility methods. (See Example 1-11.)
package oreilly.hcj.review; public class FriendBean extends MutableObject { private int value; public FriendBean( ) { super( ); } public void setValue(final int value) { if (value > 10) { throw new IllegalArgumentException( ); } int oldValue = this.value; this.value = value; propertyChangeSupport.firePropertyChange("value", oldValue, value); } public int getValue( ) { return value; } public void someMethod( ) { if (Calendar.getInstance( ).get(Calendar.DAY_OF_WEEK) == Calendar.THURSDAY) { this.value = 25; } } }
One problem with this code is that someMethod( )
sets the property value
directly while bypassing
the setter and the property change event. If any GUI objects are
registered as property change listeners, they won’t
know about the change and will display stale data. To fix this, you
could potentially alter the method so that the method fires the
property change event.
This will work, but, of course, you will have to replicate all of the
other logic in the setter method whenever you set the value in a
utility method. To make matters worse, whenever you add new logic to
the setter, you must also remember to add the same logic to
someMethod( )
. This would be extremely bad
practice in object-oriented, or even procedural, software
engineering. If you use the setter for the property, as the variation
below shows, your life will be much easier:
package oreilly.hcj.review; public class FriendBean extends MutableObject { private int value; public FriendBean( ) { super( ); } public void setValue(final int value) { if (value > 10) { throw new IllegalArgumentException( ); } int oldValue = this.value; this.value = value; propertyChangeSupport.firePropertyChange("value", oldValue, value); } public int getValue( ) { return value; } public void someBetterMethod( ) { if (Calendar.getInstance( ).get(Calendar.DAY_OF_WEEK) == Calendar.THURSDAY) { this.setValue(25); } } }
In this variation of your class, you reuse the functionality of the setter by calling the setter whenever you want to change the property’s value. This approach is far easier to maintain and to change if the need arises; you have to change only the setter and not any other code. Overall, you should not be setting the value of properties, even in the same instance, without using the setters in the class. In fact, if a property has a setter, that setter should be the only thing that ever alters that property. In addition to preventing possible bugs, this technique implements proper encapsulation.
Common Mistakes
There are a certain group of mistakes in Java programming that are made over and over again at hundreds of companies throughout the world. Knowing how to avoid these mistakes will make you stand out from the crowd and look like you actually know what you are doing.
System Streams
The Java System
streams represent the ability to write
to the console or to read from it. When you invoke a method such as
printStackTrace( )
with no arguments, its output
is written to the default System
stream, which is
usually the console that started the program. However, these streams
can cause problems in your code. Consider the following from a
hypothetical GUI:
public void someMethod( ) { try { // do a whole bunch of stuff } (catch Exception ex) { ex.printStackTrace( ); throw new MyApplicationException( ); } }
To debug this GUI, you print the stack trace if something goes wrong. The problem is that the code will print the stack trace to the console window, which may be hidden, or even running on another computer.
Printing to the console window is iffy, at best, in enterprise Java. In fact, there are times when you cannot use the console at all, such as when you write EJB code. At other times, you may be writing a library for others to use, and not have any idea of what the runtime environment is. Therefore, since one of your prime goals should be to promote reusability, you cannot count on the console always being around. The solution to the problem is to keep throwing those exceptions.
In JDK 1.4, there is a new facility that will tell you if one
exception caused another. This is called the Chained
Exception Facility
. In short, if you throw an exception
inside of a catch
block, the virtual machine will
note the exception that you are throwing, along with the exception
and stack trace that caused you to enter the catch block in the first
place. For more information on this facility, consult the JDK
documentation at http://java.sun.com/j2se/1.4.1/docs/relnotes/features.html#chained-exceptions.
The basics of using the chained exception facility are illustrated in
this code block:
public void someMethod( ) { try { // do a whole bunch of stuff } (catch IllegalAccessException ex) { throw new MyApplicationException( ); } }
This version of the method takes advantage of chained exceptions by
responding to the initial IllegalAccessException
by throwing a new exception
(MyApplicationException
). The exceptions will keep
propagating through the application until some code decides to catch
them. When you finally do print the trace (presumably through a GUI,
rather than a console window!), you will see that
MyApplicationException
was caused by
IllegalAccessException
, and the correct stack
trace will be indicated.
Feel free to use the System
streams in
main( )
methods and in other classes in which you
control their runtime environment. While you
wouldn’t want to use it in your
application’s data model classes, it may be
acceptable for a GUI’s frame window class.
When logging errors and debugging traces are inside a program or library, a much better solution is to use a logging package, which is much more robust. The Log4J package from Jakarta (http://jakarta.apache.org/log4j/docs/index.html) will allow you to print your exception messages in a configurable way, as shown in Example 1-12.
import org.apache.log4j.Logger class SomeClass { /** Logger for this class. */ private final static Logger LOGGER = Logger.getLogger(SomeClass.class); public void someMethod( ) { try { // do a whole bunch of stuff } (catch Exception ex) { LOGGER.error("Failure in SomeMethod", ex); throw new MyApplicationException( ); } } }
The benefit to the altered approach shown here is that you can route
the output to whichever stream you desire. You can send the output to
an XML file, relay it to another application via JMS, create an email
to the system administrator, or even dump the information to
/dev/null (though there’s
really no good reason to ever do this!). Using a logging package
gives you far more flexibility than Java’s
System
streams could ever provide.
Ultimately, using the System
streams is okay only
if you are writing a console-based application; for error handling, I
wouldn’t advise them at all. Also, tools and
libraries should never write directly to System
streams. Instead, they should pass errors and exceptions up to
application-specific code that will do the logging for them.
System.exit( )
Every now and then you will encounter a third-party library that has code such as the following:
if (someSeriousErrorCondition) { System.exit(-1); }
This code looks really benign, but watch out! It’s a wolf in sheep’s clothing. The problem here is that if you are a user of this library, you may not want the entire application to close because of this error. For example, if you are using this library as a part of a plug-in to another product, you should have only the plug-in crash and not the entire tool platform.
However, System.exit( )
crashes the entire
application in an exceedingly brutal and bloody fashion. Just imagine
the look of surprise, consternation, and growing anger on the face of
your users when they try to run the plug-in, and their application
simply exits without bothering to save two hours’
worth of data. Although it sounds funny, it could lead to sore feet
from looking for another job.
If you use System.exit( )
at all, your best bet is
to use it only in the main method. If you ever
write a library and embed System.exit( )
into it,
you probably deserve any resulting physical violence.
Tip
If you think this sort of thing doesn’t happen in
real life, you are in for a brutal surprise. While developing a
scripting support plug-in for Eclipse, I found that the Jython
libraries embed System.exit( )
in their code. This
resulted in a very long debugging session in which I tried to figure
out what the heck I did to get Eclipse to simply die. One more bug
report filed; at least I’m an expert at Bugzilla
now.
Default Execution
One common mistake I see developers make is shown in the following code:
if (source == yesBtn) { processYesBtn( ); } else if (source == noBtn) { processNoBtn( ); } else { processCancelBtn( ); }
In this code, the developer has three buttons in his panel. The
if
structure is designed to process the buttons if
the user clicks on them. If the user clicks on
yesBtn
, then the first branch will be executed; if
the user clicks on noBtn
, then the second branch
will be executed. Otherwise, the user must have
clicked on cancelBtn
.
However, there is one small problem. A junior programmer added a new button and registered the panel as an action listener. Unfortunately, he forgot to add handler code for the button. Now, when the user presses Load Favorites, the dialog simply doesn’t work. The user subsequently files a bug report that reads something like, “When I try to load favorites, it doesn’t work.” However, your JUnit test works fine (because you don’t depend on the action handling).
Now you get to spend 12 hours stepping through all 2,000 lines of code related to loading favorites. Eventually, you will detect the missing handler and feel an irresistible compulsion to toss your computer out the window.
Close that window and implement a coding standard instead. Change
your if
statements to look like the following:
if (source == yesBtn) { processYesBtn( ); } else if (source == noBtn) { processNoBtn( ); } elseif (source == cancelBtn)
{ processCancelBtn( );} else {
assert false : source.toString( );
}
Those of you who studied the Section 1.2.5 probably saw this
coming. The point I am trying to make here is to never use
else
clauses or default
blocks
in switch
statements to perform specific tasks
without checking your assumptions. If your
if
-then
-else
statement or switch
statement is dealing with an
enumerated set of values, go ahead and enumerate them; in your
default
or final else
clause,
throw an error.
Get Hardcore Java now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.