A Comprehensive Theory of
Adding 'const' to Java


By David R. Tribble
david@tribble.com

Revision 1.1, 2001-12-28

Contents


Note

Due to a hole in the const type system, the dangerous sections of this proposal are presented in a different color, and are also marked [DANGEROUS]. See the Problems section for more details.

Concepts

A reference variable or constant declared as 'final' has a value that is immutable and cannot be modified to refer to any other object than the one it was initialized to refer to. Thus the 'final' specifier applies to the value of the variable itself, and not to the object referenced by the variable.

A reference variable or constant declared as 'const' refers to an immutable object that cannot be modified. The reference variable itself can be modified (if it is not declared 'final'). Thus the 'const' specifier applies to the value of the object referenced by the variable.

An object is modified whenever any of its accessible member variables are the subject of an assigment (or compound assignment) or increment/decrement operator. An object is also considered modified whenever a call is made to any of its member methods that are not declared 'const' (see below).

Class Variables

Only reference (non-primitive) types can have the 'const' specifier applied to them. Primitive types that need to be declared 'const' should be declared 'final' instead.

A member variable declared 'const' and declared with a reference type (i.e., any class type extending 'Object' or any interface type), or declared as an array of any type, refers to an immutable object whose value cannot be modified. The reference variable itself can be modified, provided that it is not declared 'final'.

The following code fragment illustrates these rules:

    class Foo
    {
        int             max =       100;
        final int       LENGTH =    80;
        const int       GREAT =     15;     // Error, primitive type
        final const int LEAST =     2;      // Error, primitive type

        Bar             b =     new Bar(17);
        final Bar       bf =    new Bar(23);
        const Bar       bc =    new Bar(55);
        final const Bar bfc =   new Bar(79);
    }

The member variable 'max' is modifiable.

The member constant 'LENGTH' is not modifiable (because it is 'final').

The member variable 'b' is modifiable, and refers to an object that is modifiable.

The member constant 'bf' is not modifiable (because it is 'final'), but the object to which it refers is modifiable.

The member constant 'bc' is modifiable, but the object to which it refers is not modifiable (because it is 'const').

The member constant 'bf' is not modifiable (because it is 'final'), and the object to which it refers is not modifiable (because it is 'const').

Assigment Expressions

Expressions of reference type, either const or non-const, can be freely assigned to const variables of compatible reference types, and can be freely passed to methods as const arguments of compatible reference types.

Expressions of const reference type can only be assigned to const variables, or passed as const arguments to methods, of compatible reference types.

[DANGEROUS]
An expression of const reference type can be cast to a compatible non-const reference type in certain situations (see the Special Exemptions and Cast Expressions sections below).

Consider the following code fragment:

    class Foosball
        extends Bar
    {
        const Bar       bc;
        Foosball        fb;
        const Foosball  fc;

        void enconst1(Foosball f)
        {
            bc = f;         // Okay, implicit cast
            fb = f;         // Okay
            fc = f;         // Okay, implicit cast
        }

        void enconst2(const Foosball f)
        {
            bc = f;         // Okay, implicit cast
            fb = f;         // Error, f is const
            fc = f;         // Okay
        }
    }

Special Exemptions

[DANGEROUS]
The special cases proposed in this section open a hole in the const type safety semantics. (See the Problems section for more details.)

[DANGEROUS]
As a special case, a const member variable (static or otherwise) with default (package private) access is allowed to be modified by non-const methods of its parent class. The variable must be cast to its equivalent non-const type first, though.

[DANGEROUS]
As another special case, a const member variable (static or otherwise) with protected or public access is allowed to be modified by non-const methods of its parent class or by any non-const methods of any class extending its parent class. The variable must be cast to its equivalent non-const type first, though.

[DANGEROUS]
In order to modify a const member variable, the variable must first be cast to its equivalent non-const type. (See the Cast Expressions section below for more details.)

Private const member variables cannot be modified by any method of any class.

Local variables and parameters that are declared 'const' cannot be cast to their equivalent non-const types.

The following code fragment illustrates these rules:

    class Fooey
    {
        const Bar           bc = new Bar(44);
        private const Bar   bp = new Bar(55);

        void f(const Bar b)
        {
            b.poke(1);              // Error, Bar.poke() is not const
            ((Bar)b).poke(1);       // Error, cannot cast b
            bc = new Bar(61);       // Error, bc is const
            (Bar)bc = new Bar(61);  // Okay
            bc.peek();              // Okay, Bar.peek() is const
            bc.poke(43);            // Error, Bar.poke() is not const
            ((Bar)bc).poke(43);     // Okay
            bp = new Bar(72);       // Error, bp is private const
            bp.peek();              // Okay, Bar.peek() is const
            bp.poke(43);            // Error, Bar.poke() is not const
            ((Bar)bp).poke(43);     // Error, bp is private const
        }
    }

[DANGEROUS]
These special cases allow a class to have member variables that are readable by clients of its parent class, but which are modifiable only by methods from its parent class or classes from a restricted set of classes related to the parent class.

Interface Constants

Since all member variables of interfaces must be declared 'final', all such member variables are actually constants. In all other respects, the rules for const member constants are the same as for const class member variables.

Consider the following code fragment:

    interface Baz
    {
        static final Bar        bf =  new Bar(1);
        static const Bar        bc =  new Bar(2);   // Implicitly final
        static final const Bar  bfc = new Bar(3);
    }

Member methods of classes that implement interface 'Baz' cannot modify any of the three constants 'bf', 'bc', or 'bfc' (because they are final). Such methods are allowed to modify the Bar object referenced by 'bf', but cannot modify the objects referenced by 'bc' or 'bfc' (because they are const).

Class Methods

Static member methods declared 'const' cannot modify any static class variables of their parent class. They also cannot modify any objects referenced by any static class reference variables of their parent class.

Non-static member methods declared 'const' may not modify any member variables (static or not) of their parent class object ('this'). They also cannot modify any objects referenced by any class reference variables (static or not) of their parent class.

A method declared 'const' declares that it cannot modify any member variables of its class, nor any objects referenced by those variables. Conversely, a method that is not declared 'const' declares that it may modify member variables of its class or objects referenced by them (and should be assumed to do so, whether it actually does or not).

The following code fragment illustrates these rules:

    class Foomy
    {
        static Bar  bs =    new Bar(15);
        Bar         b =     new Bar(34);
        const Bar   b2 =    new Bar(53);

        void frob()             // Not const
        {
            bs = new Bar(48);   // Okay
            b = new Bar(81);    // Okay
            frotz();            // Okay
            b.poke(13);         // Okay
            b.peek();           // Okay
            b2.poke(13);        // Error, Bar.poke() is not const
            b2.peek();          // Okay, Bar.peek() is const
        }

        void fraz() const       // Is const
        {
            bs = new Bar(48);   // Error, fraz() is const
            b = new Bar(81);    // Error, fraz() is const
            frotz();            // Error, frotz() is not const
            b.poke(13);         // Error, Bar.poke() is not const
            b.peek();           // Okay, Bar.peek() is const
            b2.poke(13);        // Error, Bar.poke() is not const
            b2.peek();          // Okay, Bar.peek() is const
        }

        void frotz()            // Not const
        { ... }
    }

Member method 'frob()' is not declared 'const', so it can modify any non-const member variables of class 'Foomy'. It cannot modify any const member variables, though, such as 'b2'.

Member method 'fraz()' is declared 'const', so it cannot modify any member variables of class 'Foomy'. It also cannot indirectly modify any member of the class by calling non-const member methods, such as 'frotz()'. It also cannot indirectly modify any member by calling non-const methods on those members, such as 'b.poke()' (which is not declared 'const' in this example).

Member method 'frotz()' is not declared 'const', so it must be assumed to modify member variables of class 'Foomy'.

A non-static member method of a class that extends another class or implements an interface which overrides a method of the base class or implements a method of the interface must be declared with a "const-ness" that is at least as restrictive as the method it overrides. In other words, if a method in a base class is declared 'const', then all subclass methods that override that method must also be declared 'const'; on the other hand, an overriding method may be declared 'const' even if the method it overrides is not declared 'const'. (In this respect, 'const' specifiers are similar to 'throws' clauses.)

The following code fragment illustrates these rules:

    class Foomier
        extends Foomy
    {
        void frob() const       // Added const
        { ... }

        void fraz() const       // Must be const
        { ... }
    }

    class Foomiest
        extends Foomy
    {
        void fraz()             // Error, Must be const
        { ... }
    }

Local Variables

Variables local to a method body may be declared 'const', in which case the objects they refer to cannot be modified. The reference variables themselves can be modified provided they are not declared 'final'.

Consider the following code fragment:

    void f()
    {
        Object              o = new Object();
        final Object        fo = new Object();
        const Object        co = new Object();
        final const Object  fco = new Object();
        ...
    }

The object referenced by 'o' can be modified, and variable 'o' can be modified to refer to a different object.

The object referenced by 'fo' can be modified, but variable 'fo' cannot be modified to refer to any other object (because it is 'final').

The object referenced by 'co' cannot be modified (because it is const), but variable 'co' can be modified to refer to another object.

The object referenced by 'fco' cannot be modified (because it is const), and variable 'fco' cannot be modified to refer to another object (because it is 'final').

Method Parameters

The same rules apply to method parameters as to local variables. That is, a reference parameter declared 'final' cannot be modified, but the object which it references can be modified. A reference parameter declared 'const' can be modified, but the object which it references cannot be modified.

Cast Expressions

[DANGEROUS]
A reference variable declared 'const' may be cast to its equivalent non-const type in certain special cases, allowing the object to which it refers to be modified. Such casting removes the "const-ness" of the object referred to.

[DANGEROUS]
Such casting is allowed only within an expression within the body of a method that is not declared 'const' that is a member of the class containing the member variable being cast. Parameters and local variables cannot have their "const-ness" cast away in such a manner.

[DANGEROUS]
The result of casting a const reference variable to its equivalent non-const type is a reference to the same object, but which allows the object to be modified. The result of such an expression can be used anywhere a cast expression could normally be used, and also as the left-hand-side target of an assignment (or compound assignment) or increment/decrement expression.

Local variables and parameters that are declared 'const' cannot be cast to their equivalent non-const types.

The following code fragment illustrates these rules:

    class Foobar
    {
        int                 cnt;
        const Bar           b =     new Bar(99);
        private const Bar   b2 =    new Bar(33);

        void f(const Bar bc)
        {
            bc.memb++;              // Error, bc is const
            ((Bar)bc).memb++;       // Error, bc in const
            bc.peek();              // Okay, Bar.peek() is const
            bc.poke(+3);            // Error, Bar.poke() is not const
            ((Bar)bc).peek();       // Error, cannot cast bc
            ((Bar)bc).poke(-3);     // Error, cannot cast bc
        }

        void incr1()
        {
            cnt = 1;                // Okay
            cnt++;                  // Okay
            b.peek();               // Okay, Bar.peek() is const
            b.poke(5);              // Error, b is const, Bar.poke() is not
            ((Bar)b).poke(5);       // Okay
            b = new Bar(19);        // Error, b is const
            (Bar)b = new Bar(19);   // Okay
            b2.poke(7);             // Error, b2 is private const
            ((Bar)b2).poke(7);      // Error, b2 is private const
        }

        void incr2() const
        {
            cnt = 1;                // Error, incr2() is const
            cnt++;                  // Error, incr2() is const
            b.poke(5);              // Error, incr2() is const
            ((Bar)b).poke(5);       // Error, incr2() is const
            b = new Bar(19);        // Error, incr2() is const
            (Bar)b = new Bar(19);   // Error, incr2() is const
            b2.poke(7);             // Error, incr2() is const
            ((Bar)b2).poke(7);      // Error, incr2() is const
        }
    }

Conversely, a reference variable may be (implictly) cast to its equivalent const type, which adds "const-ness" to the resulting reference expression. There is no syntax for an explicit such cast, since assigning a const or non-const reference expression to a const variable does not require an explicit cast.

Consider the following code fragment:

    class Fooberry
    {
        const Bar   bc;

        const Bar enconst(Bar b)
        {
            bc = b;                 // Okay, implicit cast
            return b;               // Okay, implicit cast
        }

        const Bar addconst(Bar b)
        {
            bc = (const Bar)b;      // Error, cast is not needed
            return (const Bar)b;    // Error, cast is not needed
        }
    }

Method Return Values

Methods may return non-primitive const object types, which means that the values returned by such methods cannot be modified (but that references to such objects can be assigned and passed to other methods).

This implies that the return value of a method declared as returning a const type can be assigned to only a const reference variable or passed as an argument to a method taking a const reference parameter.

Consider the following code fragment:

    class Foodor
    {
        Bar getBar()
        { ... }

        const Bar cBar()
        {
            return new Bar();   // Okay, cast to const Bar
        }

        void f()
        {
            Bar         b;
            const Bar   bc;

            b = getBar();       // Okay
            bc = getBar();      // Okay, non-const assigned to const
            b = cBar();         // Error, cBar() returns const
            bc = cBar();        // Okay, const assigned to const
        }
    }

Problems

Allowing class methods to modify const objects opens up a hole in the const type safety semantics.

Consider the following code fragment:

    class Owner
    {
        public static const Bar     bs = new Bar(0);
        ...
    }

    class Thief
    {
        const Bar   bc;

        void steal()
        {
            // Create a reference to a const Bar object
            bc = Owner.bs;

            // Now modify the const Bar object
            ((Bar)bc).poke(9);                      // DANGEROUS
        }
    }

The statement marked DANGEROUS modifies a const object referenced by a const member of class 'Owner', by first making one of its own member variables share a reference to the object, and then by modifying the object through the member reference.

In the interests of const type safety, the portions of this proposal marked [DANGEROUS] should be omitted.

Conclusion

Adding the 'const' specifier keyword to Java would bring new forms of type safety to the language, and simplify the semantics of "read-only" objects.


Revision History

1.1, 2001-12-28.
Found a hole in the 'const' type system. Marked the relevant sections as "dangerous".

1.0, 2001-12-28.
First revision.


Copyright ©2001 by David R. Tribble, all rights reserved.

References and links to this document may be created without permission from the author. This document or portions thereof may be used or quoted without permission from the author provided that appropriate credit is given to the author. This document may be printed and distributed without permission from the author on the condition that the authorship and copyright notices remain present and unaltered. These restrictions are waived for private, national, and international standards bodies and committees, and for Sun Microsystems, Inc., which may use or reproduce the contents of this document in any form or manner.

The author can be reached by email at david@tribble.com.
The author's home web page is at http://david.tribble.com.

Last modified: Fri Dec 28 22:15:00 Central Standard Time 2001