OOP practice: Student, Bank, Employee
Pulling every Phase 2 idea together by designing three small, complete classes from scratch.
Finished reading?
Mark this session so you can track where you are.
Pulling every Phase 2 idea together by designing three small, complete classes from scratch.
Finished reading?
Mark this session so you can track where you are.
You have spent this whole phase collecting tools: classes and objects, fields, methods, constructors, private data, inheritance, and polymorphism. Today we stop adding new ideas and use the ones you have. We will design three small but complete classes from scratch, the kind you would actually write, and watch each one run.
toString method so printing an object shows something readable.private and guard changes with validation.extends, override a method, and call up to the parent with super.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.
Student, not StuffAboutSchool.toString so the object reads as a sentence, not as a memory address.Every object in Java already understands a method called toString: a method that hands back a String describing the object. If you do not write one, the built-in version returns something useless like the class name and an address. When you write your own, Java uses it automatically: System.out.println(s) and "Result: " + s both call your toString for you. That is why a good toString makes everything else easier to read.
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, 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; } public String toString() { return "Student #" + id + " " + name + " (GPA " + gpa + ")"; }}Student #901 Aisha (GPA 3.8) Student #902 Ben (GPA 2.9) Aisha made the honor roll. Ben did not make the honor roll.
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, isHonorRollreturns 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.
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; } public String toString() { return owner + " has " + balance; }}Deposited 50.0, balance now 150.0 Rejected: cannot withdraw 200.0 from 150.0 Withdrew 80.0, balance now 70.0 Final: Aisha has 70.0
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.0Encapsulation is not just hiding a field. It is putting a gatekeeper in front of it: 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, bail out early, and let the rest of the method assume everything is fine.
We made balance private, but the outside world still legitimately needs to readit. 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.
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 toString to 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; } public String toString() { return name + " earns " + salary; }} class Manager extends Employee { private double bonus; Manager(String name, double salary, double bonus) { this.name = name; this.salary = salary; this.bonus = bonus; } public String toString() { return super.toString() + " plus a bonus of " + bonus; }}Ravi earns 4400.0 Sana earns 6600.0 plus a bonus of 1500.0
This prints two lines:
Ravi earns 4400.0Sana earns 6600.0 plus a bonus of 1500.0Walk 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.
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 tacks on" 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-bonus the next, with no if deciding which is which. One loop, many shapes.
Inheritance 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.
In real Java, a subclass constructor begins by calling the parent constructor with super(name, salary);. Our in-browser interpreter does not run that form yet, so above we set the inherited name and salary fields directly, which works because they are protected. The idea is identical, the parent's data still gets set, but the idiomatic way is the super(...) call. It is shown next, and explained fully in inheritance.
Here is the same Manager constructor written the idiomatic way, with super. Read it as the real-world version of what you just ran.
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; }}Ravi earns 4400.0 Sana earns 6600.0 plus a bonus of 1500.0
The static snippet above also shows @Override written just before toString. It is an annotation: a small note to the compiler saying “I mean to be replacing a parent method”. If you misspell the name or get the parameters wrong, the compiler will flag it instead of silently making a brand-new method. It changes nothing about how the program runs; it is a safety net. You met it in method overriding and super.
Different as they are, the three designs share one skeleton. Seeing it makes the next class you design feel familiar before you start.
name/gpa, balance, salary/bonus.this.isHonorRoll, deposit/withdraw, raise.BankAccount guards its balance.toString so printing reads well.Manager extends Employee.Try each one yourself 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@1b6d?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?Take these away. They continue exactly what we just did.
Book class with fields title, author, and copiesAvailable (an int). Give it a constructor that sets all three, a method borrow() that lowers copiesAvailable by one but only if at least one copy is available (otherwise it prints “No copies left” and changes nothing), a method returnCopy() that raises it by one, and a toString. Make a book with 2 copies, borrow three times, and confirm the third borrow is refused.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 and sets both inherited fields to it. Give Square its own toString. Make one of each, put them in a Rectangle[], and loop over the array printing each shape's area.Employee design. Add a second subclass, Intern, that extends Employee and overrides raise so that an intern's raise is only half the percentage given (for example, a 10 percent raise adds only 5 percent). Put an Employee, a Manager, and an Intern into one Employee[], give each the same raise, and print them. Explain in a sentence why one loop produced three different salary outcomes.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?