Klassenattribute und Konstanten
„Wie viele Konten hat diese Bank bisher eröffnet?"
Diese Frage kann kein einzelnes Konto beantworten. Jedes Konto kennt nur sich selbst – seinen Besitzer und seinen Kontostand. Die Zahl aller Konten gehört zu keinem von ihnen. Sie gehört zur Klasse.
Ein Wert für die ganze Klasse
Ein Objektattribut gibt es einmal pro Objekt. Jedes Objekt hat seinen eigenen Wert.
Ein Klassenattribut gibt es genau einmal, unabhängig davon, wie viele Objekte existieren. Alle Objekte sehen denselben Wert. Gekennzeichnet wird es mit dem Schlüsselwort static.
private static int anzahlgehört zur Klasse. Der Konstruktor erhöht ihn bei jedem neuen Kunden – und weil es ihn nur einmal gibt, zählt er über alle Objekte hinweg.private int nummergehört zum Objekt. Der Konstruktor kopiert den aktuellen Zählerstand hinein. Ändert sich der Zähler später, bleibt die einmal vergebene Nummer, wie sie ist.public static int getAnzahl()ist eine Klassenmethode. Sie wird über den Klassennamen aufgerufen:Kunde.getAnzahl(). Ein Objekt braucht sie nicht – und darf deshalb auch keins voraussetzen.
Die Faustregel: Beantworte die Frage „Wovon gibt es das einmal – pro Objekt oder pro Klasse?" Erst danach schreibst du static hin oder nicht.
Eine Klassenmethode darf nicht auf Objektattribute zugreifen. Das ist keine Schikane, sondern Logik: Kunde.getAnzahl() wird ohne jedes Objekt aufgerufen – wessen name sollte sie dann lesen?
Umgekehrt geht es sehr wohl: Eine Objektmethode darf Klassenattribute lesen und ändern.
Konstanten
Manche Werte ändern sich nie: die Zahl π, die Höchstpunktzahl einer Arbeit, der größte erlaubte Radius. Für sie gibt es final.
finalbedeutet: Der Wert kann nach der Zuweisung nicht mehr geändert werden. Der Versuch ist schon ein Übersetzungsfehler.staticbedeutet: Der Wert gehört zur Klasse.- Zusammen ergibt das eine Konstante. Sie wird
GROSS_MIT_UNTERSTRICHgeschrieben und über den Klassennamen angesprochen:Kreis.MAX_RADIUS.
Konstanten sind kein Selbstzweck. Sie geben einer Zahl einen Namen – und damit eine Erklärung:
if (pRadius > MAX_RADIUS) // sagt, warum
if (pRadius > 1000.0) // sagt nur, was
Und sie stehen an einer Stelle. Wer den Höchstwert ändern will, ändert eine Zeile statt sieben – und übersieht keine.
Die Online-IDE schreibt eine Kommazahl ohne Nachkommastellen ohne das .0: Aus 100.0 wird in der Ausgabe 100. In echtem Java stünde dort 100.0. Am Rechenergebnis ändert das nichts.
Im Implementationsdiagramm
classDiagram
class Kunde {
-anzahl: int$
-name: String
-nummer: int
+Kunde(pName: String)
+getName() String
+getNummer() int
+getAnzahl() int$
}
Damit ist die Notation aus 1.1 vollständig:
| Schreibweise | Bedeutung |
|---|---|
-, #, + |
private, protected, public |
| unterstrichen | Klassenattribut oder Klassenmethode (static) |
GROSS_MIT_UNTERSTRICH |
eine Konstante (final) |
Ein Wert, der beides ist – unterstrichen und in Großbuchstaben –, ist eine Konstante der Klasse. Genau das ist der Normalfall: Fast jede Konstante ist static final.
Teil 1: Lesen
Aufgabe 1: Wer zählt was?
Ohne Rechner. Lies die beiden Dateien und sag die vier Ausgabezeilen voraus. Führ eine Tabelle mit einer Spalte für anzahl und je einer für die nummer der drei Objekte.
void main() {
IO.println("A: " + Kunde.getAnzahl());
Kunde ada = new Kunde("Ada");
Kunde alan = new Kunde("Alan");
IO.println("B: " + Kunde.getAnzahl());
IO.println("C: " + ada.getNummer() + " " + alan.getNummer());
Kunde grace = new Kunde("Grace");
IO.println("D: " + Kunde.getAnzahl() + " " + grace.getNummer() + " " + ada.getNummer());
}
public class Kunde {
private static int anzahl = 0;
private String name;
private int nummer;
public Kunde(String pName) {
name = pName;
anzahl = anzahl + 1;
nummer = anzahl;
}
public int getNummer() {
return nummer;
}
public static int getAnzahl() {
return anzahl;
}
}
a) Welche vier Zeilen gibt das Programm aus?
b) In Zeile D steht ada.getNummer() – und liefert nicht dasselbe wie Kunde.getAnzahl(), obwohl der Konstruktor die beiden gleichgesetzt hat. Erkläre, warum.
c) Jemand schreibt versehentlich private static int nummer; statt private int nummer;. Was gibt das Programm dann in den Zeilen C und D aus?
d) Wozu ist die Nummer, die dieser Konstruktor vergibt, brauchbar – und wozu nicht? Denk an einen Kunden, der gelöscht wird.
Prüf deine Vorhersage erst danach im Programmierbereich nach. :::
Tipp: die Zuweisung ist eine Kopie
nummer = anzahl; kopiert den Wert. Danach hat die beiden nichts mehr miteinander zu tun – so wie bei
int a = 5;
int b = a;
a = 99; // b bleibt 5
Aufgabe 2: Objekt, Klasse oder Konstante?
Ohne Rechner. Ordne jede Angabe einer der drei Spalten zu: Objektattribut, Klassenattribut oder Konstante. Begründe die Zuordnung jeweils mit der Frage „Wovon gibt es das wie oft, und ändert es sich?"
a) der Name einer Kundin
b) die Anzahl der bisher eröffneten Konten einer Bank
c) die Kreiszahl π
d) der Kontostand
e) der Zinssatz, den die Bank für alle Sparkonten festlegt und einmal im Jahr anpasst
f) die höchste je erreichte Punktzahl in einem Spiel
g) die Anzahl der Leben, mit denen jede Spielfigur startet
h) die Position einer Spielfigur
Teil 2: Schreiben
Aufgabe 3: Ein Rechteck, das mitzählt
Erweitere das Rechteck aus 1.1 um alles, was im Diagramm unterstrichen oder groß geschrieben ist, bis alle Tests grün sind.
classDiagram
class Rechteck {
-MIN_SEITE: double$
-erzeugt: int$
-breite: double
-hoehe: double
+Rechteck(pBreite: double, pHoehe: double)
+getBreite() double
+getHoehe() double
+flaeche() double
+getErzeugt() int$
+groesseres(pA: Rechteck, pB: Rechteck) Rechteck$
}
Dazu, was das Diagramm nicht sagen kann:
MIN_SEITEhat den Wert 1.0. Seiten darunter setzt der Konstruktor aufMIN_SEITE.erzeugtzählt, wie viele Rechtecke bisher erzeugt wurden.groesseresliefert von zwei Rechtecken das mit der größeren Fläche; bei Gleichstand das erste.
Tipp 1: Warum steht im Test kein einziges Objekt vor getErzeugt?
Weil getErzeugt eine Klassenmethode ist: Rechteck.getErzeugt(). Deshalb steht im Kopf static – das gibt das Gerüst schon vor, ebenso bei groesseres.
Zu tun bleibt: die beiden unterstrichenen Einträge aus dem Diagramm anlegen (MIN_SEITE und erzeugt) und die beiden Rümpfe füllen.
Tipp 2: Wo wird gezählt?
An der einen Stelle, die bei jedem neuen Rechteck durchlaufen wird: im Konstruktor. Und zwar unabhängig davon, ob die Seiten gedeckelt wurden oder nicht – der Test testZaehlerZaehltAuchDieKleinen prüft genau das.
Tipp 3: Warum zählen die Tests relativ?
int vorher = Rechteck.getErzeugt(); – und geprüft wird vorher + 2. Der Zähler gehört zur Klasse und läuft über alle Tests hinweg weiter. Ein Test, der assertEquals(2, ...) schriebe, wäre nur grün, wenn er als erster liefe.
Das ist eine Eigenschaft von Klassenattributen, die man kennen sollte: Sie machen Tests voneinander abhängig.
Zum Weiterdenken
Vertiefung 1: Was final wirklich schützt
final heißt „unveränderlich". Der folgende Programmbereich zeigt, dass das nicht ganz stimmt.
a) Sag voraus, was die drei Zeilen ausgeben.
b) Führ das Programm aus. Was ist passiert – und wieso ist STUFEN trotzdem final?
c) Formuliere in einem Satz, was genau final bei einem Objekt- oder Feldverweis festhält.
d) Wie müsste man vorgehen, wenn man wirklich verhindern will, dass jemand die Werte verändert?
Vertiefung 2: Wann static schadet
static ist bequem: Man kommt von überall an den Wert heran, ohne ein Objekt zu haben. Genau das ist auch die Gefahr.
a) Die Klasse Spielstand speichert die Punkte in einem Klassenattribut private static int punkte. Alles funktioniert – bis jemand einen Zwei-Spieler-Modus einbauen will. Was passiert?
b) In Aufgabe 3 musstest du die Tests relativ formulieren (vorher + 2 statt 2). Erkläre, warum ein Klassenattribut Tests voneinander abhängig macht – und warum das ein grundsätzliches Problem ist und keine Eigenheit dieser Aufgabe.
c) Die Klasse Math besteht ausschließlich aus Konstanten und Klassenmethoden; new Math() ergäbe keinen Sinn. Woran erkennt man solche Klassen, und wie viele davon sollte ein Programm haben?
d) Beurteile: Formuliere eine Regel in einem Satz, wann static angebracht ist und wann es ein Warnzeichen ist.