A Comprehensive Theory of Adding 'const' to Java
By David R. Tribble |
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.
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).
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').
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 } }
[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.
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).
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 { ... } }
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').
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.
[DANGEROUS]
[DANGEROUS]
[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.
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.
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 } }
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 } }
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.
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.
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.