35. Subclasses and Information Hiding
by
Douglas W. Jones
|
Now, in order to hide the details of events from outside class Simulator, we broke the inner class Event into two classes, RealEvent, a private class that contained all the details of how events are scheduled, and Event a public class that contained no useful information because its only field was a RealEvent and therefore, even if public, inscrutible from the perspective of the outside world:
private static class RealEvent { public double time; public final Action act; ... details omitted ... } public static class Event { public RealEvent e; public RealEvent( Event e ) { this.e = e; } }
This solved the information hiding problem, but at a cost: Where scheduling a new event used to involve just one constructor call, creating one new object, scheduling under this new framework requires constructing and scheduling a RealEvent and then constructing an Event as a wrapper for the new object.
public static Event schedule( double t, Action a ) { RealEvent e = new( t, a ); eventSet.add( e ); return new Event( e ); }
Object creation is not cheap! The Java run-time system must use an algorithm to search the available free memory for a block of memory appropriate to hold the new object. We have been working on storage management algorithms for overe 50 years, and the algorithms in current user are very fast, but very fast is not the same as free. Unnecesary object creation is still something to avoid.
One attractive alternative is to replace the use of a wrapper with a subclass. Consider this code:
public static class Event {} private static class RealEvent extends Event { public double time; public final Action act; ... details omitted ... }
Here, we have exactly the same two classes with exactly the same public and private properties. That is, the way we are hiding information about real events from the outside world is essentially unchanged. The difference lies in how the two classes are related. Class RealEvent is now an extension of class Event instead of a wrapper. This requires a rewrite of the methods that connect the two:
public static Event schedule( double t, Action a ) { RealEvent e = new( t, a ); eventSet.add( e ); return e; }
Note that the above code no-longer calls a constructor to wrap the real event. Instead, it simply returns the object handle for the real event, but it changes the label on this handle, so that the code in the outside world sees it as an Event, a class that allows no operations at all.
As a result, the cost of scheduling is returned to what it was when we had no information hiding at all, aside from the very small cost of the return statement. If the user never deletes or reschedules events, the user will never see any real cost for this.
There is still a cost, however, when the user cancels or reschedules an event. The code to cancel an event now looks like this:
public static void cancel( Event e ) { eventSet.remove( (RealEvent)e ); }
Here, we use type casting, with the cast (RealEvent) to convert the event handle e into a handle on a RealEvent. This type-casting operation does not change the handle, all it does is re-label it from being a reference to an Event to a RealEvent. That is, it undoes the change that was made in schedule(), where a RealEvent was returned as an Event.
Type checking in Java is mostly done at compile time. The "label" on a Java variable giving its type or class is held in the compiler, and for the vast majority of contexts where this label must be checked, the checking is done by the compiler when it reduces the .java source file to a .class object file.
There is one exception to the above general statement. When you use casting to take an expression of some parent class and cast it as a value of the a child class, some extension or implementation of the parent, the Java compiler generates code for a run-time check. Consider this rewrite of the above code:
public static void cancel( Event e ) { RealEvent re = (RealEvent)e; eventSet.remove( re ); }
All we did is separate out the casting onto a separate line of code, something with essentially no run-time cost unlss the compiler generates code to track what source line is executing in order to produce better error messages when there is a run-time exception. (That is the default in Java, but when run-time speed matters, you can turn it off.)
The run-time checking code the compiler adds to the above is basically equivalent to the following:
public static void cancel( Event e ) { if (!(e instanceof RealEvent)) throw new ClassCastException(); RealEvent re = (RealEvent)e; eventSet.remove( re ); }
The instanceof boolean operator returns true if the item on the left-hand-side is a member of the class given on the right-hand-side. This check is not free, but it is inexpensive compared to creating a new object, and even better, we only pay the price if we actually cancel or reschedule an event. We pay almost nothing for events that do not require these new operations.
What is the cost of the instanceof operator? Deep inside Java, every object has a field giving the class of that field. This field is a handle on an object of class Class describing the structure and methods of the class. Let's call this field of every object myClass. In that case, the actual implementation of cancel really looks like this: boil
public static void cancel( Event e ) { if (e.myClass != RealEvent)) throw new ClassCastException(); RealEvent re = (RealEvent)e; eventSet.remove( re ); }
The total cost of this operation therefore involves the following computations:
Using the class hierarchy to hide the details of an object, as illustrated here, is just as effective as using a wrapper class, as we illustrated in the previous lecture.
When we used a wrapper class, we paid a price of one new object creation every time we wrapped an object in order to hide it when we returned that object to the outside world, whether or not the caller discarded the returned object.
The way we used the class hierarchy to hid information here is not free, but instead of creating a new object every time we return a protected object to a user, the price we pay is primarily when that object is returned by the user to the utility that is trying to protect things.
Furthermore, the check we perform is cheap, about 4 instructions to check the class of the returned protected object so we can cast it to the secret internal internal representation for access to the protected fields of the object. That makes this a very good, low overhead approach to information hiding.