Lesson 19Phase 2: Objects
OOP practice: Student, Bank, Employee
Pulling every Phase 2 idea together by designing three small, complete classes from scratch.
By BasithUpdated 60 min
- class design
- encapsulation
- constructors
- methods
- putting it together
The last few lessons handed you a set of tools: classes and objects, fields, methods, constructors, private data, inheritance, overriding and polymorphism. This lesson adds no new ideas. Instead you design three small but complete classes from scratch, the kind real programs are full of, and watch each one run.
- Design a class end to end: choose its fields, write a constructor, and give it useful methods.
- Use a
toStringmethod so printing an object shows something readable. - Protect an object's data with
privateand guard changes with validation. - Reuse a class with
extends, override a method, and call up to the parent withsuper. - Drive different behavior from one loop over a mixed group of objects: polymorphism in action.
A small recipe for designing a class#
Before the code, a way to think. When you sit down to design a class, you are answering the same few questions every time. Keep this list nearby for all three designs below.
- 1What one kind of thing is this? Name it. A class is a single, clear noun:
Student, notStuffAboutSchool. - 2What does it have? Those are the fields, its state. Pick a type for each.
- 3How is a valid one built? That is the constructor. What must you know to make a sensible object?
- 4What does it do? Those are the methods, its behavior. Each one reads or changes the object's own fields.
- 5How should it look when printed? Write a
toStringso the object reads as a sentence, not as its class name and a number.
Every class inherits toString from Object, and println calls it whenever you print an object, as does joining an object to a string with +. You overrode it in method overriding and super: write @Override public String toString() and return the text you want. Without one, an object prints as its class name, an @ and a number, such as Student@1b6d3586.
Design one: Student#
A student record. It has a name, an id number, and a grade point average. It is built from all three at once. It can answer one question we care about: did this student make the honor roll? We will say the honor roll is a GPA of 3.5 or higher. And it prints as a tidy line.
Read it, then press Run and step through. Watch the constructor fill the fields with this, one field per step, and watch isHonorRoll compare each student's own gpa against the threshold.
public class Main { public static void main(String[] args) { Student a = new Student("Aisha", 901, 3.8); Student b = new Student("Ben", 902, 2.9); System.out.println(a); System.out.println(b); if (a.isHonorRoll()) { System.out.println(a.name + " made the honor roll."); } if (b.isHonorRoll()) { System.out.println(b.name + " made the honor roll."); } else { System.out.println(b.name + " did not make the honor roll."); } }} class Student { String name; int id; double gpa; Student(String name, int id, double gpa) { this.name = name; this.id = id; this.gpa = gpa; } boolean isHonorRoll() { return gpa >= 3.5; } @Override public String toString() { return "Student #" + id + " " + name + " (GPA " + gpa + ")"; }}That prints four lines:
Student #901 Aisha (GPA 3.8)Student #902 Ben (GPA 2.9)Aisha made the honor roll.Ben did not make the honor roll.
Three things to notice, because they recur in every design. First, the constructor parameters share names with the fields (name, id, gpa), so this.name = namesays “set my field name from the parameter name”. Second, isHonorRoll returns a boolean, which is exactly what an if wants, so if (a.isHonorRoll()) reads almost like English. Third, when we wrote System.out.println(a) with just the object, Java called our toString to turn it into that readable line.
Rectangle has width and height fields, and a setWidth method can change the width later. What is the best way to give it an area?Design two: BankAccount, with the data protected#
A bank account is the classic case for encapsulation: hiding the data so the outside world cannot put the object into a nonsense state. The balance is the thing to protect. If anyone could write acc.balance = -999 from outside, the account would be broken and nothing could stop them. So we make balance private and only let it change through methods that check the request first.
The two methods, deposit and withdraw, each validate before they act. A deposit must be positive. A withdrawal cannot take out more than is there. When a request fails, the method says so and leaves the balance alone. Run it and watch the rejected withdrawal: the balance does not move.
public class Main { public static void main(String[] args) { BankAccount acc = new BankAccount("Aisha", 100.0); acc.deposit(50.0); acc.withdraw(200.0); acc.withdraw(80.0); System.out.println("Final: " + acc); }} class BankAccount { private String owner; private double balance; BankAccount(String owner, double opening) { this.owner = owner; if (opening > 0) { this.balance = opening; } else { this.balance = 0; } } void deposit(double amount) { if (amount <= 0) { System.out.println("Deposit must be positive."); return; } balance = balance + amount; System.out.println("Deposited " + amount + ", balance now " + balance); } void withdraw(double amount) { if (amount > balance) { System.out.println("Rejected: cannot withdraw " + amount + " from " + balance); return; } balance = balance - amount; System.out.println("Withdrew " + amount + ", balance now " + balance); } double getBalance() { return balance; } @Override public String toString() { return owner + " has " + balance; }}Trace the run. Start at 100. Deposit 50, now 150. Try to withdraw 200, which is more than 150, so it is rejected and the balance stays 150. Withdraw 80, now 70. The four lines printed are:
Deposited 50.0, balance now 150.0Rejected: cannot withdraw 200.0 from 150.0Withdrew 80.0, balance now 70.0Final: Aisha has 70.0
Key ideaEncapsulation means putting a gatekeeper in front of a field: a method that decides whether a change is allowed before letting it through. Private data plus validating methods is how an object protects its own rules.
Notice the early return inside each method. When a request is invalid, the method prints a message and returns straight away, before the line that would change the balance. This is a common and tidy pattern: check for the bad cases first, leave early, and let the rest of the method assume everything is fine.
return come before the line that changes the balance.Tap a line, then tap where it should go. Or use the arrows.
The constructor checks its input too. Ben opens an account with -20.0, then tries a negative deposit, a real one, and a withdrawal. Decide what this run prints.
public class Main { public static void main(String[] args) { BankAccount acc = new BankAccount("Ben", -20.0); acc.deposit(-5.0); acc.deposit(30.0); acc.withdraw(40.0); System.out.println("Final: " + acc); }} class BankAccount { private String owner; private double balance; BankAccount(String owner, double opening) { this.owner = owner; if (opening > 0) { this.balance = opening; } else { this.balance = 0; } } void deposit(double amount) { if (amount <= 0) { System.out.println("Deposit must be positive."); return; } balance = balance + amount; System.out.println("Deposited " + amount + ", balance now " + balance); } void withdraw(double amount) { if (amount > balance) { System.out.println("Rejected: cannot withdraw " + amount + " from " + balance); return; } balance = balance - amount; System.out.println("Withdrew " + amount + ", balance now " + balance); } @Override public String toString() { return owner + " has " + balance; }}Follow every check, including the one in the constructor. Which four lines does it print?
The account opens at 0, because the constructor refuses -20.0. The deposit of -5.0 is turned away, the deposit of 30.0 brings the balance to 30, and the withdrawal of 40 is more than 30, so it is rejected. The final line is Final: Ben has 30.0. Every way into the balance was guarded, so it never went below zero.
We made balance private, but the outside world still legitimately needs to read it. That is what getBalanceis for: a method that hands the value out but gives no way to set it. The account stays in control of every change while still answering “how much is in there?”. A read-only window onto private data is the everyday shape of a getter.
double stores most decimal fractions only approximately: 0.1 + 0.2 prints 0.30000000000000004. Real banking code therefore keeps money in a BigDecimal. The amounts on this page are whole numbers, which a double holds exactly, so the output is right. Money and exact numbers shows the proper way.Design three: Employee, then Manager on top#
The last design pulls in inheritance and polymorphism. We start with an Employee: a name, a salary, and the ability to give itself a raise by a percentage. Then we notice that a manager is an employee with something extra, a bonus, so instead of copying everything we build Manager on top of Employee with extends.
Read the two classes first. Manager inherits name, salary, and the whole raise method for free. It adds a bonus field, and it overrides toStringto mention the bonus, reusing the parent's version with super.toString() rather than rewriting it.
public class Main { public static void main(String[] args) { Employee e = new Employee("Ravi", 4000.0); Manager m = new Manager("Sana", 6000.0, 1500.0); e.raise(10.0); m.raise(10.0); Employee[] staff = { e, m }; for (int i = 0; i < staff.length; i = i + 1) { System.out.println(staff[i]); } }} class Employee { protected String name; protected double salary; Employee(String name, double salary) { this.name = name; this.salary = salary; } void raise(double percent) { salary = salary + salary * percent / 100; } @Override public String toString() { return name + " earns " + salary; }} class Manager extends Employee { private double bonus; Manager(String name, double salary, double bonus) { super(name, salary); // build the Employee half first this.bonus = bonus; // then the part that is new to Manager } @Override public String toString() { return super.toString() + " plus a bonus of " + bonus; }}This prints two lines:
Ravi earns 4400.0Sana earns 6600.0 plus a bonus of 1500.0
Walk through the numbers. Ravi starts at 4000, a 10 percent raise adds 400, giving 4400. Sana starts at 6000, a 10 percent raise adds 600, giving 6600. The raise method was written once in Employee, and Manager used it unchanged: that is inheritance saving you the copy.
Two things are happening at once here
First, super.toString(). Inside Manager's toString, the call super.toString() runs the parent's version, which returns "Sana earns 6600.0", and then Manager adds " plus a bonus of 1500.0". That is how you extend behavior instead of replacing it: do what the parent did, then add to it.
Second, the loop is polymorphic. The array is typed Employee[], but one slot holds a real Manager. When the loop reaches staff[i] in println, Java looks at the actual object at runtime and runs that object's toString. So the same line of code prints a plain employee one time and a manager with a bonus the next, with no if deciding which is which.
Key ideaInheritance lets a subclass reuse a parent's fields and methods. Overriding lets it change one of them. Polymorphism means a variable typed as the parent will still run the real object's version at runtime. Together they let one piece of code handle a whole family of related objects.
How Manager builds its Employee half
Look at the first line of the Manager constructor: super(name, salary);. A manager is an employee with something extra, so before Manager sets its own bonus, it asks the Employee constructor to set up the name and salary. That call builds the Employee part of the object, so it has to come before anything that uses that part, and it is usually the very first line.
Here it is not optional. Employee only has a constructor that takes a name and a salary. If Manager left out the super(...) line, Java would try to call an Employee() constructor with no arguments, find none, and refuse to compile the program with the error constructor Employee in class Employee cannot be applied to given types. The full story is in inheritance.
super(...)had to be the very first statement of a constructor. Since Java 25 a few lines may come before it, for example to check an argument, as long as they do not read the object's fields or call its methods, because the object is not built yet. Inheritance has an example. Most code still puts super(...) first, as this page does.Manager constructor leaves out super(name, salary); and only sets bonus. What happens?toString methods carry @Override, the safety net from method overriding and super. If the name or the parameters were wrong, the compiler would say so instead of quietly creating a brand-new method. It changes nothing about how the program runs.Now a third kind of employee, written by you. An intern's raise should be half of whatever percentage the others get, and the loop in main must not need to know that.
raise in Intern so that an intern gets half the percentage: a 10 percent raise adds only 5 percent. Reuse Employee's raise instead of repeating its formula.Stepping back: the shape of all three#
Different as they are, the three designs share one skeleton. Seeing it makes the next class you design feel familiar before you start.
name and gpa, balance, salary and bonus.this.isHonorRoll, deposit and withdraw, raise.BankAccount guards its balance.toString so printing reads well.Manager extends Employee.Your turn. Here is a library book with the recipe half done: the fields, the constructor and the toString are written. The two methods that change the book are yours. It is a guarded resource, the same shape as BankAccount.
borrow and returnCopy. borrow takes one copy away and prints Borrowed Dune, 1 left, but when no copies are left it prints No copies of Dune left and changes nothing. returnCopy puts one copy back. Build the messages from the title field rather than typing “Dune”.Try it
Answer each one in your head or on paper first, then open the answer.
Student design, the call System.out.println(a) passes the whole object, not a field. Why does it print a readable line instead of something like Student@1b6d3586?3.7 or higher. With Aisha at 3.8 and Ben at 2.9, who makes it now, and what single line of code did you change?BankAccount run, the second line is the rejected withdrawal. After it, what is the balance, and why did the rejection not change it?boolean method isOverdrawn() to BankAccount that returns whether the balance is below zero. Given the validation already in place, what will it always return, and why?Employee run, the loop variable type is Employee but one slot holds a Manager. When that slot is printed, which toString runs, the one in Employee or the one in Manager?Practice
Work through these on your own. Each one builds on this page.
Course class for sign-ups. It has a name, a String[] of enrolled names whose length is the number of places, and an int count of places taken. enroll(String student) adds the name when there is room, and otherwise prints Course is full and changes nothing. toString returns something like Java 101: 2 of 3 places taken. Make a course with 3 places, enroll four students, and print it.Rectangle with width and height, a constructor, and a method double area(). Then write a class Square that extends Rectangle. A square is a rectangle whose width and height are equal, so its constructor takes a single side. Give Square its own toString. Make one of each, put them in a Rectangle[], and loop over the array printing each shape's area.Student design with a GraduateStudent that also has a thesis title. Override isHonorRoll() so that a graduate student needs a GPA of 3.7 or higher, and override toString() to add the thesis title after super.toString(). Put a Student and a GraduateStudent, both with a GPA of 3.6, into one Student[]. For each one, print it and whether it made the honor roll. Then explain in a sentence why the same GPA gave two different answers.BankAccount as your example, why making balance private and forcing all changes through deposit and withdraw is safer than leaving balance public. What could go wrong if any code anywhere could set the balance to any value?Further reading#
- Creating Classes, the dev.java tutorial on declaring a class, its fields and who may access them
- Object as a Superclass, the dev.java tutorial on toString, equals and the other methods every class inherits
- JEP 513, Flexible Constructor Bodies, the Java 25 change that allows statements before super(...)
Spotted a mistake, or a part that did not make sense? Report it by email