Abstraction in Java
-
Last Updated: July 27, 2025
-
By: javahandson
-
Series
Understand abstraction in Java with easy examples. Learn abstract classes, interfaces, benefits & best practices to write clean, flexible code.
Abstraction is the process of hiding the internal implementation details and showing only the essential features of an object. It allows us to focus on what an object does, instead of how it does it.
Ex. When we drive a car, we use the steering wheel, accelerator, brake — we don’t know how the engine, gearbox, or wiring work internally. This is abstraction. We interact with a simplified interface without knowing the complex internal mechanics.
Java achieves abstraction mainly through abstract classes and interfaces. Abstract classes let us define a partial blueprint with some shared code, while interfaces define a pure contract of methods that implementing classes must fulfill. This hides the implementation details and exposes only what’s necessary.
Java provides abstract classes as one of the main ways to achieve abstraction.
1. An abstract class in Java is a class that cannot be instantiated directly.
2. It can have abstract methods (methods without a body) as well as concrete methods (methods with a body).
Since an abstract class cannot be instantiated directly, it must be extended by a subclass, these subclass then provides implementations for the abstract methods defined in the abstract class.
package com.java.handson.abstraction;
abstract class Animal {
String name;
Animal(String name) {
this.name = name;
}
// abstract method: subclasses must implement this
abstract void makeSound();
// concrete method: common to all animals
void eat() {
System.out.println(name + " is Eating.");
}
}
class Dog extends Animal {
Dog(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + " says: Woof! Woof!");
}
}
class Cat extends Animal {
Cat(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + " says: Meow! Meow!");
}
}
public class Main {
public static void main(String[] args) {
Animal dog = new Dog("Tommy");
Animal cat = new Cat("Billi");
dog.makeSound();
dog.eat();
cat.makeSound();
cat.eat();
}
}
Output:
Tommy says: Woof! Woof!
Tommy is Eating.
Billi says: Meow! Meow!
Billi is Eating.
Explanation:
1. This program demonstrates abstraction using an abstract class Animal, which defines a general blueprint with an abstract method #makeSound() and a concrete method #eat().
2. Subclasses Dog and Cat provide their own specific implementations of makeSound(), while inheriting the common eat() behavior.
3. In the Main class, we create Dog and Cat objects using Animal references, showing how different animals behave differently while sharing common actions. This hides internal details and focuses on what actions animals perform, achieving abstraction.
So, in short, Abstract classes in Java let us define a partial blueprint (what to do) and leave the details (how to do) to subclasses, achieving abstraction.
1. When we want to share common code (methods or fields) among closely related classes.
In the above program, the Animal abstract class defines common fields (name) and a shared method (#eat()), which are inherited by both Dog and Cat. This means we don’t have to repeat these properties or behaviors in each subclass. It ensures all animal types automatically have a name and can eat, reducing duplication.
2. When we expect classes to be part of the same family/type hierarchy.
In the above program, Dog and Cat are part of the same Animal family, sharing common features like name and #eat(). But each subclass provides its own specific behavior for #makeSound() – a Dog barks while a Cat meows.
This lets us treat them uniformly as Animal types while still letting each animal behave differently. That’s the power of using an abstract class to define a type hierarchy with shared code and specialized behaviors.
3. When we want to provide a default behavior that subclasses can reuse or override.
In the above program, the Animal abstract class provides a default behavior through the #eat() method. Both Dog and Cat automatically inherit this method and can reuse it without rewriting the code. If needed, they could also override #eat() to change how they eat. This shows how abstract classes let you define common behaviors once, which subclasses can reuse or customize.
class Cat extends Animal {
Cat(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + " says: Meow! Meow!");
}
@Override
void eat() {
System.out.println(name + " is eating fish.");
}
}
Output: Billi is eating fish.
4. Use abstract classes when we need constructors, instance variables, or initialization blocks.
In the above program, the abstract class Animal has a constructor and an instance variable name to initialize and store the animal’s name. This shared data setup ensures every Dog or Cat automatically gets a name without duplicating code.
Since interfaces can’t have constructors or instance variables, an abstract class is essential here. That’s why we use abstract classes when we need to handle object construction and shared state.
An interface in Java is a contract that defines a set of abstract methods that implementing classes must provide. It specifies what operations a class should perform, without dictating how they’re implemented. They are used to achieve complete abstraction and to build loosely coupled, flexible systems.
It is like a pure abstraction – it contains only method signatures. All methods are public and abstract by default in an interface (no need to write public abstract), but from Java 8 onward, it can have default and static methods as well.
package com.java.handson.abstraction;
interface Student {
void study();
void fees();
}
This is a pure contract: any class that implements Student must provide implementations for #study() and #fees().
package com.java.handson.abstraction;
class SchoolStudent implements Student {
private String name;
public SchoolStudent(String name) {
this.name = name;
}
@Override
public void study() {
System.out.println(name + " is studying school subjects.");
}
@Override
public void fees() {
System.out.println(name + " fess is 10000 Rs.");
}
}
class CollegeStudent implements Student {
private String name;
public CollegeStudent(String name) {
this.name = name;
}
@Override
public void study() {
System.out.println(name + " is preparing for college semesters.");
}
@Override
public void fees() {
System.out.println(name + " fess is 100000 Rs.");
}
}
public class Demo {
public static void main(String[] args) {
Student schoolStudent = new SchoolStudent("Suraj");
schoolStudent.study();
schoolStudent.fees();
Student collegeStudent = new CollegeStudent("Shweta");
collegeStudent.study();
collegeStudent.fees();
}
}
Output:
Suraj is studying school subjects.
Suraj fess is 10000 Rs.
Shweta is preparing for college semesters.
Shweta fess is 100000 Rs.
Java does not support multiple inheritance with classes (i.e., a class cannot extend multiple classes), but a class can implement multiple interfaces, allowing multiple inheritance of type.
Imagine we want to model different roles that a student can have. A student can study, but he can also play sports or do a part-time job.
We can define each role as a separate interface. Then, a student can implement multiple interfaces, inheriting multiple types of behavior contracts.
package com.java.handson.abstraction;
interface Student {
void study();
void fees();
}
interface Athlete {
void playSport();
}
interface PartTimeJob {
void workPartTime();
}
class SchoolStudent implements Student, Athlete {
private String name;
public SchoolStudent(String name) {
this.name = name;
}
@Override
public void study() {
System.out.println(name + " is studying school subjects.");
}
@Override
public void fees() {
System.out.println(name + " fess is 10000 Rs.");
}
@Override
public void playSport() {
System.out.println(name + " play sports");
}
}
class CollegeStudent implements Student, PartTimeJob {
private String name;
public CollegeStudent(String name) {
this.name = name;
}
@Override
public void study() {
System.out.println(name + " is preparing for college semesters.");
}
@Override
public void fees() {
System.out.println(name + " fess is 100000 Rs.");
}
@Override
public void workPartTime() {
System.out.println(name + " do part time job as chef");
}
}
public class Demo {
public static void main(String[] args) {
SchoolStudent schoolStudent = new SchoolStudent("Suraj");
schoolStudent.study();
schoolStudent.fees();
schoolStudent.playSport();
CollegeStudent collegeStudent = new CollegeStudent("Shweta");
collegeStudent.study();
collegeStudent.fees();
collegeStudent.workPartTime();
}
}
Output:
Suraj is studying school subjects.
Suraj fess is 10000 Rs.
Suraj play cricket
Shweta is preparing for college semesters.
Shweta fess is 100000 Rs.
Shweta do part time job as chef
1. To define a contract for unrelated classes: Use interfaces when we want to ensure different, unrelated classes can promise to perform certain behaviors.
In our code, the Student, Athlete, and PartTimeJob interfaces define contracts for behavior. They let completely different kinds of students (like SchoolStudent and CollegeStudent) promise to perform actions like #study(), pay #fees(), #playSport(), or #workPartTime().
Even though studying, playing sports, and doing part-time jobs are different things, they’re unified by interfaces.
2. To achieve multiple inheritance of type: Since Java doesn’t support multiple inheritance with classes, use interfaces to let a class inherit multiple types. In our above example, SchoolStudent implements both Student and Athlete, and CollegeStudent implements Student and PartTimeJob.
This is multiple inheritance of type:
SchoolStudent is treated as a Student and as an Athlete.
CollegeStudent is treated as a Student and as a Part-Time Job.
3. To build loosely coupled, flexible systems: By coding to interfaces, we can swap implementations easily without changing the client code. This leads to better maintainability and testability.
Because of interfaces, we could later add another class, say EveningStudent, that also implements Student and PartTimeJob. Our system would work the same without changing existing code. We can write code that depends on the Student interface and freely swap in any type of student.
4. When we don’t need shared state or constructors: Interfaces are best for defining pure behavior contracts, without any shared fields or constructors. Our interfaces (Student, Athlete, PartTimeJob) only specify what actions should be possible, not how to store data or initialize objects.
The actual name field and constructors are handled inside the implementing classes (SchoolStudent and CollegeStudent). This is perfect for interfaces, since they don’t maintain state – they just enforce that certain behaviors must exist.
There are 2 types of abstraction: High-level abstraction and Low-level abstraction.
High-level abstraction talks about what objects can do and not how they do it. It is tightly tied to general types and not concrete types.
Animal animal = new Dog("Tommy");
animal.makeSound();
Here, we use the Animal type, so we only care that it can make a sound, not how the dog barks or cat meows. Similarly:
Student student = new CollegeStudent("Shweta");
student.study();
Here, we use the Student type, so we only care that it can study; we don’t care if it is SchoolStudent or CollegeStudent and if they #playsport() or do #workPartTime().
Low-level abstraction involves details of how things work internally. It is tightly tied to concrete classes, not general types means we directly work with the specific class and its details, so less flexibility.
SchoolStudent schoolStudent = new SchoolStudent("Suraj");
schoolStudent.playSport();
Now we’re working directly with the SchoolStudent class, using its actual fields and methods, caring about its specific data.
Abstraction is critically important in software development because it helps manage complexity by letting us focus on high-level interactions without caring about low-level details.
1. Reduces complexity – Abstraction allows developers to work with simpler interfaces. Developers only need to know what an object does, not how it does it.
2. Improves maintainability – If internal implementations change, external code using the abstraction doesn’t break. This leads to more maintainable systems.
3. Enables flexibility and scalability – We can swap between the implementations without affecting the client code. For example, we can switch from an ArrayList to a LinkedList without changing code that depends on the List abstraction.
4. Supports better testing and mocking – Abstractions make it easier to mock dependencies (e.g., using interfaces in unit tests).
5. Enhances team collaboration – Teams can work on different layers or modules independently, relying on agreed-upon abstractions (contracts/interfaces).
1. Program to an interface, not an implementation: Always depend on abstractions like interfaces or abstract classes, not concrete classes.
Student schoolStudent = new SchoolStudent("Suraj");
So we can later swap to CollegeStudent without changing our code.
2. Keep abstractions focused and minimal: Define only the essential methods needed. Don’t overload interfaces or abstract classes with too many unrelated responsibilities.
3. Use abstract classes for shared code and interfaces for contracts: If we have common state or default methods, prefer abstract classes. If we only need a behavior contract (with no shared state), use interfaces.
4. Use meaningful names: Give interfaces and abstract classes names that clearly indicate the role or contract they provide, like Student or Shape.
5. Avoid leaking implementation details: Our abstractions should hide how things work, not expose internal workings to the outside.
Abstraction is a core OOP principle that lets us focus on what an object does, while hiding how it does it. By wisely using abstract classes and interfaces in Java, we can write flexible, scalable, and easily maintainable applications that adapt well to future changes.
So this is all about abstraction in Java. If you have any questions on this topic, please raise them in the comments section. If you liked this article, then please share this post with your friends and colleagues.