站内搜索: 请输入搜索关键词
当前页面: 在线文档首页 > JDK 5 Documentation v1.1.8, Java 2 SDK 英文文档

JDK 5 Documentation v1.1.8, Java 2 SDK 英文文档

Contents | Prev | Next Inner Classes Specification


What are top-level classes and inner classes?

In previous releases, Java supported only top-level classes, which must be members of packages. In the 1.1 release, the Java 1.1 programmer can now define inner classes as members of other classes, locally within a block of statements, or (anonymously) within an expression.

Here are some of the properties that make inner classes useful:

Inner classes result from the combination of block structure with class-based programming, which was pioneered by the programming language Beta. Using block structure with inner classes makes it easier for the Java programmer to connect objects together, since classes can be defined closer to the objects they need to manipulate, and can directly use the names they need. With the removal of restrictions on the placement of classes, Java's scoping rules become more regular, like those of classical block structured languages such as Pascal and Scheme.

In addition, the programmer can define a class as a static member of any top-level class. Classes which are static class members and classes which are package members are both called top-level classes. They differ from inner classes in that a top-level class can make direct use only of its own instance variables. The ability to nest classes in this way allows any top-level class to provide a package-like organization for a logically related group of secondary top-level classes, all of which share full access to private members.

Inner classes and nested top-level classes are implemented by the compiler, and do not require any changes to the Java Virtual Machine. They do not break source or binary compatibility with existing Java programs.

All of the new nested class constructs are specified via transformations to Java 1.0 code that does not use inner classes. When a Java 1.1 compiler is producing Java virtual machine bytecodes, these bytecodes must represent the results of this (hypothetical) source-to-source transformation, so that binaries produced by different Java 1.1 compilers will be compatible. The bytecodes must also be tagged with certain attributes to indicate the presence of any nested classes to other Java 1.1 compilers. This is discussed further below.

Example: A simple adapter class

Consider the design of an adapter class, which receives method invocations using a specified interface type on behalf of another object not of that type. Adapter classes are generally required in order to receive events from AWT and Java Bean components. In Java 1.1, an adapter class is most easily defined as an inner class, placed inside the class which requires the adapter.

Here is an incomplete class FixedStack which implements a stack, and is willing to enumerate the elements of the stack, from the top down:

    public class FixedStack {
        Object array[];
        int top = 0;
        FixedStack(int fixedSizeLimit) {
            array = new Object[fixedSizeLimit];
        }
    
        public void push(Object item) {
            array[top++] = item;
        }
        public boolean isEmpty() {
            return top == 0;
        }
        // other stack methods go here...
    
        /** This adapter class is defined as part of its target class,
         *  It is placed alongside the variables it needs to access.
         */
        class Enumerator implements java.util.Enumeration {
            int count = top;
            public boolean hasMoreElements() {
                return count > 0;
            }
            public Object nextElement() {
                if (count == 0)
                    throw new NoSuchElementException("FixedStack");
                return array[--count];
            }
        }
        public java.util.Enumeration elements() {
            return new Enumerator();
        }
    }

The interface java.util.Enumeration is used to communicate a series of values to a client. Since FixedStack does not (and should not!) directly implement the Enumeration interface, a separate adapter class is required to present the series of elements, in the form of an Enumeration. Of course, the adapter class will need some sort of access to the stack's array of elements. If the programmer puts the definition of the adapter class inside of FixedStack, the adapter's code can directly refer to the stack object's instance variables.

In Java, a class's non-static members are able to refer to each other, and they all take their meaning relative to the current instance this. Thus, the instance variable array of FixedStack is available to the instance method push and to the entire body of the inner class FixedStack.Enumerator. Just as instance method bodies "know" their current instance this, the code within any inner class like Enumerator "knows" its enclosing instance, the instance of the enclosing class from which variables like array are fetched.

One of the ways in which the FixedStack example is incomplete is that there is a race condition among the operations of the FixedStack and its Enumerator. If a sequence of pushes and pops occurs between calls to nextElement, the value returned might not be properly related to previously enumerated values; it might even be a "garbage value" from beyond the current end of the stack. It is the responsibility of the programmer to defend against such race conditions, or to document usage limitations for the class. This point is discussed later. One defense against races looks like this:

    public class FixedStack {
        ...
        synchronized public void push(Object item) {
            array[top++] = item;
        }
        class Enumerator implements java.util.Enumeration {
            ...
            public Object nextElement() {
                synchronized (FixedStack.this) {
                    if (count > top)  count = top;
                    if (count == 0)
                        throw new NoSuchElementException("FixedStack");
                    return array[--count];
                }
            }

The expression FixedStack.this refers to the enclosing instance.

Example: A local class

When a class definition is local to a block, it may access any names which are available to ordinary expressions within the same block. Here is an example:

        Enumeration myEnumerate(final Object array[]) {
            class E implements Enumeration {
                int count = 0;
                public boolean hasMoreElements()
                    { return count < array.length; }
                public Object nextElement()
                    { return array[count++]; }
            }
            return new E();
        }

For the moment, we say nothing about how this code works, but Java's rules of scoping and variable semantics precisely require what this code does. Even after the method myEnumerate returns, array can still be used by the inner object; it does not "go away" as in C. Instead, its value continues to be available wherever that value is required, including the two methods of E.

Note the final declaration. Local final variables such as array are a new feature in 1.1. In fact, if a local variable or parameter in one class is referred to by another (inner) class, it must be declared final. Because of potential synchronization problems, there is by design no way for two objects to share access to a changeable local variable. The state variable count could not be coded as a local variable, unless perhaps it were changed a one-element array:

        Enumeration myEnumerate(final Object array[]) {
            final int count[] = {0}; // final reference to mutable array
            class E implements Enumeration {
                public boolean hasMoreElements()
                    { return count[0] < array.length; }   ...

(Sometimes the combination of inheritance and lexical scoping can be confusing. For example, if the class E inherited a field named array from Enumeration, the field would hide the parameter of the same name in the enclosing scope. To prevent ambiguity in such cases, Java 1.1 allows inherited names to hide ones defined in enclosing block or class scopes, but prohibits them from being used without explicit qualification.)

Anonymous classes

In the previous example, the local class name E adds little or no clarity to the code. The problem is not that it is too short: A longer name would convey little additional information to the maintainer, beyond what can be seen at a glance in the class body. In order to make very small adapter classes as concise as possible, Java 1.1 allows an abbreviated notation for local objects. A single expression syntax combines the definition of an anonymous class with the allocation of the instance:

        Enumeration myEnumerate(final Object array[]) {
            return new Enumeration() {
                int count = 0;
                public boolean hasMoreElements()
                    { return count < array.length; }
                public Object nextElement()
                    { return array[count++]; }
            };
        }

In general, a new expression (an instance creation expression) can end with a class body. The effect of this is to take the class (or interface) named after the new token, and subclass it (or implement it) with the given body. The resulting anonymous inner class has the same meaning as if the programmer had defined it locally, with a name, in the current block of statements.

Anonymous constructs like these must be kept simple, to avoid deeply nested code. When properly used, they are more understandable and maintainable than the alternatives-named local classes or top-level adapter classes.

If an anonymous class contains more than a line or two of executable code, then its meaning is probably not self-evident, and so a descriptive local name should be given to either the class or (via a local variable) to the instance.

An anonymous class can have initializers but cannot have a constructor. The argument list of the associated new expression (often empty) is implicitly passed to a constructor of the superclass.

As already hinted, if an anonymous class is derived from an interface I, the actual superclass is Object, and the class implements I rather than extending it. (Explicit implements clauses are illegal.) This is the only way an interface name can legally follow the keyword new. In such cases, the argument list must always be null, to match the constructor of the actual superclass, Object.


Contents | Prev | Next

Inner Classes Specification (HTML generated by dkramer on March 15, 1997)
Copyright © 1996, 1997 Sun Microsystems, Inc. All rights reserved
Please send any comments or corrections to john.rose@eng.sun.com