Zum Inhalt
wannacode

Klassen in Klassen

Kurz gesagt: Eine verschachtelte Klasse ist eine Klasse, die innerhalb einer anderen Klasse deklariert ist, weil sie nur dort Sinn ergibt; Java hat statische und innere verschachtelte Klassen, lokale und anonyme.

Problem

Eine Klasse, die nur an einer Stelle Sinn ergibt

In der Lektion über die verkettete Liste war Node eine eigene Klasse. Jeder im Programm kann Knoten anlegen, aber sie ergeben nur in der Liste Sinn. Genauso ist es mit einer Zeile der Bestellung:

class OrderLine {
String product;
int quantity;
double price;
}
class Order {
List<OrderLine> lines = new ArrayList<>();
}

OrderLine ohne Order ergibt keinen Sinn. Der Name ist lang, damit er nicht mit der Zeile einer Rechnung, InvoiceLine, oder mit der Zeile im Lager zusammenstößt. Und nichts sagt, dass die beiden Klassen zusammengehören. Kann die Zeile innerhalb der Bestellung leben?

Die Idee

Statische verschachtelte Klasse

Ja. Eine Klasse, die innerhalb einer anderen Klasse deklariert ist, heißt verschachtelte Klasse (nested class):

class Order {
static class Line {
String product;
int quantity;
double price;
Line(String product, int quantity, double price) {
this.product = product;
this.quantity = quantity;
this.price = price;
}
double total() {
return quantity * price;
}
}
private List<Line> lines = new ArrayList<>();
void add(Line line) {
lines.add(line);
}
double total() {
double sum = 0;
for (Line line : lines) {
sum += line.total();
}
return sum;
}
}
Order order = new Order();
order.add(new Order.Line("Kaffeebohnen", 2, 12.5));
order.add(new Order.Line("Tasse", 4, 3.0));
System.out.println("Gesamt: " + order.total() + " €");
Ausgabe
Gesamt: 37.0 €
  • Von außen heißt sie Order.Line. Innerhalb von Order heißt sie nur Line.
  • static bedeutet, dass die Zeile an keine bestimmte Bestellung gebunden ist. new Order.Line(...) entsteht ohne ein Order-Objekt, wie bei einer normalen Klasse.
  • Ist sie private static class Line, kann niemand außerhalb von Order sie benutzen.

So eine Klasse kennst du schon: Map.Entry aus der Lektion über HashMap ist in Map verschachtelt. Und LinkedList in Java hält ihre Knoten in einer private static class Node.

Die Idee

Innere Klasse: Sie sieht das äußere Objekt

Der Warenkorb hat einen Rabatt für alle Produkte darin. Jedes Produkt muss den Rabatt seines eigenen Warenkorbs kennen. Dann hat die Klasse kein static:

class Cart {
private int discountPercent;
private List<Item> items = new ArrayList<>();
Cart(int discountPercent) {
this.discountPercent = discountPercent;
}
class Item {
String name;
double price;
Item(String name, double price) {
this.name = name;
this.price = price;
}
double finalPrice() {
return price * (100 - discountPercent) / 100;
}
}
void add(String name, double price) {
items.add(new Item(name, price));
}
void print() {
for (Item item : items) {
System.out.println(item.name + ": " + item.finalPrice() + " €");
}
}
}
Cart cart = new Cart(10);
cart.add("Kopfhörer", 80);
cart.add("Hülle", 20);
cart.print();
Ausgabe
Kopfhörer: 72.0 €
Hülle: 18.0 €
Eine verschachtelte Klasse ohne static heißt innere Klasse (inner class). Jedes Item wird von einem bestimmten Warenkorb erzeugt und sieht dessen Felder, sogar die private. finalPrice liest den discountPercent des Warenkorbs, in dem es liegt.

Cart cart = new Cart(10);
cart.add("Kopfhörer", 80);
cart.add("Hülle", 20);
cart.print();
Schritt für Schritt, in Worten
  1. Der Warenkorb ist ein Cart-Objekt mit 10% Rabatt.
  2. add erzeugt ein Item. Neben name und price hält jedes Item einen versteckten Pfeil zu dem Warenkorb, der es erzeugt hat: Cart.this.
  3. Das zweite Item zeigt auf denselben Warenkorb.
  4. finalPrice von „Kopfhörer“ folgt dem versteckten Pfeil und liest discountPercent: 80 * (100 - 10) / 100 = 72.0.
  5. finalPrice von „Hülle“ folgt dem versteckten Pfeil und liest discountPercent: 20 * (100 - 10) / 100 = 18.0.
Genauer, für Neugierige
  • Von außen wird ein inneres Objekt über das Objekt der äußeren Klasse erzeugt: cart.new Item(...). Die Schreibweise ist selten, weil die äußere Klasse die inneren meist selbst erzeugt, wie add hier:
Cart cart = new Cart(10);
Cart.Item cable = cart.new Item("Kabel", 10);
System.out.println(cable.finalPrice());
Ausgabe
9.0
  • Jedes innere Objekt hält eine versteckte Referenz auf das äußere. Heißen die Felder der beiden Klassen gleich, schreibst du das äußere als Cart.this.discountPercent.
  • Benutzt die verschachtelte Klasse die Felder der äußeren nicht, mach sie static. So hält sie keine Referenz, die sie nicht braucht.

Probier es aus

Zwei Warenkörbe

Die Klasse Cart ist dieselbe. Ein Warenkorb hat keinen Rabatt, der andere 20 % für Clubmitglieder.

Was gibt das Programm aus?

Cart regular = new Cart(0);
Cart member = new Cart(20);
regular.add("Buch", 15);
member.add("Buch", 15);
regular.print();
member.print();
Antwort

Probier es aus

Finde den Fehler

Die Zeile soll die Währung der Bestellung zeigen. Das Programm kompiliert nicht:
Kompilierfehler
Main.java:9: error: non-static variable currency cannot be referenced from a static context
return product + ": " + price + " " + currency;
^

In welcher Zeile liegt das Problem?

Wähle die Zeile mit dem Fehler.

Die Idee

Eine Klasse in einer Methode

Eine Klasse kann man auch in einer Methode deklarieren. Dann ist sie nur dort sichtbar und heißt lokale Klasse:

static void printBestSeller(String[] names, int[] sold) {
class Sale {
String name;
int count;
Sale(String name, int count) {
this.name = name;
this.count = count;
}
}
Sale best = new Sale(names[0], sold[0]);
for (int i = 1; i < names.length; i++) {
if (sold[i] > best.count) {
best = new Sale(names[i], sold[i]);
}
}
System.out.println("Bestseller: " + best.name + " (" + best.count + " Stk.)");
}
String[] names = { "Kaffee", "Tee", "Wasser" };
int[] sold = { 120, 45, 300 };
printBestSeller(names, sold);
Ausgabe
Bestseller: Wasser (300 Stk.)

Sale hält den Namen und die Anzahl zusammen. Außerhalb von printBestSeller benutzt sie niemand, darum steht sie dort. Solche Klassen sind selten, aber es ist gut, sie zu kennen.

Die Idee

Eine Klasse ohne Namen

Die Rabatte im Shop sind ein Interface:

interface Discount {
double apply(double price);
}

Bisher hast du für jeden Rabatt eine Klasse gemacht: class BlackFriday implements Discount { ... }. Wird der Rabatt nur an einer Stelle benutzt, kann man die Klasse genau dort schreiben, ohne Namen:

Discount blackFriday = new Discount() {
@Override
public double apply(double price) {
return price * 70 / 100;
}
};
Discount none = new Discount() {
@Override
public double apply(double price) {
return price;
}
};
System.out.println(blackFriday.apply(100));
System.out.println(none.apply(100));
Ausgabe
70.0
100.0

new Discount() { ... } macht zwei Dinge. Es deklariert eine Klasse ohne Namen, die Discount implementiert, und erzeugt ein Objekt davon. So eine Klasse heißt anonyme Klasse. Innen ist sie wie eine normale Klasse: @Override und die Methoden des Interfaces.

Genauer, für Neugierige
  • Eine anonyme Klasse kann auch eine Klasse erweitern, nicht nur ein Interface implementieren: new Product("Kaffee", 12.5) { ... } ruft den Konstruktor von Product auf und überschreibt Methoden.
  • Eine anonyme Klasse kann die lokalen Variablen der Methode lesen, in der sie geschrieben ist, aber nur, wenn sie sich danach nicht mehr ändern.
  • Ein Interface mit einer einzigen Methode, wie Discount, wird im Modul „Modernes Java“ viel kürzer geschrieben. Die anonyme Klasse ist die Stufe dorthin.

Die Idee

Comparator: Sortieren nach eigener Regel

Collections.sort hat Text und Zahlen sortiert. Schüler kann es aber nicht sortieren:

Collections.sort(students);

Der Compiler sagt no suitable method found for sort(List<Student>). Er weiß nicht, wonach er Schüler sortieren soll: nach Name, nach Note, nach Alter?

Die Regel gibst du getrennt an, als Comparator. Das ist ein Interface mit einer Methode, compare(a, b). Sie gibt eine negative Zahl zurück, wenn a vor b kommen soll, null, wenn beide gleich sind, und eine positive Zahl, wenn a nach b kommt. Als anonyme Klasse ist das praktisch:

class Student {
String name;
double grade;
Student(String name, double grade) {
this.name = name;
this.grade = grade;
}
@Override
public String toString() {
return name + " " + grade;
}
}
Collections.sort(students, new Comparator<Student>() {
@Override
public int compare(Student a, Student b) {
return Double.compare(b.grade, a.grade);
}
});
System.out.println(students);
Ausgabe
[Maria 6.0, Jonas 5.5, Anna 5.5, Peter 4.75]
  • Double.compare(b.grade, a.grade) vergleicht andersherum, darum steht die höchste Note vorn.
  • Jonas und Anna haben dieselbe Note und bleiben in der Reihenfolge, in der sie waren. Collections.sort vertauscht gleiche Elemente nicht.
  • Für Text und Zahlen in umgekehrter Reihenfolge gibt es einen fertigen Comparator: Collections.reverseOrder().
List<String> cities = new ArrayList<>();
cities.add("Hamburg");
cities.add("Berlin");
cities.add("München");
Collections.sort(cities, Collections.reverseOrder());
System.out.println(cities);
Ausgabe
[München, Hamburg, Berlin]
Genauer, für Neugierige
  • Comparator liegt in java.util, mit einem Typparameter: Comparator<Student> vergleicht Schüler.
  • Text und Zahlen werden ohne Comparator sortiert, weil String und Integer das Interface Comparable mit der Methode compareTo implementieren. Auch deine Klasse kann es implementieren, wenn sie eine natürliche Reihenfolge hat. Comparator ist für die Fälle, in denen es mehrere Reihenfolgen gibt.
  • Double.compare und Integer.compare schützen dich vor Fehlern. Mit int sieht a.age - b.age kürzer aus, aber bei sehr großen Zahlen läuft die Subtraktion über, und das Vorzeichen kippt.

Probier es aus

Was steht vorn?

Was gibt das Programm aus?

List<Product> products = new ArrayList<>();
products.add(new Product("Monitor", 220));
products.add(new Product("Maus", 25));
products.add(new Product("Tastatur", 60));
Collections.sort(products, new Comparator<Product>() {
@Override
public int compare(Product a, Product b) {
return Double.compare(a.price, b.price);
}
});
for (Product p : products) {
System.out.println(p.name);
}
Antwort

Übung

Klassen, die zusammengehören

Löse die Aufgaben in deinem Editor.

1. Warenkorb. Eine Klasse ShoppingCart mit einer statischen verschachtelten Klasse Item (Name und Preis) und den Methoden addItem(Item item), getTotal() und count(). Füge in main den Artikel new ShoppingCart.Item("Milch", 2.5) hinzu, dazu Brot für 1,20 € und Kaffee für 7,50 €.

Ausgabe
Artikel: 3
Gesamt: 11.2 €

2. Monitor für einen Webserver. Eine Klasse WebServer mit einer statischen verschachtelten Klasse Request (Adresse und Statuscode) und den Methoden addRequest(Request r) und printStats(). Die Statistik sagt, wie viele Anfragen es gibt, wie viele es von jedem Statuscode gibt (sortiert) und wie viel Prozent erfolgreich sind, also mit einem Code von 200 bis 299. Die Anfragen sind /home 200, /cart 200, /login 401, /home 200, /api/orders 500.

Ausgabe
Anfragen: 5
200: 3
401: 1
500: 1
Erfolgreich: 60%

3. Hausverwaltung. Eine Klasse Building mit einer statischen verschachtelten Klasse Apartment (Nummer, Anzahl der Zimmer, Eigentümer) und den Methoden addApartment(Apartment a) und getApartmentInfo(int number). Halte die Wohnungen in einer Map<Integer, Apartment>. Füge 101 mit 3 Zimmern von Schmidt und 102 mit 2 Zimmern von Müller hinzu. Suche 101 und 999.

Ausgabe
Wohnung 101: 3 Zimmer, Eigentümer Schmidt
Wohnung 999 gibt es nicht.

4. Rangliste. Sortiere die Schüler aus der Lektion nach Note, von der höchsten abwärts. Bei gleicher Note sortiere sie nach Namen, alphabetisch. Alles steckt in einem Comparator: Ist der Vergleich nach Note nicht 0, gib ihn zurück, sonst vergleiche die Namen mit compareTo.

Ausgabe
Maria 6.0
Anna 5.5
Jonas 5.5
Peter 4.75

Weitere Aufgaben

  • Computer-Konfigurator. Eine Klasse Computer mit den statischen verschachtelten Klassen CPU (Marke, Takt in GHz), RAM (GB) und Storage (GB) und den Methoden setCPU, setRAM, setStorage. toString beschreibt die Konfiguration: Intel 3.5 GHz, 16 GB RAM, 512 GB.
  • Reiseplaner, noch einmal. Löse die Aufgabe aus der Lektion über HashMap mit einer verschachtelten Klasse TripPlanner.Destination, die den Namen und die Liste der Aktivitäten hält.
  • Kontaktnetz, noch einmal. Verschiebe Contact aus der Aufgabe in der Lektion über HashMap in ContactNetwork. Sollte sie static sein?
  • Kopfrechnen, noch einmal. Verschiebe Question aus der Aufgabe in der Lektion über Listen in MathGame.
  • Produkte auf zwei Arten. Sortiere eine Liste von Produkten nach Preis, vom teuersten an, und bei gleichem Preis nach Namen. Sortiere dann dieselbe Liste nur nach Namen, in umgekehrter Reihenfolge.
  • Dein eigener Rabatt. Schreib drei anonyme Discount-Klassen: 10 % ab 100 €, feste 5 € Rabatt und „zweites Produkt zum halben Preis“. Wende sie auf ein und denselben Warenkorb an.

Test

Teste dich

1. Was ist der Unterschied zwischen static class Line und class Item innerhalb einer anderen Klasse?
2. Warum sieht eine statische verschachtelte Klasse die normalen Felder der äußeren Klasse nicht?
3. Was erzeugt new Discount() { … }?
4. compare(a, b) gibt eine negative Zahl zurück. Was bedeutet das für Collections.sort?
5. Was sortiert die Schüler nach Note, von der höchsten abwärts?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·