Detailed Table of Contents
Guidance for the item(s) below:
Now that you know the basics about classes and objects, let's move to the next level. The sections below explain the third core concept of OOP (called inheritance) and how to use that in Java.
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
.
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
behave 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 women) 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 a Vehicle
, an Asset
and a Liability
.
Exercises
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
OOP → Inheritance → What
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). |
Exercises
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.
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 (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.
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 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:
At the class level:
public
: the class is visible to all other classespublic
At the member level:
public
: the member is visible to all other classesprotected
: same as public
public
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,
The Task
class is given below:
public class Task {
protected String description;
public Task(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
Todo
class that inherits from the Task
class.
boolean
field isDone
to indicate whether the todo is done or not done.isDone()
method to access the isDone
field and a setDone(boolean)
method to set the isDone
field.Deadline
class that inherits from the Todo
class that you implemented in the previous step. It should have,
String
field by
to store the details of when the task to be done e.g., Jan 25th 5pm
getBy()
method to access the value of the by
field, and a corresponding setBy(String)
method.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
Hint
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.
toString
methodEvery 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.
equals
methodThere 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.
==
operator checks whether objects are identical; that is, whether they are the same object.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);
time1
and time2
refer to the same object. Because they are identical, time1 == time2
is true
.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.
Hint
Guidance for the item(s) below:
Inheritance is even more powerful when combined with polymorphism (which also happens to be the fourth core concept of OOP), explained in the sections below.
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:
Animal
and still be able to store Dog
and Cat
objects in it.Animal
object as a parameter and yet be able to pass Dog
and Cat
objects to it.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's 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".
Paradigms → Object Oriented Programming → Inheritance → What
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 AcademicStaff
is an instance of a Staff
, but a Staff
is not necessarily an instance of an AcademicStaff
. 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
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
The Cat
class
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
The Dog
class
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
uses 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
The Animal
, Cat
, and Dog
classes
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:
main
method will work even if we add more animal types).Exercises
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 extends Shape {
private int radius;
public Circle(int radius) {
this.radius = radius;
}
@Override
public int area() {
return (int)(Math.PI * radius * radius);
}
}
public class Rectangle extends Shape{
private int height;
private int width;
public Rectangle(int height, int width){
this.height = height;
this.width = width;
}
@Override
public int area() {
return height * width;
}
}
You may use an array of size 100 to store the shapes.
Partial solution
Hint
Guidance for the item(s) below:
As you start adding features to your project iteratively, you'll need a way to detect if the new code breaks the existing code. Next, let's learn a rather simple way to do that using a certain type of testing (we'll be learning more sophisticated methods in later weeks).
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, you 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:
longfile.html
located in the test data
folder.longfile.html
.Test cases can be determined based on the specification, reviewing similar existing systems, or comparing to the past behavior of the SUT.
For each test case you should do the following:
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.
Exercises
Can explain regression testing
When you 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, you 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.
Exercises
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).
Resources
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.
Let's assume you are testing a CLI app called AddressBook
. Here are the detailed steps:
Store the test input in the text file input.txt
.
Example input.txt
Store the output you expect from the SUT in another text file expected.txt
.
Example expected.txt
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 PowerShell window.
Next, you compare output.txt
with the expected.txt
. This can be done using a utility such as Windows' FC
(i.e. File Compare) command, Unix's 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, you need more sophisticated ways of automating tests.
Guidance for the item(s) below:
Now that you know how to use IDE basic features, it's worth looking at even more ways of leveraging their power. In particular, the debugging feature can be indispensable at times.
Can explain debugging
Debugging is the process of discovering defects in the program. Here are some approaches to debugging:
Exiting process() method, x is 5.347
. This approach is not recommended due to these reasons:
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.
Guidance for the item(s) below:
Let's learn how to create a pull request (PRs) on GitHub; you need to create one for your project this week.
Can create PRs on GitHub
Suppose you want to propose some changes to a GitHub repo (e.g., samplerepo-pr-practice) as a pull request (PR). Here is a scenario you can try in order to learn how to create PRs:
1. Fork the repo onto your GitHub account.
2. Clone it onto your computer.
3. Commit your changes e.g., add a new file with some contents and commit it.
master
branchadd-intro
(remember to switch to the master
branch before creating a new branch) and add your commit to it.4. Push the branch you updated (i.e., master
branch or the new branch) to your fork, as explained here.
5. Initiate the PR creation:
Go to your fork.
Click on the Pull requests tab followed by the New pull request button. This will bring you to the 'Comparing changes' page.
Set the appropriate target repo and the branch that should receive your PR, using the base repository
and base
dropdowns. e.g.,
base repository: se-edu/samplerepo-pr-practice base: master
Normally, the default value shown in the dropdown is what you want but in case your fork has , the default may not be what you want.
Indicate which repo:branch contains your proposed code, using the head repository
and compare
dropdowns. e.g.,
head repository: myrepo/samplerepo-pr-practice compare: master
6. Verify the proposed code: Verify that the diff view in the page shows the exact change you intend to propose. If it doesn't, as necessary.
7. Submit the PR:
Click the Create pull request button.
Fill in the PR name and description e.g.,
Name: Add an introduction to the README.md
Description:
Add some paragraph to the README.md to explain ...
Also add a heading ...
If you want to indicate that the PR you are about to create is 'still work in progress, not yet ready', click on the dropdown arrow in the Create pull request button and choose Create draft pull request
option.
Click the Create pull request button to create the PR.
Go to the receiving repo to verify that your PR appears there in the Pull requests
tab.
The next step of the PR life cycle is the PR review. The members of the repo that received your PR can now review your proposed changes.
You can update the PR along the way too. Suppose PR reviewers suggested a certain improvement to your proposed code. To update your PR as per the suggestion, you can simply modify the code in your local repo, commit the updated code to the same master
branch, and push to your fork as you did earlier. The PR will auto-update accordingly.
Sending PRs using the master
branch is less common than sending PRs using separate branches. For example, suppose you wanted to propose two bug fixes that are not related to each other. In that case, it is more appropriate to send two separate PRs so that each fix can be reviewed, refined, and merged independently. But if you send PRs using the master
branch only, both fixes (and any other change you do in the master
branch) will appear in the PRs you create from it.
To create another PR while the current PR is still under review, create a new branch (remember to switch back to the master
branch first), add your new proposed change in that branch, and create a new PR following the steps given above.
It is possible to create PRs within the same repo e.g., you can create a PR from branch feature-x
to the master
branch, within the same repo. Doing so will allow the code to be reviewed by other developers (using PR review mechanism) before it is merged.
Problem: merge conflicts in ongoing PRs, indicated by the message This branch has conflicts that must be resolved. That means the upstream repo's master
branch has been updated in a way that the PR code conflicts with that master
branch. Here is the standard way to fix this problem:
master
branch from the upstream repo to your local repo.git checkout master
git pull upstream master
master
branch (that you updated in the previous step) onto the PR branch, in order to bring over the new code in the master
branch to your PR branch.git checkout pr-branch # assuming pr-branch is the name of branch in the PR
git merge master
master
branch.
Resolve the conflict manually (this topic is covered elsewhere), and complete the merge.master
branch, the merge conflict alert in the PR will go away automatically.