Skip to main content

Object-oriented Programming

4 main theoretical principles:

  • abstraction, and encapsulation
  • polymorphism, and inheritance

Abstraction

A lot of programming is about types. A types can tell you:

  • what the variable looks like (e.g. boolean is a simple flag)
  • what you can do with that variable (e.g. you cannot )

Java has had primitives type already, and you can define new type with OOP (type == class). Defining new types and creating objects out of them is called abstraction.

Abstraction can also be achieved by encapsulation (hide details), so it only shows interfaces. See below.

a wise man once said

Abstraction can be thought of as a natural extension of encapsulation.

Encapsulation

Hiding information inside the class is called encapsulation. It helps classes:

  • easier to use: only care about interface, not the implementation
  • harder to misuse: protect object from inconsistent changes
  • easier to change: change the implementation without breaking clients

Field Accessors & Mutators

You can hide information of class using Access Modifier.

By making some members private, we split fields & methods into 2 groups:

  • interface: public members, means it's visible from the outside to use
  • implementation: private members, contains internal details, we can easily change implementation without affect interface

Usually, we want to make fields private (so it cannot be normally directly accessed). But sometimes, we need to expose that (when encapsulation meets inheritance)

\rightarrow public/protected getters & setters for private fields

Inheritance

Copy & paste code is a bad idea \rightarrow we should reuse the common logic and extract the unique logic into a separate class

\rightarrow make some classes inherits another class

Summary
  • Inheritance is (mostly) when you want to upcasting.
  • Inheritance can be used to share code, but there are other better ways to do that (e.g. include a new field type in class a.k.a delegation).

Subclassing

Use extends.

public class Student extends Person {
private final int age;
public Student(int age) { this.age = age; }
public int getAge() { return age; }
}
info

Every class in Java inherits Object class.

Superclass

Constructors are not members, so they are not inherited by subclasses, but it can be invoked.

Constructor is meant to guarantee that objects are initialized correctly \rightarrow we have to call superclass constructor.

Use super() to call the superclass constructor.

public class Student extends Person {
public Student(string name, int age) {
super(name); // this must come first
this.age = age;
}
}

If the superclass constructor is default constructor (the constructor with no parameter), Java can automatically call it for us.

Overriding

Override the methods from superclass. The code in subclass could be duplicated as well. To avoid that we can call method of superclass using super.

class Person {
public String getAge(boolean uppercase) {
if (isAlive()) {
if (uppercase)
return this.age.toUpperCase();
else
return this.age;
}
return "";
}
}

class Student {
@Override
public String getAge(boolean uppercase) {
// this still duplicates a lot
if (isAlive()) {
if (uppercase)
return "Student age: " + this.age.toUpperCase();
else
return "Student age: " + this.age;
}
return "";
// call super class instead
String age = super.getAge(uppercase);
if (age.isEmpty())
return "Student age: " + age;
else
return "Student age: " + age;
}
}

Attempting to assign weaker access privileges

Error occurs when you try to reduce the visibility of a method or variable in a subclass. This error occurs because the subclass cannot have a lower visibility than the superclass it is extending.

public class Superclass {
protected void myMethod() { }
}

public class Subclass extends Superclass {
private void myMethod() { } // fix: public or protected or leave it
// make sense vì tự nhiên thằng này chiếm luôn cái hàm này làm private cho riêng nó thì sao
}

Another example:

interface Philosopher {
void talk();
}

public class Aristotle implements Philosopher {
@Override
protected void talk() { } // this must be public
}

Prevent Overriding

final can be used with methods or classes. If a method is declared with final, it means the method cannot be overridden in a subclass. The same with final class, it cannot be inherited.

Q: The question is why would you want to final a method? A: To prevent bugs, looks at below example:

class Parent {
Parent() {
someInitMethod();
}
protected void someInitMethod() {
// do esstential initializations
}
}
class Child extends Parent {
/**
* subclass can refine someInitMethod() and cause bug
* if that method is private then it's okay (but here, it's protected)
**/
}

A good safety rule is if methods called by constructor should be either private or final.

class Parent {
protected final void someInitMethod() { }
}
warning

Don't overuse final. The best way to use it is when you know what to do and prevent bugs.

Sealed Classes

In Java 17. It can decide exactly which other classes inherit from it. All sealed class subclasses must either be final, sealed or non-sealed.

public sealed class Person permits Student, Worker { }

// subclass can be sealed (means it permitteed subclasses)
public sealed class Worker extends Person permits FullTimeWorker { }
// or non-sealed (means it's just a regular class that you can inherit from)
public non-sealed class Student { }
// or final
public final class Student extends Person { }

public non-sealed class FullTimeWorker extends Worker { }
warning

Don't overuse sealed classes either.

Polymorphism

not so funny story

Polymorphism means “many shapes” in Greek.

Read the below subsections first to understand polymorphism.

Summary

The reason why we do polymorphism & inheritance is to just about upcasting.

We design the hierarchy just to some point in the code, we can take object of subclass and cast it to a superclass (upcasting).

Why we would want to upcast? Because we wrote the code that talks to the superclass/interface to make it easier of extension & maintenance, like this example.

Is-A Relationship

Object <|---- Animal <|---- Cat <---- Cat butter = new Cat();

Example: butter is a special case of Cat and also a special case of Animal, and Object as well. The class inheritance is also described as the is-a relationship.

Subclass can add stuff to superclass, maybe tweak it, but it cannot take things away from superclass. That would violate this notion that "subclass is-a special case".

Upcasting

Java creates object in the heap and return a reference. We usually take that reference and assign it to a variable, and we cannot assign to a different type variable. But we can assign to a superclass variable.

Casting means a type conversion. Upcasting is putting a reference into a variable whose type is a superclass of that reference (we're not changing the object from one type to another, we only effect on reference).

Student student = new Student();
Person person = student; // upcasting: cast this upper
Object greetings = "Hello, world!"; // string is a subclass of Object

When upcasting, you lose the specific functionality, in other words, you forget specific subclass. Sounds like bad things, but it turns out the cool things in Java for polymorphism.

person.goToSchool();  // you can't, this is a Student class method

Downcasting / instanceof

Java cannot do downcasting automatically because it only has ability to forget about specific subclass (upcasting). We downcast by converting reference.

Person person = new Student();
Student student = (Student)person; // down cast Person type -> Student type
student.goToSchool(); // now you can use Student method

What if we change: Person person = new Worker();? then, we'll get NullPointerException followed by ClassCastException.

warning

Because ClassCastException & NullPointerException, we should:

  • use instanceof: checking before downcasting
  • avoid downcasting: improve design to remove unnecessary type conversion

Polymorphism by Inheritance

a wise man once said

Inheritance and polymorphism work together to make powerful weapons

Person person = new Student("Tu");
person.sleep(); // call sleep() of `Student` class, not `Person`

An example

class Dashboard {
/**
* Works with whatever you pass to it
* It doesn't know and doesn't care what subclass it's dealing with
* It just talks to superclass. It allow we forgot the specific type
* -> future-proof, means no matter what new class you add to the hierarchy, code still works
**/
public void add(Alarm alarm) {
tool1.method(alarm.turnOn());
tool2.method(alarm.plug());
// ...
}
}

public static void main(String[] args) {
Dashboard dashboard = new Dashboard();
// the dashboard works with any alarm, it talks with Alarm
dashboard.add(new NoisyAlarm("zzz"));
dashboard.add(new TimeAlarm(100));
}

There are many examples of polymorphism in the core Java libraries. One of that is the System.out.print(), the first ever thing we code when approaching Java.

/**
* System.out.println() take many param type, include Object type
* Object is the root of every class
* => println() can print anything
* It turns out println() call toString() method of the passed-in object
* -> so we only need to override toString() method
**/
class Student {
@Override
public String toString() { return "I am student"; }
}
public static void main(String[] args) {
Person person = new Student();
System.out.println(person);
// this make easier of extension of println()
}
info

Not all polymorphism in Java is based on inheritance. Other ways is using abstract class and interface.

Abstract Classes

You cannot create an instance of an abstract class directly. But we can create its subclass and upcast it.

public abstract class Person {
public abstract void walk(){ }
}
public class Student extends Person {
public void walk(){ }
}
// main
Person person = new Person(); // cannot create instance of abstract class
person.walk();
Person person = new Student();
person.walk();

A class inherits from abstract class must follow rule:

  • if that class wants to be concrete (non-abstract), it must provide an implementation for all its methods (including those defined and those that inherits from its superclass)
  • otherwise, if any method is still abstract, then the class itself must be abstract.

\rightarrow abstract classes only make sense in context of upcasting for polymorphism

Interfaces

The problem of abstract class is the updating the hierarchy.

Student ----|> Person <|---- Worker

> What if we need more method `exist`, `live`
> -> create `Entity` and `Creature`
> But you cannot inherit 2 classes

Entity Creature
\ \
Creature Entity
\ \
Person Person

> Here it is But how about this?

Interface came to resolve one object, multiple roles problem. They are like extreme abstract class:

  • nearly everything in interface has to be abstract, usually, interface has no field
  • a class can only narrate from 1 superclass (except class Object), but it can implement all the interfaces at once
public interface Entity {
public abstract void exist();
void exist(); // or we don't need to specify (they are abstract by default)
}

public class Student extends Person implements Entity, Creature {
// implements all the methods
@Override
public void exist() { }
}
// if this is abstract class then no need to implement
public abstract class Person implements Entity, Creature {
// this class is abstract, duh!
}

Limitations

The point of interfaces exist because Java doesn't want to give you multiple inheritance* (it creates confusing edge cases, horrible hierarchy).

There are some limitations of interfaces:

  • interface cannot inherit from a class (it can inherit an interface)
  • no constructor in interface
  • fields need to be public static final, in other words, global constant
  • methods need to be public abstract. But there are special cases that methods can be concrete:
    • static methods (since they have nothing to do with polymorphism)
    • default methods** (methods supposed to be abstract, but they came with default implementation)
    public interface Entity {
    // we can override this or take the default
    default boolean doSomething() { return true; }
    }
    (*)(**)* We don't have multiple inheritances, so we cannot put code in an interface, except that now we can* 🫠 Having default method is make it more convenient by loosening inheritance a bit.

Tagging Interfaces

Interfaces that do not contain any methods or constants but are used to mark or tag a class with some special meaning or behavior.

public interface Serializable { }
// tag MyClass has been serialized so that we can put it in stream
public class MyClass implements Serializable { }

Best Practices

tip

These are just guidelines! Not absolute rules.

Design guidelines:

  • More private is better than less private
  • If no need, keep it private \rightarrow keep interfaces small
  • Encapsulated fields are good
  • Make fields private & only write needed setters
  • Final fields are also good
  • Make it final, make them less changeable (no surprises, no bugs). Field contains an immutable object is great (e.g. string)
  • Avoid chains of instanceof and downcasts: use polymorphism instead
  • Inheritance vs. Delegation: Inheritance is not always a good way to share code among classes \rightarrow prefer delegation, means create a new class/type to handle that for us.
  • Don't overuse static: it leads to procedural code

Working in Java

Works the Inheritance in Java:

SOLID principles

Single Responsibility

Open-closed

Liskov Substitution

Interface Segregation

Dependency Inversion

Design Patterns