13. Inner Classes, Interfaces & Anonymity

Part of CS:2820 Object Oriented Software Development Notes, Spring 2017
by Douglas W. Jones
THE UNIVERSITY OF IOWA Department of Computer Science

 

Where Were We?

We has just suggested this code for receiving a parameter where the computation of the value of that parameter was to be deferred until it was actually needed. The idea is that the caller must construct a new subclass of class ErrorMessage that has a method to do the desired computation, and then create an instance of this subclass and pass that instance as a parameter so that the called code can call the method when it needs to do the required computation.

public abstract class ErrorMessage {
    public abstract String myString();
}

static void lineEnd( Scanner sc, ErrorMessage message ) {
    String skip = sc.nextLine();
    if (!"".equals( skip )) {
        Errors.warning( message + message.myString() );
    }
}

This sounds pretty awful, but Java provides some shorthand to make it easy. We'll do the awful long-winded solution first before we look at the shorthand notation.

A Preliminary Approach

Where our original call to lineEnd said something like this:

ScanSupport.lineEnd( sc, "Intersection " + name );

We could now write this new supporting class:

class MyMessage implements ScanSupport.ErrorMessage {
    private String msg;
    MyMessage( String m ) {
        msg = m;
    }
    public myString s() {
        return "Intersection " + msg; 
    }
}

and in the code where we want to call line end, we do this:

MyMessage msg = new MyMessage( name );
ScanSupport.lineEnd( sc, msg );

The above code is hardly convenient! We had to create a new class with its own fields and initializer as well as the method that encapsulates our delayed computation, all to pass a simple 2-operand expression. Doing this over and over, once for each call to lineEnd() promises to make a totally unreadable program. Fortunately, there is an alternative.

Inner Classes

The long winded code we just gave would work equally well no matter where we declared the new class MyMessage. That forced us to pass all the information that was to be carried in the msg object as parameters to the initializer. There is an alternative. So long as the class definition is local to the context where the variables being referenced lie, we may directly reference those variables from the code of a method in the class. Here is the code, in context:

class Road {
    String name;
    ...
    public Intersection( ... ) {
        ...

        class MyMessage implements ScanSupport.ErrorMessage {
            public myString s() {
                return "Intersection " + name;
            }
        }
        MyMessage msg = new MyMessage();
        ScanSupport.lineEnd( sc, msg );
    }
}

In this code, the myString() method takes advantage of the fact that it is declared inside the Intersection() initializer. That means that it can access any variables declared locally to the initializer, up to a point -- Java's implementation of nesting is weak. Similarly, the constructor is declared inside the class Road, so it can access any variables declared within the class.

It is easy to state this scope rule: Anything declared within an outer block may be referenced from within a block nested inside that block. In Java, blocks begin with an opening curly brace { and end with a closing curly brace }.

Implementing this rule is far more difficult. In effect, each nested pair of curly braces implies the creation of an object. Each class is, in effect, the declaration of an object that holds the static fields of that class and the real constructor for objects of that class. And, when you call a method of a class (including an explicit constructor), a temporary object is allocated to hold the local variables of that method. Access to this temporary object is usually lost when the method returns.

There is always a pointer to (a handle for) the current object inside the CPU, so within the code of any object, access to the fields of that object is easy. Java actually gives a name to this handle, this. The more interesting question is, how do you access the fields of enclosing objects?

The most general implementation works as follows: Except at the outermost nesting level, each object has an implicit final field that is never explicitly mentioned in your code. It is common to call this the enclosing scope pointer or the uplink, but we'll just call it up. Whenever a new object is created, the uplink in that new object is set to point to the object that encloses this object.

We can rewrite the above code with these explicit uplinks as follows:

class Road {
    String name;
    ...
    public Intersection( ... ) {
        ...

        class MyMessage implements ScanSupport.ErrorMessage {
            public myString s() {
                return "Intersection " + up.up.up.name
            }
        }
        MyMessage msg = new MyMessage();
        ScanSupport.lineEnd( sc, msg );
    }
}

This looks ugly, but this is a problem that has been understood since the 1960s. Compilers eliminate all the uplinks that they can, and they notice common subexpressions. If you see 2*a+x(2*a), you can easily imagine a compiler smart enough to evaluate the subexpression 2*a once and then use that value first to call x() and then to add it to the result. Similarly, a compiler can see up.up.a+up.up.up.b and evaluate up.up just once before grabbing fields a and up.b relative to that subexpression.

The point is, compiler technology is good enough that you shouldn't worry about the computational cost of up-level addressing. Yes, uplinks slow things down a bit, but not so much that you should hesitate to use them.

Unfortunately, Java does not use the general mechanism we described above, it uses a simpler mechanism. When code within an inner class in Java references a variable in an outer class, Java makes a final copy of the value of the referenced variable and includes it as a field of the inner class. In order to hide the fact that it is doing so, Java forbids referencing any variables in outer classes from within inner classes that are not final or effectively final.

With this restriction in mind, you are free to use nested class delcarations if it makes your code easier to read. It even makes sense to nest things farther:

class Road {
    String name;
    ...
    public Intersection( ... ) {
        ...

        {
            class MyMessage implements ScanSupport.ErrorMessage {
                public toString s() {
                    return "Intersection " + name
                }
            }
            MyMessage msg = new MyMessage();
            ScanSupport.lineEnd( sc, msg );
        }
    }
}

What we did above is add an extra pair of braces and an extra indenting level so that class MyMessage and the object msg are totally local to this call to lineEnd(). That way, you don't need to worry about inventing new names that don't collide with the names used for other subclasses of ErrorMessage.

Interfaces

Before we finish the alternative, let's look back at the code for our abstract class ErrorMessage:

public abstract class ErrorMessage {
    public abstract String myString();
}

Notice that this class has no fields and no methods that are not abstract. All it does is define the interface to a class that may have many implementations. In Java, we can use the keyword interface instead of abstract class in this context. When we declare an interface instead of an abstract class, Java forbids declaring field, and all of the methods are implicitly abstract. So we can replace the above with this:

public interface ErrorMessage {
    public String myString();
}

When you declare something as an interface, classes that build on that interface are said to implement that interface, so you use the keyword implements instead of extends when you use the interface as the basis of a class.

Anonymous Objects and Classes

It is annoying to declare a named item in a block when that name is only used once and the name itself doesn't convey useful information. Different language designers have taken vastly different positions about the need for anonymous items. Some languages prevent most anonymity, while others permit it in some cases and not in others.

There is one context where anonymous variables are almost universally permitted. That is, within expresssions. When you write the statement:

i = j*k + l*m;

You are really writing something like this:

{
    int t1 = j*k;
    int t2 = l*m;
    i = t1 + t2;
}

Here t1 and t2 are local variables created by the compiler to hold the intermediate results during the evaluation of the expression. These variables come into existence at the point they are needed and go out of existence as soon as they are no longer needed.

Returning to the code we are working on, we can apply this idea immediately to eliminate the need for a named variable holding the object that holds our delayed parameters:

        {
            class MyMessage implements ScanSupport.ErrorMessage {
                public myString s() {
                    return "Intersection " + name
                }
            }
            ScanSupport.lineEnd( sc, new MyMessage() );
        }

That leaves us with the desire to eliminate need for a named subclass of ErrorMessage. The designers of Java could have forbidden anonymity here, since Java considers classes to be an entirely different kind of thing from objects. The desingers relented, however, and provided a way to declare an anonymous class as an instance of that class is created. The following code is exactly equivalent to the above, except that it uses an anonymous class:

        ScanSupport.lineEnd(
            sc,
            new ScanSupport.ErrorMessage() {
                public myString s() {
                    return "Intersection " + name
                }
            }
        )

Here, we put the body of the class declaration right in the constructor call after the name of the interface it implements. This completely avoids the need to give this particular implementation of the class its own name.

Admittedly, the text of this call to lineEnd() has gotten a bit long and difficult to format in a pleasing way, but we no longer have any extraneous identifiers for things that are never used again.

A Brief Historical Note

This is old stuff, dating back to the 1950's. Back when people first started designing high-level programming languages that allowed parameter passing, there were many arguments about how to do it. Several approaches were common in the early days. For example, in FORTRAN, the first commercially available high level language, if you called F(X) (a call to a function named F passing the parameter X), the expression X was first evaluated, and then the value of that expression was assigned to a static variable local to the function F. Since the designers of FORTRAN didn't worry about recursion, that was adequate.

In the programming language Algol 60, the designers decided that if f(x) was defined as a×x2+b×x+c, then the call f(z+1) should mean the same thing as a×(z+1)2+b×(z+1)+c. That is, passing a parameter is equivalent to substituting the text of that parameter into every place where the parameter occurs within the called code. They had to stand on their head a bit with this, saying that first, all identifiers in the called routine that conflicted with any identifiers in the parameter were renamed so that there were no naming conflicts. This was called passing the parameters by name and the effect it has is to defer the evaluation of any expressions in the parameter until the value of the parameter is actually needed.

(Yes, in the official documentation for Algol 60, identifiers were usually presented in italics and the text was in lower case. This was despite the fact that most computers of the era were upper-case only and had no italics.)

In the implementation of Algol 60, each parameter was used to define an anonymous function, called a thunk, and when the value of the parameter is needed in a called routine, the called code would then call the thunk. It is not wrong to refer to the objects we are passing in our Java code as thunks if the only reason for passing this object is to serve as a handle on a method that can be called to evaluate a deferred parameter.

Lambda

Java lets you abbreviate the above code in a remarkably compact form:

        ScanSupport.lineEnd(
            sc,
            () -> "Intersection " + name
        );

This is called a lambda expression or λ-expression. The text before the -> is the formal parameter list for the method we are passing, and the code after the -> is the body of the method.

This notation comes with severe restrictions: It only applies if the expression is passed in a context where the expected class of the argument is defined as an interface, and where there is just one public method of that interface that matches the argument list. Furthermore, in the interest of efficient implementation, Java restricts the set of permitted variable references inside the λ-expression. Specifically, the only variables you are allowed to reference are final or "effectively final" variables. Java's definition of the latter leaves quite a bit to be desired, but this usually doesn't cause much trouble.

Lambda?

The idea of lambda expressions is old, dating back to the 1930s, when it was first developed by Alonzon Church. Computers had not been invented yet, and church was interested in the foundations of mathematics, not programming languages. He was trying to give a clear definition of the concept of a computable function. Alan Turing was also working on this problem, and came up with a compltetely different approach, using what is now called a Turing machine. Church and Turing learned of each other's work, and it was not long before the proof that both Church's approach to defining computable functions using his lambda calculus and Turing's approach using Turing machines were equivalent. As a result, we now refer to their definition of computability as the Church-Turing thesis.

Why is it called Lambda notation (and Lambda calculus)? Because Church used the Greek letter lambda λ in his notation. Where conventional mathematical notation might define the function f as:

f(x) = x + 1

Church used a new notation:

f = λx.x + 1

The advantage of this notation is that it allows function names such as f to be treated as variables. The symbol λ can be treated as an operator that takes two arguments (separated by a period), the parameter list, in this case x, and the expression used to compute the value of the function, x+1 here.

Lambda notation was viewed as an arcane mathematical notation until 1960, when the developers of the LISP programming language at MIT decided to use lambda notation. The LISP equivalent of the above example would be:

(SET F (LAMBDA (X) (ADD X 1)))

LISP stands for LIst and String Processing language, but critics have described the LISP syntax as involving Lots of Infuriating Small Parentheses. LISP became the favored language for large areas of artificial intelligence research, and it lives on today in a variety of guises. The text editor EMACS, for example, is largely written in LISP, and uses LISP as its macro extension language. As with Church's lamda notation, LISP allows dynamic construction of new functions and it allows variables and parameters to have functions as their values.

Tools in other programming languages that allow the same basic flexibility are generally known as lambda notation even if they do not use the letter λ