Rice University - Comp 212 - Intermediate Programming

Spring 2002

Lecture #14 - NOOT vs. OOT and Introduction to Inner Classes


1. NOOT vs. OOT

Recall last lecture's in class exercise:

Write an algorithm for LRStruct to remove the last element.  Throw an exception when the list is empty.

Below are two different ways of thinking that lead to two different ways of solving the problem.

Non Object-Oriented Thinking (NOOT)

Object-Oriented Thinking (OOT)

Any LRStruct algorithm must consider the case when the LRStruct is empty  and the case when the LRStruct is not empty. As dictated by the framework, any algorithm on LRStruct is a concrete IAlgo visitor with two methods: one to deal with the empty case and one to take care of the non-empty case.

 

If the LRStruct is empty, then throw an exception as specified. When the LRStruct host is empty, throw an exception as specified.

 

If the LRStruct is not empty, traverse the LRStruct until the empty LRStruct is reached, at which point ask the preceding LRStruct to remove its first. 

The above algorithm seems straightforward and simple enough. It involves imposing an external control structure to traverse an LRStruct and stop at some appropriate point.  As illustrated in a subsequent section, such an algorithm is usually implemented in terms of a loop whose exit condition involves checking an LRStruct for emptiness!

When the LRStruct host is not empty, it has a first and a rest, which is itself an LRStruct.  In order to remove the last element from such a host, we need to answer two questions:
  1. which object should be  responsible for determining whether or not the host's first is the last element?
  2. which object should be responsible for removing the last element?  

The host's rest knows whether or not itself is empty, and thus can determine whether or not the host's first is actually the last element.  Therefore the responsibility for determining whether or not the host is holding the last element should be delegated to the host's rest.

Though the host's rest knows whether or not the host holds the last element, it cannot remove that element.  The only object capable of removing this element is the host itself.

So knowing when to remove does not necessarily means capable of the task of removal.  In general, knowing when some action must take place does not mean capable of carrying out that action.  These are two distinct responsibilities that need to be distributed to two cooperating objects.

The above OO thought process leads to the idea of decomposing the task of removing the last element into two subtasks.

  1. Ask the host's rest for help in determining whether or not the host is holding the last element.  When asking for help, the host will need to pass itself as a parameter to the helper task so that once the last element is discovered, it can then be removed.
  2. Help the preceding LRStruct remove its last element.  When helping the preceding (non-empty) LRStruct to remove an element, an empty LStruct simply tells the given LRStruct to remove its first, while a non-empty LRStruct will ask its rest for help to remove its last element passing itself as a parameter.

Note that the above algorithm involves no notion of list traversal at all.  It entails decomposing the original task into smaller subtasks ("divide-and-conquer") and is only expressed in terms of which objects should be doing what and what pieces of information must be passed to each object's method in order to carry out each of the subtasks.

OOP calls for thinking in terms of  problem abstraction, task decomposition, and delegation of responsibilities.  It is a paradigm shift that emphasizes design and promotes a different way of thinking about formulating and solving problems.

   

 

Below is an implementation of the procedural non-oo algorithm.

Non Object-Oriented Algorithm (NOOA)

Non Object-Oriented Coding (NOOC)

Algorithm to check for emptiness
public class IsEmpty implements IAlgo {
  // Singleton
  public Object emptyCase(LRStruct host, Object inp) {
    return Boolean.TRUE;
  }

  public Object nonEmptyCase(LRStruct host, Object inp) {
    return Boolean.FALSE;
  }
}
If the LRStruct is empty, throw an exception.

If the LRStruct is not empty, traverse the LRStruct until the empty LRStruct is reached, at which point ask the preceding LRStruct to remove its first. 

/**
* Static method of some class.
*/
public static void removeLast(LRStruct list) {
  if (((Boolean)list.execute(IsEmpty.Singleton), null)).booleanValue()) {
    throw new java.util.NoSuchElementException("Empty list has no data.");
  }
  LRStruct next = list.getRest();  
  while (!((Boolean)next.execute(IsEmpty.Singleton), null)).booleanValue()) {
    list = next;
    next = list.getRest();
  }
  list.removeFront();
}
   
Comment

Though the high level description of the algorithm seems simple enough, its translation into code is not that simple.

It is not apparent that the code is a faithful (and correct) translation of the algorithm statement because it involves a lot of tedious book-keeping and flow control programming constructs.

 

Now consider how the OO algorithm is implemented.

Object-Oriented Algorithm (OOA)

Object-Oriented Coding (OOC)

An algorithm to remove the last element from an LRStruct is a concrete IAlgo visitor.
public class RemLast implements IAlgo {
  // Singleton
When the LRStruct host is empty, throw an exception as specified.  The code on the right simply expresses such an action.
  public Object emptyCase(LRStruct host, Object inp) {
    throw new java.util.NoSuchElementException("Empty list has no data.");
  }

When the LRStruct host is not empty, ask the host's rest for help in determining whether or not the host is holding the last element, passing the host as a parameter just in case this is true and the last element needs to be removed.

  public Object nonEmptyCase(LRStruct host, Object inp) {
    return host.getRest().execute(RemLastHelp.Singleton, host);
  }
}
The algorithm for a host LRStruct to help remove the last element from the preceding LRStruct, p, is a concrete IAlgo visitor whose input parameter is p.
public class RemLastHelp implements IAlgo {
  // Singleton
The empty LRStruct tells the preceding LRStruct to remove its first.
  /**
  * @param inp the LRStruct preceding host.
  */  
  public Object emptyCase(LRStruct host, Object inp) {
    return ((LRStruct)inp).removeFront();
  }
A  non-empty LRStruct asks its rest for help to remove its last element passing itself as a parameter.
  /**
  * The last element of the (non-empty) host is the
  * last element of inp, the LRStruct preceding host.
  */
  public Object nonEmptyCase(LRStruct host, Object inp) {
    return host.getRest().execute(this, host);
  }
}
   
Comment:

The implementation code for the OO algorithm directly maps to the abstract high-level description of the algorithm and can be easily proved to be correct.

It is "declarative" in nature.  It does not involve any conditional to specify when to perform certain task.  It simply states what needs to be done for each state of the host LRStruct.  Polymorphism is put to use to direct all flow control reducing code complexity.

 

The RemLastHelp defined in the above will only perform correctly if it is passed the appropriate parameter: the LRStruct  that precedes the host.  Though it is called once, inside of RemLast.nonEmptyCase(...), by the original LRStruct to :"help" it determine whether or not it is holding the last element and remove that element if this is the case, we have to go through the standard process of defining a class for it.  In Comp 210, Scheme helper functions are best created "on-the-fly" as "local" lambda expressions.  Is there an analogous way in Java to create helper visitor objects dynamically (i.e. on-the-fly) without having to give names to their classes?

2.  Anonymous Inner Class

Whenever we statically (as opposed to dynamically) define a class, we first have to give it a name.  After a (concrete) class is defined, we can then instantiate instances (i.e. objects) of this class by calling new on its constructor(s).  All the classes we have defined so far are named classes.  For most of the time, this mechanism for defining classes is adequate.  However, in order to model more sophisticated systems with changing dynamic behaviors that cannot be a-priori defined, Java provides a programming construct to define a class "anonymously" and instantiate objects for this class on-the-fly.  This is akin to defining  lambda expressions as functions "without names" in Scheme.  

Aside: Once cannot claim to have understood functional programming in Scheme without knowing how to effectively use lambda expressions and higher order functions.  Similarly, one cannot claim to have understood object-oriented programming in Java without knowing how to effectively use anonymous inner classes.

The syntax for defining "anonymously" a class and instantiate its object dynamically is as follows.

new SuperClassName(...) {
   local fields;
   overriden method1;
   overriden method2;
   etc...
}

A call to make an anonymous class is always made inside of some class.  As such, the anonymous class is called an "inner" class, and the outer class that contains the class is called the context of the inner class.  This context plays the role analogous to that of the "closure" in a lambda expression.

For example, RemLastHelp can be defined and instantiated anonymously inside of the RemLast.nonEmtpyCase(...) as follows.

public class RemLast implements IAlgo {
  // other methods elided...
    public Object nonEmptyCase(LRStruct host, Object inp) {
        return host.getRest().execute(new IAlgo() { // Anonymous inner class!

            public Object emptyCase(LRStruct h, Object p) {
                return ((LRStruct)p).removeFront();
            }

            public Object nonEmptyCase(LRStruct h, Object p) {
                return h.getRest().execute(this, h);
            }
        }, host);
    }
}

In the above, note how the parameters of the anonymous inner class are renamed in order to avoid masking the parameter names in the outer object.

Let us re-examine the code for lrs.ANode.  

It overrides the toString() method by making use of an anonymous inner class which in turns calls on another anonymous inner class!

The use of anonymous inner classes here allows us to program a concrete method for ANode without referring to any concrete named implementation of IAlgo.  This is crucial in keeping ANode independent of any concrete implementations.

abstract class ANode {
    /**
     * Uses anonymous visitor class to compute a String representation.
     * Anonymous inner classes will be discussed soon!
     */
    String toString(LRStruct owner) {
        return (String)owner.execute (new IAlgo() {
            public Object emptyCase(LRStruct host, Object inp) {
                return "()";
            }

            public Object nonEmptyCase(LRStruct host, Object inp) {
                return "(" + host.getFirst() + host.getRest().execute(new IAlgo() {
                    public Object emptyCase(LRStruct h, Object i) {
                        return ")";
                    }

                    public Object nonEmptyCase(LRStruct h, Object i) {
                        return " " + h.getFirst() + h.getRest().execute (this, null);
                    }
                }
                , null);
            }
        }
        , null);
   }

// Other methods elided...
}


3. Inner Classes

Inner classes do not have to be anonymous.  They can be named as well.  Besides fields and methods, a Java class can also contain other classes.  Such classes are called "inner classes".

class X {
    // fields of X ...
    // methods of X ...
    
    /** 
    * named inner class defined inside of X:
    */

    [public | protected | private]  [static]  [final]  [abstract]  class Y [ extends A]  [implements B]  {
        // fields of Y ...
        // methods of Y ...
        // classes of Y ...
    }
}

Access specifier:

Just like any other class, a class defined inside of another class can be public, protected, package private, or private.

Scope specifier:

Just like any other class, a class defined inside of another class can be static or non-static. 

When it is defined as static, it is called a nested class. The members (i.e. fields, methods, classes) of a (static) nested class can access to only static members of the enclosing class.

When it 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 during the rest of the semester.

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 objects that have direct access to the internals of the outer object and perform complex tasks that simple methods cannot do.  For examples, "event listeners" for a 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.

In the state design pattern, the states of an object may be implemented as inner objects. Since an inner object has access to its outer object (the context), there is no need to have setter and getter methods for the state.


dxnguyen@cs.rice.edu
Copyright 2002, Dung X. Nguyen - All rights reserved.