1. Helpers are the Variants
Consider again the familiar problem of computing a
String
representation of an IList
that displays a comma-separated list of its elements delimited by a pair of matching parentheses. We have seen at least one way to compute such a String
representation using a visitor called ToStringAlgo
. Below are three different algorithms, ToString1
, ToString2
andToString3
, that compute the same String
representation.Main Visitor ToString1 | Tail-recursive helper ToString1Help |
|
|
Main Visitor ToString2 | Tail-recursive helper ToString2Help |
|
|
Main Visitor ToString3 | Non tail-recursive helper ToString3Help |
|
|
What makes each of the above different from one another is its helper visitor. Each helper defined in the above will only perform correctly if it is passed the appropriate parameter. In a sense, each helper is an implementation of the main visitor. We should hide each of them inside of its corresponding main visitor to ensure proper usage and achieve full encapsulation of the main visitor.
2. Hiding Helpers
The most secure way to hide the helper visitor is to move it inside of the main visitor and make it a
private static
class.Hiding Named Helper Visitor inside of ToString1 | Comments |
| Singleton Pattern |
| The helper visitor has a name, HiddenHelper , and is defined privately and globally (static ) inside of the main visitor ToString1WithHiddenHelper . |
| The main visitor calls on its hidden helper singleton to help complete the job. |
Hiding Named Helper Visitor inside of ToString2 | Comments |
| |
| |
| |
Hiding Named Helper Visitor inside of ToString3 | Comments |
| |
| |
| |
3. Anonymous Helpers
Anonymous Helper Visitor inside of ToString1 | Comments |
| |
| |
| |
Anonymous Helper Visitor inside of ToString2 | Comments |
| |
| |
| |
Anonymous Helper Visitor inside of ToString3 | Comments |
| |
| |
| |
4. Factory with Anonymous Inner Classes
Comments | |
| |
| |
| |
| |
| Note how the code inside the anonymous inner class references first and rest of the parameter list. first and rest are said to be in the closure of the anonymous inner class. Here is an important Java syntax rule: For an local inner class defined inside of a method to access a local variable of the method, this local variable must be declared as final . |
5. Classes defined inside of another Class
Besides fields and methods, a Java class can also contain other classes. And just like a field or method defined inside of a class, a class defined inside of another class can be static or non-static. Here is the syntax.
class X {
// fields of X ...
// methods of X ...
/**
* named class Y defined inside of class X:
*/
[public | protected | private] [static] [final] [abstract] class Y [ extends A] [implements B] {
// fields of Y ...
// methods of Y ...
// classes of Y ...
}
}
Scope Specifier
When an embedded class is defined as
static
, it is called a nested class. The members (i.e. fields, methods, classes) of a (static) nested class can access only static members of the enclosing class.When an embedded class is non-
static
, it is called an inner class. The members of an inner class can access ALL members of the enclosing class. The enclosing class (and its enclosing class, if any, and so on) contains the environment that completely defines the inner class and constitutes what is called the closure of the inner class. As all functional programmers should know, closure is a powerful concept. One of the greatest strength in the Java programming language is the capability to express closures via classes with inner classes. We shall see many examples that will illustrate this powerful concept in other modules.Inner classes do not have to be anonymous as shown in the above examples. They can be named as well.
Access Specifier
Just like any other class, a class defined inside of another class can be public, protected, package private, or private.
Extensibility Specifier
Just like a regular class, a final nested/inner class cannot extended.
Abstract Specifier
Just like a regular class, an abstract nested/inner class cannot be instantiated.
Inheritance Specifier
Just like a regular class, an nested/inner can extend any non-final class and implement any number of interfaces that are within its scope.
Usage
Nested classes are used mostly to avoid name clash and to promote and enforce information hiding. Examples?
Inner classes are used to create (at run-time) objects that have direct access to the internals of the outer object and perform complex tasks that simple methods cannot do. Most of the time, they are defined anonymously. For examples, "event listeners" for Java GUI components are implemented as inner classes. The dynamic behavior and versatility of these "listeners" cannot be achieved by the addition of a set of fixed methods to a GUI component. We shall study Java event handling soon!
An inner object can be thought as an extension of the outer object.
6. Closure
In functional programming, the closure of a function (lamdba) consists of the function itself and an environment in which the function is well-defined. In Java, a function is replaced by a class. An inner class is only defined in the context of its outer object (and the outer object of the outer object, etc...). An inner class together with its nested sequence of outer objects in which the inner class is well-defined is the equivalent of the notion of closure in functional programming. Such a notion is extremely powerful. Just like knowing how to effectively use lambda expressions and higher order functions is key to writing powerful functional programs in Scheme, effective usage of anonymous inner classes is key to writing powerful OO programs in Java.
Some important points to remember about closures and inner classes:
- An object's closure is defined at the time of its creation.
- An object "remembers" its closure for its entire lifetime.
- An inner object's closure includes any local variables that are declared as
final
, plus all fields of the enclosing object, includingprivate
ones. - Every time a factory method is run, where a new anonymous object is created, a new closure for that object is created. This is because the local variables could change between calls.
- Closures enable decoupled communication because an inner class can communicate with its outer class through its closure.
One of the most important ways in which we will use anonymous inner classes it to take advantage of their closure properties. Anonymous inner classes are the only objects in Java that can be instantiated in such a manner that the variables in their environments (closures) can be dynamically defined. That is, since an anonymous inner class can reference a local variable (that is declared
final
) and since local variables are created every time a method is called, then every the anonymous inner class object created has a different set of dynamically created variables that it is referencing. This means that we can make unique objects with unique behaviors at run time.Factories are a natural partner with anonymous inner classes. With a factory that returns anonymous inner classes, we can instantiate unique objects with unique behaviors. If the factory method takes in parameters, there local variables can be used to alter the resultant object's behavior, a process called "currying" (named after the famous mathematician/computer scientistHaskell Curry). The objects made by the factory are then sent off to various odd and sundry different parts of our OO system but all the while retaining their closures, which were determined at the time of their instantiation. Thus they retain the ability to communicate back to the factory that made them even though they are being used in another part of the system that knows nothing about the factory. We like to call these "spy objects" because they act like spies from the factory. This gives us powerful communications even though the system is decoupled.
This is the last piece of our abstraction puzzle! We have
- Abstract Structure -- abstract classes, interfaces
- Abstract Behavior -- abstract methods, strategies, visitors.
- Abstract Construction -- factories
- Abstract Environments -- anonymous inner classes, closures.
Examples
Example 1: Reversing a list using factory and anonymous inner class helper
Write
Reverse
such that it takes one parameter, the IListFactory
, but such that its helper only takes one parameter (other than the host list) which is the accumulated list.
public class Reverse implements IListAlgo {
public static final Reverse Singleton = new Reverse();
private Reverse() {}
public Object emptyCase(IMTList host0, Object... fac) {
return ((IListFactory)fac[0]).makeEmptyList();
}
public Object nonEmptyCase(INEList host0, Object... fac) {
final IListFactory f = (IListFactory) fac[0]; // final so that the anon. inner class can access it.
return host0.getRest().execute(new IListAlgo() {
public Object emptyCase(IMTList host1, Object... acc) {
return acc[0];
}
public Object nonEmptyCase(INEList host1, Object... acc) {
return host1.getRest().execute(this,
f.makeNEList(host1.getFirst(), (IList) acc[0]));
}
},f.makeNEList(host0.getFirst(), f.makeEmptyList()));
}
}
Example 2: Ant World
Imagine a world of ants that live in a one-dimensional space. A queen ant can make a bunch of worker ants. Each time she gives birth to a worker ant, she gives it a name. A worker ant can always tell what its name is. A worker ant from a particular colony can always calculate its distance from its queen. A worker ant can also move its queen to a different location. Wherever the queen moves to, ALL of her workers always know their relative distance from her. We want to keep track of all the ants in our ant world and all the ants in each of the existing colonies. We want to model the fact that each queen produces its own worker ants, each one which can move its queen around without telling the other ants in the same colony, yet ALL of the ants in the same colony would know where their queen is.
The above can be modeled by a Queen class with an abstract Worker inner class as shown below. This example illustrates the differences between static and non-static fields, methods and embedded classes.
Queen.java |
---|
|
Exercise:
Write a JUnit test program for the Queen and Worker classes.
7. Changing "states" - progressing to non-functional programming
In the above Queen example, the locations of a
Queen
object and its Worker
objects are subject to change. Every time a Worker
ant moves its Queen
, it has the side effect of changing the Queen
's origin. The Queen
object remains the "same". This seems like a reasonable thing to have. In the functional programming paradigm, to move the Queen
, the Worker
ant would have to instantiate a new Queen
object with a new location and find a way to associate itself to this new Queen
, which does not seem to model the "real" ant world very well. By allowing our objects to change their internal states in appropriate situations, our programming model becomes more "realistic", more efficient, and in many cases "simpler".To further illustrate the relevance of state changes, consider the problem of moving the minimum element of a list of integers to the front of the list. Since the current
IList
model is immutable, we can only solve the problem by returning a copy of the original list with its minimum at the front. Below is an algorithm to solve the problem that makes use of an accumulator to accumulate the current minimum and internally updates it as the list is being traversed. (Note: the list is assumed to hold no duplicate values).Min2Front.java |
---|
|
In the above, the comments are longer than the code. The above code and comments can be greatly simplified if the list is mutable. What does it mean for a list to be mutable?
No comments:
Post a Comment