2.05 Statische Attribute und Methoden


Wiedergabe stellt eine Verbindung zu YouTube her.

Statische Attribute

Bisher haben wir in allen Beispielen zur objektorientierten Programmierung nur mit sogenannten Instanzattributen (auch „Objektattribute“ genannt) gearbeitet. Das bedeutet, jede Instanz einer Klasse besitzt seine eigenen Attribute mit eigenen Werten.

Führen wir uns das mit einem Beispiel nochmal vor Augen. Stellen wir uns dazu vor, wir möchten für einen kleinen Verein eine Mitgliederverwaltung erstellen. Dazu starten wir mit dieser sehr einfachen Klasse:

public class Mitglied {
  
  private String vorname;
  private String nachname;
  
  public Mitglied(String pVorname, String pNachname){
    vorname = pVorname;
    nachname = pNachname;		
  }

  public String getVorname() {
    return vorname;
  }

  public String getNachname() {
    return nachname;
  }	
    
  public String toString(){
    return vorname +" "+nachname;
  }
  

}

Jedes Mitglied hat also einen Vor- und einen Nachnamen. Oder objektorientiert formuliert: Jede Instanz der Klasse Mitglied besitzt jeweils ein Attribut vorname und nachname. Dies sind also wieder Beispiele für Instanzattribute.

Nehmen wir an, wir möchten gerne zählen, wie viele Mitglieder unser Verein im Laufe der Zeit hatte. Dazu soll jedes Mitglied eine Mitgliedsnummer erhalten. Auf diese Weise werden die Vereinsmitglieder also einfach fortlaufend durchnummeriert.

Wie können wir diese Nummerierung erreichen? Hier sind natürlich verschiedene Lösungen denkbar. Wir könnten zum Beispiel eine weitere Klasse Verein einführen, die die Mitglieder verwaltet und das Zählen übernimmt. In Wahrheit ist diese Situation aber ideal dafür, ein Klassenattribut zu verwenden.

Im Gegensatz zu den Instanzattributen existiert ein Klassenattribut nur ein einziges Mal. Anders ausgedrückt: Alle Instanzen einer Klasse teilen sich dieses gemeinsame Attribut. Sehen wir uns die angepasste Version der Klasse Mitglied an:

public class Mitglied {
  
  private static int bisherigeMitglieder = 0;
  
  private int mitgliedsnummer;
  private String vorname;
  private String nachname;
  
  public Mitglied(String pVorname, String pNachname){
    
    bisherigeMitglieder = bisherigeMitglieder + 1;
    mitgliedsnummer = bisherigeMitglieder;
    
    vorname = pVorname;
    nachname = pNachname;
    
  }

  public String getVorname() {
    return vorname;
  }

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

  public String toString(){
    return vorname +" "+nachname +", "+mitgliedsnummer;
  }
  

}

Hier wurde das Klassenattribut bisherigeMitglieder eingeführt. Das Schlüsselwort static sorgt dafür, dass es ein Klassenattribut ist. Daher bezeichnet man Klassenattribute auch als statische Attribute.

Zu Beginn hat dieses Atribut den Wert 0. Bei Aufruf des Konstruktors wird sein Wert um 1 erhöht. Dadurch wird mitgezählt, wie oft schon eine Instanz der Klasse Mitglied erstellt wurde. Das Attribut mitgliedsnummer ist (wie gewohnt) ein Instanzattribut. Ein neu erstelltes Mitglied erhält als Mitgliednummer den aktuellen Wert von bisherigeMitglieder. Bei der toString()-Methode wurde das Attribut mitgliedsnummer hinzugefügt.

Deutlicher wird dies, wenn wir diese Klasse in einem Beispiel testen:

public class MitgliedTest {

  public static void main(String[] args) {
    
    Mitglied erster = new Mitglied ("Anton", "A.");
    Mitglied zweiter = new Mitglied ("Berta", "B.");
    Mitglied dritter = new Mitglied ("Caesar", "C.");
    
    System.out.println(erster);
    System.out.println(zweiter);
    System.out.println(dritter);
    
  }

}

Wir erhalten hier folgende Ausgabe:

Anton A., 1
Berta B, 2
Caesar C., 3

In der Tat werden also nun vollkommen automatisch die Mitgliedsnummern vergeben. Die Klasse Mitglied übernimmt dies alles von selbst.

Vielleicht ist Dir aufgefallen, dass das Attribut bisherigeMitglieder wie die anderen auch ein privates Attribut ist. Dadurch kann es von außen nicht manipuliert werden und so möglicherweise die Zählung durcheinander geraten. Außerdem kann man auch argumentieren, dass es einem Anwender gleichgültig sein kann, wie die Nummerierung der Mitglieder gelöst wurde, so dass er dieses Attribut von außen nicht sehen muss.

Vielleicht möchten wir es aber dennoch einem Benutzer ermöglichen, herauszufinden, wie viele Mitglieder es bisher gibt, d.h., welchen Wert das statische Attribut momentan hat. Hier benötigen wir einen Getter, der konsequenterweise auch statisch sein sollte, womit wir beim nächsten Abschnitt ankommen.

Statische Methoden

Einführung

So wie wir zuvor nur Instanzattribute programmiert hatten, sind auch alle bisher erstellten Methoden Instanzmethoden (oder auch „Objektmethoden“). D.h., es gibt immer ein Objekt, das die Methode ausführt. Das einfachste Beispiel ist die Methode getName() unserer Klasse Mitglied. Wenn wir diese Methode aufrufen, benötigen wir natürlich ein Objekt. Welchen Namen sollten wir hier sonst erfahren wollen?

Wir könnten nun ebenso eine get-Methode für unser statisches Attribut hinzufügen

public int getBisherigeMitglieder() {
  return bisherigeMitglieder;
}

und diese dann mithilfe eines Objektes aufrufen:

public class MitgliedTest {

  public static void main(String[] args) {
    
    Mitglied erster = new Mitglied ("Anton", "A.");
    Mitglied zweiter = new Mitglied ("Berta", "B.");
    
    System.out.println(erster.getBisherigeMitglieder());		

  }

}

Es wird hier korrekt eine 2 ausgegeben.

Da wir hier aber den Wert eines statischen Attribut abfragen dessen Wert ja nicht an ein spezielles Objekt gebunden ist, scheint es unnatürlich, ein Objekt zu verwenden, um diese Methode aufzurufen. Außerdem könnten wir dies auch gar nicht, falls es bisher noch gar kein Objekt dieser Klasse gibt.

Eleganter ist es daher, diese get-Methode ebenfalls als statisch zu kennzeichnen und damit zu einer Klassenmethode zu machen:

public static int getBisherigeMitglieder() {
  return bisherigeMitglieder;
}

Der Aufruf sieht nun etwas ungewohnt aus:

public class MitgliedTest {

  public static void main(String[] args) {
    
    Mitglied erster = new Mitglied ("Anton", "A.");
    Mitglied zweiter = new Mitglied ("Berta", "B.");
    
    System.out.println(Mitglied.getBisherigeMitglieder());		

  }

}

Statt ein Objekt mit der Ausführung der Methode zu beauftragen, wird nun die Klasse selbst beauftragt. Dies funktioniert tatsächlich auch, wenn es noch gar kein Objekt gibt. Dieser kleine Test

public class MitgliedTest {

  public static void main(String[] args) {
    
    System.out.println(Mitglied.getBisherigeMitglieder());		

  }

}

liefert die Ausgabe 0.

Kommen wir zu noch einem Beispiel. Der Klasse Mitglied können wir noch diese statische Methode hinzufügen:

static boolean nummerIstVergeben(int pNummer){
  return (pNummer <= bisherigeMitglieder && pNummer > 0);
}

Mit dieser können wir erfragen, ob eine Nummer bereits als Mitgliedsnummer vergeben wurde. Hier haben wir einen Test:

public class MitgliedTest {

  public static void main(String[] args) {
    
    Mitglied erster = new Mitglied ("Anton", "A.");
    Mitglied zweiter = new Mitglied ("Berta", "B.");
    Mitglied dritter = new Mitglied ("Casear", "C.");
    
    System.out.println(Mitglied.nummerIstVergeben(2));		
    System.out.println(Mitglied.nummerIstVergeben(6));		

  }

}

Wir erhalten wie erwartet diese Ausgabe:

true
false

Anwendung von statischen Methoden in der Bruchrechnung


Wiedergabe stellt eine Verbindung zu YouTube her.

Wir knüpfen wir nahtlos an unser Beispiel der Bruchrechnung an, da dieses noch einige Möglichkeiten zur Vertiefung bietet.

Alle Methoden, die wir bis jetzt in der Klasse Bruch programmiert haben, sind Objektmethoden. D.h., wie oben erklärt, dass es immer ein Objekt gibt, dass eine solche Methode ausführt. Beispielsweise kann die Methode kuerze() nur von einem Bruch ausgeführt werden. Gibt es keinen Bruch, ergibt es keinen Sinn, diese Methode starten zu wollen. Außerdem verändert die Methode den Bruch, falls er nicht schon gekürzt ist.

Vielleicht möchten wir aber auch einmal eine Rechnung anstellen ohne dabei einen Bruch zu verändern. In diesem Fall bieten sich wieder Klassenmethoden an. Um einen Eindruck davon zu gewinnen, inwiefern nun Klassenmethoden für Berechnungen verwendet werden sollen, sehen wir uns als Beispiele einige Methoden der in Java mitgelieferten Klasse Math an:

public class Klassenmethoden {

  public static void main(String[] args) {
    
    System.out.println( Math.sin(1.5) );
    
    System.out.println( Math.pow(2, 5) );
    
    System.out.println( Math.sqrt(81) );		
    
  }

}

Die Methode sin berechnet den Sinus, wobei der Parameter im Bogenmaß übergeben werden muss. In dem Beispiel sehen wir auch noch die Methode pow zum Berechnen von Potenzen – in diesem Fall 25 – und die Methode sqrt zur Berechnung von Wurzeln („square root“).

All diese Methoden benötigen kein Objekt, von dem sie ausgeführt werden und sie sollen auch kein Objekt verändern.

Eine weitere Klasse mit statischen Methoden ist die Klasse Integer. Diese besitzt zum Beispiel die Methode parseInt(String pText), die aus einem String einen int-Wert liefert. Der String bleibt dabei aber unverändert. Hier ein Beispiel:

public class Klassenemethoden {

  public static void main(String[] args) {
    
    String erster = "12";	
    String zweiter = "34";
    
    System.out.println(erster+zweiter);
    
    int ersteZahl = Integer.parseInt(erster);
    int zweiteZahl = Integer.parseInt(zweiter);
    
    System.out.println(ersteZahl+zweiteZahl);
    
  }

}

Es werden hier aus zwei Strings zwei int-Werte erstellt, in Variablen gespeichert und anschließend wird ihre Summe ausgegeben. Die beiden ursprünglichen Strings bleiben dabei gleich.

Wir wollen nun auch solche Klassenmethoden der Klasse Bruch hinzufügen. Als Beispiel erstellen wir eine Methode, die das Produkt zweier Brüche bestimmt, aber die beiden Brüche nicht verändert.

Da es sich um eine Klassenmethode handeln soll, wird sie als static deklariert:

public static Bruch produkt(Bruch pErster, Bruch pZweiter){
  Bruch ergebnis = new Bruch(pErster.getZaehler()*pZweiter.getZaehler(), pErster.getNenner()*pZweiter.getNenner());
  ergebnis.kuerze();

  return ergebnis;
}

Anders als die schon früher erstellte Objektmethode zur Multiplikation liefert diese hier einen Rückgabewert des Typs Bruch. Es wird hier also mithilfe der zwei gegebenen Brüche ein neuer Bruch erstellt und als Ergebnis geliefert.

Zur Verdeutlichung stellen wir noch einmal die beiden Methoden gegenüber. Links die Klassenmethode, rechts die „alte“ Objektmethode:

public static Bruch produkt(Bruch pErster, Bruch pZweiter){
  Bruch ergebnis = new Bruch(pErster.getZaehler()*pZweiter.getZaehler(), pErster.getNenner()*pZweiter.getNenner());
  ergebnis.kuerze();

  return ergebnis;
}
public void multipliziere(Bruch pBruch){
  zaehler = zaehler*pBruch.getZaehler();
  nenner = nenner*pBruch.getNenner();
  kuerze();
}

So können wir die Klassenmethode verwenden:

public class Bruchtest {

  public static void main(String[] args) {
    
    Bruch meinBruch = new Bruch(1,2);		
    Bruch deinBruch = new Bruch(1,3);

    System.out.println(Bruch.produkt(meinBruch, deinBruch));
    
    System.out.println(meinBruch);
    System.out.println(deinBruch);
    
  }

}

Zur Kontrolle werden hier am Ende die beiden gegebenen Brüche auch noch einmal ausgegeben.

In derselbe Weise können wir für die restlichen Grundrechenarten Klassenmethoden erstellen. Unsere Klasse Bruch sieht dann wie folgt aus:

public class Bruch {

  // Attribute
  private int zaehler;
  private int nenner;

  // Kontruktor
  public Bruch(int pZaehler, int pNenner){
    if (pNenner == 0){
      System.out.println("Der Nenner darf nicht 0 sein.");
      System.out.println("Setze den Bruch auf 0.");
      zaehler = 0;
      nenner = 1;
    }else{
      zaehler = pZaehler;
      nenner = pNenner;
    }

    if (nenner < 0){
      zaehler = -zaehler;
      nenner = -nenner;
    }
  }

  // Methoden zur Manipulation des Objektes
  public void kuerze(){
    int faktor = ggT();
    zaehler = zaehler / faktor;
    nenner = nenner / faktor;
    if (nenner < 0){
      zaehler = -zaehler;
      nenner = -nenner;
    }
  }

  public void erweitere(int pFaktor){
    if (pFaktor != 0){
      zaehler = zaehler*pFaktor;
      nenner = nenner*pFaktor;
      if (nenner < 0){
        zaehler = -zaehler;
        nenner = -nenner;
      }
    }
  }

  public void multipliziere(Bruch pBruch){
    zaehler = zaehler*pBruch.getZaehler();
    nenner = nenner*pBruch.getNenner();
    kuerze();
  }

  public void dividiere(Bruch pBruch){
    if (!pBruch.istNull()){
      nenner = nenner*pBruch.getZaehler();
      zaehler = zaehler*pBruch.getNenner();
    }else{
      System.out.println("Durch 0 darf man nicht teilen!");
    }
    kuerze();
  }

  public void addiere(Bruch pBruch){
    zaehler = zaehler*pBruch.getNenner() + nenner*pBruch.getZaehler();
    nenner = nenner * pBruch.nenner;
    kuerze();
  }

  public void subtrahiere(Bruch pBruch){
    zaehler = zaehler*pBruch.getNenner() - nenner*pBruch.getZaehler();
    nenner = nenner * pBruch.nenner;
    kuerze();
  }

  // get-Methoden
  public int getZaehler() {
    return zaehler;
  }

  public int getNenner() {
    return nenner;
  }	

  public boolean istNull(){
    if (zaehler == 0){
      return true;
    }else{
      return false;
    }
  }

  // Ausgabe
  public String toString(){
    return zaehler +" / "+nenner;
  }

  // private Methode
  private int ggT(){

    int a = zaehler;
    int b = nenner;
    int h;

    while(b != 0){
      h = a%b;
      a = b;
      b = h;
    }

    return a;

  }

  // Vergleich
  public boolean equals(Object pVergleich){
    if (pVergleich == null){
      return false;
    }

    if (! (pVergleich instanceof Bruch)){
      return false;
    }

    if (pVergleich == this){
      return true;
    }

    Bruch hilfsobjekt = (Bruch) pVergleich;

    if (zaehler*hilfsobjekt.getNenner() == nenner*hilfsobjekt.getZaehler()){
      return true;
    }else{
      return false;
    }

  }

  // Klassenmethoden
  public static Bruch summe(Bruch pErster, Bruch pZweiter){
    Bruch ergebnis = new Bruch(pErster.getZaehler()*pZweiter.getNenner()+pErster.getNenner()*pZweiter.getZaehler(), pErster.nenner*pZweiter.nenner);
    ergebnis.kuerze();

    return ergebnis;
  }

  public static Bruch differenz(Bruch pErster, Bruch pZweiter){
    Bruch ergebnis = new Bruch(pErster.getZaehler()*pZweiter.getNenner()-pErster.getNenner()*pZweiter.getZaehler(), pErster.nenner*pZweiter.nenner);
    ergebnis.kuerze();

    return ergebnis;
  }

  public static Bruch produkt(Bruch pErster, Bruch pZweiter){
    Bruch ergebnis = new Bruch(pErster.getZaehler()*pZweiter.getZaehler(), pErster.getNenner()*pZweiter.getNenner());
    ergebnis.kuerze();

    return ergebnis;
  }

  public static Bruch quotient(Bruch pErster, Bruch pZweiter){
    Bruch ergebnis = new Bruch(pErster.getZaehler()*pZweiter.getNenner(), pErster.getNenner()*pZweiter.getZaehler());
    ergebnis.kuerze();

    return ergebnis;
  }

}