2.09 Unveränderliche Attribute und Konstanten

Einführung

Manchmal kann es sinnvoll sein, ein Attribut komplett vor Veränderungen zu schützen – noch stärker als nur mit dem Verbergen durch das Schlüsselwort private. Zu diesem Zweck gibt es die Möglichkeit, ein Attribut als final zu deklarieren.

Zur Veranschaulichung greifen eine einfache und angepasste Version der Klasse Mitglieder auf, die wir bereits bei der Einführung der statischen Attribute kennengelernt haben:

public class Mitglied {
  
  // Klassenattribut
  private static int bisherigeMitglieder = 0;
  
  // unveränderliche Attribute
  private final int mitgliedsnummer;
  private final String vorname;
  
  // gewöhnliches Attribut
  private String nachname;
  
  // Konstruktor
  public Mitglied(String pVorname, String pNachname){
    
    bisherigeMitglieder = bisherigeMitglieder + 1;
    mitgliedsnummer = bisherigeMitglieder;
    
    vorname = pVorname;
    nachname = pNachname;
    
  }

  // Getter
  public String getVorname() {
    return vorname;
  }

  public String getNachname() {
    return nachname;
  }	
    
  public int getMitgliedsnummer() {
    return mitgliedsnummer;
  }

  // Ausgabe
  public String toString(){
    return vorname +" "+nachname +", "+mitgliedsnummer;
  }
  
  // Setter
  public void setMitgliedsnummer(int pNummer){
    mitgliedsnummer = pNummer;
    // Fehler: The final field Mitglied.mitgliedsnummer cannot be assigned.
  }
  
  public void setVorname(String pVorname){
    vorname = pVorname;
    // Fehler: The final field Mitglied.vorname cannot be assigned.
  }
  
  public void setNachname(String pNachname){
    nachname = pNachname;
    // Alles OK!
  }

}

In dieser Klasse sind die beiden Attribute mitgliedsnummer und vorname als final deklariert. Das führt dazu, dass sie nach dem Aufruf des Konstruktors nicht mehr verändert werden können. Ein Mitglied behält also für alle Zeiten dieselbe Mitgliedsnummer und kann auch seinen Vornamen nicht verändern. Der Nachname hingegen kann durchaus noch verändert werden – z.B. wegen einer Heirat.

Dass mitgliedsnummer und vorname unveränderlich sind, sehen wir auch an den Fehlermeldungen bei den set-Methoden, die versuchen, die Werte zu verändern. Nur die set-Methode für den Vornamen kann tatsächlich dem Attribut einen neuen String zuweisen.

Noch strenger als bei der Deklaration als privates Attribut, bei denen ein Zugriff von außen unterbunden wird, kann bei einem finalem Attribut noch nicht einmal mehr die Klasse bzw. eine Instanz selbst den Wert verändern.

Da die beiden set-Methoden zu Fehlern führen, sollten wir sie natürlich entfernen:

public class Mitglied {
  
  // Klassenattribut
  private static int bisherigeMitglieder = 0;
  
  // unveränderliche Attribute
  private final int mitgliedsnummer;
  private final String vorname;
  
  // gewöhnliches Attribut
  private String nachname;
  
  // Konstruktor
  public Mitglied(String pVorname, String pNachname){
    
    bisherigeMitglieder = bisherigeMitglieder + 1;
    mitgliedsnummer = bisherigeMitglieder;
    
    vorname = pVorname;
    nachname = pNachname;
    
  }

  // Getter
  public String getVorname() {
    return vorname;
  }

  public String getNachname() {
    return nachname;
  }	
    
  public int getMitgliedsnummer() {
    return mitgliedsnummer;
  }

  // Ausgabe
  public String toString(){
    return vorname +" "+nachname +", "+mitgliedsnummer;
  }
  
  // Setter
  public void setNachname(String pNachname){
    nachname = pNachname;
  }

}

Unveränderliche Referenzen

Beim Umgang mit final müssen wir zwischen primitiven Datentypen und Referenztypen unterscheiden:

  • Bei Attributen, die von einem primitiven Datentyp sind, kann der Wert nicht verändert werden.
  • Bei Attributen, die von einem Referenztyp sind, kann die Referenz nicht verändert werden. Das referenzierte Objekt kann möglicherweise aber doch verändert werden.

Das klingt im ersten Moment vielleicht etwas verwirrend. Daher kommen wir direkt zu einem Beispiel. Wir greifen die Idee der Mitglieder eines Vereins auf und modellieren damit nun den Vorstand des Vereins. Es gibt in diesem Verein einen Vereinsleiter und zwei Vertreter:

public class Vorsitz {

    // Unveränderliche Attribute
    private final Mitglied vereinstleiter;    
    private final Mitglied[] vertreter = new Mitglied[2];
    
    // Konstruktor
    public Vorsitz(Mitglied pLeiter, Mitglied pErster, Mitglied pZweiter){
        vereinstleiter = pLeiter;

        vertreter[0] = pErster;
        vertreter[1] = pZweiter;
    }

    // Getter
    public Mitglied getVereinstleiter() {
        return vereinstleiter;
    }

    public Mitglied getErsterVertreter() {
        return vertreter[0];
    }    

    public Mitglied getZweiterVertreter() {
        return vertreter[1];
    }    

    // Setter
    public void setErsterVertreter(Mitglied pNeuer){
        vertreter[0]  = pNeuer;
    }    

    public void setZweiterVertreter(Mitglied pNeuer){
        vertreter[1]  = pNeuer;
    }

}

Um deutlich zu machen, welche Auswirkungen die Verwendung von unveränderlichen Attributen hat, sind hier sowohl der Vereinsleiter als auch das Array der Vertreter als final deklariert.

Da das Attribut vereinsleiter unveränderlich ist, kann seine Referenz nach Aufruf des Konstruktors nicht mehr verändert werden. D.h., das Objekt, das es referenziert, wird von da ab dasselbe bleiben. Im Sachzusammenhang bedeutet das, dass der Vorsitz einen einmal festgelegten Vereinsleiter für immer behalten wird. Aber Vorsicht! Das bedeutet nicht, dass die Eigenschaften des Vereinsleiters sich nicht ändern können! Sehen wir uns ein Beispiel an:

public class MitgliedTest {

  public static void main(String[] args) {
    
    Mitglied erster = new Mitglied ("Anton", "A.");
    Mitglied zweiter = new Mitglied ("Berta", "B.");
    
    Vorsitz meinVorsitz = new Vorsitz(new Mitglied("Doris", "D."), erster, zweiter);
    
    System.out.println(meinVorsitz.getVereinstleiter());
    
    meinVorsitz.getVereinstleiter().setNachname("E.");
    
    System.out.println(meinVorsitz.getVereinstleiter());

  }

}

Im Konstruktor wird zunächst Doris D. als Vereinsleiterin festgelegt. Mit der Anweisung meinVorsitz.getVereinstleiter().setNachname("E."); wird jedoch ihr Nachname zu „E.“ geändert. In der Tat erhalten wir diese beiden Ausgaben:

Doris D., 3
Doris E., 3

Da aber die Attribute vorname und mitgliedsnummer der Klasse Mitglied unveränderlich sind, wird die Vereinsleiterin in diesem Beispiel immer den Vornamen Doris und die Mitgliedsnummer 3 behalten.

Nun ist noch zu klären, was es bedeutet, dass das Array vertreter in der Klasse Vorstand ebenfalls als final deklariert ist. Auch hier wird dadurch nach Aufruf des Konstruktors ein festes Objekt – in diesem Fall ein Array der Länge 2, welches Objekte der Klasse Mitglied speichert – referenziert. Obwohl das Array also immer dasselbe bleibt, kann sein Inhalt verändert werden. Auch dazu ein Beispiel:

public class MitgliedTest {

  public static void main(String[] args) {
    
    Mitglied erster = new Mitglied ("Anton", "A.");
    Mitglied zweiter = new Mitglied ("Berta", "B.");
    
    Vorsitz meinVorsitz = new Vorsitz(new Mitglied("Doris", "D."), erster, zweiter);
    
    System.out.println(meinVorsitz.getErsterVertreter());
    System.out.println(meinVorsitz.getZweiterVertreter());
    
    meinVorsitz.setErsterVertreter(new Mitglied("Emil", "E."));
    meinVorsitz.setZweiterVertreter(new Mitglied("Friedrich", "F."));;
    
    System.out.println(meinVorsitz.getErsterVertreter());
    System.out.println(meinVorsitz.getZweiterVertreter());

  }

}

Wir erhalten diese Ausgabe:

Anton A., 1
Berta B., 2
Emil E., 4
Friedrich F., 5

Konstanten

Ein Attribut, das sowohl als final als auch als static deklariert ist, nennt man eine Konstante. Eine Konstante ist also einerseits unveränderlich und andererseits nicht an ein Objekt gebunden. Ein prominentes Beispiel ist die Konstante PI der Klasse Math:

public class Konstante {

  public static void main(String[] args) {
    
    System.out.println(Math.PI);
    
  }

}

Konstanten werden üblicherweise in Großbuchstaben geschrieben. Besteht der Name aus mehreren Worten, werden diese mit einem Unterstrich abgetrennt.

Zum Beispiel könnten wir mit Konstanten in unserer Klasse Mitglied angeben, dass die kleinste zu vergebene Mitgliedsnummer 1 ist und die größte 1000 sein soll. Gibt es bereits 1000 Mitglieder, liefert der Konstruktor keine vernünftigen Instanzen mehr:

public class Mitglied {
  
  // Konstanten
  public static final int MITGLIEDSNUMMER_MIN = 1;
  public static final int MITGLIEDSNUMMER_MAX = 1000;
  
  // Klassenattribut
  private static int bisherigeMitglieder = 0;
  
  // unveränderliche Attribute
  private final int mitgliedsnummer;
  private final String vorname;
  
  // gewöhnliches Attribut
  private String nachname;
  
  // Konstruktor
  public Mitglied(String pVorname, String pNachname){
    
    if(bisherigeMitglieder < MITGLIEDSNUMMER_MAX){
    
    bisherigeMitglieder = bisherigeMitglieder + 1;
    mitgliedsnummer = bisherigeMitglieder;
    
    vorname = pVorname;
    nachname = pNachname;
    }else{
      vorname = null;
      nachname = null;
      mitgliedsnummer = 0;
    }
    
  }

  // Getter
  public String getVorname() {
    return vorname;
  }

  public String getNachname() {
    return nachname;
  }	
    
  public int getMitgliedsnummer() {
    return mitgliedsnummer;
  }

  // Ausgabe
  public String toString(){
    return vorname +" "+nachname +", "+mitgliedsnummer;
  }
  
  // Setter
  public void setNachname(String pNachname){
    nachname = pNachname;
  }

}

Wir können diese Konstanten von außen jederzeit abfragen:

public class MitgliedTest {

  public static void main(String[] args) {

    System.out.println(Mitglied.MITGLIEDSNUMMER_MIN);
    System.out.println(Mitglied.MITGLIEDSNUMMER_MAX);

  }

}

Strings sind unveränderlich

Vielleicht wunderst Du Dich, warum oben behauptet wurde, dass der Vorname eines Mitglieds nicht mehr verändert werden kann. Schließlich ist doch vorname eine Referenz auf ein Objekt der Klasse String, so dass zwar diese Referenz nicht verändert werden kann, aber doch immer noch den String, oder? Könnten wir nicht zum Beispiel einfach den Vornamen erweitern, indem wir den String durch + mit einem anderen verlängern?

Nein, das können wir nicht. Der Grund ist, dass Strings unveränderliche Objekte sind. Es gibt zwar  Methoden, die den Anschein erwecken, man könne einen String verändern, aber in Wahrheit liefern diese stets ein neues Objekt der Klasse String. Hier ein Beispiel:

public class StringTest {

  public static void main(String[] args) {

    String ersterString = "eins";
    
    String test = ersterString;
    
    ersterString = "m" + ersterString;
    
    System.out.println(ersterString);
    System.out.println(test);
  }

}

Hier referenzieren zunächst ersterString und test dasselbe String-Objekt. Danach wird vor ersterString ein „m“ hinzugefügt. Würde das Objekt einfach verändert werden, müsste sich diese Änderung sowohl auf ersterString als auch auf test auswirken, so wie wir es beispielsweise von Arrays kennen. Das String-Objekt bleibt allerdings gleich. Sattdessen wird in dieser Zeile ein neues String-Objekt mit Inhalt „meins“ erstellt und der Variablen test zugewiesen. Dadurch referenzieren schließlich ersterString und test nicht mehr dasselbe Objekt.