Eine Klasse ist ein Bauplan, ein Objekt ein daraus gebautes Exemplar. Eine Klasse bündelt Daten (Felder) und Verhalten (Methoden).
public class Hund {
// Felder (Zustand)
private String name;
private int alter;
// Konstruktor: legt das Objekt an
public Hund(String name, int alter) {
this.name = name;
this.alter = alter;
}
// Methoden (Verhalten)
public String bellen() {
return name + " sagt: Wuff!";
}
public void geburtstag() {
alter++;
}
// Getter
public String getName() { return name; }
public int getAlter() { return alter; }
@Override
public String toString() {
return "Hund[" + name + ", " + alter + "]";
}
public static void main(String[] args) {
Hund rex = new Hund("Rex", 3);
Hund mimi = new Hund("Mimi", 5);
rex.geburtstag();
System.out.println(rex.bellen() + " " + rex.getAlter());
System.out.println(rex + " " + mimi);
}
}Rex sagt: Wuff! 4 Hund[Rex, 4] Hund[Mimi, 5]
new Hund(...)erzeugt ein Objekt und ruft den Konstruktor aufthisist das aktuelle Objektprivateversteckt Felder; von außen sind sie nur über Methoden erreichbar (Kapselung)toString()bestimmt die Textdarstellung
Kapselung und Sichtbarkeit
| Modifikator | Sichtbar für |
|---|---|
public | alle |
protected | gleiches Paket und Unterklassen |
| (ohne) | nur das gleiche Paket |
private | nur die Klasse selbst |
Halte Felder private und biete kontrollierte Zugriffe an. So kann die Klasse ihre Regeln durchsetzen:
public class Konto {
private double stand;
private final String inhaber;
private static int anzahl = 0; // gehört der Klasse, nicht einem Objekt
public Konto(String inhaber, double startwert) {
if (startwert < 0) throw new IllegalArgumentException("Startwert < 0");
this.inhaber = inhaber;
this.stand = startwert;
anzahl++;
}
public Konto(String inhaber) { this(inhaber, 0); } // Konstruktoren verketten
public void einzahlen(double betrag) {
if (betrag <= 0) throw new IllegalArgumentException("Betrag muss positiv sein");
stand += betrag;
}
public void abheben(double betrag) {
if (betrag > stand) throw new IllegalStateException("Nicht genug Guthaben");
stand -= betrag;
}
public double getStand() { return stand; }
public String getInhaber() { return inhaber; }
public static int getAnzahl() { return anzahl; }
public static void main(String[] args) {
Konto k = new Konto("Mia", 100);
k.einzahlen(50);
k.abheben(30);
System.out.println(k.getInhaber() + ": " + k.getStand());
new Konto("Tom");
System.out.println("Konten: " + Konto.getAnzahl());
try {
k.abheben(1000);
} catch (IllegalStateException e) {
System.out.println("Fehler: " + e.getMessage());
}
}
}Mia: 120.0 Konten: 2 Fehler: Nicht genug Guthaben
equals und hashCode
Standardmäßig vergleicht equals nur die Speicheradresse. Soll Gleichheit nach Inhalt gelten, überschreibst du equals und hashCode:
import java.util.*;
public class Punkt {
private final int x, y;
public Punkt(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Punkt p)) return false;
return x == p.x && y == p.y;
}
@Override public int hashCode() { return Objects.hash(x, y); }
@Override public String toString() { return "(" + x + "|" + y + ")"; }
public static void main(String[] args) {
Punkt a = new Punkt(1, 2), b = new Punkt(1, 2);
System.out.println((a == b) + " " + a.equals(b));
Set<Punkt> menge = new HashSet<>(List.of(a));
System.out.println(menge.contains(b) + " " + menge);
}
}false true true [(1|2)]
Wer equals überschreibt, muss hashCode passend mitüberschreiben, sonst funktionieren HashSet und HashMap nicht richtig. Records (nächste Kapitel) erledigen das automatisch.
Initialisierung und Reihenfolge
Beim Erzeugen eines Objekts passiert in dieser Reihenfolge: statische Teile der Klasse (einmal), Felder mit Anfangswerten, Initialisierungsblöcke, Konstruktor.
public class Reihenfolge {
static { System.out.println("1 statischer Block (einmal)"); }
private int a = init("2 Feld");
{ System.out.println("3 Initialisierungsblock"); }
Reihenfolge() { System.out.println("4 Konstruktor"); }
static int init(String s) { System.out.println(s); return 1; }
public static void main(String[] args) {
new Reihenfolge();
new Reihenfolge();
}
}1 statischer Block (einmal) 2 Feld 3 Initialisierungsblock 4 Konstruktor 2 Feld 3 Initialisierungsblock 4 Konstruktor
Objekte im Speicher
- Lokale Variablen mit primitiven Typen liegen im Stack; Objekte im Heap, die Variable enthält nur einen Verweis.
- Der Garbage Collector gibt Objekte frei, auf die niemand mehr zeigt. Du musst nichts manuell löschen.
nullbedeutet "kein Objekt". Zugriff darauf löst eineNullPointerExceptionaus.
public class Verweise {
static class Box { int wert; Box(int w) { wert = w; } }
public static void main(String[] args) {
Box a = new Box(1);
Box b = a; // beide zeigen auf dasselbe Objekt
b.wert = 99;
System.out.println(a.wert);
a = null; // b hält das Objekt noch fest
System.out.println(b.wert);
String s = null;
try { s.length(); } catch (NullPointerException e) { System.out.println("NPE"); }
}
}99 99 NPE
Optional statt null
Optional<T> macht "kein Wert" im Typ sichtbar:
import java.util.*;
public class Opt {
static Optional<String> finde(List<String> l, String prefix) {
return l.stream().filter(s -> s.startsWith(prefix)).findFirst();
}
public static void main(String[] args) {
List<String> namen = List.of("Anna", "Ben", "Clara");
System.out.println(finde(namen, "B").orElse("keiner") + " " + finde(namen, "X").orElse("keiner"));
finde(namen, "C").ifPresent(n -> System.out.println("gefunden: " + n));
System.out.println(finde(namen, "A").map(String::toUpperCase).get());
}
}Ben keiner gefunden: Clara ANNA
Merke
- Klasse = Bauplan (Felder + Methoden), Objekt = Exemplar;
newruft den Konstruktor - Kapselung: Felder
private, Zugriff über Methoden thisist das aktuelle Objekt;staticgehört zur KlasseequalsundhashCodeimmer gemeinsam überschreiben- Objekte liegen im Heap, Variablen halten Verweise;
nullvermeiden,Optionalnutzen
Aufgabe
Schreibe eine Klasse Rechteck mit Breite und Höhe (Konstruktor, Prüfung auf positive Werte), flaeche(), umfang() und toString().