TIC2002 (2019)
  • Full Timeline
  • Week 1 [Aug 12]
  • Week 2 [Aug 19]
  • Week 3 [Aug 26]
  • Week 4 [Sep 2]
  • Week 5 [Sep 9]
  • Week 6 [Sep 16]
  • Week 7 [Sep 30]
  • Week 8 [Oct 7]
  • Week 9 [Oct 14]
  • Week 10 [Oct 21]
  • Week 11 [Oct 28]
  • Week 12 [Nov 4]
  • Week 13 [Nov 11]
  • Textbook
  • Admin Info
  • Report Bugs
  • Forum
  • Announcements
  • File Submissions
  • repl.it link
  • Java Coding Standard
  • Duke repo
  • Week 4 [Sep 2] - Topics

    • [W4.1] OOP: Inheritance
    • [W4.1a] Paradigms → OOP → Inheritance → What

    • [W4.1b] Paradigms → OOP → Inheritance → Overloading

    • [W4.1c] Paradigms → OOP → Inheritance → Overriding

    • [W4.1d] C++ to Java → Inheritance → Inheritance (Basics)

    • [W4.1e] C++ to Java → Inheritance → The Object Class

    • [W4.2] OOP: Polymorphism
    • [W4.2a] Paradigms → OOP → Polymorphism → What

    • [W4.2b] Paradigms → OOP → Inheritance → Substitutability

    • [W4.2c] Paradigms → OOP → Inheritance → Dynamic and Static Binding

    • [W4.2d] Paradigms → OOP → Polymorphism → How

    • [W4.2e] C++ to Java → Inheritance → Polymorphism

    • [W4.3] Testing text UIs
    • [W4.3a] Quality Assurance → Testing → Introduction → What

    • [W4.3b] Quality Assurance → Testing → Regression Testing → What

    • [W4.3c] Quality Assurance → Testing → Test Automation → What

    • [W4.3d] Quality Assurance → Testing → Test Automation → Automated Testing of CLI Apps

    • [W4.4] IDE: Intermediate
    • [W4.4a] Implementation → IDEs → Debugging → What

    • [W4.4b] Tools → Intellij IDEA → Debugging: Basic

    • [W4.4c] Tools → Intellij IDEA → Productivity Shortcuts


    [W4.1] OOP: Inheritance

    W4.1a

    Paradigms → OOP → Inheritance → What

    Can explain the meaning of inheritance

    The OOP concept Inheritance allows you to define a new class based on an existing class.

    For example, you can use inheritance to define an EvaluationReport class based on an existing Report class so that the EvaluationReport class does not have to duplicate data/behaviors that are already implemented in the Report class. The EvaluationReport can inherit the wordCount attribute and the print() method from the base class Report.

    • Other names for Base class: Parent class, Super class
    • Other names for Derived class: Child class, Sub class, Extended class

    A superclass is said to be more general than the subclass. Conversely, a subclass is said to be more specialized than the superclass.

    Applying inheritance on a group of similar classes can result in the common parts among classes being extracted into more general classes.

    Man and Woman behaves the same way for certain things. However, the two classes cannot be simply replaced with a more general class Person because of the need to distinguish between Man and Woman for certain other things. A solution is to add the Person class as a superclass (to contain the code common to men and woment) and let Man and Woman inherit from Person class.

    Inheritance implies the derived class can be considered as a sub-type of the base class (and the base class is a super-type of the derived class), resulting in an is a relationship.

    Inheritance does not necessarily mean a sub-type relationship exists. However, the two often go hand-in-hand. For simplicity, at this point let us assume inheritance implies a sub-type relationship.

    To continue the previous example,

    • Woman is a Person
    • Man is a Person

    Inheritance relationships through a chain of classes can result in inheritance hierarchies (aka inheritance trees).

    Two inheritance hierarchies/trees are given below. Note that the triangle points to the parent class. Observe how the Parrot is a Bird as well as it is an Animal.

    Multiple Inheritance is when a class inherits directly from multiple classes. Multiple inheritance among classes is allowed in some languages (e.g., Python, C++) but not in other languages (e.g., Java, C#).

    The Honey class inherits from the Food class and the Medicine class because honey can be consumed as a food as well as a medicine (in some oriental medicine practices). Similarly, a Car is an Vehicle, an Asset and a Liability.

    Which of these are correct?

    • a. Superclass is more general than the subclass.
    • b. Child class is more specialized than the parent class.
    • c. A class can inherit behavior from its ancestor classes (ancestor classes = classes above it in the inheritance hierarchy).
    • d. Code reuse can be one benefit of inheritance.
    • e. A change to the superclass will not affect its subclasses.

    (a) (b) (c) (d)

    Explanation: (e) is incorrect. Because subclasses inherit behavior from the superclass, any changes to the superclass could affect subclasses.

    W4.1b

    Paradigms → OOP → Inheritance → Overloading

    Can explain method overloading

    Method overloading is when there are multiple methods with the same name but different type signatures. Overloading is used to indicate that multiple operations do similar things but take different parameters.

    Type Signature: The type signature of an operation is the type sequence of the parameters. The return type and parameter names are not part of the type signature. However, the parameter order is significant.

    Example:

    Method Type Signature
    int add(int X, int Y) (int, int)
    void add(int A, int B) (int, int)
    void m(int X, double Y) (int, double)
    void m(double X, int Y) (double, int)

    In the case below, the calculate method is overloaded because the two methods have the same name but different type signatures (String) and (int)

    • calculate(String): void
    • calculate(int): void

    W4.1c

    Paradigms → OOP → Inheritance → Overriding

    Can explain method overriding

    Method overriding is when a sub-class changes the behavior inherited from the parent class by re-implementing the method. Overridden methods have the same name, same type signature, and same return type.

    Consider the following case of EvaluationReport class inheriting the Report class:

    Report methods EvaluationReport methods Overrides?
    print() print() Yes
    write(String) write(String) Yes
    read():String read(int):String No. Reason: the two methods have different signatures; This is a case of overloading (rather than overriding).

    Paradigms → OOP → Inheritance →

    Overloading

    Method overloading is when there are multiple methods with the same name but different type signatures. Overloading is used to indicate that multiple operations do similar things but take different parameters.

    Type Signature: The type signature of an operation is the type sequence of the parameters. The return type and parameter names are not part of the type signature. However, the parameter order is significant.

    Example:

    Method Type Signature
    int add(int X, int Y) (int, int)
    void add(int A, int B) (int, int)
    void m(int X, double Y) (int, double)
    void m(double X, int Y) (double, int)

    In the case below, the calculate method is overloaded because the two methods have the same name but different type signatures (String) and (int)

    • calculate(String): void
    • calculate(int): void

    Which of these methods override another method? A is the parent class. B inherits A.

    • a
    • b
    • c
    • d
    • e

    d

    Explanation: Method overriding requires a method in a child class to use the same method name and same parameter sequence used by one of its ancestors

    W4.1d

    C++ to Java → Inheritance → Inheritance (Basics)

    Can use basic inheritance

    Given below is an extract from the -- Java Tutorial, with slight adaptations.

    A class that is derived from another class is called a subclass (also a derived class, extended class, or child class). The class from which the subclass is derived is called a superclass (also a base class or a parent class).

    A subclass inherits all the members (fields, methods, and nested classes) from its superclass. Constructors are not members, so they are not inherited by subclasses, but the constructor of the superclass can be invoked from the subclass.

    Every class has one and only one direct superclass (single inheritance), except the Object class, which has no superclass, . In the absence of any other explicit superclass, every class is implicitly a subclass of Object. Classes can be derived from classes that are derived from classes that are derived from classes, and so on, and ultimately derived from the topmost class, Object. Such a class is said to be descended from all the classes in the inheritance chain stretching back to Object. Java does not support multiple inheritance among classes.

    The java.lang.Object class defines and implements behavior common to all classes—including the ones that you write. In the Java platform, many classes derive directly from Object, other classes derive from some of those classes, and so on, forming a single hierarchy of classes.

    The keyword extends indicates one class inheriting from another.

    Here is the sample code for a possible implementation of a Bicycle class and a MountainBike class that is a subclass of the Bicycle:

    public class Bicycle {
    
        public int gear;
        public int speed;
    
        public Bicycle(int startSpeed, int startGear) {
            gear = startGear;
            speed = startSpeed;
        }
    
        public void setGear(int newValue) {
            gear = newValue;
        }
    
        public void applyBrake(int decrement) {
            speed -= decrement;
        }
    
        public void speedUp(int increment) {
            speed += increment;
        }
    
    }
    
    public class MountainBike extends Bicycle {
    
        // the MountainBike subclass adds one field
        public int seatHeight;
    
        // the MountainBike subclass has one constructor
        public MountainBike(int startHeight, int startSpeed, int startGear) {
            super(startSpeed, startGear);
            seatHeight = startHeight;
        }
    
        // the MountainBike subclass adds one method
        public void setHeight(int newValue) {
            seatHeight = newValue;
        }
    }
    

    A subclass inherits all the fields and methods of the superclass. In the example above, MountainBike inherits all the fields and methods of Bicycle and adds the field seatHeight and a method to set it.

    Accessing Superclass Members

    If your method overrides one of its superclass's methods, you can invoke the overridden method through the use of the keyword super. You can also use super to refer to a hidden field (although hiding fields is discouraged).

    Consider this class, Superclass and a subclass, called Subclass, that overrides printMethod():

    public class Superclass {
    
        public void printMethod() {
            System.out.println("Printed in Superclass.");
        }
    }
    
    public class Subclass extends Superclass {
    
        // overrides printMethod in Superclass
        public void printMethod() {
            super.printMethod();
            System.out.println("Printed in Subclass");
        }
        public static void main(String[] args) {
            Subclass s = new Subclass();
            s.printMethod();
        }
    }
    

    Printed in Superclass.
    Printed in Subclass
    

    Within Subclass, the simple name printMethod() refers to the one declared in Subclass, which overrides the one in Superclass. So, to refer to printMethod() inherited from Superclass, Subclass must use a qualified name, using super as shown.

    Subclass Constructors

    A subclass constructor can invoke the superclass constructor. Invocation of a superclass constructor must be the first line in the subclass constructor. The syntax for calling a superclass constructor is super() (which invokes the no-argument constructor of the superclass) or super(parameters) (to invoke the superclass constructor with a matching parameter list).

    The following example illustrates how to use the super keyword to invoke a superclass's constructor. Recall from the Bicycle example that MountainBike is a subclass of Bicycle. Here is the MountainBike (subclass) constructor that calls the superclass constructor and then adds some initialization code of its own (i.e., seatHeight = startHeight;):

    public MountainBike(int startHeight, int startSpeed, int startGear) {
        super(startSpeed, startGear);
        seatHeight = startHeight;
    }
    

    Note: If a constructor does not explicitly invoke a superclass constructor, the Java compiler automatically inserts a call to the no-argument constructor of the superclass. If the superclass does not have a no-argument constructor, you will get a compile-time error. Object does have such a constructor, so if Object is the only superclass, there is no problem.

    Access Modifiers (simplified)

    Access level modifiers determine whether other classes can use a particular field or invoke a particular method. Given below is a simplified version of Java access modifiers, assuming you have not yet started placing your classes in different packages i.e., all classes are placed in the root level. A full explanation of access modifiers is given in a later topic.

    There are two levels of access control:

    1. At the class level:

      • public: the class is visible to all other classes
      • no modifier: same as public

    2. At the member level:

      • public : the member is visible to all other classes
      • no modifier: same as public
      • protected: the member is visible to sub classes only
      • private: the member is not visible to other classes (but can be accessed in its own class)

    Background: Suppose we are creating a software to manage various tasks a person has to do. Two types of such tasks are,

    • Todos: i.e., things that needs to be done some day e.g., 'Read the book Lord of the Rings'
    • Deadlines: i.e., things to be done by a specific date/time e.g., 'Read the text book by Nov 25th'

    The Task class is given below:

    public class Task {
        protected String description;
    
        public Task(String description) {
            this.description = description;
        }
    
        public String getDescription() {
            return description;
        }
    }
    
    1. Write a Todo class that inherits from the Task class.
      • It should have an additional boolean field isDone to indicate whether the todo is done or not done.
      • It should have a isDone() method to access the isDone field and a setDone(boolean) method to set the isDone field.
    2. Write a Deadline class that inherits from the Todo class that you implemented in the previous step. It should have,
      • an additional String field by to store the details of when the task to be done e.g., Jan 25th 5pm
      • a getBy() method to access the value of the by field, and a corresponding setBy(String) method.
      • a constructor of the form Deadline(String description, String by)

    The expected behavior of the two classes is as follows:

    public class Main {
        public static void main(String[] args) {
            // create a todo task and print details
            Todo t = new Todo("Read a good book");
            System.out.println(t.getDescription());
            System.out.println(t.isDone());
    
            // change todo fields and print again
            t.setDone(true);
            System.out.println(t.isDone());
    
            // create a deadline task and print details
            Deadline d = new Deadline("Read textbook", "Nov 16");
            System.out.println(d.getDescription());
            System.out.println(d.isDone());
            System.out.println(d.getBy());
    
            // change deadline details and print again
            d.setDone(true);
            d.setBy("Postponed to Nov 18th");
            System.out.println(d.isDone());
            System.out.println(d.getBy());
        }
    }
    

    Read a good book
    false
    true
    Read textbook
    false
    Nov 16
    true
    Postponed to Nov 18th
    

    Todo class is given below. You can follow a similar approach for the Deadline class.

    public class Todo extends Task {
        protected boolean isDone;
    
        public Todo(String description) {
            super(description);
            isDone = false;
        }
    }
    

    W4.1e

    C++ to Java → Inheritance → The Object Class

    Can use Object class

    As you know, all Java objects inherit from the Object class. Let us look at some of the useful methods in the Object class that can be used by other classes.

    The toString method

    Every class inherits a toString method from the Object class that is used by Java to get a string representation of the object e.g., for printing. By default it simply returns the type of the object and its address (in hexadecimal).

    Suppose you defined a class called Time, to represent a moment in time. If you create a Time object and display it with println:

    class Time {
        int hours;
        int minutes;
        int seconds;
    
        Time(int hours, int minutes, int seconds) {
            this.hours = hours;
            this.minutes = minutes;
            this.seconds = seconds;
        }
    }
    
     Time t = new Time(5, 20, 13);
     System.out.println(t);
    

    Time@80cc7c0 (the address part can vary)

    You can override the toString method in your classes to provide a more meaningful string representation of the objects of that class.

    Here's an example of overriding the toString method of the Time class:

    class Time{
    
       //...
    
       @Override
       public String toString() {
           return String.format("%02d:%02d:%02d\n", this.hours, this.minutes, this.seconds);
       }
    }
    
     Time t = new Time(5, 20, 13);
     System.out.println(t);
    

    05:20:13

    @Override is an optional annotation you can use to indicate that the method is overriding a method from the parent class.

    The equals method

    There are two ways to check whether values are equal: the == operator and the equals method. With objects you can use either one, but they are not the same.

    • The == operator checks whether objects are identical; that is, whether they are the same object.
    • The equals method checks whether they are equivalent; that is, whether they have the same value.

    The definition of identity is always the same, so the == operator always does the same thing.

    Consider the following variables:

    Time time1 = new Time(9, 30, 0);
    Time time2 = time1;
    Time time3 = new Time(9, 30, 0);
    
    • The assignment operator copies references, so time1 and time2 refer to the same object. Because they are identical, time1 == time2 is true.
    • But time1 and time3 refer to different objects. Because they are not identical, time1 == time3 is false.

    By default, the equals method inherited from the Object class does the same thing as ==. As the definition of equivalence is different for different classes, you can override the equals method to define your own criteria for equivalence of objects of your class.

    Here's how you can override the equals method of the Time class to provide an equals method that considers two Time objects equivalent as long as they represent the same time of the day:

    public class Time {
        int hours;
        int minutes;
        int seconds;
    
        // ...
    
        @Override
        public boolean equals(Object o) {
            Time other = (Time) o;
            return this.hours == other.hours
                    && this.minutes == other.minutes
                    && this.seconds == other.seconds;
        }
    }
    
    Time t1 = new Time(5, 20, 13);
    Time t2 = new Time(5, 20, 13);
    System.out.println(t1 == t2);
    System.out.println(t1.equals(t2));
    

    false
    true
    

    Note that a proper equals method implementation is more complex than the example above. See the article How to Implement Java’s equals Method Correctly by Nicolai Parlog for a detailed explanation before you implement your own equals method.

    Suppose you have the following classes Task, Todo, Deadline:

    public class Task {
        protected String description;
    
        public Task(String description) {
            this.description = description;
        }
    
        public String getDescription() {
            return description;
        }
    }
    
    public class Todo extends Task {
        protected boolean isDone;
    
        public Todo(String description) {
            super(description);
            isDone = false;
        }
    
        public void setDone(boolean done) {
            isDone = done;
        }
    
        public boolean isDone() {
            return isDone;
        }
    }
    
    public class Deadline extends Todo {
    
        protected String by;
    
        public Deadline(String description, String by) {
            super(description);
            this.by = by;
        }
    
        public void setBy(String by) {
            this.by = by;
        }
    
        public String getBy() {
            return by;
        }
    }
    

    Override the toString method of the three classes to produce the following behavior.

    public class Main {
        public static void main(String[] args) {
            // create a todo task and print it
            Todo t = new Todo("Read a good book");
            System.out.println(t);
    
            // change todo fields and print again
            t.setDone(true);
            System.out.println(t);
    
            // create a deadline task and print it
            Deadline d = new Deadline("Read textbook", "Nov 16");
            System.out.println(d);
    
            // change deadline details and print again
            d.setDone(true);
            d.setBy("Postponed to Nov 18th");
            System.out.println(d);
        }
    }
    

    description: Read a good book
    is done? No
    description: Read a good book
    is done? Yes
    description: Read textbook
    is done? No
    do by: Nov 16
    description: Read textbook
    is done? Yes
    do by: Postponed to Nov 18th
    

    You can use the super.toString from the subclass to invoke the behavior of the method you are overriding. This is useful here because the overriding method is simply adding onto the behavior of the overridden method.

    toString() method of Task class and Todo class are given below. You can follow a similar approach for the Deadline class.

    public class Task {
        // ...
    
        @Override
        public String toString() {
            return "description: " + description;
        }
    }
    
    public class Todo extends Task {
        // ...
    
        @Override
        public String toString() {
            String status = null;
            if (isDone){
                status = "Yes";
            } else {
                status = "No";
            }
            return super.toString() + System.lineSeparator() + "is done? " + status;
        }
    }
    

    [W4.2] OOP: Polymorphism

    W4.2a

    Paradigms → OOP → Polymorphism → What

    Can explain OOP polymorphism

    Polymorphism:

    The ability of different objects to respond, each in its own way, to identical messages is called polymorphism. -- Object-Oriented Programming with Objective-C, Apple

    Polymorphism allows you to write code targeting superclass objects, use that code on subclass objects, and achieve possibly different results based on the actual class of the object.

    Assume classes Cat and Dog are both subclasses of the Animal class. You can write code targeting Animal objects and use that code on Cat and Dog objects, achieving possibly different results based on whether it is a Cat object or a Dog object. Some examples:

    • Declare an array of type Animal and still be able to store Dog and Cat objects in it.
    • Define a method that takes an Animal object as a parameter and yet be able to pass Dog and Cat objects to it.
    • Call a method on a Dog or a Cat object as if it is an Animal object (i.e., without knowing whether it is a Dog object or a Cat object) and get a different response from it based on its actual class e.g., call the Animal class' method speak() on object a and get a "Meow" as the return value if a is a Cat object and "Woof" if it is a Dog object.

    Polymorphism literally means "ability to take many forms".

    W4.2b

    Paradigms → OOP → Inheritance → Substitutability

    Can explain substitutability

    Every instance of a subclass is an instance of the superclass, but not vice-versa. As a result, inheritance allows substitutability : the ability to substitute a child class object where a parent class object is expected.

    an Academic is an instance of a Staff, but a Staff is not necessarily an instance of an Academic. i.e. wherever an object of the superclass is expected, it can be substituted by an object of any of its subclasses.

    The following code is valid because an AcademicStaff object is substitutable as a Staff object.

    Staff staff = new AcademicStaff (); // OK
    

    But the following code is not valid because staff is declared as a Staff type and therefore its value may or may not be of type AcademicStaff, which is the type expected by variable academicStaff.

    Staff staff;
    ...
    AcademicStaff academicStaff = staff; // Not OK
    

    W4.2c

    Paradigms → OOP → Inheritance → Dynamic and Static Binding

    Can explain dynamic and static binding

    Dynamic Binding (aka late binding) : a mechanism where method calls in code are resolved at runtime, rather than at compile time.

    Overridden methods are resolved using dynamic binding, and therefore resolves to the implementation in the actual type of the object.

    Paradigms → OOP → Inheritance →

    Overriding

    Method overriding is when a sub-class changes the behavior inherited from the parent class by re-implementing the method. Overridden methods have the same name, same type signature, and same return type.

    Consider the following case of EvaluationReport class inheriting the Report class:

    Report methods EvaluationReport methods Overrides?
    print() print() Yes
    write(String) write(String) Yes
    read():String read(int):String No. Reason: the two methods have different signatures; This is a case of overloading (rather than overriding).

    Paradigms → OOP → Inheritance →

    Overloading

    Method overloading is when there are multiple methods with the same name but different type signatures. Overloading is used to indicate that multiple operations do similar things but take different parameters.

    Type Signature: The type signature of an operation is the type sequence of the parameters. The return type and parameter names are not part of the type signature. However, the parameter order is significant.

    Example:

    Method Type Signature
    int add(int X, int Y) (int, int)
    void add(int A, int B) (int, int)
    void m(int X, double Y) (int, double)
    void m(double X, int Y) (double, int)

    In the case below, the calculate method is overloaded because the two methods have the same name but different type signatures (String) and (int)

    • calculate(String): void
    • calculate(int): void

    Which of these methods override another method? A is the parent class. B inherits A.

    • a
    • b
    • c
    • d
    • e

    d

    Explanation: Method overriding requires a method in a child class to use the same method name and same parameter sequence used by one of its ancestors

    Consider the code below. The declared type of s is Staff and it appears as if the adjustSalary(int) operation of the Staff class is invoked.

    void adjustSalary(int byPercent) {
        for (Staff s: staff) {
            s.adjustSalary(byPercent);
        }
    }
    

    However, at runtime s can receive an object of any subclass of Staff. That means the adjustSalary(int) operation of the actual subclass object will be called. If the subclass does not override that operation, the operation defined in the superclass (in this case, Staff class) will be called.

    Static binding (aka early binding): When a method call is resolved at compile time.

    In contrast, overloaded methods are resolved using static binding.

    Paradigms → OOP → Inheritance →

    Overloading

    Method overloading is when there are multiple methods with the same name but different type signatures. Overloading is used to indicate that multiple operations do similar things but take different parameters.

    Type Signature: The type signature of an operation is the type sequence of the parameters. The return type and parameter names are not part of the type signature. However, the parameter order is significant.

    Example:

    Method Type Signature
    int add(int X, int Y) (int, int)
    void add(int A, int B) (int, int)
    void m(int X, double Y) (int, double)
    void m(double X, int Y) (double, int)

    In the case below, the calculate method is overloaded because the two methods have the same name but different type signatures (String) and (int)

    • calculate(String): void
    • calculate(int): void

    Note how the constructor is overloaded in the class below. The method call new Account() is bound to the first constructor at compile time.

    class Account {
    
        Account () {
            // Signature: ()
            ...
        }
    
        Account (String name, String number, double balance) {
            // Signature: (String, String, double)
            ...
        }
    }
    

    Similarly, the calcuateGrade method is overloaded in the code below and a method call calculateGrade("A1213232") is bound to the second implementation, at compile time.

    void calculateGrade (int[] averages) { ... }
    void calculateGrade (String matric) { ... }
    

    W4.2d

    Paradigms → OOP → Polymorphism → How

    Can explain how substitutability operation overriding, and dynamic binding relates to polymorphism

    Three concepts combine to achieve polymorphism: substitutability, operation overriding, and dynamic binding.

    • Substitutability: Because of substitutability, you can write code that expects object of a parent class and yet use that code with objects of child classes. That is how polymorphism is able to treat objects of different types as one type.
    • Overriding: To get polymorphic behavior from an operation, the operation in the superclass needs to be overridden in each of the subclasses. That is how overriding allows objects of different subclasses to display different behaviors in response to the same method call.
    • Dynamic binding: Calls to overridden methods are bound to the implementation of the actual object's class dynamically during the runtime. That is how the polymorphic code can call the method of the parent class and yet execute the implementation of the child class.

    Which one of these is least related to how OO programs achieve polymorphism?

    (c)

    Explanation: Operation overriding is the one that is related, not operation overloading.

    W4.2e

    C++ to Java → Inheritance → Polymorphism

    Can use polymorphism in Java

    Java is a strongly-typed language which means the code works with only the object types that it targets.

    The following code PetShelter keeps a list of Cat objects and make them speak. The code will not work with any other type, for example, Dog objects.

    public class PetShelter {
        private static Cat[] cats = new Cat[]{
                new Cat("Mittens"),
                new Cat("Snowball")};
    
        public static void main(String[] args) {
            for (Cat c: cats){
                System.out.println(c.speak());
            }
        }
    }
    

    Mittens: Meow
    Snowball: Meow
    
    public class Cat {
        public Cat(String name) {
            super(name);
        }
    
        public String speak() {
            return name + ": Meow";
        }
    }
    

    This strong-typing can lead to unnecessary verbosity caused by repetitive similar code that do similar things with different object types.

    If the PetShelter is to keep both cats and dogs, you'll need two arrays and two loops:

    public class PetShelter {
        private static Cat[] cats = new Cat[]{
                new Cat("Mittens"),
                new Cat("Snowball")};
        private static Dog[] dogs = new Dog[]{
                new Dog("Spot")};
    
        public static void main(String[] args) {
            for (Cat c: cats){
                System.out.println(c.speak());
            }
            for(Dog d: dogs){
                System.out.println(d.speak());
            }
        }
    }
    

    Mittens: Meow
    Snowball: Meow
    Spot: Woof
    
    public class Dog {
        public Dog(String name) {
            super(name);
        }
    
        public String speak() {
            return name + ": Woof";
        }
    }
    

    A better way is to take advantage of polymorphism to write code that targets a superclass so that it works with any subclass objects.

    The PetShelter2 use one data structure to keep both types of animals and one loop to make them speak. The code targets the Animal superclass (assuming Cat and Dog inherits from the Animal class) instead of repeating the code for each animal type.

    public class PetShelter2 {
        private static Animal[] animals = new Animal[]{
                new Cat("Mittens"),
                new Cat("Snowball"),
                new Dog("Spot")};
    
        public static void main(String[] args) {
            for (Animal a: animals){
                System.out.println(a.speak());
            }
        }
    }
    

    Mittens: Meow
    Snowball: Meow
    Spot: Woof
    
    public class Animal {
    
        protected String name;
    
        public Animal(String name){
            this.name = name;
        }
        public String speak(){
            return name;
        }
    }
    
    public class Cat extends Animal {
        public Cat(String name) {
            super(name);
        }
    
        @Override
        public String speak() {
            return name + ": Meow";
        }
    }
    
    public class Dog extends Animal {
        public Dog(String name) {
            super(name);
        }
    
        @Override
        public String speak() {
            return name + ": Woof";
        }
    }
    

    Explanation: Because Java supports polymorphism, you can store both Cat and Dog objects in an array of Animal objects. Similarly, you can call the speak method on any Animal object (as done in the loop) and yet get different behavior from Cat objects and Dog objects.

    Suggestion: try to add an Animal object (e.g., new Animal("Unnamed")) to the animals array and see what happens.

    Polymorphic code is better in several ways:

    • It is shorter.
    • It is simpler.
    • It is more flexible (in the above example, the main method will work even if we add more animal types).

    The Main class below keeps a list of Circle and Rectangle objects and prints the area (as an int value) of each shape when requested.

    Add the missing variables/methods to the code below so that it produces the output given.

    public class Main {
        //TODO add your methods here
    
        public static void main(String[] args) {
            addShape(new Circle(5));
            addShape(new Rectangle(3, 4));
            addShape(new Circle(10));
            printAreas();
            addShape(new Rectangle(4, 4));
            printAreas();
        }
    }
    

    78
    12
    314
    78
    12
    314
    16
    

    Circle class and Rectangle class is given below but you'll need to add a parent class Shape:

    public class Circle {
    
        private int radius;
    
        public Circle(int radius) {
            this.radius = radius;
        }
    
        public int area() {
            return (int)(Math.PI * radius * radius);
        }
    }
    
    public class Rectangle {
        private int height;
        private int width;
    
        public Rectangle(int height, int width){
            this.height = height;
            this.width = width;
        }
    
        public int area() {
            return height * width;
        }
    }
    

    You may use an array of size 100 to store the shapes.

    public class Main {
        private static Shape[] shapes = new Shape[100];
        private static int shapeCount = 0;
    
        public static void addShape(Shape s){
            shapes[shapeCount] = s;
            shapeCount++;
        }
    
        // ...
    
    }
    

    [W4.3] Testing text UIs

    W4.3a

    Quality Assurance → Testing → Introduction → What

    Can explain testing

    Testing: Operating a system or component under specified conditions, observing or recording the results, and making an evaluation of some aspect of the system or component. –- source: IEEE

    When testing, we execute a set of test cases. A test case specifies how to perform a test. At a minimum, it specifies the input to the software under test (SUT) and the expected behavior.

    Example: A minimal test case for testing a browser:

    • Input – Start the browser using a blank page (vertical scrollbar disabled). Then, load longfile.html located in the test data folder.
    • Expected behavior – The scrollbar should be automatically enabled upon loading longfile.html.

    Test cases can be determined based on the specification, reviewing similar existing systems, or comparing to the past behavior of the SUT.

    Other details a test case can contain extra A more elaborate test case can have other details such as those given below.
    • A unique identifier : e.g. TC0034-a
    • A descriptive name: e.g. vertical scrollbar activation for long web pages
    • Objectives: e.g. to check whether the vertical scrollbar is correctly activated when a long web page is loaded to the browser
    • Classification information: e.g. priority - medium, category - UI features
    • Cleanup, if any: e.g. empty the browser cache.

    For each test case we do the following:

    1. Feed the input to the SUT
    2. Observe the actual output
    3. Compare actual output with the expected output

    A test case failure is a mismatch between the expected behavior and the actual behavior. A failure indicates a potential defect (or a bug), unless the error is in the test case itself.

    Example: In the browser example above, a test case failure is implied if the scrollbar remains disabled after loading longfile.html. The defect/bug causing that failure could be an uninitialized variable.

    A deeper look at the definition of testing extra

    Here is another definition of testing:

    Software testing consists of the dynamic verification that a program provides expected behaviors on a finite set of test cases, suitably selected from the usually infinite execution domain. -– source: Software Engineering Book of Knowledge V3

    Some things to note (indicated by keywords in the above definition):

    • Dynamic: Testing involves executing the software. It is not by examining the code statically.
    • Finite: In most non-trivial cases there are potentially infinite test scenarios but resource constraints dictate that we can test only a finite number of scenarios.
    • Selected: In most cases it is not possible to test all scenarios. That means we need to select what scenarios to test.
    • Expected: Testing requires some knowledge of how the software is expected to behave.

    Explain how the concepts of testing, test case, test failure, and defect are related to each other.

    W4.3b

    Quality Assurance → Testing → Regression Testing → What

    Can explain regression testing

    When we modify a system, the modification may result in some unintended and undesirable effects on the system. Such an effect is called a regression.

    Regression testing is the re-testing of the software to detect regressions. Note that to detect regressions, we need to retest all related components, even if they had been tested before.

    Regression testing is more effective when it is done frequently, after each small change. However, doing so can be prohibitively expensive if testing is done manually. Hence, regression testing is more practical when it is automated.

    Regression testing is the automated re-testing of a software after it has been modified.

    c.

    Explanation: Regression testing need not be automated but automation is highly recommended.

    Explain why and when you would do regression testing in a software project.

    W4.3c

    Quality Assurance → Testing → Test Automation → What

    Can explain test automation

    An automated test case can be run programmatically and the result of the test case (pass or fail) is determined programmatically. Compared to manual testing, automated testing reduces the effort required to run tests repeatedly and increases precision of testing (because manual testing is susceptible to human errors).

    W4.3d

    Quality Assurance → Testing → Test Automation → Automated Testing of CLI Apps

    Can semi-automate testing of CLIs

    A simple way to semi-automate testing of a CLI(Command Line Interface) app is by using input/output re-direction.

    • First, we feed the app with a sequence of test inputs that is stored in a file while redirecting the output to another file.
    • Next, we compare the actual output file with another file containing the expected output.

    Let us assume we are testing a CLI app called AddressBook. Here are the detailed steps:

    1. Store the test input in the text file input.txt.

      add Valid Name p/12345 valid@email.butNoPrefix
      add Valid Name 12345 e/valid@email.butPhonePrefixMissing
      
    2. Store the output we expect from the SUT in another text file expected.txt.

      Command: || [add Valid Name p/12345 valid@email.butNoPrefix]
      Invalid command format: add 
      
      Command: || [add Valid Name 12345 e/valid@email.butPhonePrefixMissing]
      Invalid command format: add 
      
    3. Run the program as given below, which will redirect the text in input.txt as the input to AddressBook and similarly, will redirect the output of AddressBook to a text file output.txt. Note that this does not require any code changes to AddressBook.

      java AddressBook < input.txt > output.txt
      
      • The way to run a CLI program differs based on the language.
        e.g., In Python, assuming the code is in AddressBook.py file, use the command
        python AddressBook.py < input.txt > output.txt

      • If you are using Windows, use a normal command window to run the app, not a Power Shell window.

      More on the > operator and the < operator extra

      A CLI program takes input from the keyboard and outputs to the console. That is because those two are default input and output streams, respectively. But you can change that behavior using < and > operators. For example, if you run AddressBook in a command window, the output will be shown in the console, but if you run it like this,

      java AddressBook > output.txt 
      

      the Operating System then creates a file output.txt and stores the output in that file instead of displaying it in the console. No file I/O coding is required. Similarly, adding < input.txt (or any other filename) makes the OS redirect the contents of the file as input to the program, as if the user typed the content of the file one line at a time.

      Resources:

    4. Next, we compare output.txt with the expected.txt. This can be done using a utility such as Windows FC (i.e. File Compare) command, Unix diff command, or a GUI tool such as WinMerge.

      FC output.txt expected.txt
      

    Note that the above technique is only suitable when testing CLI apps, and only if the exact output can be predetermined. If the output varies from one run to the other (e.g. it contains a time stamp), this technique will not work. In those cases we need more sophisticated ways of automating tests.

    CLI App: An application that has a Command Line Interface. i.e. user interacts with the app by typing in commands.

    [W4.4] IDE: Intermediate

    W4.4a

    Implementation → IDEs → Debugging → What

    Can explain debugging

    Debugging is the process of discovering defects in the program. Here are some approaches to debugging:

    • Bad -- By inserting temporary print statements: This is an ad-hoc approach in which print statements are inserted in the program to print information relevant to debugging, such as variable values. e.g. Exiting process() method, x is 5.347. This approach is not recommended due to these reasons.
      • Incurs extra effort when inserting and removing the print statements.
      • These extraneous program modifications increases the risk of introducing errors into the program.
      • These print statements, if not removed promptly after the debugging, may even appear unexpectedly in the production version.
    • Bad -- By manually tracing through the code: Otherwise known as ‘eye-balling’, this approach doesn't have the cons of the previous approach, but it too is not recommended (other than as a 'quick try') due to these reasons:
      • It is difficult, time consuming, and error-prone technique.
      • If you didn't spot the error while writing code, you might not spot the error when reading code too.
    • Good -- Using a debugger: A debugger tool allows you to pause the execution, then step through one statement at a time while examining the internal state if necessary. Most IDEs come with an inbuilt debugger. This is the recommended approach for debugging.

    W4.4b

    Tools → Intellij IDEA → Debugging: Basic

    Can step through a program using a debugger

    This video (from LaunchCode) gives a pretty good explanation of how to use the Intellij IDEA debugger.

    W4.4c

    Tools → Intellij IDEA → Productivity Shortcuts

    Can use some useful IDE productivity shortcuts