Search code examples
javafinal

Allowing final fields to take default values


I am writing a parser for a binary file format. I have created classes to represent the deserialized structures I am reading, in which I would like to use final variables to hold the extracted data.

class MyObject {
    final int my_x;
    final int my_y;
    final int my_z;
}

The bump I am running into is that the presence of certain fields depends on certain flags being set. For example:

MyObject(InputStream is) {
    my_x = in.read();
    if (my_x == 1) {
        my_y = in.read();
        if (my_y == 1) {
            my_z = in.read();
        }
    }
}

However, this gives me an error because my_y and my_z may not be initialized. These conditionals can be 5-6 levels deep, and I don't want to track which fields may not be read at each level of the branch tree. Another complication is that, based on certain flags, there may be subobjects that I would like to handle with the same pattern as the top-level structures.

class MyObject {
    final int my_x;
    final SubObject my_subobject;

    MyObject(InputStream is) {
        my_x = is.read();
        if (my_x == 1)
            my_subobject = new SubObject(is);
    }

    class SubObject {
        final int sub_x;
        final int sub_y;

        SubObject(InputStream is) {
            sub_x = is.read();
            if (sub_x == 1)
                sub_y = is.read();
        }
    }
}

Is there any way to make my fields final without twisting the code to handle each possible combination of flags?


Solution

  • Use local variables and assign to the final fields at the end of the constructor.

    public MyObject(InputStream is) {
        int x = default_x_value;
        int y = default_y_value;
        int z = default_z_value;
        x = in.read();
        if (x == 1) {
            y = in.read();
            if (y == 1) {
                z = in.read();
            }
        }
        my_x = x;
        my_y = y;
        my_z = z;
    }
    

    Alternatively (as Jon Skeet suggests in his comment), use a static factory method that computes the appropriate values for a no-default constructor:

    public static MyObject makeMyObject(InputStream is) {
        int x = default_x_value;
        int y = default_y_value;
        int z = default_z_value;
        x = in.read();
        if (x == 1) {
            y = in.read();
            if (y == 1) {
                z = in.read();
            }
        }
        return new MyObject(x, y, z);
    }
    

    A third approach would be to define an initializer object class:

    public class MyObject {
        private static class MyObjectInitializer {
            int x = default_x_value;
            int y = default_y_value;
            int z = default_z_value;
            MyObjectInitializer(InputStream is) {
                x = in.read();
                if (x == 1) {
                    y = in.read();
                    if (y == 1) {
                        z = in.read();
                    }
                }
            }
        }
        public MyObject(InputStream is) {
            this(new MyObjectInitializer(is));
        }
        private MyObject(MyObjectInitializer init) {
            my_x = init.x;
            my_y = init.y;
            my_z = init.z;
        }
    }
    

    The initializer class may have utility on its own, in which case you could make it (and the corresponding MyObject constructor) public.