cs205: engineering software?
(none)
05 April 2010

Final Exam Comments

Average: 75.8 (out of 100)

1. (7.1 / 10) Which design is better? State clearly the reasons the design you choose is better than the other design.

Design B is better.

In Design A, all the datatypes for representing things on the map are subtypes of MapLocation. In Design B, the datatypes that represent things on the map depend on MapLocation, but are not subtypes of it. Which makes more sense? This depends on understanding if the relationship between the other types and MapLocation is a is-a relationship (subtyping) or a has-a relationship (dependency). From the description of map location ("the MapLocation datatype represents a location on the map (for example, using its longitude and latitude)"), it is clear that the has-a relationship is more appropriate. For example, a Road is not a single location on a map; it is a connection between two or more locations. (The following questions make this even more clear).

Very few people answered this correctly. This was not meant to be a trick question, but most answers favored Design A for its apparent simplicity. If you think more carefully about the subtyping relationships (it doesn't make sense for Road to be a subtype of MapLocation), and extensibility (no easy way to add new kinds of places), there is no good argument favoring design A.

2. (7.8 / 10) Cathy Cartographer suggests making Exit a subtype of Road. If this is done is Highway now a behavioral subtype of Road? Explain why or why not. If not, explain what other changes would be needed to make Highway a proper behavioral subtype of Road.
It is still not a behavioral subtype because the signature for addIntersection does not satisfy the signature rule. The parameter for the Road addIntersection method takes a Road as its second parameter. To satisfy the substitution principle, the substitute method in Highway must accept all parameters that the method it is replacing would accept. In this case, the Highway addIntersection method takes an Exit as its second parameter. This does not satisfy the substitution principle, since Exit is not a supertype of Road (it is a subtype, so there are some Road objects that are not Exit objects).
3. (11.2 / 15) What is a good data representation for the Road datatype specified above? Include both a representation invariant and abstraction function for your data representation.
A simple data representation would be:
   Vector<MapLocation> waypoints;
   Vector<Intersection> intersections;
    
   class Intersection {
      MapLocation loc;
      Road road;
   }
The abstraction function is straightforward:
  AF (rep) = < waypoints, intersections >
    where waypoints = < rep.waypoints.getAt (0), ...,
                           rep.waypoints.elementAt ([rep.waypoints.size() - 1) >
          intersections = < < rep.intersections.getAt(0).loc,
                              rep.intersections.getAt(0).road >, ...
                             < rep.intersections.getAt(rep.intersections.size() - 1).loc,
                               rep.intersections.getAt(rep.intersections.size() - 1).road > >

The rep invariant needs to ensure that the result of applying the abstraction function to any concrete value that satisfies the rep invariant satisfies the properties stated in the datatype overview. For the abstraction function to work, we need all objects involved to be non-null:
    RI(c) = c.waypoints != null
        and c.waypoints[i] != null for all 0 <= i < c.waypoints.size()
        and c.intersections != null
        and c.intersections[i] != null 
              and c.intersections[i].loc != null
              and c.intersections[i].road != null
            for all 0 <= i < c.intersections.size ()
We also need to know there are at least 2 waypoints (otherwise the road has no length). This requirement was stated explicitly in the overview specification:
       and c.waypoints.size() >= 2
The tougher constraint is that the intersections must be on the road. This is somewhat clear from the overview specification, and more clear from the addIntersection method sepcification. Expressing it formally is difficult, but we can express it clearly informally:
       and for all 0 <= i < c.intersections.size ():
          c.intersections[i].loc is on the line between 
             c.waypoints[j] and c.waypoints[j+1] for some j
          and on the line between
             c.intersections[i].road.waypoints[k] 
                 and c.intersections[i].road.waypoints[k+1]
             for some k
          and there are no pair of intersections at the same location 
              c.intersections[i].loc = c.intersections[j].loc ==> i == j
Some rep invariants also included a constratint that there must be at least one intersection, although there is nothing in the specification that seems to require this.
4. (11.8 / 15) Cathy wants to add a findNearestExit method to the Highway datatype for finding the nearest exit to a given location. It should take a MapLocation parameter, and return the Exit that is closest to the given location on the Highway object. Write a declarative, total specification for the findNearestExit method.
public Exit findNearestExit (MapLocation loc)
      throws NoExitException
   MODIFIES: nothing
   EFFECTS: If this contains no exits, throws NoExitException.
      Otherwise, returns an Exit on this such that there is no
      other exit on this that is geographically closer to loc.
Note that this specification makes it clear what "closest" means (geographically closest without regard to the highway route). A more useful function might define "closest" as distance travelled along the highway, and require the location to be on the road.
5. (6.2 / 10) Recall the first problem explained in the PS5 comments for question 5:
The race condition is that the colleague of the this object could change between the name.compareTo call in the this object's associated thread and the name.compareTo call in the colleague object thread. Since philsophize is not synchronized, the (synchronized) setColleague method could execute in another thread while philosophize is executing. Then, the wrong lock would be grabbed! Instead of locking our current colleague, we would grab the lock for our previous colleague and then call colleague.argue (which is synchronized, and will wait for the lock on our current colleague).
Suggest a way to fix this problem. A good answer will show clearly how you would change the code. Be careful to make sure that your fix does not allow any new deadlock opportunities.
We need to ensure that the setColleague method is not executed after the name comparisons are started. The most obvious solution of adding a synchronized (this) before the colleague comparison doesn't work. That would reintroduce the deadlock the name comparisons are designed to solve! It would mean that two colleagues could each grab their own locks and be in a deadlock situation where neither can grab the other's lock to make progress on arguing. To provide the necessary ordering without reintroducing the deadlock, we need another lock that can control access to the setColleague critical section. Here is a rough idea how this might be done:
public class Philosopher {
   private Philosopher colleague;
   private String name;
   private String quote;
   private Object colleagueLock;

   public Philosopher(String name, String quote) {
      this.name = name;
      this.quote = quote;
      this.colleagueLock = new Object();
   }

   public synchronized void setColleague(Philosopher p) {
      synchronized (colleagueLock) {
         colleague = p;
      }
   }

   public synchronized void argue() { ... } // elided

   public void philosophize () {
      Object lock1, lock2;

      if (colleague != null) { // Need a colleague to start an
         // argument.
         // Always grab the lock for whichever name is alphabetically
         // first
         synchronized (coleagueLock) {
            if (name.compareTo (colleague.name) < 0) {
               lock1 = this;
               lock2 = colleague;
            } else {
               lock1 = colleague;
               lock2 = this;
            }
	     
            synchronized (lock1) {
               synchronized (lock2) {
                   System.err.println (name + "[Thread " 
                     + Thread.currentThread().getName () + "] says " + quote);
                   colleague.argue ();
	       } 
            }
         } 
      }
   }
}

6. (6.7 / 10) According to the PS5 comments (question5), "Even without the race condition, there are ways in which our solution could still deadlock: we could have three philosophers who have non-reflexive colleagues." Explain in detail a scenario where such a deadlock could occur, or explain why no scenario involving three philosophers with non-reflexive colleagues could produce a deadlock.

This would have been a tough question, except of course, I warned the class after quiz for that a question exactly along these lines would be on the final. Hence, its disappointing that few people answered it well.

In fact, the PS5 comments are wrong — non-reflexive colleagues cannot be the cause of a deadlock. With three philosophers we have two situations to consider:

  1. A's colleague is B; B's colleague is C; and C's colleague is A.
  2. A's colleague is B; B's colleague is C; and C's colleague is B.
For each situation, the three threads could interleave in any way. Consider when thread A runs. It will attempt to grab lock A, then lock B. If it doesn't grab lock A, then some other thread must have lock A, and thread A is stalled without holding any locks (so it can't cause a deadlock). If it does grab lock A, then it will attempt to grab lock B. If it succeeds, then it has both locks it needs so it will argue and then release both locks. If it cannot grab lock B, then some other thread must hold lock B. In secnario 1, if B has the lock for B, it needs the lock for C to make progress. The lock for C must be available, since the only other thread that uses C is the C thread, and C's colleague is A. The C thread must grab the lock for A before grabbing the lock for C. But we know it cannot have grabbed the lock for A, since the A thread has it (if it is waiting for the lock for B). This means the B thread must be able to grab the lock for C, and thus make progress to argue and then release both locks so the A thread can make progress. In scenario 2, the B-C and C-B pairs must always make progress, so the B lock is eventually release and there is no deadlock.

Type Safety

7. (14.8 / 15) What must the Java bytecode verifier prove to know the following instruction sequence is type safe? (Hint: a good answer will explain the all preconditions that need to be true before the iload_1 instruction.)
    iload_1
    iadd
(See Class 30 for information on the instructions.)
The iload_1 instruction requires that memory location 1 contains an integer (and pushes an integer on the stack). The iadd instruction requires the top two places on the stack contain integers. Hence, before executing the two instructions the stack must have an integer on top, and memory location 1 must contain an integer.

Abstraction

8. (10.2 / 15) Identify three different types of abstraction. For each, explain what language features Java provides to support that type of abstraction. Especially good answers will also discuss how well Java's constructs support the desired form of abstraction.
Procedural Abstraction. Java does not provide support for stand-alone procedures, but does provide a way to do procedural abstraction using methods. This satisfies the basic requirements of procedural abstraction: we can hide what is done from the inputs by passing them as parameters.

Data Abstraction. Java provides good support for data abstraction since we can use the class mechanism to package methods and state, and use private visibility modifier to hide the data representation from clients.

Iteration Abstraction. Java (since version 1.5) provides support for iteration abstraction using the Iterator interface. This allows datatypes to provide methods that enable clients to define a loop that iterates over a data collection without revealing the interals of the collection implementation.

Subtype Abstraction. Java supports subtype abstraction with the extends and implements mechanisms (both of which define subtyping relationships), and through the dynamic dispatching of methods. This allows objects of many types that are subtypes of a given type to be used in the same way.

Concurrency Abstraction. Java supports concurrency abstraction by providing threads and synchronization. These mechanisms hide some of the details of event ordering from a programmer, but don't hide the problems of preventing race conditions and deadlocks from the programmer.