OOP¶
Introduction¶
The concept of Object Oriented Programming (OOP) is related to programming languages that use objects. OOP introduces and combines concepts such as:
- Inheritance
- polymorphism
- encapsulation
- abstraction
- composition
Inheritance¶
One of the most important pillars of OOP is class inheritance. This mechanism allows for sharing behavior between classes. A class can inherit from another class, which means that in addition to the existing functionality, it can define new behaviors and properties. Inheritance in Java is based on the extends
keyword, which we place after the declared class name.
public class Car {
public void turnOnEngine(){
System.out.println("Turn on engine!");
}
}
public class SportCar extends Car { // SportCar class inherits after Car class
public void drive() {
turnOnEngine();
System.out.println("I'm driving!");
}
}
Inheritance hierarchy on the Java platform¶
In Java, all classes inherit from the Object
class in thejava.lang
package. It contains common logic and implementation for each derived class that exists within the framework or is yet to be created.
Derived class properties¶
Inheriting class:
- inherits all base class methods and fields
- can only extend one class at most
- has access to all fields and methods that have been defined using the
public
andprotected
access modifiers - has access to items with the default access modifier (package-private) if the base class is in the same package
Such a class can directly use the capabilities of the base class, ie without any references it can e.g. call methods to which it has access. Also, it can still "behave" like a regular [object] (../ javaBasics/classes_objects.md), i.e. it can define new methods, fields or constants.
In addition, it has the ability to override or extend the method from the base class, using appropriate annotations. In order to extend the capabilities of a constructor or base method, we need to use the super
keyword, which allows us to call logic from the base class. Such a call, in the case of a constructor, looks like a method call, i.e. inside super ()
we can pass arguments. Their number and type are defined by the declaration in the base class, moreover, they must be used in the first line of the constructor's body. In turn, when calling a method from a base class, we treat the super
keyword as a reference to the base class. The name of the method is given after the dot, e.g. super.someMethodFromSuperclass ()
.
The following example shows class inheritance:
public class Computer {
private String cpu;
private String ram;
private String gpu;
public Computer(String cpu, String ram, String gpu) {
this.cpu = cpu;
this.ram = ram;
this.gpu = gpu;
}
public void configure() {
System.out.println("Booting ... ");
System.out.println("Configure cpu: " + cpu);
System.out.println("Configure ram: " + ram);
System.out.println("Configure gpu: " + gpu);
}
}
public class Laptop extends Computer { // Computer is the base class of the Laptop class
private int battery;
public Laptop(String cpu, String ram, String gpu, int battery) {
// in the case of a derived class, calling the base class's default constructor is required
super(cpu, ram, gpu);
this.battery = battery;
}
@Override // (1)
public void configure() {
// we call functionality from the base class
super.configure();
System.out.println("Configure battery: " + battery);
}
}
NOTE: For more information on the annotation in the line marked
(1)
, see this section.NOTE: You can inherit from a class that also inherits from another class.
Composition¶
An alternative way to implement reusable components that, unlike inheritance, it does not extend the behavior of the base classes, but focuses on assembling objects. A class aggregates other classes within itself.
The following example shows a simple application of a theme:
public class Computer {
private Processor processor;
private Ram ram;
public Computer(Processor processor, Ram ram) {
this.processor = processor;
this.ram = ram;
}
public void run() {
// usage of processor and ram object
}
}
class Processor {
String name;
int numberOfCores;
}
class Ram {
String name;
int size;
}
Composition and inheritance¶
Class inheritance is defined statically, which means that you cannot change the implementation at runtime. The implementation of subclasses depends on the base class implementation, so changes to the base class often force changes to the subclasses as well. Extensive inheritance hierarchies also negatively affect code testing and analysis of the implementation structure. Thanks to the use of compositions, we can dynamically change the implementation (e.g. thanks to [poliformism] (oop.md # polymorphism), about which in a moment), without the need to modify the aggregated components, and vice versa. This solution is transparent and does not require cascading changes to the code.
Encapsulation¶
Encapsulation is a mechanism by which data and methods can be hidden from the "outside world". Encapsulation allows you to provide only those mechanisms and data that you want to be visible from outside the class. Hiding data is done through access modifiers.
In the example below, access to employee data has been hidden for the id
and dateOfBirth
fields. It has been restricted to read-only by methods like getter
. For the name
and lastName
fields both read access to variables (getter
methods) and to their modification (setter
methods) has been granted.
public class Employee {
private String id;
private LocalDate dateOfBirth;
private String firstName;
private String lastName;
public Employee(String id, String firstName, String lastName, LocalDate dateOfBirth) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.dateOfBirth = dateOfBirth;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getId() {
return id;
}
public LocalDate getDateOfBirth() {
return dateOfBirth;
}
}
NOTE: We should always hide the implementation details of a given class from the end user of the class, using encapsulation.
Abstraction¶
This mechanism focuses on hiding implementation details from the user. Its purpose is to provide only functionality, without going into the technical details of the delivered solution. More specifically, the user should only see what what the object is doing, and not how it does it.
Let us consider the functionality of sending e-mails. The user should know that he enters the data and can send it without going into details such as the communication protocol, data security or the fact how it was done. In Java, abstraction is realized using [abstract classes] (classes_and_abstract_methods.md) and interfaces.
In the example below, we are dealing with the abstraction of data transformation. The abstract class does not give us any details about what kind of conversion we are going to be dealing with. We know that we will convert the data in text form (String
) into an object of theData
type and we know how the data will be delivered and how validation works. However, the parse details themselves are defined at the class level: CSVParser
and JsonParser
.
public abstract class DataParser { // abstract class
protected String data; // the protected modifier allows the use of a field in derived classes
public abstract Data parse();
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
public void validateData() {
if(data == null || data.isEmpty()) {
throw new IllegalArgumentException("data are not valid!");
}
}
}
public class Data {
private String info;
private int value;
public Data(String info, int value) {
this.info = info;
this.value = value;
}
}
public class CSVParser extends DataParser {
@Override
public Data parse() {
validateData();
String[] splitData = data.split(",");
return new Data(splitData[0], Integer.valueOf(splitData[1]));
}
}
public class JsonParser extends DataParser {
@Override
public Data parse() {
validateData();
Gson gson = new Gson();
return gson.fromJson(data, Data.class);
}
}
Polymorphism¶
Mechanism otherwise known as multiforme. It mainly consists in the fact that a programmer using an object does not need to know whether its functionality comes from a class that is a type of the object, or from some other class that implements or [inherits] (oop.md # inheritance) from an interface or class base. In other words, it is the ability of objects to return different responses to the same request.
Polymorphism is very often used when working with collections, e.g.:
List<Integer> ints = new ArrayList<>(); // the programmer works on the INTERFACE, using some implementation
ints.add(1);
ints.add(7);
for (int idx = 0; idx < ints.size(); idx++) {
System.out.println(ints.get(idx));
}
List<Integer> ints = new LinkedList<>(); // the programmer works on the INTERFACE, using a different implementation
ints.add(1);
ints.add(7);
for (int idx = 0; idx < ints.size(); idx++) {
System.out.println(ints.get(idx));
}
In the next example, we define some abstraction - VodPlayer
and its three implementations,NetflixPlayer
, HBOGoPlayer
, andDefaultPlayer
. In the Android TV class, we use polymorphism by working on abstraction, the implementation of which is selected based on the application's input arguments.
public abstract class VodPlayer {
public abstract void play(String title);
}
public class NetflixPlayer extends VodPlayer {
@Override
public void play(final String title) {
System.out.println("Playing " + title + " on Netflix");
}
}
public class HBOGoPlayer extends VodPlayer {
@Override
public void play(final String title) {
System.out.println("Playing " + title + " on HBO");
}
}
public class DefaultPlayer extends VodPlayer {
@Override
public void play(final String title) {
System.out.println("Playing " + title + " on default player");
}
}
public class AndroidTV {
public static void main(String[] args) {
final String player = args[0];
VodPlayer vodPlayer = null;
if (player.equals("Netflix")) {
vodPlayer = new NetflixPlayer();
} else if (player.equals("HBO")) {
vodPlayer = new HBOGoPlayer();
} else {
vodPlayer = new DefaultPlayer();
}
playEpisode(vodPlayer, "GOT_S1E1");
}
static void playEpisode(VodPlayer vodPlayer, String title) {
// we don't know what implementation we are dealing with
vodPlayer.play(title);
}
}