Skip to content
thesarfo

Concept

Object-Oriented Programming

A worked walkthrough of OOP in Java — classes, constructors, getters, abstraction, inheritance, and encapsulation, built up through a book-borrowing example.

views 0

The concept of OOP involves modelling your software around real world objects.

For instance, we can build a book-borrowing system where a user can register, log in, check for a book’s availability, borrow it if available, and return it on time. They can also check the list of books they’ve borrowed so far.

In this system there are two objects: the user and the book. We store info like the user’s name and birthday in the user object, and the book name and author in the book object.

User class

First we create a file (class) called User.java, where we initialize the User class and its properties.

User.java
import java.time.LocalDate;
public class User {
public String name;
public LocalDate birthDay;
}

We’ve made the User class public, so it can be accessed anywhere in our filesystem. When we create a class and give it properties, by default all those properties have a value of null. Now we create a new user object — similar to how we create a string.

Main.java
import java.time.LocalDate;
public class Main {
public static void main(String[] args) {
User youngerUser = new User();
youngerUser.name = "Ernest Sarfo";
youngerUser.birthDay = LocalDate.parse("2000-01-31");
System.out.printf("%s was born on %s", youngerUser.name, youngerUser.birthDay.toString());
}
}

Using something called a method, we can implement dynamic behaviour in our classes. Let’s say we want to calculate the age of our user — we can do that in the User class with a new method.

User.java
import java.time.LocalDate;
import java.time.Period;
public class User {
public String name;
public LocalDate birthDay;
public int age() {
Period age = Period.between(this.birthDay, LocalDate.now());
return age.getYears();
}
}

The Period datatype calculates time periods between specified dates, and we return the age using .getYears().

Notice

The this keyword in the code above simply points to the current object we’re referring to. Assuming we created a new User object called olderUser, and set its age with olderUser.age, this knows to print the age value we assigned to that specific object.

Now in our main class we can print out the age of the user.

Main.java
import java.time.LocalDate;
public class Main {
public static void main(String[] args) {
User youngerUser = new User();
youngerUser.name = "Ernest Sarfo";
youngerUser.birthDay = LocalDate.parse("2000-01-31");
System.out.printf("Hello %s, you were born in %s, and he is now %d years old", youngerUser.name, youngerUser.birthDay.toString(), youngerUser.age());
}
}

Note that when creating a new object we did it like this:

User youngerUser = new User();

There’s nothing wrong with it, but note how we’ve repeated User twice — this makes the code a bit noisy. To streamline this, we can use the var keyword:

var youngerUser = new User();

Book class

Create a new file called Book, where we define the Book class and its properties.

Book.java
public class Book {
public String title;
public String author;
public String toString() {
return String.format("%s by %s", this.title, this.author);
}
}

We’ve created a method called toString that returns the string representation of our book class. This is because, being a custom class, it doesn’t have a built-in toString() — without one, printing it would print some gibberish large number. This is like Python’s __str__ method.

Now we want to give our user the ability to borrow a book. In the User class, we create a new borrow method that takes a Book object as an argument, and add a books property (an ArrayList).

User.java
import java.time.LocalDate;
import java.time.Period;
import java.util.ArrayList;
public class User {
public String name;
public LocalDate birthDay;
public ArrayList<Book> books = new ArrayList<Book>();
public void borrow(Book book) {
this.books.add(book);
}
public int age() {
Period age = Period.between(this.birthDay, LocalDate.now());
return age.getYears();
}
}

Now in our main class, we create a new Book object and let the user borrow it.

Main.java
import java.time.LocalDate;
public class Main {
public static void main(String[] args) {
User user = new User();
user.name = "Ernest Sarfo";
user.birthDay = LocalDate.parse("2000-01-31");
Book book = new Book();
book.title = "Cinderella";
book.author = "Walt Disney";
user.borrow(book);
System.out.printf("Hello %s, you were born in %s, and he is now %d years old. \n", user.name, user.birthDay.toString(), user.age());
System.out.printf("%s has borrowed these books: %s", user.name, user.books.toString());
}
}

Constructors

So far we’ve declared all our methods and properties as public, which isn’t something you should do a lot. There’s a built-in constructor method responsible for initializing our properties with their default values — we can customize it to do things for us.

A constructor is initialized just by typing the constructor name (the name of the class), then specifying what you want the class to contain when it’s created, then storing that in the properties initialized at the class level.

User.java
import java.time.LocalDate;
import java.time.Period;
import java.util.ArrayList;
public class User {
public String name; // class level property
public LocalDate birthDay; // class level property
public ArrayList<Book> books = new ArrayList<Book>();
// constructor method
User(String name, String birthDay) { // we want the name and birthday when a User is created
this.name = name;
this.birthDay = LocalDate.parse(birthDay);
}
public void borrow(Book book) {
this.books.add(book);
}
public int age() {
Period age = Period.between(this.birthDay, LocalDate.now());
return age.getYears();
}
}

Now when creating an object of the User class, we just put the properties directly into the constructor call.

Main.java
import java.time.LocalDate;
public class Main {
public static void main(String[] args) {
User user = new User("Ernest Sarfo", "2000-01-31"); // put the values within the constructor call
Book book = new Book();
book.title = "Cinderella";
book.author = "Walt Disney";
user.borrow(book);
System.out.printf("Hello %s, you were born in %s, and he is now %d years old. \n", user.name, user.birthDay.toString(), user.age());
System.out.printf("%s has borrowed these books: %s", user.name, user.books.toString());
}
}

Since we can initialize the user object with the name and birthday right at creation, we can make those properties private.

User.java
import java.time.LocalDate;
import java.time.Period;
import java.util.ArrayList;
public class User {
private String name;
private LocalDate birthDay;
public ArrayList<Book> books = new ArrayList<Book>();
// constructor method
User(String name, String birthDay) {
this.name = name;
this.birthDay = LocalDate.parse(birthDay);
}
public void borrow(Book book) {
this.books.add(book);
}
public int age() {
Period age = Period.between(this.birthDay, LocalDate.now());
return age.getYears();
}
}

This means we can no longer do things like user.birthDay or user.name. But we still need to read the name and birthday of the user — for that we have getters, which simply return the value of our properties.

import java.time.LocalDate;
import java.time.Period;
import java.util.ArrayList;
public class User {
private String name;
private LocalDate birthDay;
public ArrayList<Book> books = new ArrayList<Book>();
// constructor method
User(String name, String birthDay) {
this.name = name;
this.birthDay = LocalDate.parse(birthDay);
}
// getters to return the name and birthday
public String getName() {
return this.name;
}
public String getBirthday() {
return this.birthDay.toString();
}
public void borrow(Book book) {
this.books.add(book);
}
public int age() {
Period age = Period.between(this.birthDay, LocalDate.now());
return age.getYears();
}
}

Now that we’ve implemented our getters, we can call them on our object — user.getName() and user.getBirthday().

Main.java
public class Main {
public static void main(String[] args) {
User user = new User("Ernest Sarfo", "2000-01-31");
Book book = new Book();
book.title = "Cinderella";
book.author = "Walt Disney";
user.borrow(book);
System.out.printf("Hello %s, you were born in %s, and he is now %d years old. \n", user.getName(), user.getBirthday(), user.age());
System.out.printf("%s has borrowed these books: %s", user.getName(), user.books.toString()); // note the getter calls
}
}

Abstraction

Now that the name and birthday properties are private, our Main class doesn’t have access to them directly. In fact, nobody has access to those values outside the User class, and they can’t be changed outside of it either.

We can also pass the name and birthday as strings when creating our user object — the Main class has no business knowing what datatype name and birthDay really are; all it needs to know is that passing strings in that format will create the object.

This is called abstraction — hiding the complexities of the User class behind its getter and constructor methods.

We can modify our Book class the same way, with a constructor and getters:

Book.java
public class Book {
private String title;
private String author;
Book(String title, String author) {
this.title = title;
this.author = author;
}
public String getTitle() {
return this.title;
}
public String getAuthor() {
return this.author;
}
public String toString() {
return String.format("%s by %s", this.title, this.author);
}
}

And update Main to reflect that change:

Main.java
public class Main {
public static void main(String[] args) {
User user = new User("Ernest Sarfo", "2000-01-31");
Book book = new Book("Cinderella", "Walt Disney");
user.borrow(book);
System.out.printf("Hello %s, you were born in %s, and he is now %d years old. \n", user.getName(), user.getBirthday(), user.age());
System.out.printf("%s has borrowed these books: %s", user.getName(), user.books.toString());
}
}

Inheritance

Now let’s say we have different types of books — audiobooks, ebooks, paperbacks, etc. These are all types of books, so they share common properties like title, author, and page count.

When creating an AudioBook class, it needs to inherit some properties from a Book class (its parent), and then add more properties of its own.

AudioBook.java
public class AudioBook extends Book {
private int runTime;
AudioBook(String title, String author, int runTime) {
super(title, author, 0); // the 0 is for pageCount
this.runTime = runTime;
}
}

Our child class (AudioBook) extends our parent class (Book). Inside it, we specify the additional properties we want to add, create a constructor that includes all the parent’s properties plus the new one, and use super() to pass the parent’s properties up while setting the additional property at the class level.

We can create an object of our AudioBook class and print it out:

Main.java
public class Main {
public static void main(String[] args) {
User user = new User("Ernest Sarfo", "2000-01-31");
Book cinderella = new Book("Cinderella", "Walt Disney", 270);
AudioBook dracula = new AudioBook("Dracula", "Bram Stoker", 30000);
System.out.println(dracula.toString());
}
}

Encapsulation

A good encapsulation rule of thumb: mark your instance variables private and provide public getters and setters for access control. As you gain more design and coding experience in Java, you’ll probably do things a little differently, but for now this approach will keep you safe.

  1. Mark instance variables private
  2. Mark getters and setters public

Encapsulation gives you control over who changes the data in your class and how:

  • Make an instance variable private so it can’t be changed by accessing it directly
  • Create a setter to control how other code interacts with the data — for instance, you can add validation code inside a setter to make sure the value isn’t changed to something invalid
  • Instance variables are assigned default values automatically
  • Local variables are not assigned a value by default
class GoodDog {
private int size;
public int getSize() {
return size;
}
public void setSize(int s) {
this.size = s;
}
void bark() {
if (size > 60) {
System.out.println("Woof Woof");
} else if (size > 14) {
System.out.println("Ruff Ruff");
} else {
System.out.println("Yip Yip");
}
}
}
public class GoodDogTestDrive {
public static void main(String[] args) {
// create new objects of the above class
GoodDog one = new GoodDog();
one.setSize(79); // use the setter
GoodDog two = new GoodDog();
two.setSize(8);
System.out.println("Dog one: " + one.getSize()); // use the getter
System.out.println("Dog two: " + two.getSize());
one.bark();
two.bark();
}
}