
Mit Java 10 begann am 20. März 2018 der sechsmonatige Release-Zyklus des JDKs. Statt jahrelang auf ein großes Update warten zu müssen, werden wir nun halbjährlich mit neuen Features – und Previews neuer Features – verwöhnt.
Das letzte Release nach altem Muster – mit 91 JEPs, darunter das Modulsystem – stelle ich im Artikel über Java 9 vor.
Ich habe die Änderungen nach Relevanz für die tägliche Entwicklungsarbeit sortiert. Änderungen an der Sprache selbst stehen ganz oben, gefolgt von Erweiterungen an der JDK-Klassenbibliothek.
Danach folgen Performance-Verbesserungen und zuletzt sonstige Änderungen, von denen wir Entwickler:innen wenig mitbekommen (sofern wir nicht am JDK selbst mitarbeiten).
Als Kapitel-Überschriften verwende ich die englischen Bezeichnungen der JDK Enhancement Proposals (JEPs). Diese auf Deutsch zu übersetzen würde eher verwirren.
Local-Variable Type Inference („var“)
Seit Java 10 können wir zur Deklaration lokaler Variablen (lokal heißt: innerhalb von Methoden) das Schlüsselwort var verwenden. Das ermöglicht z. B. folgende Definitionen:
var i = 10;
var hello = "Hello world!";
var list = List.of(1, 2, 3, 4, 5);
var httpClient = HttpClient.newBuilder().build();
var status = getStatus();
Zum Vergleich – so sehen die Definitionen in klassischer Schreibweise aus:
int i = 10;
String hello = "Hello world!";
List<Integer> list = List.of(1, 2, 3, 4, 5);
HttpClient httpClient = HttpClient.newBuilder().build();
Status status = getStatus();
Inwieweit man var verwendet, wird vermutlich in vielen Teams zu langen Diskussionen führen. Ich setze es dann ein, wenn es a) deutlich kürzer ist und ich b) den Datentyp klar im Code erkennen kann.
Im Beispiel oben wäre das in Zeile 3 und 4 der Fall (bei List und HttpClient). Die klassische Schreibweise ist in beiden Fällen deutlich länger. Und die Zuweisungen auf der rechten Seite – also List.of() und HttpClient.newBuilder().build() – lassen mich den Datentyp klar erkennen.
In folgenden Fällen würde ich hingegen auf var verzichten:
- In Zeile 1 spart man kein einziges Zeichen; hier würde ich bei
intbleiben. - In Zeile 2 ist
varnur minimal kürzer alsString– von daher würde ich auch hier eherStringverwenden. Ich verstehe aber auch, wenn Teams sich hier anders entscheiden. - In Zeile 5 würde ich ganz klar bei der alten Schreibweise bleiben. Ansonsten kann ich nicht auf Anhieb erkennen, was
getStatus()zurückliefert. Ist es einint? EinString? Ein Enum? Ein komplexes Value Object? Oder gar eine JPA-Entity aus der Datenbank?
Eine ausführlichere Abhandlung darüber, wann man var einsetzen sollte und wann nicht, findest Du in den offiziellen Style Guidelines. Wichtig ist, dass ihr euch im Team auf eine konsistente Verwendung einigt.
(Local-Variable Type Inference ist definiert im JDK Enhancement Proposal 286.)
Immutable Collections
Das Java Collections Framework bietet mit den Methoden Collections.unmodifiableList(), unmodifiableSet(), unmodifiableMap(), unmodifiableCollection() – und weiteren Varianten für sortierte, navigierbare und (seit Java 21) sequenzierte Collections – die Möglichkeit, unveränderbare Wrapper für Collection-Klassen zu erzeugen.
Hier ein Beispiel:
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
List<Integer> unmodifiable = Collections.unmodifiableList(list);
Versuchen wir nun über den Wrapper ein Element hinzuzufügen, erhalten wir eine UnsupportedOperationException:
unmodifiable.add(4);
⟶
Exception in thread "main" java.lang.UnsupportedOperationException
at java.base/java.util.Collections$UnmodifiableCollection.add(...)
at ...
Der Wrapper hält uns allerdings nicht davon ab, die zugrunde liegende Liste zu modifizieren. Alle nachträglichen Änderungen an dieser sind auch im Wrapper sichtbar. Denn der Wrapper enthält keine Kopie der Liste, sondern eine View:
list.add(4);
System.out.println("unmodifiable = " + unmodifiable);
⟶
unmodifiable = [1, 2, 3, 4]
List.copyOf(), Set.copyOf() und Map.copyOf()
Mit Java 10 haben wir nun auch die Möglichkeit, unveränderbare Kopien von Collections zu erzeugen. Dafür stehen uns die statischen Interface-Methoden List.copyOf(), Set.copyOf() und Map.copyOf() zur Verfügung.
Erstellen wir so eine Kopie und modifizieren dann die ursprüngliche Collection, haben die Änderungen keine Auswirkungen mehr auf die Kopie:
List<Integer> immutable = List.copyOf(list);
list.add(4);
System.out.println("immutable = " + immutable);
⟶
immutable = [1, 2, 3]
Der Versuch, die Kopie zu ändern, wird – genau wie beim Einsatz von unmodifiableList() – mit einer UnsupportedOperationException quittiert:
immutable.add(4);
⟶
Exception in thread "main" java.lang.UnsupportedOperationException
at java.base/java.util.ImmutableCollections.uoe(...)
at java.base/java.util.ImmutableCollections$AbstractImmutableCollection.add(...)
at ...
Collectors.toUnmodifiableList(), toUnmodifiableSet() und toUnmodifiableMap()
Die mittels Collectors.toList(), toSet() und toMap() erstellten Collectoren sammeln die Elemente eines Streams in veränderlichen Listen, Sets und Maps. Das folgende Beispiel zeigt den Einsatz dieser Collectoren und die nachträgliche Änderung der Ergebnisse:
List<Integer> list = IntStream.rangeClosed(1, 3).boxed().collect(Collectors.toList());
Set<Integer> set = IntStream.rangeClosed(1, 3).boxed().collect(Collectors.toSet());
Map<Integer, String> map = IntStream.rangeClosed(1, 3).boxed()
.collect(Collectors.toMap(Function.identity(), String::valueOf));
list.add(4);
set.add(4);
map.put(4, "4");
System.out.println("list = " + list);
System.out.println("set = " + set);
System.out.println("map = " + map);
Wie zu erwarten, liefert das Programm die folgende Ausgabe (wobei die Elemente des Sets und der Map auch in anderer Reihenfolge auftreten dürfen):
list = [1, 2, 3, 4]
set = [1, 2, 3, 4]
map = {1=1, 2=2, 3=3, 4=4}
In Java 10 sind die Methoden Collectors.toUnmodifiableList(), toUnmodifiableSet() und toUnmodifiableMap() hinzugekommen, mit denen wir Stream-Elemente nun auch in unveränderlichen Listen, Sets und Maps sammeln können:
List<Integer> list =
IntStream.rangeClosed(1, 3).boxed().collect(Collectors.toUnmodifiableList());
Set<Integer> set =
IntStream.rangeClosed(1, 3).boxed().collect(Collectors.toUnmodifiableSet());
Map<Integer, String> map =
IntStream.rangeClosed(1, 3)
.boxed()
.collect(Collectors.toUnmodifiableMap(Function.identity(), String::valueOf));
Der Versuch, solch eine Liste, Set oder Map zu verändern, wird mit einer UnsupportedOperationException quittiert.
(Für die Erweiterungen an List, Set, Map und Collectors gibt es keinen JDK Enhancement Proposal.)
Optional.orElseThrow()
Das in Java 8 eingeführte Optional bietet mit der Methode get() die Möglichkeit, den durch das Optional gewrappten Wert auszulesen. Vor dem Aufruf von get() sollte immer mit isPresent() geprüft werden, ob ein Wert existiert:
Optional<String> result = getResult();
if (result.isPresent()) {
System.out.println(result.get());
}
Ist das Optional leer, würde get() sonst eine NoSuchElementException werfen.
Um das Risiko einer unbeabsichtigten Exception zu minimieren, geben IDEs und statische Code-Analyse-Tools eine Warnung aus, wenn get() ohne isPresent() verwendet wird:

Es gibt allerdings auch Fälle, in denen eine solche Exception gewünscht ist. Bisher musste man den Code mit entsprechenden @SuppressWarnings-Annotationen versehen, um die Warnungen zu unterdrücken.
Java 10 bietet mit der Methode orElseThrow() eine schönere Lösung: Die Methode ist eine exakte Kopie der get()-Methode – nur der Name ist anders. Da aus dem Namen klar ersichtlich ist, dass diese Methode eine Exception werfen kann, sind Missverständnisse ausgeschlossen. Die statische Code-Analyse moniert die Verwendung nicht mehr als Code Smell.
Hier zum Vergleich der Quellcode beider Methoden:
public T get() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
public T orElseThrow() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
Die gleiche Methode haben in Java 10 auch OptionalInt, OptionalLong und OptionalDouble bekommen.
(Für diese Erweiterung gibt es keinen JDK Enhancement Proposal.)
Time-Based Release Versioning
Nachdem von Java 8 auf 9 das Versionsformat (endlich) von dem eher kryptischen 1.8.0_291 auf ein deutlich besser lesbares 9.0.4 umgestellt wurde, wurde in Java 10 mit JEP 322 noch das Release-Datum hinzugefügt – und für Java 11 bereits im Voraus ein „LTS“ (Long-Term Support) festgelegt.
Das Kommando java -version liefert in Java 8 bis 11 die folgenden Antworten:
Java 8:
$ java -version
java version "1.8.0_291"
Java 9:
$ java -version
java version "9.0.4"
Java 10:
$ java -version
java version "10.0.2" 2018-07-17
Java 11:
$ java -version
java version "11.0.11" 2021-04-20 LTS
Bis heute gab es keine weitere Änderung am Versionsschema.
Programmatisch kommst du an die Bestandteile der Versionsnummer über Runtime.version(): Die Methoden feature(), interim(), update() und patch() der Klasse Runtime.Version sind ebenfalls mit JEP 322 hinzugekommen.
Parallel Full GC for G1
Mit dem JDK 9 hat der Garbage-First (G1) Garbage Collector den Parallel Collector als Standard-GC abgelöst.
Der Parallel GC führt eine vollständige Garbage Collection („Full GC“, also das Aufräumen aller Regionen des Heaps) mit mehreren Threads durch. Beim G1 lief die Full GC bis Java 9 dagegen in einem einzigen Thread. Da beide Collectoren die Anwendung während einer Full GC anhalten („Stop-the-World“), konnte das beim G1 zu spürbar längeren Pausen führen.
Da der G1 mit dem Ziel entwickelt wurde, vollständige Collections so gut es geht zu vermeiden, stellte dies nur selten ein Problem dar.
In Java 10 wurde mit dem JDK Enhancement Proposal 307 nun auch die Full GC des G1 Collectors parallelisiert – mit derselben Anzahl Threads wie die Young und Mixed Collections. Die Worst-Case-Latenzen (Pausezeiten) erreichen damit die des Parallel Collectors.
Application Class-Data Sharing
Da viele Java-Entwickler:innen nicht damit vertraut sind, möchte ich kurz ausholen und Class-Data Sharing (also ohne den „Application“-Prefix) erklären.
Class-Data Sharing
Wenn eine JVM startet, lädt sie die JDK-Klassenbibliothek aus dem Dateisystem (bis JDK 8 aus der Datei jre/lib/rt.jar; seit JDK 9 aus dem Modul-Image lib/modules). Dabei werden die class-Dateien aus den Archiven extrahiert, in eine architekturspezifische binäre Form gebracht und im Arbeitsspeicher des JVM-Prozesses abgelegt:

Werden auf derselben Maschine mehrere JVMs gestartet, wiederholt sich dieser Prozess. Jede JVM hält ihre eigene Kopie der Klassenbibliothek im RAM:

Mit Class-Data Sharing („CDS“) werden zwei Ziele verfolgt:
- Die Start-Zeit der JVM soll verkürzt werden.
- Der Memory-Footprint (auf Deutsch: der Speicherbedarf) der JVM soll reduziert werden.
Class-Data Sharing funktioniert wie folgt:
- Eine Datei
classes.jsa(JSA steht für Java Shared Archive) enthält die komplette Klassenbibliothek im Binärformat der aktuellen Architektur. In Java 10 musste man diese Datei einmalig mit dem Kommandojava -Xshare:dumperzeugen; seit Java 12 liefert das JDK sie unterlib/server/classes.jsamit, und CDS ist standardmäßig aktiv. - Beim Start der JVM wird diese Datei per Memory Mapped I/O durch das Betriebssystem in den Arbeitsspeicher der JVM „gemappt“. Das ist erstens schneller als das Laden aus
rt.jarbzw.lib/modules. Und zweitens lädt das Betriebssystem die Datei nur ein einziges Mal in den RAM und stellt jedem JVM-Prozess eine Read-Only-Sicht auf denselben Speicherbereich zur Verfügung.
Die folgende Grafik soll dies verdeutlichen:

Application Class-Data Sharing – Schritt für Schritt
Application Class-Data Sharing (auch „Application CDS“ oder „AppCDS“) erweitert CDS um die Möglichkeit, neben der JDK-Klassenbibliothek auch die Klassen deiner Applikation in einer JSA-Datei zu speichern und diese unter den JVM-Prozessen zu teilen.
Wie das funktioniert, zeige ich dir an einem einfachen Beispiel (du findest den Quellcode auch in diesem GitHub-Repository):
Im Verzeichnis src/eu/happycoders/appcds liegen folgende zwei Java-Dateien:
Main.java:
package eu.happycoders.appcds;
public class Main {
public static void main(String[] args) {
new Greeter().greet();
}
}
Greeter.java:
package eu.happycoders.appcds;
public class Greeter {
public void greet() {
System.out.println("Hello world!");
}
}
Wir compilieren und paketieren die Klassen wie folgt und starten dann die Main-Klasse:
javac -d target/classes src/eu/happycoders/appcds/*.java
jar cf target/helloworld.jar -C target/classes .
java -cp target/helloworld.jar eu.happycoders.appcds.Main
Wir sollten nun die „Hello world!“-Begrüßung auf der Kommandozeile sehen.
Um Application CDS einzusetzen, müssen wir als Nächstes eine Liste der Klassen erstellen, die die Applikation verwendet. Dazu führen wir folgendes Kommando aus (unter Windows musst du die Backslashes weglassen und alles in eine Zeile schreiben):
java -Xshare:off -XX:+UseAppCDS \
-XX:DumpLoadedClassList=helloworld.lst \
-cp target/helloworld.jar eu.happycoders.appcds.Main
Im Arbeitsverzeichnis sollte sich jetzt die Datei helloworld.lst mit in etwa folgendem Inhalt befinden:
java/lang/Object
java/lang/String
...
eu/happycoders/appcds/Main
eu/happycoders/appcds/Greeter
...
java/lang/Shutdown
java/lang/Shutdown$Lock
Wie du siehst, werden nicht nur die Klassen der Applikation aufgelistet, sondern auch die der JDK-Klassenbibliothek.
Als Nächstes erstellen wir aus der Klassenliste die JSA-Datei.
java -Xshare:dump -XX:+UseAppCDS \
-XX:SharedClassListFile=helloworld.lst \
-XX:SharedArchiveFile=helloworld.jsa \
-cp target/helloworld.jar
Du bekommst während der Verarbeitung einige Statistiken angezeigt und findest anschließend die Datei helloworld.jsa im Arbeitsverzeichnis. Sie sollte etwa 9 MB groß sein.
Um die JSA-Datei zu verwenden, startest du die Anwendung nun wie folgt:
java -Xshare:on -XX:+UseAppCDS \
-XX:SharedArchiveFile=helloworld.jsa \
-cp target/helloworld.jar eu.happycoders.appcds.Main
Wenn alles funktioniert hat, solltest du erneut ein „Hello world!“ sehen.
Folgende Grafik fasst die Funktionsweise von Application Class-Data Sharing zusammen:

(Application CDS ist im JDK Enhancement Proposal 310 definiert.)
Experimental Java-Based JIT Compiler
Seit Java 9 wurde der Graal Compiler (ein in Java geschriebener Java-Compiler) als experimenteller Ahead-of-Time (AOT)-Compiler mit ausgeliefert. Mit dem Tool jaotc konnte man damit Java-Klassen vorab in eine Shared Library compilieren, die die JVM beim Start über -XX:AOTLibrary einbindet – zunächst nur unter Linux/x64.
In Java 10 wurde mit dem JEP 317 die Möglichkeit geschaffen, Graal auch als Just-in-Time (JIT)-Compiler einzusetzen – zumindest auf der Linux/x64-Plattform. Dazu verwendet Graal das im JDK 9 eingeführte JVM Compiler Interface (JVMCI).
Aktiviert wurde Graal in Java 10 bis 16 über die folgende Option in der java-Befehlszeile:
-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler
Sonstige Änderungen in Java 10 (die man als Java-Entwickler:in nicht unbedingt kennen muss)
In diesem Abschnitt liste ich diejenigen Java 10 Features auf, von denen ich denke, dass nicht alle Java-Entwickler:innen im Detail über sie Bescheid wissen müssen.
Andererseits schadet es nicht, wenigstens einmal von ihnen gehört zu haben. :-)
Heap Allocation on Alternative Memory Devices
Mit Umsetzung des JEP 316 kann der Java-Heap – statt auf herkömmlichem RAM – auch auf einem alternativen Speichergerät, z. B. auf NV-DIMM (nicht-flüchtigem Speicher) angelegt werden.
Der alternative Speicher muss dazu vom Betriebssystem über einen Dateisystem-Pfad zur Verfügung gestellt werden (z. B. /dev/pmem0) und wird über folgende Option in der java-Befehlszeile eingebunden:
-XX:AllocateHeapAt=<path>
Additional Unicode Language-Tag Extensions
Mit JEP 314 werden sogenannte „Language-Tag Extensions“ hinzugefügt. Diese ermöglichen es, in einem Locale-Objekt folgende Zusatzinformationen zu hinterlegen:
| Schlüssel | Bezeichnung englisch | Bezeichnung deutsch | Beispiele |
|---|---|---|---|
| cu | Currency | Währung | ISO 4217 Währungscodes |
| fw | First day of week | Erster Tag der Woche | sun (Sonntag), mon (Montag) |
| rg | Region override | Überschreibung der Region | uszzzz (US-Einheiten) |
| tz | Timezone | Zeitzone | deber (Berlin), uslax (Los Angeles) |
Die folgenden zwei Extensions existieren bereits seit Java 7:
| Schlüssel | Bezeichnung englisch | Bezeichnung deutsch | Beispiele |
|---|---|---|---|
| ca | Calendar | Kalender | gregorian, buddhist, chinese |
| nu | Numbering system | Zahlensystem | arab, roman |
Der folgende Beispiel-Quellcode zeigt die Erstellung einer deutschen Locale („de-DE“) mit US-Dollar als Währung („cu-usd“), Mittwoch als erstem Tag der Woche („fw-wed“) und der Zeitzone von Los Angeles („tz-uslax“):
Locale locale = Locale.forLanguageTag("de-DE-u-cu-usd-fw-wed-tz-uslax");
Currency currency = Currency.getInstance(locale);
Calendar calendar = Calendar.getInstance(locale);
DayOfWeek firstDayOfWeek = DayOfWeek.of((calendar.getFirstDayOfWeek() + 5) % 7 + 1);
DateFormat dateFormat = DateFormat.getTimeInstance(LONG, locale);
String time = dateFormat.format(new Date());
System.out.println("currency = " + currency);
System.out.println("firstDayOfWeek = " + firstDayOfWeek);
System.out.println("time = " + time);
Zum Zeitpunkt des Schreibens dieses Artikels (20:45 Uhr in Berlin) gibt das Beispiel-Programm folgendes aus:
currency = USD
firstDayOfWeek = WEDNESDAY
time = 11:45:50 PDT
Unter Java 9 werden die zusätzlichen Tags ignoriert, und das Programm gibt (40 Sekunden später) folgendes aus:
currency = EUR
firstDayOfWeek = MONDAY
time = 20:46:30 MESZ
Da sich vermutlich nur sehr wenige Java-Entwickler:innen mit solchen Details beschäftigen müssen, habe ich diese Erweiterung unter „Sonstiges“ einsortiert.
Garbage Collector Interface
Einige Teile der Quellcodes der Garbage Collectoren liegen bis Java 9 in langen if-else-Ketten tief in den Quellcodes des Java-Interpreters und des C1- und C2-Compilers verborgen. Um einen neuen Garbage Collector zu implementieren, mussten Entwickler:innen all diese Stellen kennen und für ihre spezifischen Anforderungen erweitern.
Durch den JDK Enhancement Proposal 304 wird im JDK-Quellcode eine saubere Garbage-Collector-Schnittstelle eingeführt und damit die Algorithmen der Garbage Collectoren vom Interpreter und den Compilern isoliert.
So können Entwickler:innen in Zukunft neue GCs hinzufügen, ohne die Codebasis des Interpreters und Compilers anpassen zu müssen.
Root Certificates
Bis zu Java 9 enthielt das OpenJDK in der Keystore-Datei cacerts keine Root-Zertifikate, so dass SSL/TLS-basierte Features nicht ohne Weiteres lauffähig waren.
Mit dem JDK Enhancement Proposal 319 wurden die im Oracle-JDK enthaltenen Root-Zertifikate in das OpenJDK übernommen.
Thread-Local Handshakes
Thread-Local Handshakes sind eine Optimierung, die die VM-Leistung verbessert. Sie wurde in Java 10 zunächst für x64 und SPARC implementiert und steht inzwischen auf allen Plattformen zur Verfügung. Die Optimierung ist standardmäßig aktiviert. Details findest du im JDK Enhancement Proposal 312.
Remove the Native-Header Generation Tool
Mit JEP 313 wurde das Tool javah entfernt, mit dem man native Header-Dateien für JNI erzeugen konnte. Die Funktionalität wurde in den Java-Compiler integriert: javac -h <verzeichnis> erzeugt die Header-Dateien beim Compilieren gleich mit.
Consolidate the JDK Forest into a Single Repository
Im JDK 9 lag der Quellcode in acht separaten Mercurial-Repositories, was bei der Entwicklung oft zu einem erheblichen Mehraufwand führte. Bei über tausend Änderungen war es erforderlich, logisch zusammenhängende Commits über mehrere Repositories zu verteilen.
Mit dem JEP 296 wurde der gesamte JDK-Quellcode in ein Monorepo konsolidiert. Die Verwendung eines Monorepos erlaubt nun atomare Commits, Branches und Pull Requests, was die Arbeit am JDK deutlich erleichtert.
Weitere Erweiterungen der Klassenbibliothek
Neben den oben beschriebenen Collection- und Optional-Methoden hat Java 10 noch eine Handvoll kleinerer Erweiterungen bekommen, für die es ebenfalls keinen JEP gibt:
Reader.transferTo(Writer)kopiert den kompletten Inhalt eines Readers in einen Writer – das Gegenstück zuInputStream.transferTo(OutputStream)aus Java 9.Scanner,PrintWriter,URLEncoder.encode(),URLDecoder.decode()sowieChannels.newReader()undChannels.newWriter()nehmen das Charset jetzt alsCharset-Objekt entgegen, nicht mehr nur als String.DateTimeFormatter.localizedBy(Locale)überschreibt Locale, Kalender, Zeitzone und Zahlenformat eines Formatters in einem Aufruf.MethodType.lastParameterType()liefert den Typ des letzten Parameters – odervoid.class, wenn es keinen gibt.FileStore.getBlockSize()liefert die Blockgröße des Dateisystems in Bytes.Propertieshat einen Konstruktor mit initialer Kapazität bekommen.
Vollständige Liste aller Änderungen in Java 10
Dieser Artikel hat alle Features von Java 10 vorgestellt, die in JDK Enhancement Proposals definiert sind, sowie Erweiterungen an der JDK-Klassenbibliothek, die keinem JEP zugeordnet sind.
Eine vollständige Liste aller Änderungen findest du in den offiziellen Java 10 Release Notes.
Fazit
Mit var, Immutable Collections und Optional.orElseThrow() hat Java 10 uns einige hilfreiche neue Werkzeuge bereitgestellt. Der G1 Garbage Collector räumt jetzt auch bei einer Full GC mit mehreren Threads auf. Und mit Application Class-Data Sharing können wir den Start unserer Applikation weiter beschleunigen und den Memory Footprint reduzieren. Wer Lust am Experimentieren hatte, konnte den in Java geschriebenen Graal-Compiler aktivieren – bis Java 17 ihn wieder entfernt hat.
Hat dir dieser Artikel Zeit gespart? Dann freue ich mich, wenn du eine Minute davon in eine Bewertung auf meinem ProvenExpert-Profil investierst. Dein Feedback zeigt mir, dass sich die Arbeit an diesen Artikeln lohnt.




