Kapselung (Encapsulation) in Java – unverzichtbar oder überflüssig?

Viele Programmierer, vor allem diejenigen, die mit objektorientierter Programmierung, z.B. in Java, begonnen haben, können nur schwer verstehen, warum es notwendig ist, Encapsulation zu verwenden, wenn es doch überhaupt nicht notwendig ist und es irgendwie auch ohne funktioniert. Sie glauben fälschlicherweise, dass es im Grunde nur darum geht, Daten vor der Außenwelt zu verstecken. Ich muss gestehen, dass ich zu einer ähnlichen Kategorie von Menschen gehörte, als ich vor vielen Jahren Java programmieren lernte. Der Zweck dieses Artikels ist es, zu erklären, was Kapselung ist, warum man damit anfangen sollte, welche Vorteile sie bringt und welche Beziehung zwischen Kapselung und Abstraktion besteht.

Kapselung (Encapsulation) in Java – unverzichtbar oder überflüssig?
Kapselung (Encapsulation) in Java – unverzichtbar oder überflüssig?

In diesem Artikel erfährst du:

    Was ist encapsulation (Verkapselung)?

    Kapselung ist eine Möglichkeit, Mitgliedsvariablen (d.h. Daten) und Verhaltensweisen (Methoden) in einer einzigen Einheit zu kombinieren, die eine Klasse, eine Schnittstelle, eine Enum usw. sein kann. Diese Kapselung stellt sicher, dass die Daten nicht direkt zugänglich sind, und die gleichen Einschränkungen können für die Verhaltensmethoden gelten, die mit den Daten arbeiten. Dies lässt sich am besten anhand eines Beispiels veranschaulichen, bei dem es sich um eine Art Kapsel (ähnlich einer Pille) handelt, die die Datenmethoden und -elemente enthält, und die Kapselung ist eine Hülle oder Schicht, die sie vor der Außenwelt schützt. Sie bietet jedoch eine klar definierte öffentliche Schnittstelle für den Zugriff und die Bearbeitung dieses Objekts. Die Kapselung stellt auch sicher, dass wir nicht in die Kapsel hineinsehen können und daher aus der Perspektive eines außenstehenden Beobachters oder Benutzers nicht wissen, wie was implementiert wurde. Verkapselung kann man sich als Verkapselung von Methoden und Mitgliedern in einer Kapsel vorstellen.

    Kapselung kann man sich als die Verkapselung von Methoden und Membern in einer Kapsel vorstellen. QUELLE: tutoringlounge.com.au/objektorientierte-programmierung/
    Kapselung kann man sich als die Verkapselung von Methoden und Membern in einer Kapsel vorstellen. QUELLE: tutoringlounge.com.au/objektorientierte-programmierung/

    Wie erreicht man Kapselung in Java

    Mechanismus zum Verbergen von Daten

    In Java wird die Kapselung durch Zugriffsmodifikatoren wie private, public und protected realisiert, die die Sichtbarkeit von Klassenmitgliedern wie Feldern und Methoden steuern. Auf private Mitglieder kann nur innerhalb der gleichen Klasse zugegriffen werden, während auf öffentliche Mitglieder überall im Programm zugegriffen werden kann. Auf geschützte Mitglieder kann innerhalb der gleichen Klasse, von Unterklassen und von Klassen im gleichen Paket zugegriffen werden. Mit den get-Methoden des öffentlichen Zugriffs können wir die in den Klassenmitgliedern gespeicherten Werte abrufen und mit den set-Methoden können wir sie ändern und somit den alten Wert durch einen neuen Wert überschreiben. Viele Programmierer sehen dies jedoch nur alsDatenverstecken und sind verwirrt, warum wir nicht einfach jede Variable in einer Klasse als öffentlich definieren und uns das Schreiben von Code für get- und set-Methoden ersparen.

    Beispiel:

    Wenn wir eine Klasse Person und einen Variablennamen vom Typ String und ein Alter vom Typ int haben, könnten wir sie ohne Kapselung so schreiben und verwenden (Werte schreiben und lesen):

    public class Osoba {
        public String meno;
        public int vek;
    }
    Osoba osoba = new Osoba();
    osoba.meno = "Dominika";
    osoba.vek = 25;
    System.out.println("Meno: " + osoba.meno);
    System.out.println("Vek: " + osoba.vek);

    Wenn wir eine ähnliche Klasse kapseln wollten, müssten wir die Datenelemente verstecken, d.h. die Zugriffsmodifikatoren z.B. in private oder protected ändern. In diesem Fall bräuchten wir jedoch zumindest get-Methoden zum Lesen und möglicherweise set-Methoden zum Schreiben. Die gesamte Deklaration der Klasse wird gestreckt:

    public class Osoba {
        private String meno;
        private int vek;
    
        public String getMeno() {
            return meno;
        }
    
        public void setMeno(String meno) {
            this.meno = meno;
        }
    
        public int getVek() {
            return vek;
        }
    
        public void setVek(int vek) {
            this.vek = vek;
        }
    }
    Osoba osoba = new Osoba();
    osoba.setMeno("Dominika");
    osoba.setVek(25);
    System.out.println("Meno: " + osoba.getMeno());
    System.out.println("Vek: " + osoba.getVek());

    Wenn man sich den Codeumfang beider Ansätze ansieht, stellt sich die Frage, warum wir nicht einfach alles öffentlich definieren und auf die Definition von get- und set-Methoden verzichten.
    Aber nehmen wir an, ein unaufmerksamer oder böswilliger Benutzer gibt folgendes als Alter ein: person.age = -25; Logischerweise kann das Alter nicht negativ sein, aber das Programm weiß das nicht. Es beachtet alle zulässigen Wertebereiche für den Datentyp int, also auch negative Werte. Das Einstellen der Daten ohne jegliche Prüfung führt dann in der Regel zu Fehlern im Programm. Mit dem zweiten Ansatz können wir jedoch die set-Methode so ändern, dass das Alter eine positive Zahl sein muss.

    public void setVek(int vek) {
        if (vek >= 0) {
            this.vek = vek;
        }
    }

    Auf ähnliche Weise könnten wir den String-Wert für den vom Benutzer angegebenen Namen überprüfen.

    public void setMeno(String meno) {
        if(meno == null || meno.equals("")) {
            throw new IllegalArgumentException("Meno musi obsahovat aspon 1 znak!");
        }
        this.meno = meno;
    }

    Es gibt viele gute Gründe, warum es sinnvoll ist, Zugriffsmethoden (Accessors) anstelle des direkten Zugriffs auf die Felder einer Klasse zu verwenden. Schauen wir uns einige davon an:

    • Durch die Kapselung in Verbindung mit get/set-Methoden können wir später leichter zusätzliche Funktionen hinzufügen (z. B. die Validierung, wie wir im Beispiel gesehen haben).
    • Die set-Methode eignet sich hervorragend als Breakpoint für das Debugging, insbesondere wenn man nachvollziehen möchte, wann eine Variable in der Klasse auf einen bestimmten Wert geändert wird. Ohne sie wäre es recht schwierig, die Änderung zu lokalisieren, vor allem in einem größeren Programm mit vielen Zuweisungen.
    • Die Möglichkeit für abgeleitete Klassen, die Semantik des Verhaltens und des Zugriffs auf ein Mitglied durch Überschreiben der Getter- und Setter-Methoden zu ändern.
    • Getter und Setter können unterschiedliche Zugriffslevel haben – zum Beispiel kann der Getter public sein, während der Setter protected ist.
    • Verbesserte Interoperabilität mit Bibliotheken, die für die Arbeit mit Gettern/Settern entwickelt wurden – wie Spring, Mocking, Serialisierung und viele andere.
    • Erlaubt die Übergabe von Gettern/Settern als Lambda-Ausdrücke anstelle von Werten.
    • Die interne Darstellung einer Klasse kann sich ändern und der Benutzer wird es natürlich nicht bemerken, wenn die öffentliche Schnittstelle dieselbe bleibt.
    • Die Kapselung ermöglicht es uns, eine Klasse und ihre Instanzen als unveränderlich zu definieren (unveränderlich nach der Erstellung der Instanz), ein Beispiel ist die Klasse String.
    • Gewährleistung der Thread-Sicherheit durch Synchronisierung der Methode.

    Um unsere Klasse richtig zu kapseln, muss nicht jedes Datenelement automatisch eine get- und set-Methode enthalten. Wir fügen diese nach Bedarf hinzu. Wenn die Klasse hauptsächlich funktionalen Code ausführt oder mit unveränderlichen Objekten arbeitet, werden andere Accessor-Methoden definiert, als wenn sie mit zustandsabhängigen, veränderlichen Objekten arbeitet. Obwohl Programmieranfänger durch das Schreiben von mehreren Zeilen Code entmutigt werden könnten, ist es sicherlich notwendig, sich an die Kapselung und die Verwendung von Accessor-Methoden zu gewöhnen. Viele Programmierumgebungen und Editoren bieten inzwischen Unterstützung für die Generierung von get/set-Methoden (entweder in der IDE oder als kostenlose Plug-ins), was die Auswirkungen dieses Problems bis zu einem gewissen Grad verringert. Wenn wir rein dateninvariante Klassen schreiben wollen, ist die Verwendung von Java Records, die wir im letzten Artikel behandelt haben, sehr einfach.

    Encapsulation vs data hiding

    In der Objektwelt ist das Verstecken von Daten vor der Außenwelt keine Kapselung, sondern nur ein notwendiger Teil davon. Das bedeutet, dass es bei der Kapselung nicht nur um die Definition von Zugriffs- und Mutationsmethoden für eine Klasse geht, sondern dass es sich um ein breiteres Konzept in der objektorientierten Programmierung handelt, das die Minimierung von Abhängigkeiten zwischen Klassen beinhaltet und in der Regel durch das Verbergen von Informationen umgesetzt wird. Ein richtig gekapseltes Objekt kapselt den Zustand und die Algorithmen zur Manipulation dieses Zustands. Es ist jedoch wichtig zu betonen, dass der Zustand nur gekapselt und durch Algorithmen manipuliert werden sollte, die vom Objekt selbst bereitgestellt werden. Das Schöne an der Kapselung ist die Möglichkeit, Dinge zu ändern, ohne dass sich dies auf die Benutzer auswirkt. Das Ziel ist also nicht, die Daten selbst zu verbergen, sondern die Implementierungsdetails, wie diese Daten manipuliert werden. Die Hauptidee hinter der Kapselung besteht darin, eine öffentliche Schnittstelle bereitzustellen, über die der Benutzer auf diese Daten zugreifen kann. Später müssen wir möglicherweise die interne Darstellung der Daten ändern, ohne dass dies Auswirkungen auf die Benutzer der bestehenden Schnittstelle hat. Wenn wir jedoch die Daten selbst verfügbar machen, gefährden wir die Kapselung und damit die Möglichkeit, die Art und Weise, wie die Daten manipuliert werden, zu ändern. Wir schaffen eine Abhängigkeit von den Daten selbst und nicht von der öffentlichen Schnittstelle der Klasse, was früher oder später zu großen Problemen führen wird. In Java können wir ganze Klassen ausblenden und so die Implementierungsdetails der gesamten API verbergen. Beispielsweise gibt die Methode Arrays.asList() eine Implementierung der Schnittstelle List zurück, aber es ist uns egal, um welche Implementierung es sich handelt, solange sie die Schnittstelle List erfüllt (implementiert), richtig? Die Implementierung kann in Zukunft geändert werden, ohne dass dies Auswirkungen auf die Benutzer dieser Methode hat, aber der Benutzer wird es wahrscheinlich nicht bemerken.

    Verkapselung und Abstraktion

    Um das Konzept der Kapselung besser zu verstehen, ist es hilfreich, zunächst die Abstraktion zu verstehen. Nehmen wir ein gewöhnliches Auto als Beispiel. Ein Auto besteht aus einer Reihe von Komponenten, ähnlich wie ein Programm. Es hat mehrere Untersysteme wie Getriebe, Bremsen, Kraftstoffsystem usw. Wir wissen, dass alle Autos ein Lenkrad haben, mit dem wir die Richtung steuern, sie haben ein Pedal, das, wenn es gedrückt wird, das Auto beschleunigt und ein anderes, das, wenn es gedrückt wird, das Auto stoppt. Dann gibt es noch den Schalthebel, mit dem wir steuern können, ob wir vorwärts oder rückwärts fahren. Diese Funktionen bilden die öffentliche Schnittstelle der Autoabstraktion. Über diese öffentliche Schnittstelle ihrer Abstraktion behandeln wir alle Autos auf der Welt auf dieselbe Weise. Wir können morgens ein Auto fahren und dann aussteigen und am Nachmittag ein anderes Auto fahren, als ob es dasselbe Auto wäre. Aber nur wenige von uns kennen die Details, wie all diese Funktionen unter der Motorhaube implementiert sind. Eines Tages werden wir vielleicht ein Elektroauto kaufen, bei dem ein Großteil der Komponenten unter der Motorhaube weggefallen ist, dessen Steuersystem (d.h. die öffentliche Schnittstelle) aber gleich geblieben ist, und deshalb werden wir es fast sofort fahren können. Wenn jedoch z.B. das Lenkrad durch die Sprachsteuerung eines selbstfahrenden Autos ersetzt wird (wir haben die etablierte Schnittstelle geändert), müssen wir erst wieder lernen, wie wir das neue Auto benutzen. Wie in diesem Beispiel zu sehen ist, besteht das Ziel der Kapselung darin, Abhängigkeiten zu minimieren und Änderungen zu erleichtern. Wir maximieren die Verkapselung, indem wir die Details der Implementierung für den Benutzer so wenig wie möglich offenlegen. Der Zustand der Klasse sollte nur über die öffentliche Schnittstelle zugänglich sein.

    Vorteile der Verkapselung

    Sicherheit: Die Kapselung trägt zur Sicherheit des Codes bei, indem sie sicherstellt, dass andere Entitäten (Klassen, Schnittstellen usw.) keinen direkten Zugriff auf die Daten haben. Flexibilität: Durch die Kapselung wird der Code flexibler, was wiederum dem Programmierer ermöglicht, den Code leicht zu ändern oder zu aktualisieren. Kontrolle: Die Kapselung ermöglicht die Kontrolle über die in Mitgliedsvariablen gespeicherten Daten. Wiederverwendbarkeit: Gekapselter Code oder Einheiten können überall in der Anwendung oder in völlig anderen Programmen wiederverwendet werden, ohne dass der Code neu geschrieben werden muss. Praktisch alle Bibliotheken funktionieren auf diese Weise. Wartbarkeit: Gekapselte Komponenten sind einfach zu warten, weil wir Dinge leicht finden oder ändern können.

    Fazit

    Die Kapselung ist ein fundamentaler Konzept objektorientierter Sprachen, das die Kombination von Daten und Methoden sowie die Steuerung des Zugriffs darauf umfasst. Sie nutzt dabei die Datenverbergung, die darauf abzielt, den direkten Zugriff auf bestimmte Daten oder Methoden einzuschränken. Ebenso wird eine Abstraktionsschicht eingesetzt, die dem Benutzer eine öffentliche Schnittstelle bietet, ohne dass er den internen Zustand der Kapsel – der Encapsulation – kennen muss.

    Wenn du ein Java Programmierer bist und nach Arbeit suchst, schau dir unsere Mitareiterbenefits an und reagiere auf die neuesten Stellenangebote.

    Über den Autor

    Jozef Wagner

    Leitender Java-Entwickler

    Ich programmiere seit mehr als 10 Jahren in Java, arbeite derzeit bei msg life Slovakia als leitender Java-Programmierer und helfe Kunden bei der Umsetzung ihrer Anforderungen in die Versicherungssoftware Life Factory. In meiner Freizeit entspanne ich gerne in den Bergen oder spiele ein gutes Computerspiel.

    Informieren Sie uns über sich