Abstract: Ben Evans and Martijn Verburg explain to us in their new book what it takes to be a well-grounded Java developer. The book contains a section on the new Java 7 features and also vital techniques that we use for producing robust and performant systems.
Welcome to the 204th issue of The Java(tm) Specialists' Newsletter sent to you from a warm Crete. We spoke to a French art dealer in the sea at Kalathas today and asked him why he came to Crete on holiday. We have a lot of Italians, French and Russians here this year, plus of course thousands of Scandinavians. He told us that countries like Croatia had become overrun with visitors and also quite expensive for holidays. In comparison to other popular destinations, Crete offered excellent value for money. It is true. The prices in restaurants have not changed much since 2006. I can get a delicious freshly squeezed orange juice in Chorafakia for just 2 Euro at Pantelis' cafeteria. And nothing beats the cooking of Irene's next door. The only group of holiday makers that is missing this year are my fellow Germans. Angsthasen ;-)
A few weeks ago, we went to apply for a Greek ID card for my 14 year old son. When we came to Greece, my name was converted into the Greek alphabet as XAINZ KAMPOYTZ. Greek does not have an "H" sound, so they used "X", which is pronounced as "CH". The "U" sound is made up of the diphthong "OY". Unfortunately someone had the bright idea of automatically reverse engineering Latin from Greek names. So the computer was fired up somewhere in Athens and converted me to CHAINZ KAMPOUTZ. Knowing how much trouble incorrect names can cause, I asked them to fix it. This turned out to be rather difficult for them. After all, who can argue with a computer translation? At one point, the policewoman tried to convince me that their system was correct and that I had it wrong. Gosh, 40 years of being called Heinz (like ketchup) Kabutz and only now I find out that it was wrong all the time? Must let my mom know!
javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.
Ben Evans and Martjin Verburg are both well known Java experts who consult in the financial industry in London. They have many years of experience as well-grounded Java developers. Together they wrote The Well-Grounded Java Developer: Vital techniques of Java 7 and polyglot programming [ISBN 1617290068] .
They kindly asked me to write the foreword, which you can read on Manning's website. Hope you enjoy it. It is a bit different. And yes, the fact that I wrote the foreword is an endorsement for the book. I certainly would not have agreed if I did not like the book. I had the great privilege of getting a sneak peak at the book and also to meet Ben and Marty in person when they came to Crete in April.
In the book they explain why it is so expensive to add new features to the JVM. Adding new library extensions such as fork/join or syntactic sugar like switch-on-string is relatively easy, but a JVM instruction such as invokedynamic is very costly to add. This is why we have not seen many changes to Java's fundamental infrastructure since Java 1.0. I was always wondering why change flowed so slowly in the Java environment. This is all described in chapter 1 of the book, which you can download as a sample chapter.
One of the new features in Java 7 are binary literals.
We can now write numbers as 0b101010101
. Unfortunately you
are also allowed to write long binary numbers with a lower
case L, such as: 0b111110111011101101101l
. This is quite
confusing to readers of the code, as they can easily mistake
the lower case L at the end of the number for a one. It is
much clearer to write 0b111110111011101101101L
. I would have
welcomed it if they had decided to not allow the lower case L
for binary numbers, but they probably wanted to stay
consistent with other primitive number representations.
In previous versions of Java, if we caught the general "Exception" and then re-threw that, we needed to declare that our method throws "Exception" as well:
public void foo() throws Exception { try { doSomethingWhichMightThrowIOException(); doSomethingElseWhichMightThrowSQLException(); } catch (Exception e) { // do something with e ... throw e; } }
In Java 7, the compiler is clever enough to figure out that only the checked exceptions need to be declared. Thus we can write:
public void foo() throws IOException, SQLException { try { doSomethingWhichMightThrowIOException(); doSomethingElseWhichMightThrowSQLException(); } catch (Exception e) { // do something with e ... throw e; } }
In the book, Ben and Martijn recommend that you mark the Exception as "final". The compiler does not insist on this, so it is just a convention that they use to signal their intention. In my opinion, this is not necessary, since a lot of code already would have the exception marked as final.
They make an important point about try-with-resource. We need to declare each of the objects that we want to have automatically closed in the try section. For example, this would not be correct:
try ( ObjectInputStream in = new ObjectInputStream( new BufferedInputStream( new FileInputStream("someFile.bin"))); ) { // use the ObjectInputStream }
If the FileInputStream construction succeeds (because the file does exist) but the ObjectInputStream construction fails (because the file is corrupt) or the BufferedInputStream fails (because of an OutOfMemoryError), then the FileInputStream will not be closed automatically.
The correct way to write the code is like this:
try ( FileInputStream fis = new FileInputStream("someFile.bin"); BufferedInputStream bis = new BufferedInputStream(fis); ObjectInputStream in = new ObjectInputStream(bis); ) { // use the ObjectInputStream }
Now, if any part of the construction fails, the previously declared and constructed objects will be automatically closed.
The book contains a nice discussion on the javap tool and how we can use it to analyse what is going on in our code. I have mentioned the javap tool in 15 newsletters already (042, 064, 066, 068, 069, 083, 091, 105, 109, 115, 129, 136, 137, 147 and 174). As you can imagine, it is a technique that I often employ to understand what the byte code looks like. However, I do not recall seeing it written about in any Java book to date, at least at the level that Ben and Martijn did. Jack Shirazi mentioned javap very briefly in his excellent book, Java Performance Tuning. Warning: Even though Shirazi's book is fantastic, it is quite dated. As always with clever performance tricks, you need to measure that the tricks works for you. Some parts of his book, such as his methodologies for tuning performance, are still very relevant today.
One of the most surprising classes was their CacheTester. I have seen a number of benchmarks that try to show how fast Fork/Join is by iterating over a very large array in parallel. For example, the code might try to find the largest int inside the array.
Usually the benchmark bottlenecks on memory, thus incorrectly proving that fork/join does not give any performance gains. In the CacheTester, Ben and Marty show how iterating over the array one element at the time is not much slower than looking at every 16th element. Here is their CacheTester:
public class CacheTester { private final int ARR_SIZE = 1 * 1024 * 1024; private final int[] arr = new int[ARR_SIZE]; private void doLoop2() { for (int i=0; i<arr.length; i++) arr[i]++; } private void doLoop1() { for (int i=0; i<arr.length; i += 16) arr[i]++; } private void run() { for (int i=0; i<10000; i++) { doLoop1(); doLoop2(); } for (int i=0; i<100; i++) { long t0 = System.nanoTime(); doLoop1(); long t1 = System.nanoTime(); doLoop2(); long t2 = System.nanoTime(); long el = t1 - t0; long el2 = t2 - t1; System.out.println("Loop1: "+ el +" nanos ; Loop2: "+ el2); } } public static void main(String[] args) { CacheTester ct = new CacheTester(); ct.run(); } }
I ran their code on my 8-core server and got the following results in microseconds:
Average Variance Loop1 239 12 Loop2 549 48
We can thus see that even though we are reading 16x as many array elements, it is only 2.3 times slower to do that.
Even though the results are good in that the variance is not too high, they could be better if we changed a couple of things. First off, to measure time at the nanosecond granularity invites slight abberations in the system to have an influence on our variance. In my CacheTester, I repeat the iteration 1000 times, thus getting the results in milliseconds. Secondly, I usually try to produce output that I can then copy and paste directly into a spreadsheet. Comma separated values seem to work nicely. Thirdly, the number 10000 in the CacheTester is significant. Typically, after you have called a method 10000 times, the HotSpot compiler starts profiling and optimizing the code. However, it may be a while before the new optimized code is available. Thus we sleep for a second after the 10000 warm-up calls in order to immediately have the fastest times:
public class CacheTester { private final int ARR_SIZE = 1 * 1024 * 1024; private final int[] arr = new int[ARR_SIZE]; private static final int REPEATS = 1000; private void doLoop2() { for (int i = 0; i < arr.length; i++) arr[i]++; } private void doLoop1() { for (int i = 0; i < arr.length; i += 16) arr[i]++; } private void run() throws InterruptedException { for (int i = 0; i < 10000; i++) { doLoop1(); doLoop2(); } Thread.sleep(1000); // allow the hotspot compiler to work System.out.println("Loop1,Loop2"); for (int i = 0; i < 100; i++) { long t0 = System.currentTimeMillis(); for (int j = 0; j < REPEATS; j++) doLoop1(); long t1 = System.currentTimeMillis(); for (int j = 0; j < REPEATS; j++) doLoop2(); long t2 = System.currentTimeMillis(); long el = t1 - t0; long el2 = t2 - t1; System.out.println(el + "," + el2); } } public static void main(String[] args) throws InterruptedException { CacheTester ct = new CacheTester(); ct.run(); } }
Here are the results of my CacheTester, which show almost no variance at all:
Average Variance Loop1 238 0.3 Loop2 546 1.8
When I ran the code on my MacBook Pro with an Intel Core 2 Duo, I had the following results with my benchmark:
Average Variance Loop1 168 17 Loop2 580 37
You can see that the variance was again quite high, because my laptop had too many other programs running on it. On my MacBook Pro hardware, iterating through every element in the array was 3.4 times slower.
Another sample chapter you can download is the one on concurrency. Both Ben and Martijn are certified to present my concurrency course. Ben has a lot of experience in the subject, which led to many interesting discussions when they came here in April.
Just one minor gripe. In the book
they use Math.random() * 10
in order to
calculate a random delay. Since Java 7, we should rather use
ThreadLocalRandom.current().nextInt(10)
. This
has several benefits. First off, ThreadLocalRandom keeps a
Random instance per thread, so that we do not have any
contention on the random seed. Secondly, the random
distribution is fairer with the nextInt(10)
method call. The fairness is a minor point, but the
contention is not. Math.random() shares an instance of
Random and the seed is protected by compare and swap. Thus
if a lot of threads call this at the same time, they will
need to repeat a lot of expensive calculations to eventually
update the random seed.
The book is filled with many other interesting tidbits, and is definitely on my "recommended reading" list for the Java specialist.
Kind regards
Heinz
P.S. Hot off the press: Martin Thompson just published an article on the cost of memory access, showing different approaches to traversing the elements. This is closely related to the CacheTester in Evans and Verburg. Click here to read it.
We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)
We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.