Tuesday, October 21

Eccentric Java: oddities and corner cases

Java Puzzlers is a worthy complement to Effective Java. It provides a collection of puzzles that illustrate obscure (and some times bizarre) behavior of the Java programming language and its core libraries.

I've highlighted some of the book's puzzles below, grouped by Numerical, API, and OOP-related traps & solutions.


§ Numerical Traps


  • Use the bitwise AND (&) instead of the MODULO (%) operator. The example in the book is a particular case of the more general equivalence: i % 2n = i & (2n - 1), which I first read about in Hacker's Delight. The bitwise expression on the right is likely faster and works safely with negative operands. [#1, page 5]
  • Use integrals types (such as int or long) or BigDecimal for precise floating-point calculations, e.g.: 200 - 110 = 90 instead of 2.00 - 1.10 = 0.89999...
    The impossibility of representing 0.1 in binary floating-point is explained with all the detail one can handle in What Every Computer Scientist Should Know About Floating-Point Arithmetic. [#2, page 8]
  • Beware of hidden integer overflows. When working with large numbers add the suffix 'L' (uppercase to improve legibility) to promote every literal to a long, e.g.: final long MICROS_PER_DAY = 24L * 60 * 60 * 1000;. [#3, page 9]
  • Watch out for loop guards <= Integer.MAX_VALUE. All integers are <= Integer.MAX_VALUE and when Integer.MAX_VALUE is incremented it silently wraps around to Integer.MIN_VALUE. [#26, page 57]
  • Beware of math operations at integer boundaries, e.g.: Math.abs(Integer.MIN_VALUE); If the argument is Integer.MIN_VALUE or Long.MIN_VALUEMath.abs returns its argument. [#64, page 149]


§ API Traps


  • Pattern.quote(String) converts a String into a regex pattern that will match the string. Use String.replaceAll(Pattern.quote("."), ","); instead of String.replaceAll("\\."), ",");, for example, to prevent the accidental omission of escape characters. [#20, page 43]
  • The finally block is always executed, even if the try block returns. Err...except if the try block calls System.exit(0). @See #39 below. I first read about this in Peter Norvig's Java Infrequently Answered Questions. [#36, page 77]
  • The set of checked exceptions that a method can throw is the intersection of the set of checked exceptions that is declared to throw in all applicable types. [#37, page 79]
  • The System.exit(0) method stops the execution of the current thread & all others, but does not cause finally blocks to execute.
    We can use shutdown hooks to add behavior that must occur before the VM exits, e.g.:

    public class HelloWorld {
      public static void main(String[] args) {
        System.out.println("Hello");
        Runtime.getRuntime().addShutdownHook(new Thread() {
          public void run() {
            System.out.println(" world");
          }
        });
        System.exit(0);
      }
    }
    

    [#39, page 83]
  • The close method of input & output streams can throw IOException too; from JSE5, input and output streams implement the Closeable interface. This lets us write a generic close method for streams, e.g.:

    ...
    InputStream in = null;
    try {
      in = new FileInputStream(src);
      ...
    } finally {
      closeAndIgnoreException(in);
    }
    
    private static void closeAndIgnoreException(Closeable c) {
      if (c != null) {
        try {
          c.close();
        } catch(IOException ioe){ // ignored }
      }
    }
    

    [#41, page 87]
  • The JVM has a stack depth limit; when the limit is exceeded a StackOverflowError is thrown, regardless of the infinite nature of the recursion. [#45, page 101]
  • Use Method m = <object variable>.class.getMethod(<method name>); instead of Method m = <object variable>.getClass().getMethod(<method name>);to prevent possible runtime exceptions caused by the runtime type returned by reflection. [#79, page 189]


§ OOP Traps


  • Java's overload resolution process selects the most specific of the methods or constructor available. Supposing we declare the methods:

    void f(float) { ... }
    void f(double) { ... }
    


    Calling f with f(42), the parameter types for the first method are assignable to the parameter types of the second method through a widening primitive conversion. That is, double = float. By contrast, float = double is not valid without a cast. Thus, f(double) is removed from the set of possible methods to call and f(float) is called. [#46, page 105]
  • A static member, such as a thread-safe counter, must be synchronized on the class object itself, e.g.:

    public class SynchronizedStatic {
      private static Long counter;
      public SynchronizedStatic() {
        synchronized (SynchronizedStatic.class) {
          counter++;
        }
      }
    }
    

    [#55, page 127]
  • When a variable and a type have the same name and are both in scope, the variable name takes precedence, e.g. (from the book):

    public class ShadesOfGray {
      public static void main(String[] args){
        System.out.println(X.Y.Z); // prints 'White'
      }
    }
    class X {
      static class Y {
        static String Z = "Black";
      } 
      static C Y = new C(); // variable Y takes precedence over type Y
    }
    class C {
      String Z = "White";
    }
    

    [#68, page 163]
  • Package-private methods cannot be directly overridden by a method in a different package (hence the name, I'd say.) [#70, page 168]
  • The final modifier means different things on methods & fields: final instance methods cannot be overridden; final static methods may not be hidden; final fields may not be assigned more than once. [#72, page 171]
  • Overriding = replacement with same name, signature, & exceptions thrown -without reducing visibility;
    Hiding = static override; Overloading = supplement with same name but different signature & exceptions thrown;
    Shadowing (textual scope) = enclosing scope is shadowed by local variables, methods, and types with the same name;
    Obscuring = static shadowing. [#Glossary, page 180]


Resources:


What Every Computer Scientist Should Know About Floating-Point Arithmetic
IEEE Standard 754 Floating Point Numbers
Why integer overflow "wraps around"
Hacker's Delight
Peter Norvig's Java Infrequently Answered Questions
JVM Overview
Overload Resolution
Algorithms for Overload Resolution
Function Overloading With Partial Ordering
The Java Language Specification
Effective Java: Second Edition
Java Puzzlers: Traps, Pitfalls, and Corner Cases
Advanced Topics in Programming Languages: Java Puzzlers, Episode VI
Java Puzzlers: Scraping the Bottom of the Barrel

1 comment:

Anonymous said...

For the non-regex String replacement issue (API traps #1), you should really use the string's replace method instead:

myString.replace(".", ",")