A Comprehensive Theory of Adding 'const' to Java
By David R. Tribble |
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 (the referrent of the reference variable).
A reference variable or constant declared as 'const' refers to an object that cannot be modified. The reference variable itself can be modified (if it is not declared 'final'), but the referrent object cannot be modified. 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 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.
An expression of const reference type cannot be cast to a compatible non-const (see the Cast Expressions section 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 } }
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 (it is assumed that 'Bar.peek()' is a const method and 'Bar.poke()' is not):
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 implements an interface method or overrides a method of a base class must be declared with a "const-ness" that is no more restrictive than the method it overrides. In other words, if a method in a base class is not declared as 'const', then all subclass methods that override that method must also not be declared as 'const'; on the other hand, an overriding method may omit the 'const' specifier if the method it overrides is declared as '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 // Error, cannot be const { ... } void fraz() const // Okay, same as base class method { ... } } class Foomiest extends Foomy { void fraz() // Okay, can be non-const { ... } }
These rules prevent accessing an object with a 'const' method by calling a the non-'const' version of the method in the object type's base class (or in one of the interface types it implements).
In the example above, for instance, an object of type 'Foomiest' can be modified by calling its 'fraz()' method, but cannot be modified by calling the same method of its base class 'Foomier'. Thus any reference to the object of the base class type has more restricted access to the object than references of its derived type.
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.
A reference variable declared 'const' cannot be cast to its equivalent non-const type. Such casting would remove the "const-ness" of the object referred to, and thereby violate const type safety.
Conversely, a reference variable may be implictly cast to its equivalent const type (by assignment or by passing it as an argument to a method), 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 } }
I did not specify an explicit syntax for const declarations, but this can be deduced from my code examples.
One sticky issue remains, though, which is how to declare const arrays (or to be precise, arrays of references to const objects). Possible syntaxes:
const T[] a; // Array of const T const T a[]; // Array of const T - -or- T const[] a; // Array of const T T a const[]; // Array of const T
This is the simple case. Reference variables a and a[i] are modifiable, but the referrent object of a[i] is not. Thus 'a=b' and 'a[i]=x' are okay, but 'a[i].poke()' is an error.
const T[][] b; // Array of array of const TThis is trickier. Variable b[i][j] is modifiable, as is b[i], and also b, but the referrent object of b[i][j] is not.
const T const[][] c; // Const array of array of const TVariable c is modifiable, but variable c[i] is not. However, variable c[i][j] is modifiable, but its referrent object is not.
const T[] const[] d; // Array of const array of const TVariable d is modifiable, as is variable d[i]. However, variable d[i][j] is not modifiable, nor is its referrent object.
T const[] const[] e; // Const array of const array of TVariable e is not modifiable, nor is e[i], nor is e[i][j]. However, the referrent object of e[i][j] is modifiable.
Confused yet? Keep in mind that this is only a suggested syntax. I don't particularly like it, but it seems to be the most efficient syntax, and it is the same syntax as that used for const array function parameters in C99.
(BTW, for those who have argued against using the keyword 'const', I must point out that const is already a reserved word in Java; we might as well use it instead of inventing yet another one.)
It might be prudent to simply mandate that arrays cannot be const, and that only the objects they refer to (their referrent objects) can be const. If so, the examples for a and b utilize the only valid syntax, and the other examples are not legal. The drawback to such a mandate is that while we could declare methods as returning arrays of const objects, we would not be able to declare them as returning const arrays (which seems like a fairly useful thing to be able to do).
Another (better) prudent approach might be to say that only whole arrays can be const, i.e., the dimensions of an array are either all const or all non-const. Thus we could have syntax something like:
const T[] a; // Array of const T T const[] b; // Const array of T const T const[] c; // Const array of const T const T[][] d; // Array of array of const T T const[][] e; // Const array of const array of T const T const[][] f; // Const array of const array of const TThe leading const applies to the referrent objects of an array declaration, and the const[]...[] modifier applies to the reference elements of the array variable itself.
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.