☕ Java

OOP: Kapselung und Polymorphie in Java: Eine ausführliche Anleitung

What is it and why? (a real-life analogy)

Imagine you are a car driver. When you get behind the wheel, you are not interested in how exactly the engine works, how the fuel injection system operates, or how the electronics distribute torque. You only need to know three things: the steering wheel (to turn), the gas pedal (to go), and the brake pedal (to stop). Everything else is hidden under the hood.

This is encapsulation in its pure form. We hide the complexity inside the object and show only what is needed for work to the outside. A car is an object, its internals are data and methods, and the steering wheel, pedals, and levers are the public interface.

Now imagine you approach different cars: a sports car, a truck, and an electric car. They all have a steering wheel, a gas pedal, and a brake. But they behave differently. The sports car accelerates in 3 seconds, the truck is slow but can carry tons of cargo, and the electric car is quiet and has no exhaust. You press the gas pedal — and each car reacts in its own way.

This is polymorphism. The same action (pressing the gas) causes different behavior depending on which object (which car) is in front of you. As a driver, you don't think about how the engine works — you just press the pedal, and the car does everything itself.

💡 Main idea: Encapsulation protects data and hides complexity. Polymorphism allows different objects to react differently to the same commands. Together, they make code flexible, safe, and easy to extend.

Theory: how it works from the inside

Encapsulation: the "black box" principle

In programming, encapsulation is the bundling of data (fields) and methods (functions) inside a class, as well as controlling access to them. Imagine a class is a capsule that contains variables and methods. Some of them are visible to everyone (public), and some are hidden (private).

Why is this needed? Imagine you have a BankAccount class with a balance field. If this field were public, any programmer could write: account.balance = -1000000. That would be a disaster! Encapsulation forces us to use methods to change data, and inside these methods, we can check that the balance doesn't go negative.

In Java, encapsulation is implemented through access modifiers:

Modifier Access within the class Access within the same package Access in subclasses Access from everywhere
private ✅ ❌ ❌ ❌
default (no modifier) ✅ ✅ ❌ ❌
protected ✅ ✅ ✅ ❌
public ✅ ✅ ✅ ✅

Standard practice: make all fields private, and create getters (methods for reading) and setters (methods for writing) to access them.

Polymorphism: one interface — many implementations

Polymorphism (from Greek "many forms") is the ability of objects with the same interface to behave differently. In Java, there are two main types of polymorphism:

  1. Compile-time polymorphism (static) — this is method overloading. When you have several methods with the same name but different parameters. The compiler decides which method to call based on the arguments.
  2. Runtime polymorphism (dynamic) — this is method overriding. When a child class changes the behavior of a parent class method. Which version of the method is called is decided at runtime.

Dynamic polymorphism works through inheritance and interfaces. If you have a variable of type Animal that holds a Dog object, then when you call animal.sound(), the dog's method will be executed, not the abstract animal's.

⚠️ Important: Polymorphism only works with instance (non-static) methods. Static methods are tied to the class, not the object, so they are not overridden — they are hidden.

Syntax in Java: all forms and variations

Encapsulation: full syntax

Basic structure of an encapsulated class:

public class Person {
    // Private fields — no one has direct access to them
    private String name;
    private int age;
    private double salary;

    // Constructor — creates an object with initial values
    public Person(String name, int age, double salary) {
        this.name = name;
        this.age = age;
        this.salary = salary;
    }

    // Getter for name — read-only, cannot be changed
    public String getName() {
        return name;
    }

    // Getter for age
    public int getAge() {
        return age;
    }

    // Setter for age — with validation
    public void setAge(int age) {
        if (age > 0 && age < 150) {  // Check that the age is realistic
            this.age = age;
        } else {
            System.out.println("Error: invalid age!");
        }
    }

    // Getter for salary
    public double getSalary() {
        return salary;
    }

    // Setter for salary — with protection against negative values
    public void setSalary(double salary) {
        if (salary >= 0) {
            this.salary = salary;
        }
    }
}