JMU CS239 - Advanced Programming
Help Policies Syllabus Tools
Lab: Experimenting with Abstract Classes


Instructions: Answer the following questions one at a time. After answering each question, check your answer (by clicking on the check-mark icon if it is available) before proceeding to the next question.

Getting Ready: Before going any further, you should:

  1. Depending on your development environment, create either a directory or a project for this lab.
  2. Setup your development environment.
  3. Download the following files:
    to an appropriate directory/folder. (In most browsers/OSs, the easiest way to do this is by right-clicking/control-clicking on each of the links above and then selecting Save as... or Save link as....)
  4. Add the appropriate files you downloaded to the directory/project you created for this lab.

    .java Files: In most IDEs, .java files (i.e., source files) can just be copied into the project.

    .class and .jar Files: In some IDEs it is easier to use .class files and in others it is easier to use a .jar file that contains the .class files. Hence, you have been provided with both. See the course "Help" page on your IDE for more information.

    Resources: In some IDEs, resources (e.g., images, data files) need to be in a special directory whereas in others they need to be in the working directory. See the course "Help" page on your IDE for more information.

1. Basics: This part of the lab will help you understand the basics of abstract classes.
  1. Open TwoPartNumber.java.
  2. Compile TwoPartNumber.java to ensure that it has no syntax errors.
  3. Remove the abstract modifier from the declaration of the TwoPartNumber class.
  4. Compile the TwoPartNumber class.
  5. What error message was generated?


    TwoPartNumber.java:16: error: TwoPartNumber is not abstract and does not overr
    ide abstract method initializeUnits() in TwoPartNumber
    public class TwoPartNumber
           ^
    1 error
    
    Expand
  6. Why was this error message generated?


    This error message was generated because the TwoPartNumber class has a method in it that is not implemented but the class is not declared to be abstract.
    Expand
  7. What problems could arise at run-time if this class could be compiled as is?


    Someone could create an instance of a TwoPartNumber object and then call it's initializeUnits() method. But, there would be no instructions to execute because the method is not implemented.
    Expand
  8. Replace the abstract modifier in the declaration of the class.
  9. Remove the abstract modifier from the declaration of the initializeUnits method.
  10. Compile the TwoPartNumber class.
  11. What error message was generated?


    TwoPartNumber.java:132: missing method body, or declare abstract
        protected void initializeUnits();
    
    Expand
  12. Why was this error message generated?


    This error message was generated because the initializeUnits() method is declared but not implemented.
    Expand
  13. Replace the abstract modifier in the declaration of the initializeUnits() method.
  14. Compile the TwoPartNumber class.
  15. Why does this class compile even though it has a method with a missing method body?


    Abstract classes can contain abstract methods. The compiler only needs to know that concrete children will implement a method with the same signature.
    Expand
  16. "Comment out" the initializeUnits method.
  17. Compile the TwoPartNumber class.
  18. What error message was generated?


    TwoPartNumber.java:67: cannot resolve symbol
    symbol  : method initializeUnits ()  
    
    Expand
  19. Why was this error message generated?


    This error message was generated because the constructor with the signature TwoPartNumber(int, int, boolean) calls the initializeUnits() method and there is no such method.
    Expand
  20. "Un-comment out" the initializeUnits method.
  21. Compile the TwoPartNumber class.
  22. Why does this class compile even though the constructor calls initializeUnits() and the initializeUnits() method is not implemented?


    The compiler knows that this method will be implemented in all concrete derived classes. Hence, at run-time (when the constructor that calls initializeUnits() is executed), there will be instructions to execute (in the derived class).
    Expand
  23. Why is there no chance that the code in the constructor that calls initializeUnits() will be executed if there are no concrete derived classes?


    Because the TwoPartNumber class is abstract no instances can be created. If no instances can be created then the code in the constructor can't be executed.
    Expand
  24. Open Driver0.java.
  25. Compile Driver0.java.
  26. What error message was generated?


    Driver0.java:8: TwoPartNumber is abstract; cannot be instantiated
            number = new TwoPartNumber();
    
    Expand
  27. Why was this error message generated?


    This error message was generated because you cannot create an instance of an abstract class.
    Expand
  28. Why doesn't the declaration of the number variable generate a compile-time error message?


    You can declare a variable to have an abstract type. It can later hold a reference to an object of a derived type. (Remember the "is a" relationship.)
    Expand
2. A Brief Review: This part of the lab will help you remember some topics that were discussed earlier in the semester.
  1. Why aren't the "constants" (e.g., smallsPerLarge) in the TwoPartNumber class declared to be final?


    Because they can't be initialized when declared or in the constructor (since they are initialized in the initializeUnits() method of the concrete subclasses).
    Expand
  2. Does this make defects more likely?


    It does. Programmers implementing the concrete subclasses need to be very careful.
    Expand
  3. Why aren't the "constants" (e.g., smallsPerLarge) in the TwoPartNumber class declared to be static?


    Because they are not attributes of the class. Different objects must be able to have different "constants" because they may be members of different concrete subclasses.
    Expand
  4. Does this make defects more likely?


    No.
    Expand
3. Specializing an Abstract Class: This part of the lab will help you understand extensions/specializations of abstract classes.
  1. Open Length.java.
  2. Is this class "concrete"? Why or why not?


    It is declared to be concrete (or, more correctly, it is not declared to be abstract). The declaration is correct because the Length class implements the abstract method initializeUnits().
    Expand
  3. Open Driver1.java.
  4. Compile and execute Driver1.
  5. What output was generated?


    Age 12:
      Mike: 5 feet  5 inches
      Bill: 5 feet  5 inches
        ==   SAME    ==
    
    
    Age 16:
      Mike: 5 feet  5 inches
      Bill: 5 feet  9 inches
        == DIFFERENT ==
    
    
    Age 20:
      Mike: 6 feet  6 inches
      Bill: 6 feet  7 inches
        == DIFFERENT ==
    
    
    Age 70:
      Mike: 6 feet  6 inches
      Bill: 6 feet  6 inches
        ==   SAME    ==
    
    Expand
  6. Is this output correct?


    Yes.
    Expand
  7. "Comment out" the implementation of the initializeUnits() in the Length class.
  8. Compile the Length class.
  9. What error message was generated?


    Length.java:10: Length is not abstract and does not override abstract method 
    initializeUnits() in TwoPartNumber
    
    public class Length extends TwoPartNumber
    
    Expand
  10. Why was this error message generated?


    This error message was generated because the class declares itself to be a concrete extensions of the TwoPartNumber class but does not implement the abstract method initializeUnits() in that class.
    Expand
  11. "Un-comment out" the implementation of the initializeUnits() in the Length class.
  12. Create a Weight class (containing pounds and ounces) that extends the TwoPartNumber class.
  13. What code did you have in your implementation?


    /**
     * A weight in traditional U.S. units (i.e., pounds and ounces)
     *
     * This version extends the abstract TwoPartNumber class that
     * implements most behaviors
     */
    public class Weight extends TwoPartNumber
    {
        /**
         * Default Constructor.
         */
        public Weight()
        {
    	super(0, 0);
        }
    
        /**
         * Explicit Value Constructor.
         *
         * @param pounds  The number of pounds (must be positive)
         * @param ounces  The number of ounces (must be positive)
         */
        public Weight(int pounds, int ounces)
        {
    	super(pounds, ounces);
        }
    
        /**
         * Initialize the "constants" used by a TwoPartNumber.
         */
        protected void initializeUnits()
        {
    	smallsPerLarge     = 16;
    
    	largeUnitsSingular = "pound";
    	largeUnitsPlural   = "pounds";
    
    	smallUnitsSingular = "ounce";
    	smallUnitsPlural   = "ounces";
        }
    }
    
    Expand
  14. What is the advantage of having the Length and Weight classes extend the TwoPartNumber class?


    It eliminates an enormous amount of code duplication. If these classes did not extend TwoPartNumber they would both need to include all of the code in the TwoPartNumber class, some of which is fairly complicated.
    Expand
  15. Suppose that, while testing the Weight class, you found a fault in the changeBy() method. Assuming the current implementation, would you have to correct the fault in both the Weight class and the Length class?


    No, the fault is actually in the TwoPartNumber class. So, the correction would be made there and inherited by both the Weight and Length classes.
    Expand
  16. Suppose, instead, that, after copying the Weight class to create a Length, you found a fault in the changeBy() method. Would you have to correct the fault in both the Weight class and the Length class?


    Yes, that's one of the big problems with having duplicate code (whether in different methods or in different classes).
    Expand
4. Type Safety and Abstract Classes: This part of the lab will help you understand "type safety" issues that sometimes arise with specializations of abstract classes.
  1. Open Driver2.java.
  2. Compile the Driver2 class.
  3. Why doesn't the expression myLength.changeBy(myWeight) generate a compile-time error message?


    The formal parameter of the changeBy() method is a TwoPartNumber. Since Weight extends TwoPartNumber the variable myWeight is a TwoPartNumber.
    Expand
  4. Execute Driver2.
  5. What output is generated?


    2 feet  1 inch plus 1 pound  9 ounces equals 4 feet  2 inches
    
    Expand
  6. Why?


    The changeBy() method operates on TwoPartNumber objects using "small" units. myLength has a value attribute of 25 and myWeight has a value attribute of 25. So, after changeBy() is executed, the value attribute of myWeight is 50 which the toString() method reports as 4 feet 2 inches.
    Expand
  7. We can, fortunately, easily correct this defect. The first step is to make the changeBy() method in the TwoPartNumber class visible/accessible only to derived classes that understand this issue. What change did you make?


    I made the changeBy() method in the TwoPartNumber class protected.
    Expand
  8. The second step is to add a type-safe changeBy() method to the Length class. Of course, it shouldn't duplicate code in the base class. What code did you add?


        /**
         * Change this Length by a given amount.
         *
         * @param other   The amount to change this Length by
         */
        public void changeBy(Length other)
        {
            super.changeBy(other);
        }
    
    Expand
  9. The final step is to add a similar type-safe changeBy() method to the Weight. What code did you add?


        /**
         * Change this Weight by a given amount.
         *
         * @param other   The amount to change this Weight by
         */
        public void changeBy(Weight other)
        {
            super.changeBy(other);
        }
    
    Expand
  10. Now, re-compile Driver2.java. Why isn't an error generated?


    This is subtle, and an artifact of the simple setup we used for this lab.

    Because Driver2 is in the same package as TwoPartNumber, the protected changeBy() method is visible/accessible to it. This isn't a problem, though, because in a more realistic setting they would be in different packages.

    Expand
5. Collections of Abstract (and Concrete) Classes: This part of the lab will help you understand some of the ways in which collections of abstract classes and their concrete children can be used, and the advantages and disadvantages of each.
  1. Open LengthDatabase.java and DatabaseDriver1.java.
  2. Compile and execute DatabaseDriver1.
  3. Did it execute correctly?


    Yes.
    Expand
  4. Create a file named DatabaseDriver2.java that creates and stores Weight objects (rather than Length objects) in the LengthDatabase.
  5. What code did you have in your implementation?


    import java.io.*;
    import java.util.*;
    
    /**
     * A driver.
     */
    public class DatabaseDriver2
    {
        /**
         * The entry point of the application
         *
         * @param args   The command line arguments
         */
        public static void main(String[] args) throws IOException
        {
           Weight                  weight, result;
           LengthDatabase          database;
           Scanner                 scanner;        
           String                  name, prompt;
    
           prompt   = "\nName to look for (Ctrl-Z or Ctrl-D to exit): ";       
           database = new LengthDatabase();
    
           weight = new Weight(155, 8);
           database.add("Mike", weight);
    
           weight = new Weight(186, 2);
           database.add("Bill", weight);
    
           weight = new Weight(114, 9);
           database.add("Nancy", weight);
    
           scanner = new Scanner(System.in);
    
           System.out.print(prompt);
           while (scanner.hasNext())
           {
              name = scanner.nextLine();
               
              result = database.get(name);
              if (result != null)
              {
                 System.out.println(name+" is "+result);
              }
              else
              {
                 System.out.println("I don't know how heavy "+name+" is.");
              }
    
              System.out.print(prompt);
           }
        }
    }
    
    Expand
  6. Compile your implementation of DatabaseDriver2.
  7. Why didn't it compile?


    Because the second formal parameter of the add() method in the LengthDatabase class is a Length and because the get() method in the LengthDatabase class returns a Length.
    Expand
  8. Open and read TwoPartNumberDatabase.java.
  9. What are the differences between the TwoPartNumberDatabase class and the LengthDatabase class?


    In the TwoPartNumberDatabase class values is a TwoPartNumber[] (rather than a Length[])), the add() method is passed a TwoPartNumber (rather than a Length), and the get() method returns a TwoPartNumber (rather than a Length).
    Expand
  10. What are the advantages of the TwoPartNumberDatabase class (as compared to the LengthDatabase class)?


    Since TwoPartNumber objects are less specialized than Length objects, the TwoPartNumberDatabase is less specialized than the LengthDatabase. As a result, a TwoPartNumberDatabase object can be used with any kind of TwoPartNumber object (e.g., Length objects or Weight objects).
    Expand
  11. What are the disadvantages of the TwoPartNumberDatabase class (as compared to the LengthDatabase class)?


    Since the TwoPartNumberDatabase class contains TwoPartNumber objects it could contain multiple different specializations of TwoPartNumber at the same time, which could lead to confusion/problems.
    Expand
  12. Modify DatabaseDriver1 and DatabaseDriver2 so that the database they use is a TwoPartNumberDatabase. Make sure you change both the declaration and the instantiation.
  13. Compile DatabaseDriver1.
  14. What error message was generated?


    DatabaseDriver1.java:44: incompatible types
    found   : TwoPartNumber
    required: Length
                result = database.get(name);
    
    Expand
  15. Why was this error message generated?


    Because result is declared to be a Length but the get() method returns (a reference to) a TwoPartNumber object.
    Expand
  16. Correct this defect (and the corresponding defect in DatabaseDriver2 by typecasting the value returned by database.get(name) to the appropriate type.
  17. What changes did you make?


      result = (Length)database.get(name);
    

    and

      result = (Weight)database.get(name);
    
    Expand
  18. Remove the two typecasts that you just added.
  19. Correct this defect (and the corresponding defect in DatabaseDriver2 by declaring result and length to be TwoPartNumber objects.
  20. What changes did you make?


           TwoPartNumber          length, result;
    

    and

           TwoPartNumber          weight, result;
    
    Expand
  21. What are the advantages and disadvantages of these two approaches to correcting this defect?


    The advantage of the first approach is that all of the functionality of the returned object can be used, not just the functionality that is in the TwoPartNumber class. So, if either Length or Weight added important functionality to the the TwoPartNumber class, that functionality could be used. The disadvantage is that the typecast could cause a run-time exception if, somehow, the wrong kind of TwoPartNumber object is added to the database.

    There is no risk of a run-time exception in the second approach. The disadvantage is that only the functionality of the TwoPartNumber class can be used (since any attempt to use other functionality will result in a compile-time error message being generated).

    There is actually a better way to correct this defect that we will discuss later in the semester.

    Expand
  22. Add the following method to the LengthDatabase class (with an appropriate comment).
        public Length total()
        {
            Length     result;
            
            result = new Length();
            for (int i=0; i<nextIndex; i++)
            {
                result.changeBy(values[i]);
            }
    
            return result;
        }
    
  23. What comment did you add? In other words, what functionality does this method provide?


        /**
         * Calculate the total Length of all objects in this LengthDatabase.
         *
         * @return  The total
         */
    
    Expand
  24. Try to add a similar method to the TwoPartNumberDatabase class. What makes it impossible?


    The TwoPartNumber class is abstract, so it is not possible to construct an instance.
    Expand
  25. How might we take advantage of the functionality in the TwoPartNumber database class and add type safety?


    One way is to make the TwoPartNumberDatabase class abstract and have concrete specializations that do nothing but provide type safety.
    Expand
  26. Add the following method to the TwoPartNumberDatabase class.
        /**
         * Get the total TwoPartNumber of all objects in this 
         * TwoPartNumberDatabase.
         *
         * @return  The total
         */
        protected TwoPartNumber totalValue()
        {
            TwoPartNumber     result;
            
            result = createElement();
            for (int i=0; i<nextIndex; i++)
            {
                result.changeBy(values[i]);
            }
    
            return result;
        }
    
  27. Why doesn't the class compile?


    It doesn't compile because there is no createElement() method.
    Expand
  28. What is the apparent purpose of the createElement() method?


    It looks like it is supposed to construct a TwoPartNumber object.
    Expand
  29. Why is it going to be impossible to implement this method?


    Again, because the TwoPartNumber class is abstract.
    Expand
  30. Are there any objects that specialize TwoPartNumber that can be constructed?


    Yes, Length "is a" TwoPartNumber and Weight is a TwoPartNumber.
    Expand
  31. Why shouldn't the createElement() method in the TwoPartNumber class construct an object that it is a specialization of TwoPartNumber?


    It doesn't/shouldn't know what specializations exist. In other words, in the future, there might be hundreds of classes that specialize TwoPartNumber.
    Expand
  32. The TwoPartNumber class can't implement the createElement() method but it's specializations can. So, what should be true of the createElement() method in the TwoPartNumber class.


    It should be abstract.
    Expand
  33. Add such a method. What code did you add?


        /**
         * Create a default (specialization of) a TwoPartNumber.
         *
         * @return  The TwoPartNumber
         */
        public abstract TwoPartNumber createElement();
    
    Expand
  34. Why doesn't the class compile?


    It doesn't compile because it has an abstract method (and, hence, objects of this class can't be instantiated).
    Expand
  35. Make the TwoPartNumberDatabase class abstract. What code did you add?


    public abstract class TwoPartNumberDatabase
    
    Expand
  36. Create a WeightDatabase class (just to be different) that extends the TwoPartNumberDatabase class and provides all of the desired functionality (including the public Weight total() method) in a type-safe way.

    What code is in this class?


    /**
     * A simple database that contains weights indexed by a name.
     */
    public class WeightDatabase extends TwoPartNumberDatabase
    {
        /**
         * Default Constructor.
         */
        public WeightDatabase()
        {
            super();
        }
    
        /**
         * Add a Weight to the database.  The Weight
         * can be referred to (i.e., indexed by) the given name
         *
         * @param name   The name of the Weight
         * @param value  The Weight
         */
        public void add(String name, Weight value)
        {
            super.add(name, value);
        }
    
        /**
         * Create a default (specialization of) a TwoPartNumber.
         *
         * @return  The TwoPartNumber
         */
        public TwoPartNumber createElement()
        {
            return new Weight();
        }
    
        /**
         * Get the Weight with the given name (or null if
         * no such name exists).
         *
         * @param name   The name of the Weight
         * @return       The Weight (or null)
         */
        public Weight get(String name)
        {
            return (Weight)super.get(name);
        }
    
        /**
         * Get the total TwoPartNumber of all objects in this 
         * TwoPartNumberDatabase.
         *
         * @return  The total
         */
        public Weight total()
        {
            return (Weight)super.totalValue();
        }
    }
    
    Expand
  37. How much work would it be to create a similar class for other specializations of TwoPartNumber?


    Very little but, as we will see later in the semester, there is an even better way.
    Expand

Copyright 2021