Java is an object-oriented language. This means that you need to write Java programs using an object-oriented paradigm. And this paradigm entails using objects and classes in your programs.
Let's try using examples to understand what classes and objects are, and how to apply basic OOP principles (abstraction, inheritance, polymorphism and encapsulation) in practice.
What is an object?
The world we live in is made up of objects. Looking around, we can see that we're surrounded by houses, trees, cars, furniture, dishes, and computers. All these things are objects, and each of them has a set of specific characteristics, behaviors, and purposes. We are accustomed to objects, and we always use them for very specific purposes. For example, if we need to get to work, we use a car. If we want to eat, we use dishes. And if we want to rest, we find a comfortable sofa. Humans are used to thinking in terms of objects to solve problems in everyday life. This is one reason why objects are used in programming. This approach is called object-oriented programming. Let's give an example. Imagine that you've developed a new phone and want to start mass production. As the phone's developer, you know what it's for, how it functions, and what its parts are (body, microphone, speaker, wires, buttons, etc.). What's more, only you know how to connect these parts. But you don't plan to make the phones personally — you have a whole team of workers to do this. To eliminate the need to repeatedly explain how to connect the phone's parts, and to ensure that all the phones are made in the same way, before you start producing them, you need to make a drawing that describes how the phone is organized. In OOP, we call such a description, drawing, diagram, or template a class. It forms the basis of creating objects when the program is running. A class is a description of objects of certain type — like a common template consisting of fields, methods and a constructor. An object is an instance of a class. It is created based of the description found in the class.Abstraction
Let's now think about how we can move from an object in the real world to an object in a program. We'll use the phone as an example. This means of communication has a history that spans more than 100 years. The modern telephone is a much more complex device than its 19th-century predecessor. When using the phone, we do not think about its organization and the processes occurring inside it. We simply use the functions provided by the phone's developers: buttons or a touch screen to enter a phone number and make calls. One of the first phone interfaces was a crank that needed to be rotated to make a call. Of course, this was not very convenient. But it fulfilled its function flawlessly. If you compare the most modern and the very first phones, you can immediately identify the functions most important for the late 19th-century device and for the modern smartphone. They are the ability to make calls and the ability to receive calls. In fact, this is what makes the phone a phone, and not something else. Now just applied a principle of OOP: identify an object's most important characteristics and information. This principle is called abstraction. In OOP, abstraction can also be defined as a method of representing elements of a real-world task as objects in a program. Abstraction is always associated with the generalization of certain properties of an object, so the main thing is to separate meaningful information from the insignificant in the context of the task at hand. Additionally, there can be several levels of abstraction. Let's try applying the principle of abstraction to our phones. To begin, we'll identify the most common types of phones — from the very first phones to those of the present day. For example, we could represent them in the form of the diagram in Figure 1. Using abstraction, we can now identify the general information in this object hierarchy: the general abstract object (telephone), common characteristics of the telephone (e.g. year of its creation), and the common interface (all telephones can receive and make calls). Here's how it looks in Java:
public abstract class AbstractPhone {
private int year;
public AbstractPhone(int year) {
this.year = year;
}
public abstract void call(int outgoingNumber);
public abstract void ring(int incomingNumber);
}
In a program, we can create new types of phones using this abstract class and applying other basic principles of OOP, which we'll explore below.
Encapsulation
With abstraction, we identify what is common for all objects. But each kind of phone is unique, somehow differing from the others. In a program, how do we draw boundaries and identify this individuality? How do we make it so nobody can accidentally or deliberately break our phone or try to convert one model into another? In the real world, the answer is obvious: you need to put all the parts in a phone case. After all, if you don't — instead leaving all the phone's internal parts and connecting wires on the outside — some curious experimenter will definitely want to "improve" our phone. To prevent such tinkering, the principle of encapsulation is used in an object's design and operation. This principle states that an object's attributes and behavior are combined in a single class, the object's internal implementation is hidden from the user, and a public interface is provided for working with the object. The programmer's task is to determine which of an object's attributes and methods should be available for public access, and which are internal implementation details that should be inaccessible.Encapsulation and access control
Suppose that information about a phone (its production year or the manufacturer's logo) is engraved on its back when it is made. The information (its state) is specific to this particular model. We can say that the manufacturer made sure this information was immutable — it's unlikely that anyone would think to remove the engraving. In the Java world, a class describes the state of future objects using fields, and their behavior is described using methods. Access to an object's state and behavior is controlled using modifiers applied to fields and methods: private, protected, public, and default. For example, we decided that the production year, manufacturer name, and one of the methods are internal implementation details of the class and cannot be changed by other objects in the program. In code, the class can be described as follows:
public class SomePhone {
private int year;
private String company;
public SomePhone(int year, String company) {
this.year = year;
this.company = company;
}
private void openConnection(){
// findSwitch
// openNewConnection...
}
public void call() {
openConnection();
System.out.println("Calling");
}
public void ring() {
System.out.println("Ring-ring");
}
}
The private modifier allows the class's fields and methods to be accessed only within this class. This means that it is impossible to access private fields from the outside, because the private methods cannot be called. Restricting access to the openConnection method also leaves us with the ability to freely change the method's internal implementation, since the method is guaranteed not to be used by or interrupt the work of other objects. To work with our object, we leave the call and ring methods available using the public modifier. Providing public methods for working with objects is also part of encapsulation, since if access were denied completely, it would become useless.
Inheritance
Let's take another look at the diagram of phones. You can see that it is a hierarchy in which a model has all the features of the models located higher along its branch, and adds some of its own. For example, a smartphone uses a cellular network for communication (has the properties of a cell phone), is wireless and portable (has the properties of a cordless phone), and can receive and make calls (has the properties of a phone). What we have here is inheritance of object properties. In programming, inheritance means to use existing classes to define new ones. Let's consider an example of using inheritance to create a smartphone class. All cordless phones are powered by rechargeable batteries, which have a certain battery life. Accordingly, we add this property to the cordless phone class:
public abstract class CordlessPhone extends AbstractPhone {
private int hour;
public CordlessPhone (int year, int hour) {
super(year);
this.hour = hour;
}
}
Cell phones inherit the properties of a cordless phone, and we implement the call and ring methods in this class:
public class CellPhone extends CordlessPhone {
public CellPhone(int year, int hour) {
super(year, hour);
}
@Override
public void call(int outgoingNumber) {
System.out.println("Calling " + outgoingNumber);
}
@Override
public void ring(int incomingNumber) {
System.out.println("Incoming call from " + incomingNumber);
}
}
And finally, we have the smartphone class, which, unlike classic cell phones, has a full-fledged operating system. You can expand your smartphone's functionality by adding new programs that can run on its operating system. In code, the class can be described as follows:
public class Smartphone extends CellPhone {
private String operationSystem;
public Smartphone(int year, int hour, String operationSystem) {
super(year, hour);
this.operationSystem = operationSystem;
}
public void install(String program) {
System.out.println("Installing " + program + " for " + operationSystem);
}
}
As you can see, we created quite a bit of new code to describe the Smartphone class, but we got a new class with new functionality. This principle of OOP makes it possible to significantly reduce the amount of Java code required, thus making life easier for the programmer.
Polymorphism
Despite differences in the appearance and design of various kinds of phones, we can identify some common behavior: they all can receive and make calls and all have a fairly clear and simple set of controls. In terms of programming, the principle of abstraction (which we are already familiar with) lets us say that phone objects have a common interface. That's why people can easily use different models of phones that have the same controls (mechanical buttons or a touchscreen), without delving into the technical details of the device. Thus, you use a cell phone constantly and you can easily make a call from your friend's landline. The principle of OOP that says that a the program can use objects with a common interface without any information about the object's internal structure is called polymorphism. Let's imagine that we need our program to describe a user who can use any phone to call another user. Here's how we can do it:
public class User {
private String name;
public User(String name) {
this.name = name;
}
public void callAnotherUser(int number, AbstractPhone phone){
// And here's polymorphism: using the AbstractPhone type in the code!
phone.call(number);
}
}
}
Now we will describe several kinds of phones. One of the first phones:
public class ThomasEdisonPhone extends AbstractPhone {
public ThomasEdisonPhone(int year) {
super(year);
}
@Override
public void call(int outgoingNumber) {
System.out.println("Crank the handle");
System.out.println("What number would you like to connect to?");
}
@Override
public void ring(int incomingNumber) {
System.out.println("The phone is ringing");
}
}
An ordinary landline phone:
public class Phone extends AbstractPhone {
public Phone(int year) {
super(year);
}
@Override
public void call(int outgoingNumber) {
System.out.println("Calling " + outgoingNumber);
}
@Override
public void ring(int incomingNumber) {
System.out.println("The phone is ringing");
}
}
And finally, a cool video phone:
public class VideoPhone extends AbstractPhone {
public VideoPhone(int year) {
super(year);
}
@Override
public void call(int outgoingNumber) {
System.out.println("Connecting video call to " + outgoingNumber);
}
@Override
public void ring(int incomingNumber) {
System.out.println("Incoming video call from " + incomingNumber);
}
}
We'll create objects in the main() method and test the callAnotherUser() method:
AbstractPhone firstPhone = new ThomasEdisonPhone(1879);
AbstractPhone phone = new Phone(1984);
AbstractPhone videoPhone=new VideoPhone(2018);
User user = new User("Jason");
user.callAnotherUser(224466, firstPhone);
// Crank the handle
// What number would you like to connect to?
user.callAnotherUser(224466, phone);
// Calling 224466
user.callAnotherUser(224466, videoPhone);
// Connecting video call to 224466
Calling the same method on the user object produces different results. A specific implementation of the call method is selected dynamically inside the callAnotherUser() method based on the specific type of object passed when the program is running. This is polymorphism's main advantage – the ability to choose an implementation at runtime.
In the examples of phone classes given above, we used method overriding — a trick where we change the implementation of a method defined in the base class without changing the method signature. This essentially replaces the method: the new method defined in the subclass is called when the program is executed.
Usually, when we override a method, the @Override annotation is used. It tells the compiler to check the signatures of the overridden and overriding methods. Finally, to ensure your Java programs are consistent with the principles of OOP, follow these tips:
- identify an object's main characteristics;
- identify common properties and behavior and use inheritance when creating classes;
- use abstract types to describe objects;
- try to always hide methods and fields related to a class's internal implementation.
GO TO FULL VERSION