Using OOP concepts to write high-performance Java code (2023)
Posted Feb 3, 2023 | 14 min. (2908 words)Java is a class-based object-oriented programming (OOP) language built around the concept of objects. OOP concepts are intended to improve code readability and reusability by defining how to structure your Java program efficiently. There are seven core principles of object-oriented programming, as follows.
List of OOP concepts in Java:
Java comes with specific code structures for each OOP concept, such as the extends
keyword for the inheritance principle or the getter and setter methods for the encapsulation principle.
While these concepts are crucial for creating well-structured Java programs in the development phase, implementing crash reporting can also help you catch the errors your end-users encounter in the operation and maintenance phase of the software development life cycle.
In this guide, we’ll look into both the theory and practice of object-oriented programming to help you write performant and error-free Java code.
(Beyond Java, we’d also recommend exploring this guide to object-oriented design patterns.)
What are OOP concepts in Java?
Object-oriented programming focuses on objects made up of both data (fields) and code (properties or attributes). Using OOP, programmers can create classes in order to iteratively reuse code, with the same set of “instructions” being sent to different objects, rather than coding each operation from scratch each time. This works particularly well for large team projects where states change often.
Java supports object-oriented programming, along with other popular languages like Python and C++. In Java, OOP concepts allow us to create specific interactions between Java objects. They make it possible to reuse code without creating security risks or harming performance and code readability.
Advantages of OOP
- Classes provide easy “building blocks” for faster coding and make code highly maintainable
- Create more stable and consistent code
- Make large and complex code bases more efficient and manageable
- Teams working from the same code base will benefit from existing well-written classes
- Java in particular enforces OOP, which creates good habits (but may be challenging for beginners)
Definitions of OOP concepts
There are four main and three secondary principles of object-oriented Java programming. Let’s take a look at what they are and why they’re useful. You can also download or clone the code examples below from this GitHub repo.
1. Abstraction
Abstraction aims to hide complexity from users and show them only relevant information. For example, if you’re driving a car, you don’t need to know about its internal workings.
The same is true of Java classes. You can hide internal implementation details using abstract classes or interfaces. On the abstract level, you only need to define the method signatures (name and parameter list) and let each class implement them in their own way.
Abstraction in Java:
- Hides the underlying complexity of data
- Helps avoid repetitive code
- Presents only the signature of internal functionality
- Gives flexibility to programmers to change the implementation of abstract behavior
- Partial abstraction (0-100%) can be achieved with abstract classes
- Total abstraction (100%) can be achieved with interfaces
Example: How abstraction works in practice
As mentioned above, abstraction allows you to hide the internal workings of an object and only show the features the user needs to know about.
Java provides two ways to implement abstraction: abstract classes and interfaces. With abstract classes, you can achieve partial abstraction, while interfaces make total (100%) abstraction possible.
Abstract classes
An abstract class is a superclass (parent class) that cannot be instantiated. To create a new object, you need to instantiate one of its child classes. Abstract classes can have both abstract and concrete methods. Abstract methods contain only the method signature, while concrete methods declare the method body as well. Abstract classes are defined with the abstract
keyword.
In the code example below, we create an abstract class called Animal
with two abstract and one concrete method.
abstract class Animal {
// abstract methods
abstract void move();
abstract void eat();
// concrete method
void label() {
System.out.println("Animal's data:");
}
}
Then, we extend it with two child classes: Bird
and Fish
. Both of them define their own implementations of the move()
and eat()
abstract methods.
class Bird extends Animal {
void move() {
System.out.println("Moves by flying.");
}
void eat() {
System.out.println("Eats birdfood.");
}
}
class Fish extends Animal {
void move() {
System.out.println("Moves by swimming.");
}
void eat() {
System.out.println("Eats seafood.");
}
}
Now, we’ll test it with the help of the TestBird
and TestFish
classes. Both initialize an object (myBird
and myFish
) and call the one concrete (label()
) and the two abstract (move()
and eat()
) methods.
Note, however, that you don’t necessarily have to call all the methods if you don’t want to — this is how abstract classes make partial abstraction possible (for example, you could call just move()
).
class TestBird {
public static void main(String[] args) {
Animal myBird = new Bird();
myBird.label();
myBird.move();
myBird.eat();
}
}
class TestFish {
public static void main(String[] args) {
Animal myFish = new Fish();
myFish.label();
myFish.move();
myFish.eat();
}
}
As you can see below, the concrete method has been called from the Animal
abstract class, while the two abstract methods have been called from Bird
and Fish
, respectively.
[Console output of TestBird]
Animal's data:
Moves by flying.
Eats birdfood.
[Console output of TestFish]
Animal's data:
Moves by swimming.
Eats seafood.
Interfaces
An interface is a 100% abstract class. It can only have static, final, and public fields and abstract methods. It’s frequently referred to as a blueprint of a class as well. Java interfaces allow you to implement multiple inheritances in your code, as a class can implement any number of interfaces. Classes can access an interface with the implements
keyword.
In the example, we define two interfaces: Animal
with two abstract methods (interface methods are abstract by default) and Bird
with two static fields and an abstract method.
interface Animal {
public void eat();
public void sound();
}
interface Bird {
int numberOfLegs = 2;
String outerCovering = "feather";
public void fly();
}
The class Eagle
implements both interfaces. It defines its own functionality for the three abstract methods. The eat()
and sound()
methods come from the Animal
class, while fly()
comes from Bird
.
class Eagle implements Animal, Bird {
public void eat() {
System.out.println("Eats reptiles and amphibians.");
}
public void sound() {
System.out.println("Has a high-pitched whistling sound.");
}
public void fly() {
System.out.println("Flies up to 10,000 feet.");
}
}
In the TestEagleInterfaces
test class, we instantiate a new Eagle
object (called myEagle
) and print out all the fields and methods to the console.
As static fields (numberOfLegs
and outerCovering
) don’t belong to a specific object but to the interface, we need to access them from the Bird
interface instead of the myEagle
object.
class TestEagleInterfaces {
public static void main(String[] args) {
Eagle myEagle = new Eagle();
myEagle.eat();
myEagle.sound();
myEagle.fly();
System.out.println("Number of legs: " + Bird.numberOfLegs);
System.out.println("Outer covering: " + Bird.outerCovering);
}
}
The Java console returns all the information we wanted to access:
[Console output of TestEagleInterfaces]
Eats reptiles and amphibians.
Has a high-pitched whistling sound.
Flies up to 10,000 feet.
Number of legs: 2
Outer covering: feather
2. Encapsulation
Encapsulation helps with data security, allowing you to protect the data stored in a class from system-wide access. As the name suggests, it safeguards the internal contents of a class like a capsule.
You can implement encapsulation in Java by making the fields (class variables) private and accessing them via their public getter and setter methods. JavaBeans are examples of fully encapsulated classes.
Encapsulation in Java:
- Restricts direct access to data members (fields) of a class
- Fields are set to private
- Each field has a getter and setter method
- Getter methods return the field
- Setter methods let us change the value of the field
Example: how encapsulation works in practice
With encapsulation, you can protect the fields of a class. To do so, you need to declare the fields as private
and provide access to them with getter and setter methods.
The Animal
class below is fully encapsulated. It has three private fields, and each has its own pair of getter and setter methods.
class Animal {
private String name;
private double averageWeight;
private int numberOfLegs;
// Getter methods
public String getName() {
return name;
}
public double getAverageWeight() {
return averageWeight;
}
public int getNumberOfLegs() {
return numberOfLegs;
}
// Setter methods
public void setName(String name) {
this.name = name;
}
public void setAverageWeight(double averageWeight) {
this.averageWeight = averageWeight;
}
public void setNumberOfLegs(int numberOfLegs) {
this.numberOfLegs = numberOfLegs;
}
}
The TestAnimal
class first creates a new Animal
object (called myAnimal
), then defines a value for each field with the setter methods, and finally prints out the values using the getter methods.
class TestAnimal {
public static void main(String[] args) {
Animal myAnimal = new Animal();
myAnimal.setName("Eagle");
myAnimal.setAverageWeight(1.5);
myAnimal.setNumberOfLegs(2);
System.out.println("Name: " + myAnimal.getName());
System.out.println("Average weight: " + myAnimal.getAverageWeight() + "kg");
System.out.println("Number of legs: " + myAnimal.getNumberOfLegs());
}
}
As you can see below, the Java console returns all the values we have set with the setter methods:
[Console output of TestAnimal]
Name: Eagle
Average weight: 1.5kg
Number of legs: 2
3. Inheritance
Inheritance makes it possible to create a child class that inherits the fields and methods of the parent class. The child class can override the values and methods of the parent class, but it’s not necessary. It can also add new data and functionality to its parent.
Parent classes are also called superclasses or base classes, while child classes are known as subclasses or derived classes as well. Java uses the extends
keyword to implement the principle of inheritance in code.
Inheritance in Java:
- A class (child class) can extend another class (parent class) by inheriting its features
- Implements the DRY (Don’t Repeat Yourself) programming principle
- Improves code reusability
- Multi-level inheritance is allowed in Java (a child class can have its own child class as well)
- Multiple inheritances are not allowed in Java (a class can’t extend more than one class)
Example: How inheritance works in practice
Inheritance lets you extend a class with one or more child classes that inherit the fields and methods of the parent class. It’s an excellent way to achieve code reusability. In Java, you need to use the extends
keyword to create a child class.
In the example below, the Eagle
class extends the Bird
parent class. It inherits all of its fields and methods, plus defines two extra fields that belong only to Eagle
.
class Bird {
public String reproduction = "egg";
public String outerCovering = "feather";
public void flyUp() {
System.out.println("Flying up...");
}
public void flyDown() {
System.out.println("Flying down...");
}
}
class Eagle extends Bird {
public String name = "eagle";
public int lifespan = 15;
}
The TestEagleInheritance
class instantiates a new Eagle
object (called myEagle
) and prints out all the information (both the inherited fields and methods and the two fields defined by the Eagle
class).
class TestEagleInheritance {
public static void main(String[] args) {
Eagle myEagle = new Eagle();
System.out.println("Name: " + myEagle.name); System.out.println("Reproduction: " + myEagle.reproduction);
System.out.println("Outer covering: " + myEagle.outerCovering);
System.out.println("Lifespan: " + myEagle.lifespan);
myEagle.flyUp();
myEagle.flyDown();
}
}
Here’s the console output we get:
[Console output of TestEagleInheritance]
Name: eagle
Reproduction: egg
Outer covering: feather
Lifespan: 15
Flying up...
Flying down...
4. Polymorphism
Polymorphism refers to the ability to perform a certain action in different ways. In Java, polymorphism can take two forms: method overloading and method overriding.
Method overloading happens when various methods with the same name are present in a class. When they are called, they are differentiated by the number, order, or types of their parameters. Method overriding occurs when a child class overrides a method of its parent.
Polymorphism in Java:
- The same method name is used several times
- Different methods of the same name can be called from an object
- All Java objects can be considered polymorphic (at the minimum, they are of their own type and instances of the
Object
class) - Static polymorphism in Java is implemented by method overloading
- Dynamic polymorphism in Java is implemented by method overriding
Example: How polymorphism works in practice
Polymorphism makes it possible to use the same code structure in different forms. In Java, this means that you can declare several methods with the same name as long as they are different in certain characteristics.
As mentioned above, Java provides two ways to implement polymorphism: method overloading and method overriding.
Static polymorphism (method overloading)
Method overloading means that you can have several methods with the same name within a class. However, the number, names, or types of their parameters need to be different.
For example, the Bird()
class below has three fly()
methods. The first one doesn’t have any parameters, the second one has one parameter (height
), and the third one has two parameters (name
and height
).
class Bird {
public void fly() {
System.out.println("The bird is flying.");
}
public void fly(int height) {
System.out.println("The bird is flying " + height + " feet high.");
}
public void fly(String name, int height) {
System.out.println("The " + name + " is flying " + height + " feet high.");
}
}
The test class instantiates a new Bird
object and calls the fly()
method three times: first, without parameters, second, with one integer parameter for height
, and third, with two parameters for name
and height
.
class TestBirdStatic {
public static void main(String[] args) {
Bird myBird = new Bird();
myBird.fly();
myBird.fly(10000);
myBird.fly("eagle", 10000);
}
}
In the console, you can see that Java could have differentiated the three polymorphic fly() methods:
[Console output of TestBirdStatic]
The bird is flying.
The bird is flying 10000 feet high.
The eagle is flying 10000 feet high.
Dynamic polymorphism (method overriding)
Using the method overriding feature of Java, you can override the methods of a parent class from its child class.
In the code example below, the Bird
class extends the Animal
class. Both have an eat()
method. By default, Bird
inherits its parent’s eat()
method. However, as it also defines its own eat()
method, Java will override the original method and call eat()
from the child class.
class Animal {
public void eat() {
System.out.println("This animal eats insects.");
}
}
class Bird extends Animal {
public void eat() {
System.out.println("This bird eats seeds.");
}
}
The TestBirdDynamic
class first instantiates a new Animal
object and calls its eat()
method. Then, it also creates a Bird
object and calls the polymorphic eat()
method again.
class TestBirdDynamic {
public static void main(String[] args) {
Animal myAnimal = new Animal();
myAnimal.eat();
Bird myBird = new Bird();
myBird.eat();
}
}
The console returns the values of the relevant methods properly because Java could have differentiated the two eat()
methods:
[Console output of TestBirdDynamic]
This animal eats insects.
This bird eats seeds.
Abstraction, encapsulation, polymorphism, and inheritance are the four main theoretical principles of object-oriented programming. But Java also works with three further OOP concepts: association, aggregation, and composition. Aggregation is a special form of association, while composition is a special form of aggregation. While that may sound a bit convoluted, we’re about to explain. Read on!
5. Association
Association means the act of establishing a relationship between two unrelated classes. For example, when you declare two fields of different types (e.g. Car
and Bicycle
) within the same class and make them interact with each other, you have created an association.
Association in Java:
- Two separate classes are associated through their objects
- The two classes are unrelated, each can exist without the other one
- Can be a one-to-one, one-to-many, many-to-one, or many-to-many relationship
6. Aggregation
Aggregation is a narrower kind of association. It occurs when there’s a one-way (HAS-A) relationship between the two classes we associate through their objects.
For example, every Passenger
has a Car
, but a Car
doesn’t necessarily have a Passenger
. When you declare the Passenger
class, you can create a field of the Car
type that shows which car the passenger belongs to. Then, when you instantiate a new Passenger
object, you can access the data stored in the related Car
as well.
Aggregation in Java:
- One-directional association
- Represents a HAS-A relationship between two classes
- Only one class is dependent on the other
7. Composition
Composition is a stricter form of aggregation. It occurs when the two classes you associate are mutually dependent and can’t exist without each other.
For example, take a Car
and an Engine
class. A Car
cannot run without an Engine
, while an Engine
also can’t function without being built into a Car
. This kind of relationship between objects is also called a PART-OF relationship.
Composition in Java:
- A restricted form of aggregation
- Represents a PART-OF relationship between two classes
- Both classes are dependent on each other
- If one class ceases to exist, the other can’t survive alone
Remember, the code examples in this post are also available to clone or download from this GitHub repo.
Summary
OOP concepts in Java help you to structure your program more efficiently. The seven object-oriented principles we’ve explored here (abstraction, encapsulation, polymorphism, inheritance, association, aggregation, and composition) can help you reuse your code, prevent security issues, and improve the performance of your Java applications.
Object-oriented programming is a broad and complex field, and if you’re upskilling in this area, we’d also recommend exploring this guide to object-oriented design patterns.
While you’re focused on boosting the performance of your Java code, add Raygun Application Performance Monitoring and Crash Reporting in minutes and detect problems in your software as they happen.