„Ich werde dir etwas über ‚Zugriffsmodifikatoren‘ erzählen. Ich habe schon einmal von ihnen erzählt, aber Wiederholung ist eine der Säulen des Lernens.“

Du kannst den Zugriff (die Sichtbarkeit) anderer Klassen auf die Methoden und Variablen deiner Klasse kontrollieren. Ein Zugriffsmodifikator beantwortet die Frage „Wer hat Zugang zu dieser Methode/Variable?“. Du kannst nur einen Modifikator pro Methode oder Variable angeben.

1)public‘-Modifikator.

Eine Variable, Methode oder Klasse, die mit dem public-Modifikator markiert ist, kann von überall im Programm aufgerufen werden. Dies ist der höchste Grad an Offenheit: Es gibt keine Einschränkungen.

2)private‘-Modifikator.

Eine Variable, Methode oder Klasse, die mit dem private-Modifikator markiert ist, kann nur in der Klasse, in der sie deklariert ist, aufgerufen werden. Die markierte Methode oder Variable wird vor allen anderen Klassen verborgen. Dies ist der höchste Grad an Privatheit: Sie sind nur für deine Klasse zugänglich. Solche Methoden werden nicht vererbt und können nicht überschrieben werden. Außerdem kann auf sie nicht in einer abstammenden Klasse zugegriffen werden.

3) ‚Standard-Modifikator‘.

Wenn eine Variable oder Methode nicht mit einem Modifikator markiert ist, dann wird sie als mit dem „Standard“-Modifikator markiert betrachtet. Variablen und Methoden mit diesem Modifikator sind für alle Klassen in dem Paket, in dem sie deklariert sind, sichtbar und nur für diese Klassen. Dieser Modifikator wird auch als „Paket“ oder „paketprivat“-Zugriff bezeichnet, was darauf hindeutet, dass der Zugriff auf Variablen und Methoden für das gesamte Paket, das die Klasse enthält, offen ist.

4)protected‘-Modifikator.

Diese Zugriffsebene ist etwas breiter als das Paket. Eine Variable, Methode oder Klasse, die mit dem protected-Modifikator markiert ist, kann von ihrem Paket (wie „Paket“) und von allen geerbten Klassen aus aufgerufen werden.

Diese Tabelle erklärt alles:

Sichtbarkeit Schlüsselwort Zugriff
Deine Klasse Dein Paket Nachfahre Alle Klassen
Private private Ja Nein Nein Nein
Paket (kein Modifikator) Ja Ja Nein Nein
Protected protected Ja Ja Ja Nein
Public public Ja Ja Ja Ja

Diese Tabelle kann man sich ziemlich einfach merken. Stelle dir vor, du schreibst dein Testament. Du teilst alle deine Sachen in vier Kategorien ein. Wer darf deine Sachen benutzen?

Wer hat Zugriff Modifikator Beispiel
Nur ich private Persönliches Tagebuch
Familie (kein Modifikator) Familienfotos
Familie und Erben protected Familienbesitz
Alle public Memoiren

„Das ist so ähnlich, als seien die Klassen im selben Paket Teil einer Familie.“

„Ich will dir auch ein paar interessante Aspekte für das Überschreiben von Methoden mit auf den Weg geben.“

1) Implizite Implementierung einer abstrakten Methode.

Nehmen wir an, du hast den folgenden Code:

Code
class Cat
{
 public String getName()
 {
  return "Oscar";
 }
}

Und du hast beschlossen, eine Tiger-Klasse zu erstellen, die von dieser Klasse erbt, und ein Interface zu der neuen Klasse hinzuzufügen.

Code
class Cat
{
 public String getName()
 {
   return "Oscar";
 }
}
interface HasName
{
 String getName();
 int getWeight();
}
class Tiger extends Cat implements HasName
{
 public int getWeight()
 {
  return 115;
 }

}

Wenn du einfach alle fehlenden Methoden implementierst, die IntelliJ IDEA dir zum Implementieren vorschlägt, wirst du später vielleicht viel Zeit damit verbringen, nach einem Fehler zu suchen.

Es stellt sich heraus, dass die Tiger-Klasse eine von Cat geerbte getName-Methode hat, die als die Implementierung der getName-Methode für das HasName-Interface verwendet wird.

„Das sieht für mich nicht nach einem Problem aus.“

„So schlimm ist es nicht, aber dort können sich schnell Fehler einschleichen.“

Aber es kann noch schlimmer kommen:

Code
interface HasWeight
{
 int getValue();
}
interface HasSize
{
 int getValue();
}
class Tiger extends Cat implements HasWeight, HasSize
{
 public int getValue()
 {
  return 115;
 }
}

Es stellt sich heraus, dass du nicht immer von mehreren Interfaces erben kannst. Genauer gesagt, man kann sie zwar von ihnen erben, aber sie nicht korrekt implementieren. Sieh dir das Beispiel an. Beide Interfaces setzen voraus, dass du die getValue()-Methode implementierst, aber es ist nicht klar, was sie zurückgeben soll: das Gewicht oder die Größe? Es ist ziemlich unangenehm, sich damit herumschlagen zu müssen.

„Ich glaube auch. Du willst eine Methode implementieren, kannst es aber nicht. Du hast bereits eine Methode mit dem gleichen Namen von der Basisklasse geerbt. Es ist kaputt.“

„Aber es gibt gute Neuigkeiten.“

2) Erweiterung der Sichtbarkeit. Wenn du einen Typ erbst, kannst du die Sichtbarkeit einer Methode erweitern. Das sieht dann so aus:“

Java-Code Beschreibung
class Cat
{
 protected String getName()
 {
  return "Oscar";
 }
}
class Tiger extends Cat
{
 public String getName()
 {
  return "Oscar Tiggerman";
 }
}
Wir haben die Sichtbarkeit der Methode von protected zu public erweitert.
Code Warum das „erlaubt“ ist
public static void main(String[] args)
{
 Cat cat = new Cat();
 cat.getName();
}
Alles ist bestens. Hier wissen wir nicht einmal, dass die Sichtbarkeit in einer abstammenden Klasse erweitert wurde.
public static void main(String[] args)
{
 Tiger tiger = new Tiger();
 tiger.getName();
}
Hier rufen wir die Methode auf, deren Sichtbarkeit erweitert wurde.

Wenn das nicht möglich wäre, könnten wir jederzeit eine Methode in Tiger deklarieren:
public String getPublicName()
{
super.getName(); //protected-Methode aufrufen
}

Mit anderen Worten, wir reden nicht über eine Sicherheitsverletzung.

public static void main(String[] args)
{
 Cat catTiger = new Tiger();
 catTiger.getName();
}
Wenn alle Bedingungen für den Aufruf einer Methode in einer Basisklasse (Cat) erfüllt sind, dann sind sie für den Aufruf der Methode mit dem Nachfahren (Tiger) sicher erfüllt. Denn die Einschränkungen beim Methodenaufruf waren schwach, nicht stark.

„Ich weiß nicht, ob ich das richtig verstanden habe, aber ich werde mir merken, dass das möglich ist.“

3) Eingrenzung des Rückgabetyps.

In einer überschriebenen Methode können wir den Rückgabetyp zu einem engeren Referenztyp ändern.

Java-Code Beschreibung
class Cat
{
 public Cat parent;
 public Cat getMyParent()
 {
  return this.parent;
 }
 public void setMyParent(Cat cat)
 {
  this.parent = cat;
 }
}
class Tiger extends Cat
{
 public Tiger getMyParent()
 {
  return (Tiger) this.parent;
 }
}
Wir haben die Methode getMyParent überschrieben und jetzt gibt sie ein Tiger-Objekt zurück.
Code Warum das „erlaubt“ ist
public static void main(String[] args)
{
 Cat parent = new Cat();

 Cat me = new Cat();
 me.setMyParent(parent);
 Cat myParent = me.getMyParent();
}
Alles ist bestens. Hier wissen wir nicht einmal, dass der Rückgabetyp der Methode getMyParent in der abstammenden Klasse erweitert wurde.

Wie der „alte Code“ funktionierte und funktioniert.

public static void main(String[] args)
{
 Tiger parent = new Tiger();

 Tiger me = new Tiger();
 me.setMyParent(parent);
 Tiger myParent = me.getMyParent();
}
Hier rufen wir die Methode auf, deren Rückgabetyp erweitert wurde.

Wenn das nicht möglich wäre, könnten wir jederzeit eine Methode in Tiger deklarieren:
public Tiger getMyTigerParent()
{
return (Tiger) this.parent;
}

Mit anderen Worten, es gibt keine Sicherheitsverletzungen und/oder Typumwandlungsverletzungen.

public static void main(String[] args)
{
 Tiger parent = new Tiger();

 Cat me = new Tiger();
 me.setMyParent(parent);
 Cat myParent = me.getMyParent();
}
Und alles funktioniert hier einwandfrei, obwohl wir den Typ der Variablen auf die Basisklasse (Cat) eingeschränkt haben.

Durch das Überschreiben wird die korrekte setMyParent-Methode aufgerufen.

Und beim Aufruf der getMyParent-Methode gibt es keinen Grund zur Sorge, denn der Rückgabewert der Tiger-Klasse kann auch weiterhin problemlos an die myParent-Variable der Basisklasse (Cat) zugewiesen werden.

Tiger-Objekte können sowohl in Tiger-Variablen als auch in Cat-Variablen sicher gespeichert werden.

„Klar. Alles klar. Wenn man Methoden überschreibt, muss man sich bewusst sein, wie das alles funktioniert, wenn wir unsere Objekte an Code übergeben, der nur die Basisklasse handhaben kann und nichts über unsere Klasse weiß

„So ist es! Dann ist die große Frage, warum können wir den Typ des Rückgabewertes nicht einschränken, wenn wir eine Methode überschreiben?“

„Es ist offensichtlich, dass in diesem Fall der Code in der Basisklasse nicht mehr funktionieren würde:“

Java-Code Erläuterung des Problems
class Cat
{
 public Cat parent;
 public Cat getMyParent()
 {
  return this.parent;
 }
 public void setMyParent(Cat cat)
 {
  this.parent = cat;
 }
}
class Tiger extends Cat
{
 public Object getMyParent()
 {
  if (this.parent != null)
   return this.parent;
  else
   return "I'm an orphan";
 }
}
Wir haben die getMyParent-Methode überladen und den Typ ihres Rückgabewertes eingeschränkt.

Hier ist alles in Ordnung.

public static void main(String[] args)
{
 Tiger parent = new Tiger();

 Cat me = new Tiger();
 Cat myParent = me.getMyParent();
}
Dann wird dieser Code nicht mehr funktionieren.

Die Methode getMyParent kann jede Instanz eines Objektes zurückgeben, da sie tatsächlich mit einem Tiger-Objekt aufgerufen wird.

Und wir haben keine Überprüfung vor der Zuweisung. Daher ist es durchaus möglich, dass die myParent-Variable vom Typ Cat eine String-Referenz speichert.

„Wunderbares Beispiel, Amigo!“

In Java wird vor dem Aufruf einer Methode nicht geprüft, ob das Objekt eine solche Methode besitzt. Alle Überprüfungen erfolgen zur Laufzeit. Und ein [hypothetischer] Aufruf einer nicht vorhandenen Methode würde höchstwahrscheinlich dazu führen, dass das Programm versucht, nicht existierenden Bytecode auszuführen. Dies würde letztendlich zu einem fatalen Fehler führen, und das Betriebssystem würde das Programm zwangsweise schließen.

„Oh. Jetzt weiß ich es.“