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.
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)
public/protected getters & setters for private fields
Inheritance
Copy & paste code is a bad idea we should reuse the common logic and extract the unique logic into a separate class
make some classes inherits another class
- 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; }
}
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 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() { }
}
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 { }
Don't overuse sealed
classes either.
Polymorphism
Polymorphism means “many shapes” in Greek.
Read the below subsections first to understand polymorphism.
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
.
Because ClassCastException & NullPointerException, we should:
- use
instanceof
: checking before downcasting - avoid downcasting: improve design to remove unnecessary type conversion
Polymorphism by Inheritance
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()
}
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.
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 beabstract
, 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)
(*)(**)* 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.public interface Entity {
// we can override this or take the default
default boolean doSomething() { return true; }
}
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
These are just guidelines! Not absolute rules.
Design guidelines:
- More private is better than less private
- If no need, keep it private 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 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: