Before C was tamed into ANSI C, we had a little joke: my code compiles, so it should run! (Ha ha!).
This was funny only if you understood C, because at that time the C compiler would accept just about anything C was truly a portable assembly language, created to see if it was possible to develop a portable operating system (Unix) that could be moved from one machine architecture to another without rewriting it from scratch in the new machines assembly language. So C was actually created as a side effect of building Unix, and not as a general-purpose programming language. Comment
Because C was targeted at programmers who wrote operating systems in assembly language, it was assumed that those programmers knew what they were doing and didnt need safety nets. For example, assembly-language programmers didnt need the compiler to check argument types and usage, and if they decided to use a data type in a different way than it was originally intended, they certainly must have good reason to do so, and the compiler didnt get in the way. Thus, getting your pre-ANSI C program to compile was only the first step in the long process of developing a bug-free program. Comment
The development of ANSI C along with stronger rules about what the compiler would accept came after lots of people used C for projects other than writing operating systems, and after the appearance of C++ which greatly improved your chances of having a program run decently once it compiled. Much of this improvement came through strong static type checking: strong, because the compiler prevented you from abusing the type, static because ANSI C and C++ perform type checking at compile time. Comment
To many people (myself included), the improvement was so dramatic that it appeared that strong static type checking was the answer to a large portion of our problems. Indeed, one of the motivations for Java was that C++s type checking wasnt strong enough (primarily because C++ had to be backward-compatible with C, and so was chained with its limitations). Thus Java has gone even further to take advantage of the benefits of type checking, and since Java has language-checking mechanisms that exist at run-time (C++ doesnt; whats left at run-time is basically assembly language very fast, but with no self-awareness) it isnt restricted to only static type checking[76]. Comment
It seems, however, that language-checking mechanisms can take us only so far in our quest to develop a correctly-working program. C++ gave us programs that worked a lot sooner than C programs, but often still had problems like memory leaks and subtle, buried bugs. Java went a long way towards solving those problems, and yet its still quite possible to write a Java program containing nasty bugs. In addition (despite the amazing performance claims always touted by the flaks at Sun) all the safety nets in Java added additional overhead, so sometimes we run into the challenge of getting our Java programs to run fast enough for a particular need (although most of the time, its more important to have a working program than one that runs at a particular speed). Comment
This chapter presents tools to solve the problems that the compiler doesnt. In a sense, we are admitting that the compiler can only take us so far in the creation of robust programs, and so we are moving beyond the compiler and creating a build system and code that know more about what a program is and isnt supposed to do. Comment
One of the biggest steps forward is the incorporation of automated unit testing, which doesnt just mean writing tests, but incorporating those tests into a build system that not only compiles your code but runs the tests every single time, as if the tests were part of the compilation process (youll soon start relying upon them as if they are). For this book, a custom testing system was developed to ensure the correctness of the program output (and to display the output directly in the code listing), but the defacto standard JUnit testing system will also be used when appropriate. To make sure that testing is automatic, tests are run as part of the build process using Ant, an open-source tool that has also become a defacto standard in Java development, and CVS, another open-source tool that maintains a repository containing all your source code for a particular project. Comment
JDK 1.4 introduced an assertion mechanism to aid in the verification of code at run time. One of the more compelling uses of assertions is Design by Contract (DBC), a formalized way to describe the correctness of a class. In conjunction with automated testing, DBC can be a powerful tool. Comment
Sometimes unit testing isnt enough, and you need to track down problems in a program that runs, but doesnt run right. In JDK 1.4, the logging API was introduced to allow you to easily report information about your program. This is a significant improvement over adding and removing println() statements in order to track down a problem, and this section will go into enough detail to give you a thorough grounding in this API. This chapter also provides an introduction to debugging, showing the information a typical debugger can provide to aid you in the discovery of subtle problems. Finally, youll learn about profiling and how to discover the bottlenecks that cause your program to run too slowly. Comment
A recent realization in programming practice is the dramatic value of unit testing. This is the process of building integrated tests into all the code that you create, and running those tests every time you do a build. That way, the build process can check for more than just syntax errors, since you teach it how to check for semantic errors as well. C-style programming languages, and C++ in particular, have typically valued performance over programming safety. The reason that developing programs in Java is so much faster than in C++ (roughly twice as fast, by most accounts) is because of Javas safety net: features like garbage collection and improved type checking. By integrating unit testing into your build process, you can extend this safety net, resulting in faster development. You can also be bolder in the changes that you make, more easily refactor your code when you discover design or implementation flaws, and in general produce a better product, faster. Comment
The effect of unit testing on development is so significant that it is used throughout this book, not only to validate the code in the book but also to display the expected output. My own experience with unit testing began when I realized that, to guarantee the correctness of code in a book, every program in that book must be automatically extracted and organized into a source tree, along with an appropriate build system. The build system used in this book is Ant (described later in this chapter), and with it in place you can just type ant to build all the code for the book. The effect of the automatic extraction and compilation process on the code quality of the book was so immediate and dramatic that it soon became (in my mind) a requisite for any programming book how can you trust code that you didnt compile? I also discovered that if I wanted to make sweeping changes, I could do so using search-and-replace throughout the book or just by bashing the code around. I knew that if I introduced a flaw, the code extractor and the build system would flush it out. Comment
As programs became more complex, however, I also found that there was a serious hole in my system. Being able to successfully compile programs is clearly an important first step, and for a published book it seems a fairly revolutionary one usually due to the pressures of publishing, its quite typical to randomly open a programming book and discover a coding flaw. However, I kept getting messages from readers reporting semantic problems in my code. These problems could only be discovered by running the code. Naturally, I understood this and took some early faltering steps towards implementing a system that would perform automatic execution tests, but I had succumbed to publishing schedules, all the while knowing that there was definitely something wrong with my process and that it would come back to bite me in the form of embarrassing bug reports (in the open source world[77], embarrassment is one of the prime motivating factors towards increasing the quality of ones code!). Comment
The other problem was that I was lacking a structure for the testing system. Eventually, I started hearing about unit testing and JUnit, which provided a basis for a testing structure. I found the initial versions of JUnit to be intolerable because they required the programmer to write too much code in order to create even the simplest test suite. More recent versions have reduced the code significantly by using reflection, and so are much more satisfactory. Comment
I needed to solve another problem, however, and that was to validate the output of a program, and to show the validated output in the book. I had gotten regular complaints that I didnt show enough program output in the book. My attitude was that the reader should be running the programs while reading the book, and many readers did just that and benefited from it. A hidden reason for that attitude, however, was that I didnt have a way to test that the output shown in the book was correct. From experience, I knew that over time, something would happen so that the output was no longer correct (or, I wouldnt get it right in the first place). The simple testing framework shown here not only captures the console output of the program and most programs in this book produce console output but it also compares it to the expected output which is printed in the book as part of the source-code listing, so readers can see what the output will be and also know that this output has been verified by the build process, and that they can verify it themselves. Comment
I wanted to see if the test system could be even easier and simpler to use, applying the Extreme Programming principle of do the simplest thing that could possibly work as a starting point, and then evolving the system as usage demands (In addition, I wanted to try to reduce the amount of test code, in an attempt to fit more functionality in less code for screen presentations). The result is the simple testing framework described next. Comment
The primary goal of this framework[78] is to verify the output of the examples in the book. You have already seen lines such as
static Test monitor = new Test();
at the beginning of most classes that contain a main() method. The task of the monitor object is to intercept and save a copy of standard output and standard error into a text file. This file is then used to verify the output of an example program, by comparing the contents of the file to the expected output. An alternative approach might be to save the output produced by a program into a collection, where each element represents a single output line. However, this approach introduces additional complexity it would have to keep track of the platform-specific issues such as end-of-line characters and line breaks. Sending output to a file uses the Java I/O classes and native methods to perform that work automatically. Comment
We can start by defining the exceptions that will be thrown by this test system. The first is the base class for the others, and general-purpose exception:
//: com:bruceeckel:simpletest:SimpleTestException.java package com.bruceeckel.simpletest; public class SimpleTestException extends RuntimeException { public SimpleTestException(String cname, String msg) { super("Error testing output of class " + cname + ": " + msg); } } ///:~
One particular thing that can go wrong is that the number of lines sent to the console by the program is different than the expected number of lines:
//: com:bruceeckel:simpletest:NumOfLinesException.java package com.bruceeckel.simpletest; public class NumOfLinesException extends SimpleTestException { public NumOfLinesException(String className, int exp, int out) { super(className, "num of lines of output and " + "expected output did not match expected: <" + exp + "> lines output: <" + out + "> lines)"); } } ///:~
Or, the number of lines might be correct, but one or more lines might not match:
//: com:bruceeckel:simpletest:LineMismatchException.java package com.bruceeckel.simpletest; import java.io.PrintStream; public class LineMismatchException extends SimpleTestException { public LineMismatchException(String className, int lineNum, String expected, String output) { super(className, "line " + lineNum + " of output did not match " + "expected output" + " expected: <" + expected + ">" + " output: <" + output + ">"); } } ///:~
This test system works by intercepting the console output using the TestStream class to replace the standard console output and console error:
//: com:bruceeckel:simpletest:TestStream.java // Simple utility for testing program output. Intercepts // System.out to print both to the console and a buffer. package com.bruceeckel.simpletest; import java.io.*; import java.util.*; import java.util.regex.*; public class TestStream extends PrintStream { private PrintStream console = System.out, err = System.err, fout; // To store lines sent to System.out or err private InputStream stdin; private String className; public TestStream(String className) { super(System.out); System.setOut(this); System.setErr(this); stdin = System.in; // Save to restore in cleanup() this.className = className; // Replace the default version with one that // automatically produces input on demand: System.setIn(new BufferedInputStream(new InputStream(){ char[] input = ("test" + System.getProperty("line.separator")).toCharArray(); int index = 0; public int read() { return (int) input[index = (index + 1) % input.length]; } })); openOutputFile(); } public PrintStream getConsole() { return console; } public void cleanup() { System.setOut(console); System.setErr(err); System.setIn(stdin); } // This will write over an old Output.txt file: public void openOutputFile() { try { fout = new PrintStream(new FileOutputStream( new File(className + "Output.txt"))); } catch (FileNotFoundException e) { throw new RuntimeException(e); } } // Override all possible print/println methods to send // intercepted console output to both the console and // the Output.txt file: public synchronized void print(boolean b) { console.print(b); fout.print(b); } public synchronized void print(char c) { console.print(c); fout.print(c); } public synchronized void print(int i) { console.print(i); fout.print(i); } public synchronized void print(long l) { console.print(l); fout.print(l); } public synchronized void print(float f) { console.print(f); fout.print(f); } public synchronized void print(double d) { console.print(d); fout.print(d); } public synchronized void print(char[] s) { console.print(s); fout.print(s); } public synchronized void print(String s) { console.print(s); fout.print(s); } public synchronized void print(Object obj) { console.print(obj); fout.print(obj); } public synchronized void println() { console.println(); fout.println(); } public synchronized void println(boolean x) { console.println(x); fout.println(x); } public synchronized void println(char x) { console.println(x); fout.println(x); } public synchronized void println(int x) { console.println(x); fout.println(x); } public synchronized void println(long x) { console.println(x); fout.println(x); } public synchronized void println(float x) { console.println(x); fout.println(x); } public synchronized void println(double x) { console.println(x); fout.println(x); } public synchronized void println(char[] x) { console.println(x); fout.println(x); } public synchronized void println(String x) { console.println(x); fout.println(x); } public synchronized void println(Object x) { console.println(x); fout.println(x); } public synchronized void write( byte[] buf, int off, int len) { console.write(buf, off, len); fout.write(buf, off, len); } public synchronized void write(int b) { console.write(b); fout.write(b); } } ///:~
The constructor for TestStream, after calling the constructor for the base class, first saves references to standard output and standard error, and then redirects both streams to the TestStream object. The static methods setOut() and setErr() both take a PrintStream argument. System.out and System.err references are unplugged from their normal object and instead are plugged into the TestStream object, so TestStream must also be a PrintStream (or equivalently, something inherited from PrintStream). The original standard output PrintStream reference is captured in the console reference inside TestStream, and every time console output is intercepted, it is sent to the original console as well as to an output file. The cleanup() method is used to set standard I/O references back to their original objects when TestStream is finished with them.Comment
To provide for the automatic testing of examples that require user input from the console, the constructor redirects calls to standard input. The current standard input is stored in a reference so that the cleanup() method can restore it to its original state. Using System.setIn(), an anonymous inner class is set to handle any requests for input by the program under test. The read() method of this inner class produces the letters test followed by a newline. Comment
TestStream overrides a variety of PrintStream print() and println() methods for each type. Each of these functions writes both to the standard output and to an output file. The expect() method can then be used to test whether output produced by a program matches the expected output provided as argument to expect(). Comment
These are used in the Test class:
//: com:bruceeckel:simpletest:Test.java // Simple utility for testing program output. Intercepts // System.out to print both to the console and a buffer. package com.bruceeckel.simpletest; import java.io.*; import java.util.*; import java.util.regex.*; public class Test { // Bit-shifted so they can be added together: public static final int DELAY_SHORT = 1, DELAY_MEDIUM = 1 << 1, DELAY_LONG = 1 << 2, DELAY_VERY_LONG = 1 << 3, EXACT = 1 << 4, // Lines must match exactly AT_LEAST = 1 << 5, // Must be at least these lines IGNORE_ORDER = 1 << 6, // Ignore line order // To detect if any delay bits are set: DELAY_MASK = DELAY_SHORT + DELAY_MEDIUM + DELAY_LONG + DELAY_VERY_LONG, DELAY_BASE = 1000; // Sleep multiplier // Discover the name of the class this // object was created within: String className = new Throwable().getStackTrace()[1].getClassName(); private TestStream testStream= new TestStream(className); public static List fileToList(String fname) { boolean isOutput = fname.endsWith(".txt")? true :false; ArrayList list = new ArrayList(); try { BufferedReader in = new BufferedReader(new FileReader(fname)); try { String line; while ((line = in.readLine()) != null) { if (isOutput) list.add(line); else list.add(new TestExpression(line)); } } finally { in.close(); } } catch (IOException e) { throw new RuntimeException(e); } return list; } public static List arrayToList(Object[] array) { List l = new ArrayList(); for (int i = 0; i < array.length; i++) { if (array[i] instanceof TestExpression) { TestExpression re = (TestExpression) array[i]; for (int j = 0; j < re.getNumber(); j++) l.add(re); } else l.add(new TestExpression(array[i].toString())); } return l; } public void expect(Object[] exp, int flags) { // If any delay bits are set, sleep: if ((flags & DELAY_MASK) != 0) try { Thread.currentThread().sleep( (flags & DELAY_MASK) * DELAY_BASE); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (this) { List output = fileToList(className + "Output.txt"); if ((flags & IGNORE_ORDER) == IGNORE_ORDER) OutputVerifier.verifyIgnoreOrder(className, output, exp); else if ((flags & AT_LEAST) == AT_LEAST) OutputVerifier.verifyAtLeast(className, output, arrayToList(exp)); else OutputVerifier.verify(className, output, arrayToList(exp), testStream.getConsole()); } //Clean up the output file - see Detergent.java testStream.openOutputFile(); } public void expect(Object[] expected) { expect(expected, EXACT); } public void expect(Object[] expectFirst, String fname, int flags) { List expected = fileToList(fname); for (int i = 0; i < expectFirst.length; i++) expected.add(i, expectFirst[i]); expect(expected.toArray(), flags); } public void expect(Object[] expectFirst, String fname) { expect(expectFirst, fname, EXACT); } public void expect(String fname) { expect(new Object[] {}, fname, EXACT); } public void expect(String fname, int flags) { expect(new Object[] {}, fname, flags); } } ///:~
There are several overloaded versions of expect() provided for convenience (so the client programmer can, for example, provide the name of the file containing the expected output instead of an array of expected output lines). These overloaded methods all call the main expect() method, which takes as arguments an array of Objects containing expected output lines and an int containing various flags. Flags are implemented using bit shifting, with each bit corresponding to a particular flag as defined at the beginning of Test.java. Comment
The expect() method first inspects the flags argument to see if it should delay processing to allow a slow program to catch up. It then calls a static method fileToList(), which converts the contents of the output file produced by a program into a List. The fileToList() method also wraps each String object in an OutputLine object; the reason for this will become clear in a second. Notice that this section of code must be synchronized, since otherwise it would be possible for multiple threads from the client program to read and write the output file at the same time. Finally, the expect() method calls the appropriate verify() method based on the flags argument. There are three versions of verify(): verify(), verifyIgnoreOrder(), and verifyAtLeast(), corresponding to EXACT, IGNORE_ORDER, and AT_LEAST modes, respectively. Each of these use the verifyLength() method to test the number of lines of output. EXACT mode requires that both output and expected output arrays be the same size, and that each output line is equal to the corresponding line in the expected output array. IGNORE_ORDER still requires that both arrays be the same size, but the actual order of appearance of the lines is disregarded (the two output arrays must be permutations of one another). IGNORE_ORDER mode is used for testing threading examples in which, due to non-deterministic scheduling of threads by the JVM, it is possible that the sequence of output lines produced by a program cannot be predicted. AT_LEAST mode does not require the two arrays to be the same size, but each line of expected output must be contained in the actual output produced by a program, regardless of order. This feature is particularly useful for testing program examples which contain output lines that may or may not be printed, as is the case with most of the examples dealing with garbage collection. Notice that the three modes are canonical; that is, if a test passes in IGNORE_ORDER mode, then it will also pass in AT_LEAST mode, and if it passes in EXACT mode, it will also pass in the other two modes. The verify methods test whether the output produced by a program matches the expected output as specified by the particular mode. If this is not the case, the verify methods raise an exception that aborts the build process.Comment
Objects in the expect() array can be either Strings or TestExpressions, which can encapsulate a regular expression. Regular expressions are a powerful feature of Java 1.4 that allows a set of strings to be specified by simply defining a pattern that such strings should match. The TestExpression class encapsulates a String representing a particular regular expression. Comment
//: com:bruceeckel:simpletest:TestExpression.java // Regular expression for testing program output lines package com.bruceeckel.simpletest; import java.util.regex.*; public class TestExpression implements Comparable { private Pattern p; private String expression; private boolean isRegEx; // Default to only one instance of this expression: private int duplicates = 1; public TestExpression(String s) { this.expression = s; if (expression.startsWith("%%")) { this.isRegEx = true; this.p = Pattern.compile(expression.substring(2)); } } // For duplicate instances: public TestExpression(String s, int duplicates) { this(s); this.duplicates = duplicates; } public String toString() { if (isRegEx) return p.pattern(); return expression; } public boolean equals(Object obj) { if(this == obj) return true; if(isRegEx) return (compareTo(obj) == 0); return expression.trim().equals(obj.toString().trim()); } public int compareTo(Object obj) { if((isRegEx) && (p.matcher(obj.toString()).matches())) return 0; return expression.trim().compareTo(obj.toString().trim()); } public int getNumber() { return duplicates; } public String getExpression() { return expression;} public boolean isRegEx() { return isRegEx; } } ///:~
TestExpression can distinguish regular expression patterns from String literals. Regular expressions are particularly useful for testing examples in the book that make use of a random number generator to produce different output values each time they are executed. Comment
The second constructor allows multiple identical expression lines to be wrapped in a single object for convenience. Comment
Finally, here is the class containing the verify methods:
//: com:bruceeckel:simpletest:OutputVerifier.java package com.bruceeckel.simpletest; import java.util.*; import java.io.PrintStream; public class OutputVerifier { public static void verifyIgnoreOrder(String className, List output, Object[] expected) { verifyLength(className, expected.length, output.size(), Test.EXACT); if (!(expected instanceof String[])) throw new RuntimeException("IGNORE_ORDER mode " + "only works with String objects"); String[] out = new String[output.size()]; Iterator it = output.iterator(); for (int i = 0; i < out.length; i++) out[i] = it.next().toString(); Arrays.sort(out); Arrays.sort(expected); int i =0; if (!Arrays.equals(expected, out)) { while (expected[i].equals(out[i])) {i++;} throw new SimpleTestException(className, ((String) out[i]).compareTo(expected[i]) < 0 ? "output: <" + out[i] + ">" : "expected: <" + expected[i] + ">"); } } public static void verify(String className, List output, List expected, PrintStream console) { verifyLength(className, output.size(), expected.size(), Test.EXACT); if (!expected.equals(output)) { //find the line of mismatch ListIterator it1 = expected.listIterator(); ListIterator it2 = output.listIterator(); while (it1.hasNext() && it2.hasNext() && it1.next().equals(it2.next())); throw new LineMismatchException(className, it1.nextIndex(), it1.previous().toString(), it2.previous().toString()); } } private static void verifyLength(String className, int output, int expected, int compare) { if ((compare == Test.EXACT && expected != output) || (compare == Test.AT_LEAST && output < expected)) throw new NumOfLinesException(className, expected, output); } public static void verifyAtLeast(String className, List output, List expected) { verifyLength(className, output.size(), expected.size(), Test.AT_LEAST); if (!output.containsAll(expected)) { ListIterator it = expected.listIterator(); while (output.contains(it.next())) {} throw new SimpleTestException(className, "expected: <" + it.previous().toString() + ">"); } } } ///:~
Notice how simple the implementation of the verify() methods is. verify(), for example, simply calls the equals() method provided by the List class, and verifyAtLeast() calls List.containsAll(). Remember that the two output Lists can contain both OutputLine or RegularExpression objects. The reason for wrapping the simple String object in OutputLines should now become clear: this approach allows us to override the equals() method, which is necessary in order to take advantage of the Java Collections API. Comment
Although the testing framework just described allows you to simply and easily verify program output, in some cases you may want to perform more extensive functionality testing on a program. JUnit, available at http://www.junit.org, is a quickly emerging standard for writing repeatable tests for Java programs, and provides both simple and complex testing. Comment
The original JUnit was presumably based on JDK 1.0 and thus could not make use of Javas reflection facilities. As a result, writing unit tests with the old JUnit was a rather busy and wordy activity, and I found the design to be unpleasant. Because of this, I wrote my own unit testing framework for Java[79], going to the other extreme and doing the simplest thing that could possibly work[80]. Since then, JUnit has been modified and uses reflection to greatly simplify the process of writing unit test code. Although you still have the option of writing code the old way with test suites and all the other complex details, I believe that in the great majority of cases you can follow the simple approach shown here (and make your life more pleasant). Comment
In the simplest approach to using JUnit, you put all your tests in a subclass of TestCase. Each test must be public, take no arguments, return void, and have a method name beginning with the word test. JUnits reflection will identify these methods as individual tests, and set up and run them one at a time, taking measures to avoid side effects between the tests. Comment
Traditionally, the setUp() method creates and initializes a common set of objects which will be used in all the tests; however, you can also just put all such initialization in the constructor for the test class. JUnit creates an object for each test to ensure there will be no side effects between test runs. However, all the objects for all the tests are created at once (rather than creating the object right before the test) so the only difference between using setUp() and the constructor is that setUp() is called directly before the test. In most situations this will not be an issue, and you can use the constructor approach for simplicity. Comment
If you need to perform any cleanup after each test (if you modify any statics which need to be restored, open files that need to be closed, open network connections, etc.), you write a tearDown() method. This is also optional. Comment
The following example uses this simple approach to create JUnit tests that exercise the standard Java ArrayList class. To trace how JUnit creates and cleans up its test objects, CountedList is inherited from ArrayList and tracking information is added: Comment
//: X1:JUnitDemo.java // Simple use of JUnit to test ArrayList // {Depends: junit.jar} import java.util.*; import junit.framework.*; // So we can see the list objects being created, // and keep track of when they are cleaned up: class CountedList extends ArrayList { private static int counter = 0; private int id = counter++; public CountedList() { System.out.println("CountedList #" + id); } public int getId() { return id; } } public class JUnitDemo extends TestCase { static com.bruceeckel.simpletest.Test monitor = new com.bruceeckel.simpletest.Test(); private CountedList list = new CountedList(); // You can use the constructor instead of setUp(): public JUnitDemo(String name) { super(name); for(int i = 0; i < 3; i++) list.add("" + i); } // Thus, setUp() is optional, but is run right // before the test: protected void setUp() { System.out.println("Set up for " + list.getId()); } // tearDown() is also optional, and is called after // each test. setUp() and tearDown() can be either // protected or public: public void tearDown() { System.out.println("Tearing down " + list.getId()); } // All tests have method names beginning with "test": public void testInsert() { System.out.println("Running testInsert()"); assertEquals(list.size(), 3); list.add(1, "Insert"); assertEquals(list.size(), 4); assertEquals(list.get(1), "Insert"); } public void testReplace() { System.out.println("Running testReplace()"); assertEquals(list.size(), 3); list.set(1, "Replace"); assertEquals(list.size(), 3); assertEquals(list.get(1), "Replace"); } // A "helper" method to reduce code duplication. As long // as the name doesn't start with "test," it will not // be automatically executed by JUnit. private void compare(ArrayList lst, String[] strs) { Object[] array = lst.toArray(); assertTrue("Arrays not the same length", array.length == strs.length); for(int i = 0; i < array.length; i++) assertEquals(strs[i], (String)array[i]); } public void testOrder() { System.out.println("Running testOrder()"); compare(list, new String[] { "0", "1", "2" }); } public void testRemove() { System.out.println("Running testRemove()"); assertEquals(list.size(), 3); list.remove(1); assertEquals(list.size(), 2); compare(list, new String[] { "0", "2" }); } public void testAddAll() { System.out.println("Running testAddAll()"); list.addAll(Arrays.asList(new Object[] { "An", "African", "Swallow"})); assertEquals(list.size(), 6); compare(list, new String[] { "0", "1", "2", "An", "African", "Swallow" }); } public static void main(String[] args) { // Invoke JUnit on the class: junit.textui.TestRunner.run(JUnitDemo.class); monitor.expect(new Object[] { "CountedList #0", "CountedList #1", "CountedList #2", "CountedList #3", "CountedList #4", // '.' indicates the beginning of each test: ".Set up for 0", "Running testInsert()", "Tearing down 0", ".Set up for 1", "Running testReplace()", "Tearing down 1", ".Set up for 2", "Running testOrder()", "Tearing down 2", ".Set up for 3", "Running testRemove()", "Tearing down 3", ".Set up for 4", "Running testAddAll()", "Tearing down 4", "", "%%Time: .*", "", "OK (5 tests)", "", }); } } ///:~
To set up unit testing, you must only import junit.framework.* and extend TestCase, as JUnitDemo does. In addition, you must create a constructor that takes a String argument and passes it to its super constructor. Comment
For each test, a new JUnitDemo object will be created, and thus all the non-static members will also be created. This means a new CountedList object (list) will be created and initialized for each test, since it is a field of JUnitDemo. In addition, the constructor will be called for each test, so list will be initialized with the strings 0, 1 and 2 before each test is run. Comment
To observe the behavior of setUp() and tearDown(), these methods are created to display information about the test thats being initialized or cleaned up. Note that the base-class methods are protected, so the overridden methods may be either protected or public. Comment
testInsert() and testReplace() demonstrate typical test methods, since they follow the required signature and naming convention. JUnit discovers these methods using reflection and runs each one as a test. Inside the methods, you perform any desired operations, and use JUnit assertion methods (all which start with the name assert) to verify the correctness of your tests (the full range of assert statements can be found in the JUnit Javadocs for junit.framework.Assert). If the assertion fails, the expression and values that caused the failure will be displayed. This is usually enough, but you can also use the overloaded version of each of the JUnit assertion statements and include a String that will be printed if the assertion fails. Comment
The assertion statements are not required; you can also just run the test without assertions and consider it a success if no exceptions are thrown. Comment
The compare() method is an example of a helper method that is not executed by JUnit but instead is used by other tests in the class. As long as the method name doesnt begin with test, JUnit doesnt run it or expect it to have a particular signature. Here, compare() is private to emphasize that it is only used within the test class, but it could also be public. The remaining test methods eliminate duplicate code by refactoring it into the compare() method. Comment
To execute the JUnit tests, the static method TestRunner.run() is invoked in main(). This method is handed the class that contains the collection of tests, and it automatically sets up and runs all the tests. From the expect() output, you can see that all the objects needed to run all the tests are created first, in a batch this is where the construction happens[81]. Before each test, the setUp() method is called. Then the test is run, followed by the tearDown() method. JUnit demarcates each test with a .. Comment
Although you can probably survive easily by only using the simplest approach to JUnit as shown above, JUnit was originally designed with a plethora of complicated structures. If you are curious, you can easily learn more about them, as the JUnit download from www.JUnit.org comes with documentation and tutorials. Comment
Assertions, which youve seen used in earlier examples in this book, were added to the JDK 1.4 version of Java in order to aid programmers in improving the reliability of their programs. Properly used, assertions can add to program robustness by verifying that certain conditions are satisfied during the execution of your program. For example, suppose you have a numerical field in an object that represents the month on the Julian calendar. You know that this value must always be in the range 1-12, and an assertion can be used to check this and report an error if it somehow falls outside of that range. If youre inside a method, you can check the validity of an argument with an assertion. These are important tests to make sure that your program is correct, but they cannot be performed by compile-time checking, and they do not fall into the purview of unit testing. In this section, well look at the mechanics of the assertion mechanism, and the way that you can use assertions to partially implement the design by contract concept. Comment
Since you can simulate the effect of assertions using other programming constructs, it can be argued that the whole point of adding assertions to Java is that they are easy to write. Assertion statements come in two forms: Comment
assert boolean-expression; assert boolean-expression: information-expression;
Both of these statements say I assert that the boolean-expression will produce a true value. If this is not the case, the assertion will produce an AssertionError exception. This is a Throwable subclass, and as such doesnt require an exception specification. Comment
Unfortunately, the first form of assertion does not produce any information containing the boolean-expression in the exception produced by a failed assertion (in contrast with most other languages assertion mechanisms). Heres an example showing the use of the first form: Comment
//: X1:Assert1.java // Non-informative style of assert // Compile with: javac -source 1.4 Assert1.java // {JVMArgs: -ea} // Must run with -ea // {ThrowsException} public class Assert1 { public static void main(String[] args) { assert false; } } ///:~
Assertions are turned off in JDK 1.4 by default (this is annoying, but the designers managed to convince themselves it was a good idea). To prevent compile-time errors you must compile with the flag: Comment
-source 1.4
If you dont use this flag youll get a chatty message saying that assert is a keyword in JDK 1.4 and cannot be used as an identifier anymore. Comment
If you just run the program the way you normally do, without any special assertion flags, nothing will happen. You must enable assertions when you run the program. The easiest way to do this is with the -ea flag, but you can also spell it out: -enableassertions. This will run the program and execute any assertion statements, so youll get: Comment
Exception in thread "main" java.lang.AssertionError at Assert1.main(Assert1.java:8)
You can see that the output doesnt contain much in the way of useful information. On the other hand, if you use the information-expression, youll produce a helpful message when the assertion fails. Comment
To use the second form, you provide an information-expression which will be displayed as part of the exception stack trace. This information-expression can produce any data type at all. However, the most useful information-expression will typically be a string with text that is useful to the programmer. Heres an example: Comment
//: X1:Assert2.java // Assert with an informative message // {JVMArgs: -ea} // {ThrowsException} public class Assert2 { public static void main(String[] args) { assert false: "Here's a message saying what happened"; } } ///:~
Now the output is:
Exception in thread "main" java.lang.AssertionError: Here's a message saying what happened at Assert2.main(Assert2.java:6)
Although what you see above is just a simple String object, the information-expression can produce any kind of object, so you will typically construct a more complex string containing, for example, the value(s) of objects that were involved with the failed assertion. Comment
Because the only way to see useful information from a failed assertion is to use the information-expression, that is the form that is always used in this book, and the first form will be considered to be a poor choice. Comment
You can also decide to turn assertions on and off based on class name or package name (that is, you can enable or disable assertions in an entire package). You can find the details in the JDK 1.4 documentation on assertions. This can be useful if you have a large project instrumented with assertions and you want to turn some of them off. However, logging or debugging (both described later in this chapter) are probably better tools for capturing that kind of information. This book will just turn on all assertions when necessary, and so we will ignore the fine-grained control of assertions. Comment
Theres one other way you can control assertions: programmatically, by hooking into the ClassLoader object. JDK 1.4 added several new methods to ClassLoader that allow the dynamic enabling and disabling of assertions, including setDefaultAssertionStatus(), which sets the assertion status for all the classes loaded afterwards. So you might think you could almost silently turn on all assertions like this: Comment
//: X1:LoaderAssertions.java // Using the class loader to enable assertions // Compile with: javac -source 1.4 LoaderAssertions.java // {ThrowsException} public class LoaderAssertions { public static void main(String[] args) { ClassLoader.getSystemClassLoader() .setDefaultAssertionStatus(true); new Loaded().go(); } } class Loaded { public void go() { assert false: "Loaded.go()"; } } ///:~
Although this does eliminate the need to use the -ea flag on the command line when the Java program is run, its not a complete solution because you must still compile everything with the -source 1.4 flag. It may be just as straightforward to enable assertions using command-line arguments; when delivering a standalone product you probably have to set up an execution script for the user to start the program with, anyway, in order to configure other startup parameters. Comment
It does make sense, however, to decide that you want to require assertions to be enabled when the program is run. You can accomplish this with the following static clause, placed in the main class of your system: Comment
static { boolean assertionsEnabled = false; // Note intentional side effect of assignment: assert assertionsEnabled = true; if (!assertionsEnabled) throw new RuntimeException("Assertions disabled"); }
If assertions are enabled, then the assert statement will be executed and assertionsEnabled will be set to true. The assertion will never fail, because the return value of the assignment is the assigned value. If assertions are not enabled the the assert statement will not be executed and assertionsEnabled will remain false, resulting in the exception. Comment
Design by Contract (DBC) is a concept developed by Bertrand Meyer[82], creator of the Eiffel programming language, to help in the creation of robust programs by guaranteeing that objects follow certain rules that cannot be verified by compile-time type checking. These rules are determined by the nature of the problem that is being solved, which is outside the scope of what the compiler can know about and test. Comment
Although assertions are do not directly implement DBC (as does the Eiffel language), they can be used to create an informal style of DBC programming. Comment
The fundamental idea of Design by Contract is that a clearly-specified contract exists between the supplier of a service and the consumer or client of that service. In object-oriented programming, services are usually supplied by objects, and the boundary of the object the division between the supplier and consumer is the interface of the objects class. When a client calls a particular public method, they are expecting certain behavior from that call: a state change in the object, and a predictable return value. Meyers thesis is that: Comment
Whether or not you agree that point #1 is always true, it does appear to be true for enough situations to make DBC an interesting approach (I believe that, like any solution, there are boundaries to its usefulness. But if you know these boundaries, you know when to try to apply it). In particular, a very valuable part of the design process is the expression of the DBC constraints for a particular class if you are unable to specify the constraints, you probably dont know enough about what youre trying to build. Comment
Before going into in-depth DBC facilities, consider the simplest use for assertions, which Meyer calls the check instruction. A check instruction expresses your convinction that a particular property will be satisfied at this point in your code. The idea of the check instruction is to express non-obvious conclusions in code, not only to verify the test but as documentation to future readers of the code. Comment
For example, in a chemistry process you may be titrating one clear liquid into another and when you reach a certain point everything turns blue. This is not obvious from the color of the two liquids; it is part of a complex reaction. A useful check instruction at the completion of the titration process would assert that the resulting liquid is blue. Comment
Another example is the Thread.holdsLock() method introduced in JDK 1.4. This is used for complex threading situations (such as iterating through a collection in a thread-safe way) where you must rely on the client programmer or another class in your system using the library properly, rather than on the synchronized keyword alone. To ensure that the code is properly following the dictates of your library design, you can assert that the current thread does indeed hold the lock: Comment
assert Thread.holdsLock(this); // lock-status assertion
Check instructions are a valuable addition to your code. Since assertions can be disabled, check instructions should be used whenever you have non-obvious knowledge about the state of your object or program. Comment
A precondition is a test to make sure that the client (the code calling this method) has fulfilled their part of the contract. This almost always means checking the arguments at the very beginning of a method call (before you do anything else in that method), to make sure that those arguments are appropriate for use in the method. Since you never know what a client is going to hand you, precondition checks are always a good idea. Comment
A postcondition test checks the results of what you did in the method. This code is placed at the end of the method call, before the return statement, if there is one. For long, complex methods where the result of the calculations should be verified before returning them (that is, in situations where for some reason you cannot always trust the results), postcondition checks are essential, but any time you can describe constraints on the result of the method its wise to express those constraints in code, as a postcondition. In Java these are coded as assertions, but the assertion statements will vary from one method to another. Comment
An invariant gives guarantees about the state of the object that will be maintained between method calls. However, it doesnt restrain a method from temporarily diverging from those guarantees during the execution of the method. It just says that the state information of the object will always obey these rules: Comment
In addition, the invariant is a guarantee about the state of the object after construction. Comment
According to the above description, an effective invariant would be defined as a method, probably named invariant(), which would be invoked after construction, and at the beginning and end of each method. The method could be invoked as:
assert invariant();
This way, if you chose to disable assertions for performance reasons, there would be no overhead at all. Comment
Although he emphasizes the importance of being able to express preconditions, postconditions and invariants, and the value of using these during development, Meyer admits that it is not always practical to include all DBC code in a shipping product. You may relax DBC checking based on the amount of trust you can place in the code at a particular point. Here is the order of relaxation, from safest to least safe: Comment
In no situation above should you actually remove the code that performs the checks described above as you disable the checks. If a bug is discovered, you want to be able to easily turn on all of the checks so that you can rapidly discover the problem. Comment
The following example demonstrates the potency of combining concepts from design by contract with unit testing. It shows a small first-in, first-out (FIFO) queue class which is implemented as a circular array that is, an array that is used in a circular fashion. When the end of the array is reached, the class wraps back around to the beginning. Comment
We can make a number of contractual definitions for this queue:
Here is one way you could implement these rules, using explicit method calls for each type of DBC element: Comment
//: X1:Queue.java // Demonstration of Design by Contract (DBC) combined // with white-box unit testing. // {Depends: junit.jar} import junit.framework.*; import java.util.*; public class Queue { private Object[] data; private int in = 0, // Next available storage space out = 0; // Next gettable object // Has it wrapped around the circular queue? private boolean wrapped = false; public Queue(int size) { data = new Object[size]; assert invariant(); // Must be true after construction } public boolean empty() { return !wrapped && in == out; } public boolean full() { return wrapped && in == out; } public void put(Object item) { precondition(item != null, "put() null item"); precondition(!full(), "put() into full Queue"); assert invariant(); data[in++] = item; if(in >= data.length) { in = 0; wrapped = true; } assert invariant(); } public Object get() { precondition(!empty(), "get() from empty Queue"); assert invariant(); Object returnVal = data[out]; data[out] = null; out++; if(out >= data.length) { out = 0; wrapped = false; } assert postcondition( returnVal != null, "Null item in Queue"); assert invariant(); return returnVal; } // Design-by-contract support methods: private static void precondition(boolean cond, String msg) { if(!cond) throw new RuntimeException(msg); } private static boolean postcondition(boolean cond, String msg) { if(!cond) throw new RuntimeException(msg); return true; } private boolean invariant() { // Guarantee that no null values are in the // region of 'data' that holds objects: for(int i = out; i != in; i = (i + 1) % data.length) if(data[i] == null) throw new RuntimeException("null in queue"); // Guarantee that only null values are outside the // region of 'data' that holds objects: if(full()) return true; for(int i = in; i != out; i = (i + 1) % data.length) if(data[i] != null) throw new RuntimeException( "non-null outside of queue range: " + dump()); return true; } private String dump() { return "in = " + in + ", out = " + out + ", full() = " + full() + ", empty() = " + empty() + ", queue = " + Arrays.asList(data); } // JUnit testing. // As an inner class, this has access to privates: public static class WhiteBoxTest extends TestCase { private Queue queue = new Queue(10); private int i = 0; public WhiteBoxTest(String name) { super(name); while(i < 5) // Preload with some data queue.put("" + i++); } // Support methods: private void showFullness() { assertTrue(queue.full()); assertFalse(queue.empty()); // Dump is private, white-box testing allows access: System.out.println(queue.dump()); } private void showEmptiness() { assertFalse(queue.full()); assertTrue(queue.empty()); System.out.println(queue.dump()); } public void testFull() { System.out.println("testFull"); String msg = ""; System.out.println(queue.dump()); System.out.println(queue.get()); System.out.println(queue.get()); while(!queue.full()) queue.put("" + i++); try { queue.put(""); } catch(RuntimeException e) { msg = e.getMessage(); System.out.println(msg); } assertEquals(msg, "put() into full Queue"); showFullness(); } public void testEmpty() { System.out.println("testEmpty"); String msg = ""; while(!queue.empty()) System.out.println(queue.get()); try { queue.get(); } catch(RuntimeException e) { msg = e.getMessage(); System.out.println(msg); } assertEquals(msg, "get() from empty Queue"); showEmptiness(); } public void testNullPut() { System.out.println("testNullPut"); String msg = ""; try { queue.put(null); } catch(RuntimeException e) { msg = e.getMessage(); System.out.println(msg); } assertEquals(msg, "put() null item"); } public void testCircularity() { System.out.println("testCircularity"); while(!queue.full()) queue.put("" + i++); showFullness(); // White-box testing accesses private field: assertTrue(queue.wrapped); while(!queue.empty()) System.out.println(queue.get()); showEmptiness(); while(!queue.full()) queue.put("" + i++); showFullness(); while(!queue.empty()) System.out.println(queue.get()); showEmptiness(); } } public static void main(String[] args) { junit.textui.TestRunner.run(Queue.WhiteBoxTest.class); } } ///:~
The in counter indicates the place in the array where the next object will go in, and the out counter indicates where the next object will come from. The wrapped flag shows that in has gone around the circle and is now coming up from behind out. When in and out coincide, the queue is empty (if wrapped is false) or full (if wrapped is true). Comment
You can see that the put() and get() methods call the methods precondition(), postcondition() and invariant(), which are private methods defined further down in the class. precondition() and postcondition() are helper methods designed to clarify the code. Note that precondition() returns void, because it is not used with assert. As previously noted, youll generally want to keep preconditions in your code; however, by wrapping them in a precondition() method call you have better options if you are reduced to the dire move of turning them off. Comment
postcondition() and invariant() return a Boolean value so that they can be used in assert statements. Then, if assertions are disabled for performance reasons, there will be no method calls at all. Comment
invariant() performs internal validity checks on the object. You can see that this is an expensive operation to do at both the beginning and ending of every method call, as Meyer suggests. However, its very valuable to have clearly represented in code, and it helped me get the implementation to be correct. In addition, if you make any changes to the implementation the invariant() will ensure that you havent broken the code. But you can see that it would be fairly trivial to move the invariant tests from the method calls into the unit test code. If your unit tests are reasonably thorough then you can have a reasonable level of confidence that the invariants will be respected. Comment
Notice that the dump() helper function returns a string containing all the data, rather than printing the data directly. This approach allows many more options as to how the information can be used. Comment
The TestCase subclass WhiteBoxTest is created as an inner class so that it has access to the private elements of Queue and is thus able to validate the underlying implementation, not just the behavior of the class as in a white box test. The constructor adds some data so that the Queue is partially full for each test. The support methods showFullness() and showEmptiness() are meant to be called to verify that the Queue is full or empty, respectively. Each of the four test methods ensures that a different aspect of the Queue operation functions correctly. Comment
Note that by combining design by contract with unit testing, you not only get the best of both worlds, but you also have a migration path you can move DBC tests to unit tests rather than simply disabling them, so you still have some level of testing. Comment
I began my career writing programs in assembly-language which controlled real-time devices. These programs usually fit into a single file, so when I was introduced to the make utility I wasnt too excited, because the most complex thing I had ever needed to do was run an assembler or a C compiler on a few files of code. Back then, building a project wasnt the difficult part of my task, and it wasnt too cumbersome to run everything by hand. Comment
Time passed, and two events occurred. First, I started to create more complex projects comprising many more files. Keeping track of which files needed compliation became more than I was able (or wanted) to think about. Second, because of this complexity I began to realize that no matter how simple the build process might be, if you do something more than a couple of times you begin to get sloppy, and parts of the process start to fall through the cracks. Comment
I came to realize that for a system to be built in a robust and reliable fashion, I needed to automate everything that goes into the build process. This requires some concentration up front, just like writing a program requires concentration, but the payoff is that you solve the problems once, and then you rely on your build configuration to take care of the details from then on. Its a variation of the fundamental programming principle of abstraction: we raise ourselves up from the grinding details by hiding those details inside a process, and giving that process a name. For many years, the name of that process was make. Comment
The make utility appeared along with C as a tool to create the Unix operating system. makes primary function is to compare the date of two files and to perform some operation that will bring those two files up to date with each other. The relationships between all the files in your projects and the rules necessary to bring them up to date (the rule is usually running the C/C++ compiler on a source file) are contained in a makefile. The programmer creates a makefile containing the description of how to build the system. When you want to bring the system up to date, you simply type make at the command line. To this day, installing Unix/Linux programs consists of unpacking them and typing make commands. Comment
The concept of make is clearly a good idea, and this idea proliferated to produce many versions of make. C and C++ compiler vendors typically included their own variation of make along with their compiler these variations often took liberties with what people considered to be the standard makefile rules, and so the resulting makefiles wouldnt run with each other. The problem was finally solved (as has often been the case) by a make that was, and still is, superior to all the other makes, and is also free so theres no resistance to using it[83]: GNU make. This tool has a significantly better feature set than the other versions of make, and is available on all platforms. Comment
In the previous two editions of Thinking in Java, I used makefiles to build all the code in the books source-code tree. I automatically generated these makefiles one in each directory, and a master makefile in the root directory that would call the rest using a tool that I originally wrote for Thinking in C++, in C++ (in about 2 weeks) and later rewrote in Python (in about half a day) called MakeBuilder.py[84]. It worked for both Windows and Linux/Unix, but I had to write extra code to make this happen, and I never tried it on the Macintosh. Therein lies the first problem with make: you can get it to work on multiple platforms, but its not inherently cross-platform. So for a language thats supposed to be write once, run anywhere (that is, Java), you can spend a lot of effort getting the same behavior in the build system if you use make. Comment
The rest of the problems with make can probably be summarized by saying that it is like a lot of tools developed for Unix: the person creating the tool couldnt resist the temptation to create their own language syntax, and as a result Unix is filled with tools that are all remarkably different, and equally incomprehensible. That is to say, the make syntax is quite difficult to understand in its entirety Ive been learning it for years and has lots of annoying things like its insistence on tabs instead of spaces. Comment
All that said, note that I still find GNU make indispensable for many of the projects I create. Comment
All of these issues with make irritated a Java programmer named James Duncan Davidson enough to cause him to create ant as an open-source tool that migrated to the Apache project at http://jakarta.apache.org/ant. This site contains the full download including the ant executable and documentation. Ant has grown and improved until it is now generally accepted as the defacto standard build tool for Java projects. Comment
To make ant cross-platform, the format for the project description files is XML (which youll learn about in detail in Chapter XXX). Instead of a makefile, you create a buildfile, which is named by default build.xml (this allows you to just say ant on the command line. If you name your buildfile something else you have to specify that name with a command-line flag). Comment
The only rigid requirement for your buildfile is that it be a valid XML file. Ant compensates for platform-specific issues like end-of-line characters and directory path separators. You can use tabs or spaces in the buildfile as you prefer. In addition, the syntax and tag names used in buildfiles result in readable, understandable (and thus, maintainable) code. Comment
On top of all this, ant is designed to be extensible, with a standard interface that allows you to write your own tasks, if the ones that come with ant arent enough (however, they usually are, and the arsenal is regularly expanding). Comment
Unlike make, the learning curve for ant is reasonably gentle. You dont need to know that much in order to create a buildfile that compiles Java code in a directory. Heres a very basic build.xml file, for example, from chapter 2 of this book: Comment
<?xml version="1.0"?> <project name="Thinking in Java (c02)" default="c02.run" basedir="."> <!-- build all classes in this directory --> <target name="c02.build"> <javac srcdir="${basedir}" classpath="${basedir}/.." source="1.4" /> </target> <!-- run all classes in this directory --> <target name="c02.run" depends="c02.build"> <antcall target="HelloDate.run"/> </target> <target name="HelloDate.run"> <java taskname="HelloDate" classname="HelloDate" classpath="${basedir};${basedir}/.." fork="true" failonerror="true" /> </target> <!-- delete all class files --> <target name="clean"> <delete> <fileset dir="${basedir}" includes="**/*.class"/> <fileset dir="${basedir}" includes="**/*Output.txt"/> </delete> <echo message="clean successful"/> </target> </project>
The first line states that this file conforms to version 1.0 of XML. XML looks a lot like HTML (notice the comment syntax is identical), except that you can make up your own tag names and the format must strictly conform to XML rules. For example, an opening tag like <project must either end within the tag at its closing angle brace with a slash: /> or have a matching closing tag like you see at the end of the file: </project>. Within a tag you can have attributes, but the attribute values must be surrounded in quotes. XML allows free formatting, but indentation like you see above is typical. Comment
Each buildfile can manage a single project, described by its <project> tag. The project has an optional name attribute which is used when displaying information about the build. The default attribute is required, and refers to the target that is built when you just type ant at the command line, without giving a specific target name. basedir is a directory reference that can be used in other places in the buildfile. Comment
A target has dependencies and tasks. The dependencies say which other targets must be built before this target can be built? Youll notice that the default target to build is c02.run, and the c02.run target says that it in turn depends on c02.build. Thus, the c02.build target must be executed before c02.run can be executed. Partitioning the buildfile this way not only makes it easier to understand, it also allows you choose what you want to do via the ant command line: if you say ant c02.build, then it will only compile the code, but if you say ant co2.run (or, because of the default target, just ant), then it will first make sure things have been built, and then run the examples. Comment
So, for the project to be successful, targets c02.build and c02.run must first succeed, in that order. c02.build contains a single task, which is a command that actually does the work of bringing things up to date. This task runs the javac compiler on all the Java files in this current base directory notice the ${} syntax used to produce the value of a previously-defined variable, and that the orientation of slashes in directory paths is not important, since ant compensates depending on the operating system you run it on. The classpath attribute gives a directory list to add to ants classpath, and source specifies the compiler to use (this is actually only noticed by JDK 1.4 and beyond). Note that the Java compiler is responsible for sorting out the dependencies between the classes themselves, so you dont have to explicitly state inter-file dependencies like you must with make and C/C++ (this saves a lot of effort). Comment
To run the programs in the directory (which, in this case, is only the single program HelloDate), this buildfile uses a task named antcall. This task does a recursive invocation of ant on another target, which in this case just uses java to execute the program. Note that the java task has a taskname attribute this attribute is actually available for all tasks, and is used when ant outputs logging information. Comment
As you might expect, the java tag also has options to establish the class name to be executed, and the classpath. In addition, the
fork="true" failonerror="true"
attributes tell ant to fork off a new process to run this program, and to fail the ant build if the program fails. You can look up all the different tasks and their attributes in the documentation that comes with the ant download. Comment
The last target is one thats typically found in every buildfile it allows you to say ant clean and delete all the files that have been created in order to perform this build. Whenever you create a buildfile, you should be careful to include a clean target, because youre the person who typically knows the most about what can be deleted and what should be preserved. Comment
The clean target introduces some new syntax. You can delete single items with the one-line version of this task, like this:
<delete file="${basedir}/HelloDate.class"/>
The multi-line version of the task allows you to specify a fileset, which is a more complex description of a set of files and may specify files to include and exclude using wildcards. The above filesets to delete include all files in this directory and all subdirectories that have a .class extension, and all files in the current subdirectory that end with Output.txt. Comment
The above buildfile is fairly simple; within this books source code tree (which is on the CD ROM and downloadable from www.BruceEckel.com) youll find more complex buildfiles. Also, ant is capable of doing much more that what we use for this book for the full details of its capabilities, see the documentation that comes with the ant installation. Comment
Ant comes with an extension API, so that you can create your own tasks by writing them in Java. You can find full details in the official ant documentation, and in the published books on ant. Comment
As an alternative, you can simply write a Java program and call it from ant this way you dont have to learn the extension API. For example, to compile the code in this book we need to verify that the version of Java that the user is running is JDK 1.4 or greater, so we just created the following program: Comment
//: com:bruceeckel:tools:CheckVersion.java // {RunByHand} package com.bruceeckel.tools; public class CheckVersion { public static void main(String[] args){ String version = System.getProperty("java.version"); char minor = version.charAt(2); if (minor < '4') throw new RuntimeException("JDK 1.4.0 or higher " + "is required to run the examples in this book."); System.out.println( "JDK version " + version + " found"); } } ///:~
This simply uses System.getProperty() to discover the java version, and throws an exception if it isnt at least 1.4. When ant sees the exception it will halt. Now you can include the following in any buildfile where you want to check the version number: Comment
<java taskname="CheckVersion" classname="com.bruceeckel.tools.CheckVersion" classpath="${basedir}" fork="true" failonerror="true" />
If you use this approach to adding tools, you can write them and test them quickly, and if its justified you can invest the extra effort and write an ant extension. Comment
The revision control system is a class of tool that has been developed over many years to help manage large team programming projects. It has also turned out to be fundamental to the success of virtually all open-source projects, because open-source teams are almost always distributed globally via the Internet. So even if there are only two people working on a project, they benefit from using a revision control system. Comment
The defacto standard revision control system for open-source projects is called CVS, available at http://www.cvshome.org. Because it is open-source and because so many people know how to use it, CVS is also a common choice for closed projects. Some projects even use CVS as a way to distribute the system. CVS has the usual benefits of a popular open-source project: the code has been thoroughly reviewed, its available for your review and modification, and if any new flaws are discovered they are corrected very rapidly. Comment
CVS keeps your code in a repository on a server. This server may be on a local area network, but it is typically available on the Internet so that people on the team can get updates without being at a particular location. To connect to CVS, you must have an assigned user name and password, so theres a reasonable level of security; for more security you can use the ssh protocol (although these are Linux tools, they are readily available in Windows using Cygwin see http://www.cygwin.com). Some graphical development environments (like the free Eclipse editor; see http://www.eclipse.org) provide excellent integration with CVS. Comment
Once the repository is initialized by your system administrator, team members may get a copy of the code tree by checking it out. For example, once your machine is logged into the appropriate CVS server (details of which are left out here) you can perform the initial checkout with a command like this: Comment
cvs z5 co TIJ3code
This will connect with the CVS server and negotiate the checkout (co) of the code repository called TIJ3code. The -z5 argument tells the CVS programs at both ends to communicate using a gzip compression level of 5, in order to speed up the transfer over the network. Comment
Once this command is completed, youll have a copy of the code repository on your local machine. In addition, youll see that each directory in the repository has an additional subdirectory named CVS. This is where all the CVS information about the files in that directory are stored. Comment
Now that you have your own copy of the CVS repository, you can make changes to the files in order to develop the project. Typically these changes include corrections and feature additions, along with test code and modified buildfiles necessary to compile and run the tests. Youll find that its very unpopular to check in code that doesnt successfully run all its tests, because then everyone else on the team will get the broken code (and thus fail their builds). Comment
When youve made your improvements and youre ready to check them in, you must go through a two-step process which is the crux of CVS code synchronization. First, you update your local repository to synchronize it with the main CVS repository by moving into the root of your local code repository and running this command: Comment
cvs update dP
At this point, you arent required to log in because the CVS subdirectory keeps the login information for the remote repository, and the remote repository keeps signature information about your machine as a double check to verify your identity. Comment
The -dP flag is optional. -d tells CVS to create any new directories on your local machine that might have been added to the main repository and -P tells CVS to prune off any directories on your local machine that have been emptied on the main repository. Niether of these things happens by default. Comment
The main activity of update, however, is quite interesting. You should actually run update on a regular basis, not just before you do a checkin, because it synchronizes your local repository with the main repository. If it finds any files in the main repository that are newer than files on your local repository, it brings the changes onto your local machine. However, it doesnt just copy the files, but instead it does a line-by-line comparison of the files, and patches the changes from the main repository into your local version. If youve made some changes to a file and someone else has made changes to the same file, CVS will patch the changes together as long as the changes dont happen to the same lines of code (CVS matches the contents of the lines, and not just the line numbers, so even if line numbers change it will be able to synchronize properly). Thus, you can be working on the same file as someone else, and when you do an update any changes the other person has committed to the main repository will be merged with your changes. Comment
Of course, its possible that two people might make changes to the same lines of the same file. This is an accident due to lack of communication normally youll tell each other what youre working on so as not to tread on each others code (also, if files are so big that it makes sense for two different people are working on different parts of the same file, you might consider breaking the big files up into smaller files, for easier project management). If this happens, CVS simply notes the collision and forces you to resolve it by fixing the lines of code that collide. Comment
Note that no files from your machine are moved into the main repository during an update. The update only brings changed files from the main repository onto your machine, and patches in any modifications youve made. So how do your modifications get into the main repository? This is the second step the commit. Comment
When you type
cvs commit
CVS will start up your default editor and ask you to write a description of your modification. This description will be entered into the repository so that others will know whats been changed. After that, your modified files will be placed into the main repository so they are available to everyone else, the next time they do an update. Comment
CVS has other capabilities, but checking out, updating and committing are what youll be doing most of the time. For detailed information about CVS, books are available, and the main CVS web site has full documentation: http://www.cvshome.org. In addition, you can search on the Internet using Google or other search engines; there are some very nice condensed introductions to CVS which can get you started without bogging you down with too many details (the Gentoo Linux CVS Tutorial by Daniel Robbins is particularly straightforward). Comment
By incorporating compiling and testing into your buildfiles, you can follow the practice of performing daily builds, advocated by the Extreme Programming folks and others. Regardless of the number of features that you currently have implemented, you always keep your system in a state that it can be successfully built, so that if someone performs a checkout and runs ant, the buildfile will perform all the compilations and run all the tests without failing. Comment
This is a powerful technique. It means that you always have, as a baseline, a system that compiles and passes all its tests. At any time, you can always see what the true state of the development process is by examining the features that are actually implemented in the running system. One of the timesavers of this approach is that no one has to waste time coming up with a report explaining what is going on with the system anyone can see for themselves by checking out a current build and running the program. Comment
Running builds daily, or more often, also ensures that if someone (accidentally, we presume) checks in changes that cause tests to fail, youll know about it in short order, before those bugs have a chance to propagate further problems in the system. Ant even has a task that will send email, because many teams set up their buildfile as a cron[85] job to automatically run daily, or even several times a day, and send email if it fails. There is also an open-source tool that automatically performs builds and provides a web page to show the project status; see http://cruisecontrol.sourceforge.net. Comment
Logging is the process of reporting information about a running program. In a program which is debugged, this information can be ordinary status data which describes the progress of the program (for example, if you have an installation program you may log the steps taken during installation the directories where you stored files, startup values for the program, etc.). Comment
Logging is also very useful during debugging. Without logging, you might try to decipher the behavior of a program by inserting println( ) statements. Many examples in this book use that very technique, and in the absence of a debugger (a topic that will be introduced shortly) its about all you have. However, once you decide the program is working properly, youll probably take the println( ) statements out. Then if you run into more bugs, you may need to put them back in. Its much nicer if you can put in some kind of output statements, which will only be used when necessary.Comment
Prior to the availability of the logging API in JDK1.4, programmers would often use a technique that relies on the fact that the Java compiler will optimize away code that will never be called. If debug is a static final boolean and you say:Comment
if(debug) { System.out.println("Debug info"); }
then when debug is false, the compiler will completely remove the code within the braces (thus the code doesnt cause any run-time overhead at all when it isnt used). Using this technique you can place trace code throughout your program and easily turn it on and off. One drawback to the technique, however, is that you must recompile your code in order to turn your trace statements on and off, whereas its generally more convenient to be able to turn on the trace without recompiling the program by using a configuration file that you can change to modify the logging properties.Comment
The logging API in JDK1.4 provides a more sophisticated facility to report information about your program, with almost the same efficiency of the above technique. For very simple informational logging, you can do something like this:Comment
//: X1:InfoLogging.java import com.bruceeckel.simpletest.*; import java.util.logging.*; import java.io.*; public class InfoLogging { static Test monitor = new Test(); private static Logger logger = Logger.getLogger("InfoLogging"); public static void main(String[] args) { logger.info("Logging an INFO-level message"); monitor.expect(new Object[] { "%%.* InfoLogging main", "INFO: Logging an INFO-level message" }); } } ///:~
The output during one run is:
Jul 7, 2002 6:59:46 PM InfoLogging main INFO: Logging an INFO-level message
Notice that the logging system has detected the class name and method name from which the log message originated. Its not guaranteed that these names will be correct, so you shouldnt rely on their accuracy. If you want to ensure that the proper class name and method are printed, you can use a more complex method to log the message, like this:Comment
//: X1:InfoLogging2.java // Guaranteeing proper class and method names import com.bruceeckel.simpletest.*; import java.util.logging.*; import java.io.*; public class InfoLogging2 { static Test monitor = new Test(); private static Logger logger = Logger.getLogger("InfoLogging2"); public static void main(String[] args) { logger.logp(Level.INFO, "InfoLogging2", "main", "Logging an INFO-level message"); monitor.expect(new Object[] { "%%.* InfoLogging2 main", "INFO: Logging an INFO-level message" }); } } ///:~
The logp() method takes arguments of the logging level (youll learn about this next), the class name and method name, and the logging string. You can see that its much simpler to just rely on the automatic approach if the class and method names reported during logging are not critical.Comment
The logging API provides multiple levels of reporting and the ability to change to a different level during program execution. Thus, you can dynamically set the logging level to any of the following states: Comment
Level |
Effect |
Numeric Value |
OFF |
No logging messages reported |
Integer.MAX_VALUE |
SEVERE |
Only logging messages with the level SEVERE will be reported |
1000 |
WARNING |
Logging messages with levels of WARNING and SEVERE will be reported |
900 |
INFO |
Logging messages with levels of INFO and above are reported |
800 |
CONFIG |
Logging messages with levels of CONFIG and above are reported |
700 |
FINE |
Logging messages with levels of FINE and above are reported |
500 |
FINER |
Logging messages with levels of FINER and above are reported |
400 |
FINEST |
Logging messages with levels of FINEST and above are reported. |
300 |
ALL |
All logging messages reported |
Integer.MIN_VALUE |
You can even inherit from java.util.Logging.Level (which has protected constructors) and define your own level. This could, for example, have a value of less than 300 so the level is less than FINEST. Then logging messages at your new level would not appear when the level is FINEST. Comment
You can see the effect of trying out the different levels of logging in the following example:Comment
//: X1:LoggingLevels.java import com.bruceeckel.simpletest.*; import java.util.logging.Level; import java.util.logging.Logger; import java.util.logging.Handler; import java.util.logging.LogManager; public class LoggingLevels { static Test monitor = new Test(); static Logger lgr = Logger.getLogger("com"), lgr2 = Logger.getLogger("com.bruceeckel"), util = Logger.getLogger("com.bruceeckel.util"), test = Logger.getLogger("com.bruceeckel.test"), rand = Logger.getLogger("random"); static void logMessages() { lgr.info("com : info"); lgr2.info("com.bruceeckel : info"); util.info("util : info"); test.severe("test : severe"); rand.info("random : info"); } public static void main(String[] args) { lgr.setLevel(Level.SEVERE); System.out.println("com level: SEVERE"); logMessages(); util.setLevel(Level.FINEST); test.setLevel(Level.FINEST); rand.setLevel(Level.FINEST); System.out.println( "individual loggers set to FINEST"); logMessages(); lgr.setLevel(Level.SEVERE); System.out.println("com level: SEVERE"); logMessages(); monitor.expect("LoggingLevels.out"); } } ///:~
The first few lines of main() are necessary because the default level of logging messages that will be reported is INFO and greater (more severe). If you do not change this then the messages of level CONFIG and below will not be reported (try taking out the lines to see this happen).Comment
You can have multiple logger objects in your program, and these loggers are organized into a hierarchical tree, which can be programmatically associated with the package namespace. Child loggers keep track of their immediate parent and by default pass the logging records up to the parent.Comment
The root logger object is always created by default, and is the base of the tree of logger objects. You get a reference to the root logger by calling the static method Logger.getLogger(""). Notice that it takes an empty string, rather than no arguments.Comment
Each Logger object can have one or more Handler objects associated with it. Each Handler object provides a strategy[86] for publishing the logging information, which is contained in LogRecord objects. To create a new type of Handler, you simply inherit from the Handler class and override the publish() method (along with flush() and close(), to deal with any streams you may use in the Handler).Comment
The root logger always has one associated handler by default, which sends output to the console. In order to access the handlers, you call getHandlers() on the Logger object. In the above code, we know that theres only one handler so we dont technically need to iterate through the list, but its safer to do so in general because someone else may have added other handlers to the root logger. The default level of each handler is INFO, so in order to see all the messages we set the level to ALL (which is the same as FINEST).Comment
The levels array allows easy testing of all the Level values. The logger is set to each value and all the different logging levels are attempted. In the output you can see that only messages at the currently selected logging level, and those messages which are more severe, are reported.Comment
A LogRecord is an example of a Messenger object[87], whose job is simply to carry information from one place to another. All the methods in the LogRecord are getters and setters. Heres an example that dumps all the information stored in a LogRecord using the getter methods:Comment
//: X1:PrintableLogRecord.java // Override LogRecord toString() import com.bruceeckel.simpletest.*; import java.util.ResourceBundle; import java.util.logging.*; public class PrintableLogRecord extends LogRecord { static Test monitor = new Test(); public static String nl = System.getProperty("line.separator"); public PrintableLogRecord(Level level, String str) { super(level, str); } public String toString() { String result = "Level<" + getLevel() + ">" + nl; result += "LoggerName<" + getLoggerName() + ">" + nl; result += "Message<" + getMessage() + ">" + nl; result += "CurrentMillis<" + getMillis() + ">" + nl; Object objParams[] = getParameters(); result += "Params"; if(objParams == null) result += "<" + null +">" + nl; else for(int i = 0; i < objParams.length; i++) result += " Param # <" + i + " value " + objParams[i].toString() + ">" + nl; ResourceBundle rb = getResourceBundle(); if(rb == null) result += "ResourceBundle<" + null + ">" + nl; else result += "ResourceBundle<" + rb.toString() + ">" + nl; String strName = getResourceBundleName(); if(strName == null) result += "ResourceBundleName<" + null + ">" + nl; else result += "ResourceBundleName<" + strName + ">" + nl; result += "SequencNumber<" + getSequenceNumber() + ">" + nl; Object objSrcName = getSourceClassName(); if(objSrcName == null) result += "SourceClassName<" + null + ">" + nl; else result += "SourceClassName<" + objSrcName + ">" + nl; result += "SourceMethodName<" + getSourceMethodName() + ">" + nl; result += "Thread Id<" + getThreadID() + ">" + nl; Throwable t = getThrown(); if(t == null) result += "Thrown<" + null + ">"; else result += "Thrown<" + t.toString() + ">"; return result; } public static void main(String[] args) { PrintableLogRecord logRecord = new PrintableLogRecord(Level.FINEST, "Simple Log Record"); System.out.println(logRecord); monitor.expect(new Object[] { "Level<FINEST>", "LoggerName<null>", "Message<Simple Log Record>", "%%CurrentMillis<.+>", "Params<null>", "ResourceBundle<null>", "ResourceBundleName<null>", "SequencNumber<0>", "SourceClassName<null>", "SourceMethodName<null>", "Thread Id<10>", "Thrown<null>" }); } } ///:~
PrintableLogRecord is a simple extension of LogRecord that overrides toString() to call all the getter methods available in LogRecord.Comment
As noted above, you can easily create your own handler by inheriting from Handler and defining publish() to perform your desired operations. However, there are predefined handlers which will probably satisfy your needs without doing any extra work:Comment
StreamHandler |
Writes formatted records to an OutputStream |
ConsoleHandler |
Writes formatted records to System.err |
FileHandler |
Writes formatted log records either to a single file, or to a set of rotating log files |
SocketHandler |
Writes formatted log records to remote TCP ports |
MemoryHandler |
Buffers log records in memory |
For example, you often want to store logging output to a file. The FileHandler makes this easy:Comment
//: X1:LogToFile.java import com.bruceeckel.simpletest.*; import java.util.logging.*; public class LogToFile { static Test monitor = new Test(); private static Logger logger = Logger.getLogger("LogToFile"); public static void main(String[] args) throws Exception { logger.addHandler(new FileHandler("LogToFile.xml")); logger.info("A message logged to the file"); monitor.expect(new Object[] { "%%.* LogToFile main", "INFO: A message logged to the file" }); } } ///:~
When you run this program, youll notice two things. First, even though were sending output to a file, youll still see console output. Thats because each message is converted to a LogRecord, which is first used by the local logger object, which passes it to its own handlers. At this point the LogRecord is passed to the parent object, which has its own handlers. This process continues until the root logger is reached. The root logger comes with a default ConsoleHandler, so the message appears on the screen as well as appearing in the log file (you can turn off this behavior by calling setUseParentHandlers(false)).Comment
The second thing youll notice is that the contents of the log file is in XML format, which will look something like this:Comment
<?xml version="1.0" standalone="no"?> <!DOCTYPE log SYSTEM "logger.dtd"> <log> <record> <date>2002-07-08T12:18:17</date> <millis>1026152297750</millis> <sequence>0</sequence> <logger>LogToFile</logger> <level>INFO</level> <class>LogToFile</class> <method>main</method> <thread>10</thread> <message>A message logged to the file</message> </record> </log>
The default output format for a FileHandler is XML. If you want to change the format, you must attach a different Formatter object to the handler. Here, a SimpleFormatter is used for the file in order to output as plain text format:Comment
//: X1:LogToFile2.java import com.bruceeckel.simpletest.*; import java.util.logging.*; public class LogToFile2 { static Test monitor = new Test(); private static Logger logger = Logger.getLogger("LogToFile2"); public static void main(String[] args) throws Exception { FileHandler logFile = new FileHandler("LogToFile2.txt"); logFile.setFormatter(new SimpleFormatter()); logger.addHandler(logFile); logger.info("A message logged to the file"); monitor.expect(new Object[] { "%%.* LogToFile2 main", "INFO: A message logged to the file" }); } } ///:~
The LogToFile2.txt file will look like this:Comment
Jul 8, 2002 12:35:17 PM LogToFile2 main INFO: A message logged to the file
You can register multiple handlers with each Logger object. When a logging request comes to the Logger, it notifies all the handlers that have been registered with it[88], as long as the logging level for the Logger is greater than or equal to that of the logging request. Each handler, in turn, has its own logging level; if the level of the LogRecord is greater than or equal to the level of the handler, then that handler publishes the record.Comment
Heres an example that adds a FileHandler and a ConsoleHandler to the Logger object:Comment
//: X1:MultipleHandlers.java import com.bruceeckel.simpletest.*; import java.util.logging.*; public class MultipleHandlers { static Test monitor = new Test(); private static Logger logger = Logger.getLogger("MultipleHandlers"); public static void main(String[] args) throws Exception { FileHandler logFile = new FileHandler("MultipleHandlers.xml"); logger.addHandler(logFile); logger.addHandler(new ConsoleHandler()); logger.warning("Output to multiple handlers"); monitor.expect(new Object[] { "%%.* MultipleHandlers main", "WARNING: Output to multiple handlers", "%%.* MultipleHandlers main", "WARNING: Output to multiple handlers" }); } } ///:~
When you run the program, youll notice that the console output occurs twice thats because the root loggers default behavior is still enabled. If you want to turn this off, make a call to setUseParentHandlers(false):
//: X1:MultipleHandlers2.java import com.bruceeckel.simpletest.*; import java.util.logging.*; public class MultipleHandlers2 { static Test monitor = new Test(); private static Logger logger = Logger.getLogger("MultipleHandlers2"); public static void main(String[] args) throws Exception { FileHandler logFile = new FileHandler("MultipleHandlers2.xml"); logger.addHandler(logFile); logger.addHandler(new ConsoleHandler()); logger.setUseParentHandlers(false); logger.warning("Output to multiple handlers"); monitor.expect(new Object[] { "%%.* MultipleHandlers2 main", "WARNING: Output to multiple handlers" }); } } ///:~
Now youll see only one console message. Comment
You can easily write custom handlers by inheriting from the Handler class. To do this, you must not only implement the publish() method (which performs the actual reporting) but also flush() and close(), which ensure that the stream that is used for reporting is properly cleaned up. Heres an example that stores information from the LogRecord into another object (a List of String). At the end of the program, the object is printed to the console:Comment
//: X1:CustomHandler.java // How to write custom handler import com.bruceeckel.simpletest.*; import java.util.logging.*; import java.util.*; public class CustomHandler { static Test monitor = new Test(); private static Logger logger = Logger.getLogger("CustomHandler"); private static List strHolder = new ArrayList(); public static void main(String[] args) { logger.addHandler(new Handler() { public void publish(LogRecord logRecord) { strHolder.add(logRecord.getLevel() + ":"); strHolder.add(logRecord.getSourceClassName()+":"); strHolder.add(logRecord.getSourceMethodName()+":"); strHolder.add("<" + logRecord.getMessage() + ">"); strHolder.add( System.getProperty("line.separator")); } public void flush() {} public void close() {} }); logger.warning("Logging Warning"); logger.info("Logging Info"); System.out.print(strHolder); monitor.expect(new Object[] { "%%.* CustomHandler main", "WARNING: Logging Warning", "%%.* CustomHandler main", "INFO: Logging Info", "[WARNING:, CustomHandler:, main:, <Logging Warning>, ", ", INFO:, CustomHandler:, main:, <Logging Info>, ", "]"}); } } ///:~
The console output comes from the root logger. When the ArrayList is printed you can see that only selected information has been captured into the object.Comment
When you write the code to send a logging message to a Logger object, you often decide at the time youre writing the code what level the logging message should be (the logging API certainly allows you to devise more complex systems wherein the level of the message can be determined dynamically, but this is less common in practice). The Logger object has a level that can be set so that it can decide what level of message to accept; all others will be ignored. This can be thought of as a basic filtering functionality, and its often all you need.Comment
Sometimes, however, you need more sophisticated filtering, so that you can decide whether to accept or reject a message based on something more than just the current level. To accomplish this you can write custom Filter objects. Filter is an interface that has a single method, boolean isLoggable(LogRecord record), which decides whether or not this particular LogRecord is interesting enough to report.Comment
Once you create a Filter, you register it with either a Logger or a Handler using the setFilter() method. For example, suppose youd like to only log reports about Ducks:Comment
//: X1:SimpleFilter.java import com.bruceeckel.simpletest.*; import java.util.logging.*; public class SimpleFilter { static Test monitor = new Test(); private static Logger logger = Logger.getLogger("SimpleFilter"); static class Duck {}; static class Wombat {}; static void sendLogMessages() { logger.log(Level.WARNING, "A duck in the house!", new Duck()); logger.log(Level.WARNING, "A Wombat at large!", new Wombat()); } public static void main(String[] args) { sendLogMessages(); logger.setFilter(new Filter() { public boolean isLoggable(LogRecord record) { Object[] params = record.getParameters(); if(params == null) return true; // No parameters if(record.getParameters()[0] instanceof Duck) return true; // Only log Ducks return false; } }); logger.info("After setting filter.."); sendLogMessages(); monitor.expect(new Object[] { "%%.* SimpleFilter sendLogMessages", "WARNING: A duck in the house!", "%%.* SimpleFilter sendLogMessages", "WARNING: A Wombat at large!", "%%.* SimpleFilter main", "INFO: After setting filter..", "%%.* SimpleFilter sendLogMessages", "WARNING: A duck in the house!" }); } } ///:~
Before setting the Filter, messages about Ducks and Wombats are reported. The Filter is created as an anonymous inner class which looks at the LogRecord parameter to see if a Duck was passed as an extra argument to the log() method if so, it returns true to indicate that the message should be processed.Comment
Notice that the signature of getParameters() says that it will return an Object[]. However, if no additional arguments have been passed to the log() method, getParameters() will return null (in violation of its signature this is a bad programming practice). So instead of assuming that an array is returned (as promised) and checking to see if it is of zero length, we must check for null. If you dont do this correctly, then the call to logger.info() will cause an exception to be thrown.Comment
A Formatter is a way to insert a formatting operation into a Handlers processing steps. If you register a Formatter object with a Handler, then before the LogRecord is published by the Handler, it is first sent to the Formatter. After formatting, the LogRecord is returned to the Handler, which then publishes it. Comment
To write a custom Formatter, extend the Formatter class and override format(LogRecord record). Then, register the Formatter with the Handler using the setFormatter() call, as seen here: Comment
//: X1:SimpleFormatterExample.java import com.bruceeckel.simpletest.*; import java.util.logging.*; import java.util.*; public class SimpleFormatterExample { static Test monitor = new Test(); private static Logger logger = Logger.getLogger("SimpleFormatterExample"); private static void logMessages() { logger.info("Line One"); logger.info("Line Two"); } public static void main(String[] args) { logger.setUseParentHandlers(false); Handler conHdlr = new ConsoleHandler(); conHdlr.setFormatter(new Formatter() { public String format(LogRecord record) { return record.getLevel() + " : " + record.getSourceClassName() + " -:- " + record.getSourceMethodName() + " -:- " + record.getMessage() + System.getProperty("line.separator"); } }); logger.addHandler(conHdlr); logMessages(); monitor.expect(new String[] { "INFO : SimpleFormatterExample -:- logMessages " + "-:- Line One", "INFO : SimpleFormatterExample -:- logMessages " + "-:- Line Two" }); } } ///:~
Remember that a logger like myLogger has a default handler that it gets from the parent logger (the root logger, in this case). Here, we are turning off the default handler by calling setUseParentHandlers(false), and then adding in a console handler to use instead. The new Formatter is created as an anonymous inner class in the setFormatter() statement. The overridden format() statement simply extracts some of the information from the LogRecord and formats it into a string. Comment
You can actually have one of your logging handlers send you an email, so that you can be automatically notified of important problems. The following example uses the JavaMail API to develop a Mail User Agent to send an email. Comment
The JavaMail API is a set of classes that interface to the underlying mailing protocol (IMAP, POP, SMTP). You can devise a notification mechanism on some exceptional condition in the running code by registering an additional Handler to send an email. Comment
//: X1:EmailLogger.java // {RunByHand} Must be connected to the Internet // {Depends: mail.jar,activation.jar} import java.util.logging.*; import java.io.*; import java.util.Properties; import javax.mail.*; import javax.mail.internet.*; public class EmailLogger { private static Logger logger = Logger.getLogger("EmailLogger"); public static void main(String[] args) throws Exception { logger.setUseParentHandlers(false); Handler conHdlr = new ConsoleHandler(); conHdlr.setFormatter(new Formatter() { public String format(LogRecord record) { return record.getLevel() + " : " + record.getSourceClassName() + ":" + record.getSourceMethodName() + ":" + record.getMessage() + System.getProperty("line.separator"); } }); logger.addHandler(conHdlr); logger.addHandler( new FileHandler("EmailLoggerOutput.xml")); logger.addHandler(new MailingHandler()); logger.log(Level.INFO, "Testing Multiple Handlers", "SendMailTrue"); } } // A handler that sends mail messages class MailingHandler extends Handler { public void publish(LogRecord record) { Object[] params = record.getParameters(); if(params == null) return; // Send mail only if the parameter is true if (params[0].equals("SendMailTrue")) { new MailInfo("bruce@theunixman.com", new String[] { "bruce@theunixman.com" }, "smtp.theunixman.com", "Test Subject", "Test Content").sendMail(); } } public void close() {} public void flush() {} } class MailInfo { private String fromAddr; private String[] toAddr; private String serverAddr; private String subject; private String message; public MailInfo(String from, String[] to, String server, String subject, String message) { fromAddr = from; toAddr = to; serverAddr = server; this.subject = subject; this.message = message; } public void sendMail() { try { Properties prop = new Properties(); prop.put("mail.smtp.host", serverAddr); Session session = Session.getDefaultInstance(prop, null); session.setDebug(true); // Create a message Message mimeMsg = new MimeMessage(session); // Set the from and to address Address addressFrom = new InternetAddress(fromAddr); mimeMsg.setFrom(addressFrom); Address[] to = new InternetAddress[toAddr.length]; for (int i = 0; i < toAddr.length; i++) to[i] = new InternetAddress(toAddr[i]); mimeMsg.setRecipients(Message.RecipientType.TO,to); mimeMsg.setSubject(subject); mimeMsg.setText(message); Transport.send(mimeMsg); } catch (Exception e) { throw new RuntimeException(e); } } } ///:~
MailingHandler is one of the Handlers registered with the logger. To send an email, the MailingHandler uses the MailInfo object. When a logging message is sent with an additional parameter of SendMailTrue the MailingHandler sends an email. Comment
The MailInfo object contains the necessary state information such as the to address, from address and the subject information required to send an email. This state information is provided to the MailInfo object through the constructor when it is instantiated. Comment
To send an email you must first establish a Session with the Simple Mail Transfer Protocol (SMTP) server. This is done by passing the address of the server inside a Properties object, in a property named mail.smtp.host. You establish a session by calling Session.getDefaultInstance(), passing it the Properties object as the first argument. The second argument is an instance of Authenticator that may be used for authenticating the user. Passing a null value for the Authenticator argument specifies no authentication. If the debugging flag in the Properties object is set, information regarding the communication between the SMTP server and the program will be printed. Comment
MimeMessage is an abstraction of an Internet email message that extends the class Message. It constructs a message that complies with the MIME (Multipurpose Internet Mail Extensions) format. A MimeMessage is constructed by passing it an instance of Session. You may set the from and to addresses by creating an instance of InternetAddress class (a subclass of Address). You send the message using the static call Transport.send() from the abstract classTransport. An implementation of Transport uses a specific protocol (generally SMTP) to communicate with the server to send the message. Comment
Although not mandatory, its advisable to give a logger the name of the class in which it is used. This allows you to manipulate the logging level of groups of loggers that reside in the same package hierarchy, at the granularlity of the directory package structure. For example, you can modify all the logging levels of all the packages in com, or just the ones in com.bruceeckel, or just the ones in com.bruceeckel.util, as shown in the following example: Comment
//: X1:LoggingLevelManipulation.java import com.bruceeckel.simpletest.*; import java.util.logging.Level; import java.util.logging.Logger; import java.util.logging.Handler; import java.util.logging.LogManager; public class LoggingLevelManipulation { static Test monitor = new Test(); static Logger lgr = Logger.getLogger("com"), lgr2 = Logger.getLogger("com.bruceeckel"), util = Logger.getLogger("com.bruceeckel.util"), test = Logger.getLogger("com.bruceeckel.test"), rand = Logger.getLogger("random"); static void printLogMessages(Logger logger) { logger.finest(logger.getName() + " Finest"); logger.finer(logger.getName() + " Finer"); logger.fine(logger.getName() + " Fine"); logger.config(logger.getName() + " Config"); logger.info(logger.getName() + " Info"); logger.warning(logger.getName() + " Warning"); logger.severe(logger.getName() + " Severe"); } static void logMessages() { printLogMessages(lgr); printLogMessages(lgr2); printLogMessages(util); printLogMessages(test); printLogMessages(rand); } static void printLevels() { System.out.println(" -- printing levels -- " + lgr.getName() + " : " + lgr.getLevel() + " " + lgr2.getName() + " : " + lgr2.getLevel() + " " + util.getName() + " : " + util.getLevel() + " " + test.getName() + " : " + test.getLevel() + " " + rand.getName() + " : " + rand.getLevel()); } public static void main(String[] args) { printLevels(); lgr.setLevel(Level.SEVERE); printLevels(); System.out.println("com level: SEVERE"); logMessages(); util.setLevel(Level.FINEST); test.setLevel(Level.FINEST); rand.setLevel(Level.FINEST); printLevels(); System.out.println( "individual loggers set to FINEST"); logMessages(); lgr.setLevel(Level.FINEST); printLevels(); System.out.println("com level: FINEST"); logMessages(); monitor.expect("LoggingLevelManipulation.out"); } } ///:~
As you can see in the above code, if you pass getLogger() a string representing a namespace, the resulting Logger will control the severity levels of that namespace that is, all the packages within that namespace will be affected by changes to the severity level of the logger. Comment
Each Logger keeps a track of its existing ancestor Logger. If a child logger already has a logging level set, then that level is used instead of the parent's logging level. Changing the logging level of the parent does not affect the logging level of the child once the child has its own logging level. Comment
Although the level of individual loggers is set to FINEST, only messages with a logging level equal to or more severe than INFO are printed since we are using the ConsoleHandler of the root logger, which is at INFO. Comment
Because it isnt in the same namespace, the logging level of random remains unaffected when the logging level of the logger com or com.bruceeckel is changed.Comment
At first glance, the Java logging API can seem rather over-engineered for most programming problems. The extra features and abilities dont come in handy until you start building larger projects. In this section well look at these features and recommended ways to use them. If youre only using logging on smaller projects you probably wont need to use these features. Comment
The file below shows how you can configure loggers in a project by using a properties file:
//:! X1:log.prop #### Configuration File #### # Global Params # Handlers installed for the root logger handlers= java.util.logging.ConsoleHandler java.util.logging.FileHandler # Level for root logger is used by any logger # that does not have its level set .level= FINEST # Initialization class the public default constructor # of this class is called by the Logging framework config = ConfigureLogging # Configure FileHandler # Logging file name - %u specifies unique java.util.logging.FileHandler.pattern = java%g.log # Write 100000 bytes before rotating this file java.util.logging.FileHandler.limit = 100000 # Number of rotating files to be used java.util.logging.FileHandler.count = 3 # Formatter to be used with this FileHandler java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter # Configure ConsoleHandler java.util.logging.ConsoleHandler.level = FINEST java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter # Set Logger Levels # com.level=SEVERE com.bruceeckel.level = FINEST com.bruceeckel.util.level = INFO com.bruceeckel.test.level = FINER random.level= SEVERE ///:~
The configuration file allows you to associate handlers with the root logger. The property handlers specify the comma-separated list of handlers you wish to register with the root logger. Here, we register the FileHandler and the ConsoleHandler with the root logger. The .level property species the default level for the logger. This level is used by all the loggers that are children of the root logger and do not have their own level specified. You should note that while we were not using the properties file, the default logging level of the root logger was INFO. This is because, in absence of a custom configuration file, the virtual machine uses the configuration from the JAVA_HOME\jre\lib\logging.properties file. Comment
The configuration file above generates rotating log files, which are used to prevent any log file from becoming too large. By setting the FileHandler.limit value you give the maximum number of bytes allowed in one log file before the next one begins to fill. FileHandler.count determines the number of rotating log files to use; the configuration file above specifies three files. If all three files are filled to their maximum, then the first file begins to fill again, overwriting the old contents. Comment
Alternatively, all the output can be put in a single file by giving a FileHandler.count value of one. (FileHandler parameters are explained in detail in the Java API documentation). Comment
In order for the program below to use the configuration file shown above, you must specify the parameter java.util.logging.config.file on the command line: Comment
java -Djava.util.logging.config.file=log.prop ConfigureLogging
The configuration file can only modify the root logger. If you want to add filters and handlers for other loggers, you must write the code to do it inside a Java file, as noted in the constructor: Comment
//: X1:ConfigureLogging.java // {JVMArgs: -Djava.util.logging.config.file=log.prop} import com.bruceeckel.simpletest.*; import java.util.logging.*; public class ConfigureLogging { static Test monitor = new Test(); static Logger lgr = Logger.getLogger("com"), lgr2 = Logger.getLogger("com.bruceeckel"), util = Logger.getLogger("com.bruceeckel.util"), test = Logger.getLogger("com.bruceeckel.test"), rand = Logger.getLogger("random"); public ConfigureLogging() { /* Set Additional formatters, Filters and Handlers for the loggers here. You cannot specify the Handlers for loggers except the root logger from the configuration file. */ } public static void main(String[] args) { sendLogMessages(lgr); sendLogMessages(lgr2); sendLogMessages(util); sendLogMessages(test); sendLogMessages(rand); monitor.expect("ConfigureLogging.out"); } private static void sendLogMessages(Logger logger) { System.out.println(" Logger Name : " + logger.getName() + " Level: " + logger.getLevel()); logger.finest("Finest"); logger.finer("Finer"); logger.fine("Fine"); logger.config("Config"); logger.info("Info"); logger.warning("Warning"); logger.severe("Severe"); } } ///:~
The configuration will result in the output being sent to the files named java0.log, java1.log, and java2.log in the directory from which this program is executed.Comment
Although its not mandatory, you should generally consider using a logger for each class, following the standard of setting the logger name to be the same as the fully qualified name of the class. As shown earlier, this allows for finer-grained control of logging because of the ability to turn logging on and off based on namespaces. Comment
If you dont set the logging level for individual classes in that package, then the individual classes default to the logging level set for the package (assuming you name the loggers according to their package and class). Comment
If you control the logging level in a configuration file instead of changing it dynamically in your code, then you can modify logging levels without recompiling your code. Recompilation is not always an option when the system is deployed; often only the class files are shipped to the destination environment. Comment
Sometimes there is a requirement to execute some code to perform initialization activities such as adding Handlers, Filters and Formatters to loggers. This can be achieved by setting the config property in the properties file. You can have multiple classes whose initialization can be done using the config property. These classes should be specified using space-delimited values like this: Comment
config = ConfigureLogging1 ConfigureLogging2 Bar Baz
Classes specified in this fashion will have their default constructors invoked. Comment
Although this has been a fairly thorough introduction to the logging API, it doesnt include everything. For instance, we havent talked about the LogManager or details of the various built-in handlers such as MemoryHandler, FileHandler, ConsoleHandler, etc. You should go to the JDK documentation for further details.Comment
Although judicious use of System.out statements or logging information can produce valuable insight into the behavior of a program[89], for difficult problems this approach becomes cumbersome and time-consuming. In addition, you may need to peek more deeply into the program than print statements will allow. For this, you need a debugger. Comment
In addition to more quickly and easily displaying information that you could produce with print statements, a debugger will also set breakpoints and then stop the program when it reaches those breakpoints. A debugger can also display the state of the program at any instant, view the values of variables that youre interested in, step through the program line by line, connect to a remotely running program, and more. Especially when you start building larger systems (where bugs can easily become buried), it pays to become familiar with debuggers. Comment
The Java Debugger (JDB) is a command line debugger that ships with the JDK. JDB is at least conceptually a descendant of the Gnu Debugger (GDB, which was inspired by the original Unix DB), in terms of the instructions for debugging and its command line interface. JDB is useful for learning about debugging and performing simpler debugging tasks, and its helpful to know that its always available wherever the JDK is installed. However, for larger projects youll probably want to use a graphical debugger, described later. Comment
Suppose youve written the following program:
//: X1:SimpleDebugging.java // {ThrowsException} public class SimpleDebugging { private static void foo1() { System.out.println("In foo1"); foo2(); } private static void foo2() { System.out.println("In foo2"); foo3(); } private static void foo3() { System.out.println("In foo3"); int j = 1; j--; int i = 5 / j; } public static void main(String[] args) { foo1(); } } ///:~
If you look at foo3(), the problem is obvious youre dividing by zero. But suppose this code is buried in a large program (as is implied here by the sequence of calls) and you dont know where to start looking for the problem. As it turns out, the exception that will be thrown will give enough information for you to locate the problem (this is just one of the great things about exceptions). But lets just suppose that the problem is more difficult than that, and that you need to drill into it more deeply and get more information than what an exception provides. Comment
To run JDB, you must tells the compiler to generate debugging information by compiling SimpleDebugging.java with the g flag. Then you start debugging the program with the command line:
jdb SimpleDebugging
This brings up JDB and gives you a command prompt. You can view the list of available JDB commands by typing a ? at the prompt. Comment
Heres an interactive debugging trace that shows how to chase down a problem:
Initializing jdb ... > catch Exception
The > indicates JDB is waiting for a command, and the commands typed in by the user are shown in bold. The command catch Exception causes a breakpoint to be set at any point where an exception is thrown (however, the debugger will stop anyway, even if you dont explicitly give this comment exceptions appear to be default breakpoints in JDB). Comment
Deferring exception catch Exception. It will be set after the class is loaded. > run
Now the program will run till the next breakpoint, which in this case is where the exception occurs. Heres the result of the above run command:
run SimpleDebugging > VM Started: In foo1 In foo2 In foo3 Exception occurred: java.lang.ArithmeticException (uncaught)"thread=main", SimpleDebugging.foo3(), line=18 bci=15 18 int i = 5 / j;
The program runs till line 18 where the exception generated, but jdb does not exit when it hits the exception. The debugger also displays the line of code that caused the exception. You can list the point where the execution stopped in the program source by the list command as shown below. Comment
main[1] list 14 private static void foo3() { 15 System.out.println("In foo3"); 16 int j = 1; 17 j--; 18 => int i = 5 / j; 19 } 20 21 public static void main(String[] args) { 22 foo1(); 23 }
The pointer (=>) in the above listing shows the current point from where the execution will resume. You could resume the execution by the cont (continue) command. But doing that will make JDB exit at the exception, printing the stack trace. Comment
The locals command dumps the value of all the local variables:
main[1] locals Method arguments: Local variables: j = 0
You can see the value of j=0 is what caused the exception. Comment
The wherei command prints the stack frames pushed in the method stack of the current thread:
main[1] wherei [1] SimpleDebugging.foo3 (SimpleDebugging.java:18), pc = 15 [2] SimpleDebugging.foo2 (SimpleDebugging.java:11), pc = 8 [3] SimpleDebugging.foo1 (SimpleDebugging.java:6), pc = 8 [4] SimpleDebugging.main (SimpleDebugging.java:22), pc = 0
Each line in the above trace after wherei represents a method call and the point where the call will return (which is shown by the value of the program counter pc). Here the calling sequence was main(), foo1(), foo2() and foo3(). You can pop the stack frame pushed when the call was made to foo3() with the pop command: Comment
main[1] pop main[1] wherei [1] SimpleDebugging.foo2 (SimpleDebugging.java:11), pc = 8 [2] SimpleDebugging.foo1 (SimpleDebugging.java:6), pc = 8 [3] SimpleDebugging.main (SimpleDebugging.java:22), pc = 0
You can make the JDB step through the call to foo3() again with the reenter command:
main[1] reenter > Step completed: "thread=main", SimpleDebugging.foo3(), line=15 bci=0 System.out.println("In foo3");
The list command shows us that the execution begins at the start of foo3():
main[1] list 11 foo3(); 12 } 13 14 private static void foo3() { 15 => System.out.println("In foo3"); 16 int j = 1; 17 j--; 18 int i = 5 / j; 19 } 20
JDB also allows you to modify the value of the local variables. The divide by zero that was caused by executing this piece of code the last time can be avoided by changing the value of j. You can do this directly in the debugger, so that you can continue debugging the program without going back and changing the source file. Before you set the value of j, you will have to execute through line 25 since that is where j is declared. Comment
main[1] step > In foo3 Step completed: "thread=main", SimpleDebugging.foo3(), line=16 bci=8 16 int j = 1; main[1] step > Step completed: "thread=main", SimpleDebugging.foo3(), line=17 bci=10 17 j--; main[1] list 13 14 private static void foo3() { 15 System.out.println("In foo3"); 16 int j = 1; 17 => j--; 18 int i = 5 / j; 19 } 20 21 public static void main(String[] args) { 22 foo1();
At this point j is defined and you can set its value so that the exception can be avoided.
main[1] set j=6 j=6 = 6 main[1] next > Step completed: "thread=main", SimpleDebugging.foo3(), line=18 bci=13 18 int i = 5 / j; main[1] next > Step completed: "thread=main", SimpleDebugging.foo3(), line=19 bci=17 19 } main[1] next > Step completed: "thread=main", SimpleDebugging.foo2(), line=12 bci=11 12 } main[1] list 8 9 private static void foo2() { 10 System.out.println("In foo2"); 11 foo3(); 12 => } 13 14 private static void foo3() { 15 System.out.println("In foo3"); 16 int j = 1; 17 j--; main[1] next > Step completed: "thread=main", SimpleDebugging.foo1(), line=7 bci=11 7 } main[1] list 3 public class SimpleDebugging { 4 private static void foo1() { 5 System.out.println("In foo1"); 6 foo2(); 7 => } 8 9 private static void foo2() { 10 System.out.println("In foo2"); 11 foo3(); 12 } main[1] next > Step completed: "thread=main", SimpleDebugging.main(), line=23 bci=3 23 } main[1] list 19 } 20 21 public static void main(String[] args) { 22 foo1(); 23 => } 24 } ///:~ main[1] next > The application exited
next executes a line at a time. You can see that the exception is avoided and we can continue stepping through the program. list is used to show the position in the program from where execution will proceed. Comment
Using a command line debugger like JDB can be inconvenient. You must use explicit commands to do things like looking at the state of the variables (locals, dump), listing the point of execution in the source code (list), finding out the threads in the system(threads), setting breakpoints (stop in, stop at) etc. A graphical debugger allows you to do all these things with a few clicks and also view the latest details of program being debugged without using explicit commands. Comment
Thus, although you may want to get started by experimenting with JDB, youll probably find it much more productive to learn to use a graphical debugger in order to quickly track down your bugs. During the development of this edition of this book, we began using IBMs Eclipse editor and development environment, which contains a very good graphical debugger for Java. Eclipse is very well designed and implemented, and you can download it for free from www.Eclipse.org (this is a free tool, not a demo or shareware. Thanks to IBM for investing the money, time and effort to make this available to everyone). Comment
Other free development tools have graphical debuggers as well, such as Suns Netbeans and the free version of Borlands JBuilder. Comment
We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Donald Knuth
Although you should always keep the above quote in mind, especially when you are sliding down the slipperly slope of premature optimization, sometimes there comes a time when you need to decide where your program is spending all its time, and to see if you can improve the performance of those sections. Comment
A profiling tool assists in this process by gathering information that allows us to see what parts of the program consume memory, and what methods consume maximum time. Some profilers even allow you to disable the garbage collector to help determine patterns of memory allocation. Comment
A profiler can also be a useful tool in detecting threading deadlock in your program. Comment
Here is the kind of help a profiler can give you about how memory is used in your program:
Profilers also keep track of how much time the CPU spends in various parts of your code. They can tell you:
This way you can decide what sections of your code need optimizing. Comment
Coverage Testing shows you the lines of code in your program that were not executed during the test. This is not always so useful as the other profiling features, but it may help draw your attention to code that is not used and might therefore be a candidate for removal or refactoring. Comment
To get coverage testing information for SimpleDebugging.java, you use the command:
java Xrunjcov:type=M SimpleDebugging
As an experiment, try putting lines of code that will not be executed into SimpleDebugging.java (youll have to be somewhat clever about this since the compiler can detect unreachable lines of code). Comment
The profiler agent communicates the events it is interested in to the JVM. The JVM profiling interface supports the following events:
Allocate, move, and free an object
Create and delete a heap arena
Allocate and free a JNI global reference
Allocate and free a JNI weak global reference
Load and unload a compiled method
While profiling, the JVM sends these events to the profiler agent, which then transfers the desired information to the profiler front end, which can be a process running on another machine, if desired. Comment
In this example you can see how to run the profiler that ships with the JDK. Although the information produced by this profiler is in the somewhat crude form of text files rather than a graphical representation which is typical of most of the commercial profilers, it still provides valuable help in determining the characteristics of your program. Comment
You run the profiler by passing an extra argument to the JVM when you invoke the program. This argument must be in the form of a single string, without any spaces after the commas, like this (although it should be on a single line, it has wrapped in the book): Comment
java Xrunhprof:heap=sites,cpu=samples,depth=10,monitor=y,thread=y,doe=y ListPerformance
The listing below does not have the entire file but some traces of the file produced by HPROF. This file is created in the current directory and is named java.hprof.txt. Comment
The top section of this file describes the details of the sections contained in the file. The data produced by the profiler is in different sections e.g. TRACE represents a trace section in the file. You will see a lot of TRACE sections each numbered so that they can be referenced later. The file also has a SITES section that shows the sites of memory allocation. The section has several rows, sorted by the number of bytes that are allocated and are being referenced the live bytes. The memory is listed in bytes. The column self represents the percentage of memory taken up by this site, the next column, accum, represents the cumulative memory percentage. The live bytes and live objects columns represent the number of live bytes at this site and the number of objects that were created that consumes these bytes. The allocated bytes and objects represent the total number of objects and bytes that are instantiated including the ones that are being used and the ones that are not being used. The difference in the number of bytes listed in allocated and live represent the bytes that can be garbage collected. The trace column actually references a TRACE in the file. The first row references trace 668 is shown below. The name represents the class whose instance was created. Comment
SITES BEGIN (ordered by live bytes) Thu Jul 18 11:23:06 2002 percent live alloc'ed stack class rank self accum bytes objs bytes objs trace name 1 59.10% 59.10% 573488 3 573488 3 668 java.lang.Object 2 7.41% 66.50% 71880 543 72624 559 1 [C 3 7.39% 73.89% 71728 3 82000 10 649 java.lang.Object 4 5.14% 79.03% 49896 232 49896 232 1 [B 5 2.53% 81.57% 24592 310 24592 310 1 [S TRACE 668: (thread=1) java.util.Vector.ensureCapacityHelper(Vector.java:222) java.util.Vector.insertElementAt(Vector.java:564) java.util.Vector.add(Vector.java:779) java.util.AbstractList$ListItr.add(AbstractList.java:495) ListPerformance$3.test(ListPerformance.java:40) ListPerformance.test(ListPerformance.java:63) ListPerformance.main(ListPerformance.java:93)
The trace above shows the sequence of execution that does the memory allocation. Going through the trace as indicated by the line numbers you will find that there are two allocations that happen on this path. Object allocations take place on line number 222 of Vector.java (elementData = new Object[newCapacity];). This helps you infer what part of the program uses up a big chunk of memory (59.10 %). Note the [C shown above in SITE 1 represents the primitive type char. This is the internal representation of the JVM for the primitive types. Comment
To determine the CPU utilization you can look up the CPU SAMPLES section. Below is a part of trace from this section. Comment
SITES END CPU SAMPLES BEGIN (total = 514) Thu Jul 18 11:23:06 2002 rank self accum count trace method 1 28.21% 28.21% 145 662 java.util.AbstractList.iterator 2 12.06% 40.27% 62 589 java.util.AbstractList.iterator 3 10.12% 50.39% 52 632 java.util.LinkedList.listIterator 4 7.00% 57.39% 36 231 java.io.FileInputStream.open 5 5.64% 63.04% 29 605 ListPerformance$4.test 6 3.70% 66.73% 19 636 java.util.LinkedList.addBefore
The organization of this listing is similar to the organization of the SITES listings. The rows are sorted on basis of CPU utilization with the row on the top having the maximum CPU utilization as indicated in the self column. The accum column lists the cumulative CPU utilization. The count field specifies the number of times this trace was active. The next two columns respectively specify the trace number and the method that took this time. Consider the first row of the CPU SAMPLES section shown above. 28.12% of total CPU time was utilized in the method java.util.AbstractList.iterator() and it was called 145 times. The details of this call can be found out by looking at the trace number 662 which is listed below. Comment
TRACE 662: (thread=1) java.util.AbstractList.iterator(AbstractList.java:332) ListPerformance$2.test(ListPerformance.java:28) ListPerformance.test(ListPerformance.java:63) ListPerformance.main(ListPerformance.java:93)
This trace helps you trace what caused a call to this method. You can infer that iterating through a list takes all that time. Comment
The above technique is too basic and the information presented is in a primitive form. For large projects it is more desirable to have the information represented in graphical form. Graphic display of profiling results is provided by a number of profilers and is beyond the scope of this book. Comment
Although it might be a bit surprising to think of a tool that was developed for documentation support as something that helps you track down problems in your programs, doclets can be surprisingly useful. Because a doclet hooks into the Javadoc parser, it has all the information available to that parser. With this, you can programmatically examine the class names, field names and method signatures in your code and thus flag potential problems. Comment
The process of producing the API documentation from the Java source files involves the parsing of the source file and the formatting of this parsed file using the standard doclet. You can write a custom doclet to customize the formatting of your javadoc comments. However, doclets allow you to do far more than just formatting the comment since a doclet has available much of the information about the source file thats being parsed. Comment
You can extract information about all the members of the class: fields, constructors, methods, and the comments associated with each of the members (alas, the method code body is not available). Details about the members are encapsulated inside special objects, which contain information about the properties of the member (private, static, final etc.). This information can be helpful in detecting poorly-written code, such as member variables that should be private but are public, method parameters without comments, and identifiers that do not follow naming conventions. Comment
Javadoc may not catch all compilation errors. It will spot syntax errors such as an unmatched brace but it may not catch semantic errors. The safest approach is to run the Java compiler on your code before attempting to use a doclet-based tool. Comment
The parsing mechanism provided by javadoc parses the entire source file and stores it in memory, in an object of class RootDoc. The entry point for the doclet submitted to javadoc is start(RootDoc doc). It is comparable to a normal Java programs main(String[] args). You may traverse through the RootDoc object and extract the necessary information. The following example shows how to write a simple doclet it just prints out all the members of each class that was parsed: Comment
//: X1:PrintMembersDoclet.java // Doclet that prints out all members of the class. import com.sun.javadoc.*; public class PrintMembersDoclet { public static boolean start(RootDoc root) { ClassDoc[] classes = root.classes(); processClasses(classes); return true; } private static void processClasses(ClassDoc[] classes) { for (int i = 0; i < classes.length; i++) { processOneClass(classes[i]); } } private static void processOneClass(ClassDoc cls) { FieldDoc fd[] = cls.fields(); for (int i = 0; i < fd.length; i++) processDocElement(fd[i]); ConstructorDoc cons[] = cls.constructors(); for (int i = 0; i < cons.length; i++) processDocElement(cons[i]); MethodDoc md[] = cls.methods(); for (int i = 0; i < md.length; i++) processDocElement(md[i]); } private static void processDocElement(Doc dc) { MemberDoc md = (MemberDoc)dc; System.out.print(md.modifiers()); System.out.print(" " + md.name()); if (md.isMethod()) System.out.println("()"); else if(md.isConstructor()) System.out.println(); } } ///:~
You can use the above doclet to print the members like this:
javadoc -doclet PrintMembersDoclet private PrintMembersDoclet
This invokes javadoc on the last argument in the command, which means it will parse the PrintMembersDoclet.java file. The -doclet option tells javadoc to use the custom doclet PrintMembersDoclet. The -private tag instructs javadoc to also print private members (the default is to only print protected and public members). Comment
RootDoc contains a collection of ClassDoc that hold all the information about the class. Classes such as MethodDoc, FieldDoc and ConstructorDoc contain information regarding methods, fields and constructors, respectively. The method processOneClass() extracts the list of these members and prints them. Comment
You can also create taglets, which allow you to implement custom Javadoc tags. The JDK documentation presents an example that implements a @todo tag, which displays its text in yellow in the resulting Javadoc output. Search for taglet in the JDK documentation for more details. Comment
This chapter introduced what Ive come to realize may be the most essential issue in programming, superceding language syntax and design issues: how do you make sure your code is correct, and keep it that way?
Recent experience has shown that the most useful and practical tool to date is unit testing, which may be combined very effectively with design by contract. There are other types of tests as well, such as conformance testing to verify that your use cases/user stories have all been implemented. But for some reason we have in the past relegated testing to be done later by someone else. Extreme programming insists that the unit tests be written before the code you create the test framework for the class, and then the class itself (on one or two occasions Ive successfully done this, but Im generally pleased if testing appears somewhere during the initial coding process). There remains resistance to testing, usually by those who havent tried it and believe they can write good code without testing. But the more experience I have, the more I repeat to myself:
If its not tested, its broken.
This a worthwhile mantra, especially when youre thinking about cutting corners. The more of your own bugs you discover, the more attached you grow to the security of built-in tests.
Build systems (in particular, Ant) and revision control (CVS) were also introduced in this chapter because they provide structure for your project and its tests. To me, the primary goal of Extreme Programming is velocity the ability to rapidly move your project forward (but in a reliable fashion), and to quickly refactor it when you realize that it can be improved. Velocity requires a support structure to give you confidence that things wont fall through the cracks when you start making big changes to your project. This iscludes a reliable repository, which allows you to roll back to any previous version, and an automatic build system that, once configured, guarantees that the project can be compiled and tested in a single step.
Once you have reason to believe your program is healthy, logging provides a way to monitor its pulse, and even (as shown in this chapter) to automatically email you if something starts to go wrong. When it does, debugging and profiling help you track down bugs and performance issues.
Perhaps its the nature of computer programming to want a single, clear, concrete answer. After all, we work with ones and zeros, which do not have fuzzy boundaries (they actually do, but the electronic engineers have gone to great lengths to give us the model we want). When it comes to solutions, its great to believe that theres one answer. But Ive found that there are boundaries to any technique, and understanding where those boundaries are is far more powerful than any single approach can be, because it allows you to use a method where its greatest strength lies, and to combine it with other approaches where it isnt so strong. For example, in this chapter design by contract was presented in combination with white-box unit testing, and as I was creating the example I discovered that the two working in concert were much more useful than either one alone.
I have found this idea to be true in more than just the issue of discovering problems, but also in building systems in the first place. For example, using a single programming language or tool to solve your problem is attractive from the standpoint of consistency, but Ive often found that I can solve certain problems much more quickly and effectively by using the Python programming language instead of Java, to the general benefit of the project. You may also discover that ant works in some places, and in other make is more useful. Or, if your clients are on Windows platforms, it may make more sense to make the radical decision of using Delphi or Visual Basic to develop client-side programs more rapidly than you could in Java. The important thing is to keep an open mind, and remember that you are trying to achieve results, not necessarily use a certain tool or technique. This can be difficult, but if you remember that the project failure rate is quite high and your chances of success are proportionally low, you may think twice about considering solutions that may be more productive. One of my favorite phrases from Extreme Programming (and one I find that I violate often for usually silly reasons) is do the simplest thing that could possibly work. Most of the time, the simplest and most expedient approach, if you can discover it, is the best one.
[76] It is primarily oriented to static checking, however. There is an alternative system, called latent typing or dynamic typing or weak typing, in which the type of an object is still enforced, but it is enforced at run-time, when the type is used, rather than at compile time. Writing code in such a language Python (http://www.python.org) is an excellent example gives the programmer much more flexibility and requires far less verbiage to satisfy the compiler, and yet still guarantees that objects are used properly. However, to a programmer convinced that strong, static type checking is the only sensible solution, latent typing is anathema and serious flame wars have resulted from comparisons between the two approaches. As someone who is always in pursuit of greater productivity, I have found the value of latent typing to be very compelling. In addition, the ability to think about the issues of latent typing help you, I believe, to solve problems that are difficult to think about in strong, statically typed languages.
[77] Although the electronic version of this book is freely available, it is not open source.
[78] Inspired by Pythons doctest module.
[79] Originally placed in Thinking in Patterns, available at www.BruceEckel.com. However, with the addition of the reflection approach in JUnit, my framework doesnt make much sense anymore and will probably be removed.
[80] A key phrase from Extreme Programming (XP). Ironically, one of the JUnit authors (Kent Beck) is also the author of Extreme Programming Explained (Addison-Wesley 2000) and a main proponent of XP.
[81] Bill Venners and I have discussed this at some length, and we havent been able to figure out why it is done this way rather than creating each object right before the test is run. It is likely that it is simply an artifact of the way JUnit was originally implemented.
[82] Design by contract is described in detail in Chapter 11 of Object-Oriented Software Construction, 2nd Edition, by Bertrand Meyer, Prentice-Hall 1997.
[83] Except by the occasional company which, for reasons beyond comprehension, is still convinced that closed-source tools are somehow better or have superior tech support. The only situations where Ive seen this to be true are when tools have a very small user base, but even then it would be safer to hire consultants to modify open-source tools, and thus leverage prior work and guarantee that the work you pay for wont become unavailable to you (and also make it more likely that youll find other consultants already up to speed on the program).
[84] This is not available on the web site because its too customized to be generally useful.
[85] Cron is a program that was developed under Unix to run programs at specified times. However, it is also available in free versions under Windows, and as a Windows NT/2000 service: http://www.kalab.com/freeware/cron/cron.htm.
[86] A pluggable algorithm. Strategies allow you to easily change one part of a solution while leaving the rest unchanged. They are often used (as in this case) as ways to allow the client programmer to provide a portion of the code needed to solve a particular problem. For more details, see Thinking in Patterns with Java, downloadable at www.BruceEckel.com.
[87] A term coined by Bill Venners. This may or may not be a design pattern.
[88] This is the Observer design pattern (ibid).
[89] I learned C++ primarily by printing information, since at the time I was learning there were no debuggers available.