7.4 Local Classes

A local class is an inner class that is defined in a block. This could be a method body, a constructor, a local block, a static initializer, or an instance initializer.

Blocks in a non-static context have a this reference available, which denotes an instance of the class containing the block. An instance of a local class, which is declared in such a non-static block, has an instance of the enclosing class associated with it. This gives such a non-static local class much of the same capability as a non-static member class.

However, if the block containing a local class declaration is defined in a static context (i.e., a static method or a static initializer), then the local class is implicitly static in the sense that its instantiation does not require any outer object. This aspect of local classes is reminiscent of static member classes. However, note that a local class cannot be specified with the keyword static.

Some restrictions that apply to local classes are

  • Local classes cannot have static members, as they cannot provide class-specific services. However, final static fields are allowed, as these are constants. This is illustrated in Example 7.9 at (1) and (2) in the NonStaticLocal class, and also by the StaticLocal class at (11) and (12).

  • Local classes cannot have any accessibility modifier. The declaration of the class is only accessible in the context of the block in which it is defined, subject to the same scope rules as for local variable declarations.

Example 7.9 Access in Enclosing Context
class Base {
    protected int nsf1;
}
class TLCWithLocalClasses {            // Top level Class
    private double nsf1;               // Non-static field
    private int    nsf2;               // Non-static field
    private static int sf;             // Static field

    void nonStaticMethod(final int fp) { // Non-static Method
        final int flv  = 10;           // final local variable
        final int hlv  = 30;           // final (hidden) local variable
              int nflv = 20;           // non-final local variable

        class NonStaticLocal extends Base { // Non-static local class
        //        static int f1;       // (1) Not OK. Static members not allowed.
            final static int f2 = 10;  // (2) final static members allowed.
            int    f3  = fp;    // (3) final param from enclosing method.
            int    f4  = flv;   // (4) final local var from enclosing method.
        //  double f5  = nflv;  // (5) Not OK. Only finals from enclosing method.
            double f6  = nsf1;         // (6) Inherited from superclass.
            double f6a = this.nsf1;    // (6a) Inherited from superclass.
            double f6b = super.nsf1;   // (6b) Inherited from superclass.
            double f7  = TLCWithLocalClasses.this.nsf1;// (7) In enclosing object.
            int    f8  = nsf2;         // (8)  In enclosing object.
            int    f9  = sf;           // (9)  static from enclosing class.
            int    hlv;                // (10) Hides local variable.
        }
    }

    static void staticMethod(final int fp) { // Static Method
        final int flv  = 10;           // final local variable
        final int hlv  = 30;           // final (hidden) local variable
              int nflv = 20;           // non-final local variable

        class StaticLocal extends Base { // Static local class
        //        static int f1;       // (11) Not OK. Static members not allowed.
            final static int f2 = 10;  // (12) final static members allowed.
            int    f3  = fp;    // (13) final param from enclosing method.
            int    f4  = flv;   // (14) final local var from enclosing method.
        //  double f5  = nflv;  // (15) Not OK. Only finals from enclosing method.
            double f6  = nsf1;         // (16) Inherited from superclass.
            double f6a = this.nsf1;    // (16a) Inherited from superclass.
            double f6b = super.nsf1;   // (16a) Inherited from superclass.
        //  double f7  = TLCWithLocalClasses.this.nsf1;//(17) No enclosing object.
        //  int    f8  = nsf2;         // (18)  No enclosing object.
            int    f9  = sf;           // (19)  static from enclosing class.
            int    hlv;                // (20) Hides local variable.
        }
    }
}

Accessing Declarations in Enclosing Context

Example 7.9 illustrates how a local class can access declarations in its enclosing context. Declaring a local class in a static or a non-static block, influences what the class can access in the enclosing context.

Accessing Local Declarations in the Enclosing Block

A local class can access final local variables, final method parameters, and final catch-block parameters in the scope of the local context. Such final variables are also read-only in the local class. This situation is shown at (3) and (4), where the final parameter fp and the final local variable flv of the method nonStaticMethod() in the NonStaticLocal class are accessed. This also applies to static local classes, as shown at (13) and (14) in the StaticLocal class.

Access to non-final local variables is not permitted from local classes, as shown at (5) and (15).

Declarations in the enclosing block of a local class can be hidden by declarations in the local class. At (10) and (20), the field hlv hides the local variable by the same name in the enclosing method. There is no way for the local class to refer to such hidden declarations.

Accessing Members in the Enclosing Class

A local class can access members inherited from its superclass in the usual way. The field nsf1 in the superclass Base is inherited by the local subclass NonStaticLocal. This inherited field is accessed in the NonStaticLocal class as shown at (6), (6a), and (6b) by using the field's simple name, the standard this reference, and the super keyword, respectively. This also applies for static local classes as shown at (16), (16a), and (16b).

Fields and methods in the enclosing class can be hidden by member declarations in the local class. The non-static field nsf1, inherited by the local classes, hides the field by the same name in the TLCWithLocalClasses class. The special form of the this construct can be used in non-static local classes for explicit referencing of members in the enclosing class, regardless of whether these members are hidden or not.

double f7 = TLCWithLocalClasses.this.nsf1; // (7)

However, the special form of the this construct cannot be used in a static local class, as shown at (17), since it does not have any notion of an outer object. The static local class cannot refer to such hidden declarations.

A non-static local class can access both static and non-static members defined in the enclosing class. The non-static field nsf2 and static field sf are defined in the enclosing TLCWithLocalClasses class. They are accessed in the NonStaticLocal class at (8) and (9), respectively. The special form of the this construct can also be used in non-static local classes, as previously mentioned.

However, a static local class can only directly access members defined in the enclosing class that are static. The static field sf in the TLCWithLocalClasses class is accessed in the StaticLocal class at (19), but the non-static field nsf1 cannot be accessed, as shown at (18).

Instantiating Local Classes

Clients outside the scope of a local class cannot instantiate the class directly because such classes are, after all, local. A local class can be instantiated in the block in which it is defined. Like a local variable, a local class must be declared before being used in the block.

A method can return instances of any local class it declares. The local class type must then be assignable to the return type of the method. The return type cannot be the same as the local class type, since this type is not accessible outside of the method. A supertype of the local class must be specified as the return type. This also means that, in order for the objects of the local class to be useful outside the method, a local class should implement an interface or override the behavior of its supertypes.

Example 7.10 illustrates how clients can instantiate local classes. The non-static local class Circle at (5) is defined in the non-static method createCircle() at (4), which has the return type Shape. The static local class Map at (8) is defined in the static method createMap() at (7), which has the return type IDrawable. The inheritance hierarchy of the local classes and their supertypes Shape and IDrawable is depicted in Figure 6.5, p. 273. The main() method creates a polymorphic array drawables of type IDrawable[] at (10), which is initialized at lines (10) through (13) with instances of the local classes.

Example 7.10 Instantiating Local Classes
interface IDrawable {                   // (1)
    void draw();
}
class Shape implements IDrawable {      // (2)
    public void draw() { System.out.println("Drawing a Shape."); }
}

class Painter {                         // (3) Top-level Class
    public Shape createCircle(final double radius) { // (4) Non-static Method
        class Circle extends Shape {    // (5) Non-static local class
            public void draw() {
                System.out.println("Drawing a Circle of radius: " + radius);
            }
        }
        return new Circle();            // (6) Object of non-static local class
    }
    public static IDrawable createMap() {  // (7) Static Method
        class Map implements IDrawable {   // (8) Static local class
            public void draw() { System.out.println("Drawing a Map."); }
        }
        return new Map();                  // (9) Object of static local class
    }
}

public class LocalClassClient {
    public static void main(String[] args) {
        IDrawable[] drawables = {       // (10)
          new Painter().createCircle(5),// (11) Object of non-static local class
          Painter.createMap(),          // (12) Object of static local class
          new Painter().createMap()     // (13) Object of static local class
        };
        for (int i = 0; i < drawables.length; i++)     // (14)
            drawables[i].draw();

        System.out.println("Local Class Names:");
        System.out.println(drawables[0].getClass());   // (15)
        System.out.println(drawables[1].getClass());   // (16)
    }
}

Output from the program:

Drawing a Circle of radius: 5.0
Drawing a Map.
Drawing a Map.
Local Class Names:
class Painter$1$Circle
class Painter$1$Map

Creating an instance of a non-static local class requires an instance of the enclosing class. The non-static method createCircle() is invoked on the instance of the enclosing class to create an instance of the non-static local class, as shown at (11). In the non-static method, the reference to the instance of the enclosing context is passed implicitly in the constructor call of the non-static local class at (6).

A static method can be invoked either through the class name or through a reference of the class type. An instance of a static local class can be created either way by calling the createMap() method as shown at (12) and (13). As might be expected, no outer object is involved.

As references to a local class cannot be declared outside of the local context, the functionality of the class is only available through supertype references. The method draw() is invoked on objects in the array at (14). The program output indicates which objects were created. In particular, note that the final parameter radius of the method createCircle() at (4) is accessed by the draw() method of the local class Circle at (5). An instance of the local class Circle is created at (11) by a call to the method createCircle(). The draw() method is invoked on this instance of the local class Circle in the loop at (14). The value of the final parameter radius is still accessible to the draw() method invoked on this instance, although the call to the method createCircle(), which created the instance in the first place, has completed. Values of final local variables continue to be available to instances of local classes whenever these values are needed.

The output in Example 7.10 also shows the actual names of the local classes. In fact, the local class names are reflected in the class filenames.

Another use of local classes is shown in the following example. The code shows how local classes can be used, together with assertions, to implement certain kinds of postconditions (see Section 5.10, p. 218). The basic idea is that a computation wants to save some data that is later required when checking a postconditon. For example, a deposit is made into an account, and we want to check that the transaction is valid after it is done. The computation can save the old balance before the transaction, so that the new balance can be correlated with the old balance after the transaction.

The local class Auditor at (2) acts as a repository for data that needs to be retrieved later to check the postconditon. Note that it accesses the final parameter, but declarations that follow its declaration would not be accessible. The assertion in the method check() at (4) ensures that the postcondition is checked, utilizing the data that was saved when the Auditor object was constructed at (5).

class Account {
    int balance;

    /** (1) Method makes a deposit into an account. */
    void deposit(final int amount) {

        /** (2) Local class to save the necessary data and to check
            that the transaction was valid. */
        class Auditor {

            /** (3) Stores the old balance. */
            private int balanceAtStartOfTransaction = balance;

            /** (4) Checks the postcondition. */
            void check() {
                assert balance - balanceAtStartOfTransaction == amount;
            }
        }

        Auditor auditor = new Auditor(); // (5) Save the data.
        balance += amount;               // (6) Do the transaction.
        auditor.check();                 // (7) Check the postcondition.
    }

    public static void main(String[] args) {
        Account ac = new Account();
        ac.deposit(250);
    }
}