- Chapter 17: Class Inheritance (OOP)
- Case Study: Lengths
- Case Study: LMS ExampleNext, we will analyze a more complex example. The program is a basic learning management/gradebook system (LMS) which keeps track of student accounts and teacher accounts. This will be significantly more involved than the previous example and use more features of inheritance. The program will allow teachers or students to login. Once logged in, students can view their grades or log back out. Teachers can view any student’s grade or set their grade.
- App Class
- User Class
- Student Class
- Teacher Class
- Main Program Loop
- Final Testing
- Polymorphism
- Review
When used properly, inheritance allows us to extend the properties or methods of a class to its subclasses. It’s like how a child inherits traits from their biological parents. Inheritance allows us to form an “is-a” relationship between different classes, where the child class extends the parent class. This was briefly touched upon back in Chapter 15. In this chapter, we will explore the use of inheritance in more detail and you will learn how to work on projects involving it.
Case Study: Lengths
Let’s start by looking at a relatively simple example. A program that contains object types representing different units of length. The program has four classes:
- App – has the main method and test logic
- Length – An object holding a generic unit of length
- Foot – An object representing a length measured in imperial feet
- Meter – An object representing a length measured in metric meters
Length will be the parent class to Foot and Meter, since feet and meters are units of length. All lengths will have two properties – the length measurement (a double) and the unit name (a string). All lengths will also have a toMeters method which converts the unit into meters.
App.java
public class App {
public static void main(String[] args) {
//an array of 3 different types of Length objects
Length[] lengths = new Length[3];
lengths[0] = new Length(5.0);
lengths[1] = new Meter(10.0);
lengths[2] = new Foot(10.0);
for (Length length : lengths) {
//I can print out any length object and it will print out the correct unit
System.out.println(length);
System.out.println("In meters: " + length.toMeters());
}
}
}
Code language: JavaScript (javascript)
The App class creates a “lengths” array which is of type Length. The array holds 3 lengths – one measured in generic Length units, one in meters, and one in feet. Then a loop prints out all the lengths along with the conversion to meters.
Length.java
public class Length {
//instance variables
double length;
String unit;
//constructor
public Length(double length) {
this.length = length;
this.unit = "units";
}
/**
* Returns the length of this Length object.
*/
public String toString() {
return this.length + " " + this.unit;
}
// we cannot convert an unknown unit to meters
public double toMeters() {
return -1.0;
}
}
Code language: JavaScript (javascript)
The Length class contains instance variables length and unit. The length amount is whatever number provided to the constructor as a double. The unit is fixed set to “units” since we don’t know what type of unit a generic “Length” is. The toString method returns a string containing the amount and the unit type. The toMeters method returns -1, since we cannot convert an unknown unit type into meters.
Meter.java
public class Meter extends Length{
public Meter(double length) {
super(length);
this.unit = "meters";
}
public double toMeters() {
return this.length;
}
}
Code language: JavaScript (javascript)
The Meter class extends Length. This is how we can officially declare that a Meter is a type or subclass of Length. A Meter is created the same way a generic Length is created, by providing the length amount. The super keyword calls the constructor from the parent Length class and sets the length of the Mater/Length to the given number. We then modify the “unit” string to be in “meters” instead of “units”.
There is no need to recreate the toString method for the Meter class since it’s already complete in the parent Length class. The toMeters method is still created, but it just returns the length, since a Meter is already measured in meters.
Finally, the Foot class.
Foot.java
public class Foot extends Length {
public Foot(double length) {
super(length);
this.unit = "feet";
}
public double toMeters() {
return this.length * 0.3048;
}
}
Code language: JavaScript (javascript)
The Foot class is almost identical to the Meter class, except it sets the unit type to “feet” and the toMeters method returns the conversion of feet to meters with simple multiplication.
Remember, the lengths array contains all the 3 different types of Length objects and then the program prints the object out (toString is automatically called) and the conversion to meters. When I run the program, it produces this output:
5.0 units In meters: -1.0 10.0 meters In meters: 10.0 10.0 feet In meters: 3.048
We could follow the same formula to create any other type of Length. We can then add any combination of length types to the array and as long as we include right calculation in the toMeters method, they will work without breaking the existing array loop.
Case Study: LMS ExampleNext, we will analyze a more complex example. The program is a basic learning management/gradebook system (LMS) which keeps track of student accounts and teacher accounts. This will be significantly more involved than the previous example and use more features of inheritance. The program will allow teachers or students to login. Once logged in, students can view their grades or log back out. Teachers can view any student’s grade or set their grade.
To keep this example simple, the program does not actually save any of the information to secondary storage. It also does not support any features like classes and setting additional grades. We can only use it to login to our account, set a grade as a teacher, and view a grade as a student.
Examine the UML diagram below. It summarizes each class in this program, and the relationship between them.
App Class
The first class is App. Three important variables are created in the App class.
- loggedInUser – The object for the currently logged in User
- users – An array of User objects containing each User in the system.
- input – The user input scanner
The App class also contains the main method and a tryLogin method, which is used to log a given User in with their username and password.
The main method will create the users array. The users array will be a predefined set of User objects. Since the App class will need to create and use teacher/student objects, we’re not quite ready to code here. We need to create the other classes first.
User Class
The User class (User.java) is a parent of the Teacher and Student classes. It contains the methods and instance variables which are shared between the two subclasses.
All Users must have a username and a password, regardless of if they are a Student or a Teacher. So I have placed the “username” and “password” instance variables in the User class. Like all instance variables, they should be declared private. We will create getters for them so we can get them in our other classes. The accessLevel variable will function as a sort of user-group identifier. I will use 1 for student and 2 for teachers. The isLoggedIn boolean simply keeps track of if the given User is logged in.
The User class will be declared abstract, meaning we cannot instantiate User objects by themselves. We can only instantiate its concrete subclasses (Teacher or Student).
With that, we’re ready to work on the User class. Begin by creating the class file User.java and declaring it abstract.
User.java
public abstract class User {
//code here
}
Code language: PHP (php)
Next, we need to declare the instance variables and initialize them in a constructor. Although the User class is abstract, it still must have a constructor. We will call this constructor from the subclasses later. I’ve implemented the get methods too.
public abstract class User {
private String username;
private String password;
private int accessLevel;
private boolean isLoggedIn;
// Constructor
public User(String username, String password, int accessLevel) {
this.username = username;
this.password = password;
this.accessLevel = accessLevel; //1 will be student 2 for teacher
this.isLoggedIn = false;
}
// Getters
public String getUsername(){
return this.username;
}
public int getAccessLevel(){
return this.accessLevel;
}
public boolean getIsLoggedIn(){
return this.isLoggedIn;
}
}
Code language: JavaScript (javascript)
Now that the boilerplate is out of the way, let’s address the other methods of the User class.
The viewAccountDetails method will print out the user’s information to the screen. So it’s pretty straightforward.
public void viewAccountDetails(){
System.out.println("Username: " + this.username);
System.out.println("Password: " + this.password);
System.out.println("Is Logged In: " + this.isLoggedIn);
}
Code language: JavaScript (javascript)
The login method will take a password guess and check if it matches the user’s actual password. If it does, we will set the isLoggedIn variable to true.
// check if password matches
public void login(String passwordGuess) {
if (passwordGuess.equals(this.password)) {
this.isLoggedIn = true;
System.out.println("You are now logged in.");
}
else{
System.out.println("Incorrect password.");
}
}
Code language: JavaScript (javascript)
The logout method just logs the user out.
// change login status to logout
public void logout() {
this.isLoggedIn = false;
System.out.println("You are now logged out.");
}
Code language: JavaScript (javascript)
And finally, the chooseAndExecuteAction method is not directly implemented in the User class. However, it will allow the User to select their options (view grades, set grades, logout) based on if they’re a Teacher or a Student. We will need to override this method later. If we try to access this method from a User object where it is not overridden, it will just print out a simple error message.
// this is not implemented here, but will be implemented in the subclasses
public void chooseAndExecuteAction(){
System.out.println("Error, chooseAndExecuteAction should be overridden.");
}
Code language: JavaScript (javascript)
And with that, the User class is complete.
You may view the complete class file if you need it.
Student Class
The Student class extends the User class. So all Student objects are Users, and can call all the methods we created earlier.
The Student class contains one additional instance variable – grade. For simplicity, the grade variable will just be an int holding any number set by a teacher. So Students in our system are not graded with letters or across different classes. Each student only has one single numerical grade.
Lets begin by creating the class file, declaring it and its instance variable, and creating the constructor.
Student.java
public class Student extends User{
// each student has a single grade
private int grade;
// constructor extends User constructor
public Student(String username, String password){
// superconstructor
super(username, password, 1);
// instnace variable specific to a Student
this.grade = 0;
}
}
Code language: JavaScript (javascript)
On the first line, the class Student is declared with the extends keyword. We use this when we want a class to extend another.
The grade instance variable will only be changed directly within the Student class, so we can declare it private.
The constructor is very similar to the constructor for a User. It requires a username and a password. It doesn’t need an access level, since the access level for all Students will be the number 1. Within the constructor, you will notice I use the super keyword to call the superconstructor. A superconstructor is a constructor defined in a class above the current class. In this case, the only class above the Student class in our hierarchy is the User class. So this calls the User() constructor we created earlier in the User class. Finally, the Student constructor sets the initial grade to zero.
Testing
With this in place, we have everything we need to create a Student User object elsewhere in the program. So now would be a good time to test some of our earlier work out.
Go to, or create, an App class with a main method if you haven’t already. I’m also going to declare the three variables I discussed earlier from the UML.
import java.util.Scanner;
public class App {
private static User loggedInUser; // currently logged in User object
static User[] users; // this is a list of all users in the system
static Scanner input;
public static void main(String[] args) {
}
}
Code language: JavaScript (javascript)
Let’s begin by creating a single test Student and printing out their details using the viewAccountDetails method.
Add the following to your main method:
public static void main(String[] args) {
Student test = new Student("kevin", "1234");
test.viewAccountDetails();
}
Code language: JavaScript (javascript)
When you run the program, you should get this output:
Username: kevin Password: 1234 Is Logged In: false
Now consider this, since a Student is a User, we can also do this:
User test = new Student("kevin", "1234");
test.viewAccountDetails();
Code language: JavaScript (javascript)
Username: kevin Password: 1234 Is Logged In: false
I changed the test variable declaration from Student to User. Remember, we cannot directly instantiate User objects. However, I’m still creating a “new Student” so this code is acceptable and runs just as before.
If I try to create a new User, without explicitly defining it as a student, it will error out. This is because the class User is abstract.
//this will not work
User test = new User("kevin", "1234");
test.viewAccountDetails();
Code language: JavaScript (javascript)
Exception in thread "main" java.lang.Error: Unresolved compilation problem: Cannot instantiate the type User
Let’s revert to the test user being a Student and test the remaining methods the Student inherits from the User class.
User test = new Student("kevin", "1234");
test.login("1234");
test.viewAccountDetails();
test.logout();
Code language: JavaScript (javascript)
You are now logged in. Username: kevin Password: 1234 Is Logged In: true You are now logged out.
Everything appears to be in working order. You can delete the test code. Go back to the Student class and we’ll continue working on the remaining methods.
Student Class (cont)
We need to implement the viewGrades, setGrade, and chooseAndExecuteAction methods.
The viewGrades method simply prints out the grades for this Student.
// print out the students grade
public void viewGrades(){
System.out.println("Grade for: " + this.getUsername());
System.out.println("Grade: " + this.grade);
}
Code language: JavaScript (javascript)
Because the username instance variable belongs to the parent User class, and not the Student class, to access it we must call the public getUsername method. If you tried this.username, it would not work. This is an important distinction. Private scope variables and methods may only be used by the class they belong to, not the classes extending them.
The setGrade method sets a student’s grade. It isn’t called directly from within the Student class (Students cannot set their own grades) – but we add it so the Teachers can set a Student’s grade in the future.
// set their grade (called from Teacher class)
public void setGrade(int grade){
this.grade = grade;
}
Code language: JavaScript (javascript)
Finally, the chooseAndExecuteAction method will allow a logged in student to do allowed tasks. Students will ultimately be able to view their existing grades or logout. That’s all they can do.
Before I can ask a student to select an option, I need to initialize the user input Scanner I declared earlier in the App class. Java is quite particular about Scanners. Only one user input scanner can be open at a time, so to keep things simple, I’m going to use the public input scanner I declare in the App.java class across all the other classes.
App.java
import java.util.Scanner;
public class App {
private static User loggedInUser; // currently logged in User object
static User[] users; // this is a list of all users in the system
static Scanner input;
public static void main(String[] args) {
input = new Scanner(System.in);
}
}
Code language: JavaScript (javascript)
Back in the Student class, I can implement the chooseAndExecuteAction method.
Student.java
// let the student view their grades or logout
@Override
public void chooseAndExecuteAction(){
System.out.println("Choose an action:");
System.out.println("1. View Grades");
System.out.println("2. Logout");
String action = App.input.nextLine();
if (action.equals("1")){
this.viewGrades();
}
else if (action.equals("2")){
this.logout();
}
else{
System.out.println("Invalid action.");
}
}
Code language: JavaScript (javascript)
The @Override annotation is used to indicate that we are overriding, or replacing, the method of a superclass with the following method from the subclass. You should type this before any method you’re overriding. The program may compile without it, but it’s best practice, as it tells the compiler to double check that you’re actually overriding a method where you mean to. Now, whenever we execute chooseAndExecuteAction on a student object, it will call the Student’s version of the method rather than the User’s version or the Teacher’s version.
This method is pretty straightforward. It asks the user if they want to view their grades or logout. The action String takes the user input from the scanner initialized back in the main method of App.java. If they type 1, it calls the viewGrades method from the Student class. If 2, it calls the logout method defined back in the parent User class. Remember, the this keyword just means we’re referring to this particular instance – that is, whatever the current object is.
At this point, our Student class is complete. We will test the chooseAndExecuteAction method after we implement the main program loop in the App class.
Teacher Class
The Teacher class also extends the User class. Teachers have the ability to view any students grade by providing a valid student username. They can also set the grade int for any given student.
Create Teacher.java, have it extend the User class, and make the constructor. The constructor will look the same as the Student class, except it sets the access level to 2 instead of 1. We aren’t saving any additional info for Teachers beyond their username and password, so we don’t need any new instance variables.
Teacher.java
public class Teacher extends User{
// constructor with superconstructor
public Teacher(String username, String password){
super(username, password, 2);
}
}
Code language: JavaScript (javascript)
View and Set Grades
The viewGrades method takes a Student as the parameter. All it really does is call the .viewGrades() method of the Student class.
// view a given students grades
public void viewGrades(Student s){
System.out.println("You are viewing " + s.getUsername() + "'s grades.");
s.viewGrades();
}
Code language: JavaScript (javascript)
Teachers can also set any student’s grade.
// set a given students grades
public void setGrade(Student s, int grade){
System.out.println("You are setting " + s.getUsername() + "'s grade to " + grade);
s.setGrade(grade);
}
Code language: JavaScript (javascript)
The most complex part of the Teacher class is the chooseAndExecuteAction part. We need to add the options to view a student’s grade based on their username, set grade based on username, and log out.
// let the teacher view a students grade, set their grade, or logout
@Override
public void chooseAndExecuteAction(){
System.out.println("Choose an action:");
System.out.println("1. View Student Grade");
System.out.println("2. Set Student Grade");
System.out.println("3. Logout");
String action = App.input.nextLine();
if (action.equals("1")){
System.out.println("Enter the username of the student whose grades you want to view:");
String username = App.input.nextLine();
for (User user : App.users) {
if (user.getUsername().equals(username)) {
if (user.getAccessLevel() == 1){
this.viewGrades((Student)user);
}
else{
System.out.println("You do not have permission to view this user's grades.");
}
}
}
}
else if (action.equals("2")){
System.out.println("Enter the username of the student whose grades you want to set:");
String username = App.input.nextLine();
for (User user : App.users) {
if (user.getUsername().equals(username)) {
if (user.getAccessLevel() == 1){
System.out.println("Enter the grade you want to set:");
String grade = App.input.nextLine();
this.setGrade((Student)user, Integer.parseInt(grade));
}
else{
System.out.println("You do not have permission to set this user's grades.");
}
}
}
}
else if (action.equals("3")){
this.logout();
}
else{
System.out.println("Invalid action.");
}
}
Code language: JavaScript (javascript)
The method allows the user to select from the options by entering a number. If they want to view a student’s grade, it asks them to enter the username of that student, captures the input, and checks if the given user exists in the App.users array by looping through it. If it finds a username matching the entered value, and the accessLevel of that user is 1 (so we’re checking student grades, not teacher grades which do not exist), it calls the viewGrades method and shows the teacher that student’s grades.
The option to set a student’s grade does basically the same thing as viewing, with the addition of asking the teacher to enter a new grade and calling the setGrade method.
That’s it for the Teacher class.
Main Program Loop
With all the User related classes complete, we need to work on the main loop for the program. The loop needs to do the following:
- Allow a user to log in to the system
- Call the chooseAndExecuteAction method once a user logs in
- Allow the user to keep doing things until they log out
- Let them log in as someone else or exit the program when they log out
I have decided to separate the App.java file into two methods – the main method and a separate “tryLogin” method.
The main method will contain the loop for the program, and the tryLogin will allow the user to enter a username and password to try logging in. The main method will also set up some test users in the users array for us to work with.
Begin by adding users to the users array. Add at least 1 student and 1 teacher.
import java.util.Scanner;
public class App {
private static User loggedInUser; // currently logged in User object
static User[] users; // this is a list of all users in the system
static Scanner input;
public static void main(String[] args) {
input = new Scanner(System.in);
// setup test accounts
users = new User[3];
users[0] = new Teacher("teacher", "password");
users[1] = new Student("mary", "password");
users[2] = new Student("jason", "password");
// say hi
System.out.println("Welcome to super advanced LMS");
/* **** .... continues ... *** */
Code language: JavaScript (javascript)
As you see, I made a teacher with the username “teacher” and two students with usernames “mary” and “jason”. To keep things straightforward, all their passwords are password. I then welcome the user to the program with a message.
Since the tryLogin method needs to be called from the main loop, I’m going to create that before I add the main loop. So outside the main method, add a method that allows the user to try logging in.
// try to login to an account
private static void tryLogin() {
System.out.println("Please enter your username:");
String username = input.nextLine();
System.out.println("Please enter your password:");
String password = input.nextLine();
System.out.println();
// loop through users and login if username and password match
for (User user : users) {
if (user.getUsername().equals(username)) {
user.login(password);
if (user.getIsLoggedIn()) {
loggedInUser = user;
return;
}
}
}
System.out.println("Incorrect username or password.");
}
Code language: JavaScript (javascript)
The method collects the strings the user enters, checks if the username exists in the users array, and then calls the login method we created back in the User class. If the User successfully logs in, we just set the logged in user variable to that user object.
With that complete, we can finish the main loop for the program.
public static void main(String[] args) {
input = new Scanner(System.in);
// setup test accounts
users = new User[3];
users[0] = new Teacher("teacher", "password");
users[1] = new Student("mary", "password");
users[2] = new Student("jason", "password");
// say hi
System.out.println("Welcome to super advanced LMS");
// main program loop
while (true) {
// prompt user to login or exit
if (loggedInUser == null || !loggedInUser.getIsLoggedIn()) {
System.out.println("Please choose an action:");
System.out.println("1. Login");
System.out.println("2. Exit");
String action = System.console().readLine();
if (action.equals("1")) {
// call login method
tryLogin();
} else if (action.equals("2")) {
System.out.println("Goodbye.");
input.close();
return;
} else {
System.out.println("Invalid action.");
}
} else {
// if they are logged in, the user can choose an action to do next
loggedInUser.chooseAndExecuteAction();
}
}
}
Code language: JavaScript (javascript)
If you examine the loop, you see it checks if the person wants to login or exit. It then calls the tryLogin method or closes the program by returning. If they’re logged in, this means they were successful at the first step and the else block is executed the next time the loop runs, calling our chooseAndExecute… method and allowing the logged in user to access their account options.
Final Testing
We can finally run the program and see if everything’s working. To test it, I first tried logging in as someone not in the system. That failed as expected and it let me try again. I then logged in as teacher, viewed mary’s grade, changed mary’s grade to 100, logged out, logged back in as mary, and viewed my grade which was now 100. Then I logged out and exited the program. Everything appears to be working normally.
Welcome to super advanced LMS Please choose an action: 1. Login 2. Exit 1 Please enter your username: kevin Please enter your password: pass Incorrect username or password. Please choose an action: 1. Login 2. Exit 1 Please enter your username: teacher Please enter your password: password You are now logged in. Choose an action: 1. View Student Grade 2. Set Student Grade 3. Logout 1 Enter the username of the student whose grades you want to view: mary You are viewing mary's grades. Grade for: mary Grade: 0 Choose an action: 1. View Student Grade 2. Set Student Grade 3. Logout 2 Enter the username of the student whose grades you want to set: mary Enter the grade you want to set: 100 You are setting mary's grade to 100 Choose an action: 1. View Student Grade 2. Set Student Grade 3. Logout 3 You are now logged out. Please choose an action: 1. Login 2. Exit 1 Please enter your username: mary Please enter your password: password You are now logged in. Choose an action: 1. View Grades 2. Logout 1 Grade for: mary Grade: 100 Choose an action: 1. View Grades 2. Logout 2 You are now logged out. Please choose an action: 1. Login 2. Exit 2 Goodbye.
If you ran into any issues, please compare with the source files.
Polymorphism
Recall that polymorphism is a feature of inheritance which enables us to implement different objects which share similar logic. It occurs when we make general references to class types and their methods, and Java automagically runs the right method based on the most specific implementation for a given object. Can you identify examples of polymorphism in the LMS example?
One clear example of Polymorphic classes was when the array of Users was created. The users array can hold any User object type, so any combination of Teacher and Student objects can be in the user array. If we created additional subclasses of User in the future, they could also be added to this array.
Another example of polymorphism occurred when the chooseAndExecuteAction() method was overridden. You can call this method on any User object in the users array and it will perform the appropriate actions based on the more specific object type. In the Student class it only showed the single student’s grade, but the same method in the Teacher class added extra functionality! When we called the method from the main loop, I didn’t need to specify which implementation was executed. I just told JVM to run the chooseAndExecuteAction method on a user object and the JVM figured out which implementation to used based on what type of user object was logged in.
Review
A lot was discussed in this chapter. Please be sure you are familiar with the keywords and paradimic terms.
Occurs whenever classes share properties and functions between each other in a top-down hierarchical manner.
An “is-a” relationship is formed.
Refers to objects and methods “having multiple forms.”
The getArea() method illustrated is polymorphic because it’s the same method with multiple implementations.
The toString() method on any object is polymorphic because it can be implemented differently across different classes, without having to change the way its called from elsewhere.
The extends keyword is used to denote a class inheriting attributes from a parent class.
public class Square extends Shape{
///
}
Code language: PHP (php)
Used to access the parent class’s instance variables, methods, or constructor.
For example, if the parent class has an instance variable for radius, its subclasses can access the radius using the super keyword. You don’t need to recreate the radius instance variable for every different type of sphere.
The superconstructor is the constructor of the parent class. It can be called from within the constructor of a child class using the super keyword.
This important annotation should be used before you override any method. It indicates that the method following is supposed to override another method. If it’s not actually overriding something, you’ll get a compile error to help you debug!
An abstract class is declared using the abstract keyword.
Abstract classes cannot be directly instantiated. They represent a general idea or group of objects. You must create concrete subclasses and instantiate those.