Tuesday, 18 September 2012

Puzzle 38: The Unwelcome Guest


The program in this puzzle models a system that attempts to read a user ID from its environment, defaulting to a guest user if the attempt fails. The author of the program was faced with a situation whereby the initializing expression for a static field could throw an exception. Because Java doesn't allow static initializers to throw checked exceptions, the initialization must be wrapped in a try-finally block. What does the program print?
public class UnwelcomeGuest {

    public static final long GUEST_USER_ID = -1;



    private static final long USER_ID;

    static {

        try {

            USER_ID = getUserIdFromEnvironment();

        } catch (IdUnavailableException e) {

            USER_ID = GUEST_USER_ID;

            System.out.println("Logging in as guest");

        }

    }



    private static long getUserIdFromEnvironment()

            throws IdUnavailableException {

        throw new IdUnavailableException(); // Simulate an error

    }



    public static void main(String[] args) {

        System.out.println("User ID: " + USER_ID);

    }

}



class IdUnavailableException extends Exception {

    IdUnavailableException() { }

}


Solution 38: The Unwelcome Guest

This program seems straightforward. The call to getUserIdFromEnvironment appears to throw an exception, causing the program to assign the value of GUEST_USER_ID (-1L) to USER_ID and to print Logging in as guest. Then the main method executes, causing the program to print User ID: -1. Once again, appearances are deceiving. The program doesn't compile. If you tried to compile it, you saw an error message that looked something like this:
UnwelcomeGuest.java:10:

    variable USER_ID might already have been assigned

            USER_ID = GUEST_USER_ID;

            ^


What's the problem? The USER_ID field is a blank final, which is a final field whose declaration lacks an initializer [JLS 4.12.4]. It is clear that the exception can be thrown in the try block only if the assignment to USER_ID fails, so it is perfectly safe to assign to USER_ID in the catch block. Any execution of the static initializer block will cause exactly one assignment to USER_ID, which is just what is required for blank finals. Why doesn't the compiler know this?
Determining whether a program can perform more than one assignment to a blank final is a hard problem. In fact, it's impossible. It is equivalent to the classic halting problem, which is known to be unsolvable in general [Turing36]. To make it possible to write a Java compiler, the language specification takes a conservative approach to this issue. A blank final field can be assigned only at points in the program where it is definitely unassigned. The specification goes to great lengths to provide a precise but conservative definition for this term [JLS 16]. Because it is conservative, there are some provably safe programs that the compiler must reject. This puzzle illustrates one such program.
Luckily, you do not have to learn the gory details of definite assignment to write Java programs. Usually the definite assignment rules don't get in the way. If you happen to write a program that really can assign to a blank final more than once, the compiler will helpfully point this out to you. Only rarely, as in this puzzle, will you write a program that is safe but does not satisfy the formal requirements of the specification. The compiler will complain just as if you had written an unsafe program, and you will have to modify your program to satisfy it.
The best way to solve this kind of problem is to turn the offending field from a blank final into an ordinary final, replacing the static initializer block with a static field initializer. This is best done by refactoring the code in the static block into a helper method:
public class UnwelcomeGuest {

    public static final long GUEST_USER_ID = -1;

    private static final long USER_ID = getUserIdOrGuest();

    private static long getUserIdOrGuest() {

        try {

            return getUserIdFromEnvironment();

        } catch (IdUnavailableException e) {

            System.out.println("Logging in as guest");

            return GUEST_USER_ID;

        }

    }

    ... // The rest of the program is unchanged

}


This version of the program is clearly correct and is more readable than the original because it adds a descriptive name for the field value computation, where the original version had only an anonymous static initializer block. With this change to the program, it works as expected.
In summary, most programmers do not need to learn the details of the definite assignment rules. Usually the rules just do the right thing. If you must refactor a program to eliminate a compilation error caused by the definite assignment rules, consider adding a new method. Besides solving the definite assignment problem, it may offer an opportunity to make the program more readable.

No comments:

Post a Comment

Your comments are welcome!