|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Effective Java
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Java 8 wird
im März 2014, etwa zeitgleich mit diesem Beitrag, frei gegeben. Deshalb
wollen wir uns in
diesem Artikel einen Überblick verschaffen und
die Feature von Java 8 kurz erläutern. Dabei haben wir diejenigen Neuerungen
ausgewählt, die für Java-Entwickler am ehesten interessant sind: neue
Sprachmittel und größere Erweiterung an den JDK-Core-Bibliotheken. Einige
dieser Themen sind umfangreicher; sie werden hier nur kurz betrachtet und
in nachfolgenden Beiträgen ausführlicher vorgestellt. Deshalb werden
wir in nachfolgenden Beiträgen noch einmal auf Streams, Concurrency Utilities
und das Date/Time API zurückkommen.
Beginnen wir mit einer Übersicht über die
wesentlichen Neuerungen; die vollständige Liste ist den Release Notes
oder unter /
FEAT
/ zu finden. Die einzelnen
Features sind in JEPs (JEP = JDK Extension Proposal) herunter gebrochen.
Ein JEP ist ein Art Arbeitspaket für die Erweiterung von Java.
Die ersten Themenkomplexe werden wir nachfolgend kurz darstellen. Auf die in der Tabelle grauen Themen gehen wir nicht ein. Es geht dabei um spezielle Aspekte (Internationalisierung, Security, JDBC, etc.), die nicht jeden Java-Entwickler betreffen oder aber um Interna des JDK (javac, Modularisierung, Runtime). Wir beginnen mit der offensichtlichsten Neuerung: den Lambdas und ihrer Verwendung im JDK. Sie wurden bereits in den Artikeln / KRE / ausführlich besprochen. Der Vollständigkeit halber fassen wir sie nachfolgend noch einmal zusammen. Lambda-Ausdrücke und die "Lambdafication" des JDK
Java als Programmiersprache ist mit den
letzten beiden Releases 6 und 7 nicht nennenswert verändert worden.
Die letzte größere Spracherweiterung hat uns Java 5 mit den Generics
beschert. Nun kommt mit Java 8 wieder eine substantielle Ergänzung der
Sprache in Form von Lambda-Ausdrücken (oder kurz: Lambdas) auf die Java-Entwickler
zu. Zusammen mit den Lambdas sind Methoden- und Konstruktor-Referenzen
und Default-Interface-Methoden als neue Sprachmittel definiert worden.
All diese Neuerungen dienen unter anderem dem Zwecke der Weiterentwicklung
des JDK. Insbesondere der Collection-Framework (im Package
java.util
)
ist in großem Stil überarbeitet worden und unterstützt in Java 8 mit
Hilfe von Streams die parallele Ausführung von sogenannten Bulk Operations;
das sind Operationen, die auf viele oder alle Elemente einer Sequenz angewandt
werden.
Lambda-Ausdrücke
Mit einem
Lambda-Ausdruck
lässt
sich Funktionalität knapp und kurz formulieren. Ein Lambda-Ausdruck
ist so etwas wie eine anonyme Funktion. Hier ist ein Beispiel:
Stream<Person> people = ...
people.filter(
(Person
p)->
{return
p.
speaksEnglish()
;}
);
Der Ausdruck
(Person
p)->
{return
p.
speaksEnglish()
;}
ist der Lambda-Ausdruck. Ein Lambda-Ausdruck besteht aus einer Argumentenliste
(wie bei einer Methode), dem Pfeil-Symbol
'->'
und einem Body mit Anweisungen (ebenfalls wie bei einer Methode). Verglichen
mit einer Methode fehlen der Name, der Returntyp und die
throws
-Klausel.
Ein Name wird nicht benötigt, ähnlich wie bei anonymen Klassen. Der Returntyp
und die
throws
-Klausel werden vom Compiler
automatisch aus der Implementierung des Lambda-Bodys deduziert. Lambda-Ausdrücke
können weiter verkürzt werden. Hier sind Varianten des obigen Beispiels:
people.filter( p->p.speaksEnglish() );
people.filter( Person::speaksEnglish );
Im der ersten Variante ist der Typ des Arguments
entfallen. Das ist erlaubt, sofern der Compiler aus dem Kontext die fehlende
Typinformation ermitteln kann. Der Body ist zu einem einfachen Ausdruck
zusammengeschmolzen. Die zweite Variante
Person::speaksEnglish
ist
eine sogenannte
Methoden-Referenz
. Es gibt auch
Konstruktor-Referenzen
.
Ein Beispiel wäre
ArrayList::new
.
Zu erwähnen ist noch, dass Lambda-Ausdrücke mit Hilfe der Bytecode-Instruktion invokedynamic übersetzt werden. Der statische javac-Compiler beschreibt lediglich, wie ein bestimmter Lambda-Ausdruck in eine ablauffähige Methode übersetzt werden kann. Die tatsächliche Umsetzung wird erst zur Laufzeit von der virtuellen Maschine gemacht. Die JVM kann dann völlig flexibel entscheiden, wie sie es macht und wie sie es optimiert. Default-Methoden
Neben den Lambdas gibt es eine weitere
Sprachneuerung: Interfaces dürfen ab Java 8 auch nicht-abstrakte Methoden
haben. Bislang waren alle Methoden, die in einem Interface definiert
wurden, abstrakt, d.h. das Interface konnte keine Implementierung für
seine Methoden liefern. In Java 8 gibt es nun die Möglichkeit, in einem
Interface sogenannte
Default-Methoden
zu definieren. Diese Default-Methoden
haben eine Implementierung. Hier ist ein Beispiel:
public interface Iterable<T> { Iterator<T> iterator(); public default void forEach(Consumer<? super T> action) { for (T t : this) { action.accept(t); } }
}
Früher hat das Erweitern eines Interfaces
um eine zusätzliche Methode stets dazu geführt, dass alle abgeleiteten
Klassen angepasst und um eine Implementierung für die neue Methode ergänzt
werden mussten. Das ist jetzt mit den Default-Methoden nicht mehr nötig.
Wenn mit einer zusätzlichen Interface-Methode auch gleich eine Default-Implementierung
geliefert wird, dann erben die abgeleiteten Klassen diese Implementierung
und müssen nicht geändert werden.
Die Default-Methoden wurden erfunden, um
den Collection-Framework des JDK zu überarbeiten. Damit wurde beispielsweise
- wie oben im Beispiel gezeigt - das
Iterable
Interface erweitert.
Daneben gibt es im Übrigen - ebenfalls neu in Java 8 - statische Interface-Methoden, auch mit Implementierung. Sie wurden im unseren Beitrag über Interface-Methoden (siehe / JAVA8-3 /) erläutert. Überarbeitung des Collection-Framework
Der Collection-Framework im JDK ist erheblich
erweitert worden mit dem Ziel, eine benutzerfreundliche, einfach zu benutzende
Parallelisierung durch sogenannte Bulk Operations zu unterstützen. Bulk
Operations sind Operationen, die auf mehrere oder alle Elemente der Collection
angewandt werden. Ein Beispiel ist die zuvor schon gezeigte
filter
-Methode.
Stream<Person> people = ...
people.
filter
(Person::speaksEnglish);
Sie wendet auf alle Elemente in einer Sequenz
eine Bewertungsfunktion, das sogenannte Prädikat, an und liefert eine
Sequenz zurück, in der nur noch die "schönen" Elemente vorkommen, d.h.
diejenigen, für die das Prädikat
true
geliefert hat. Die Bulk Operationen werden zusammen mit den neuen Sprachmitteln
verwendet: typischerweise sind die Argumente, die an eine Bulk Operation
übergeben werden, Lambda-Ausdrücke oder Methoden-Referenzen.
Die zentrale neue Abstraktion im Collection-Framework
ist der
Stream
. Es ist das Interface,
in dem die Bulk Operations definiert sind. Beispiele für die Bulk Operations
sind
filter
,
forEach
,
map
und
reduce
. Einen Stream bekommt man,
indem man eine Collection hernimmt und daraus mit der Methode
stream
oder
parallelStream
in einen Stream erzeugt. Die Bulk Operations werden dann - je nachdem,
ob es ein sequentieller oder ein paralleler Stream ist - mit einem Thread
und mit mehreren Threads abgearbeitet. Die Parallelisierung erfolgt automatisch
mit Hilfe des Fork-Join-Frameworks und einem Singleton-Thread-Pool (dem
Common
Pool
, den wir später in diesem Beitrag erläutern werden). Der Common
Pool ist mit Java 8 zum JDK hinzugekommen. Hier ist ein Beispiel für
sequentielle und parallele Abarbeitung:
List<Person> people = new ArrayList<>(...); List<Person> minors = people. stream ().filter(p -> p.age() < 18).collect(toList()); // Zeile 2
people.
parallelStream
().filter(Person::speaksEnglish).forEach(System.out::println);
// Zeile 3
In Zeile 2 suchen wir sequentiell mit einem
Thread alle Minderjährigen aus einer Liste von Personen heraus. Die
filter
-Methode
gibt wieder einen
Stream
zurück, auf
den wir gleich die
collect
-Methode anwenden,
die das Filterergebnis in eine Liste kopiert. Da viele
Stream
-Methoden
wieder einen
Stream
zurückgeben, bildet
man Sequenzen von Operationen, in denen jede Operation auf das Ergebnis
der vorangegangenen aufsetzt. Man bezeichnet diesen Programmierstil als
Fluent
Programming
.
In Zeile 3 suchen wir parallel mit mehreren
Threads alle englischsprechenden Personen heraus und geben das Ergebnis
auf
System
.out
aus. Wie man sieht, ist es ausgesprochen einfach, eine parallele Verarbeitung
anzustoßen; es geht genauso wie die sequentielle Verarbeitung.
Auf das Stream API werden wir in einem der
nächsten Beiträge noch genauer eingehen.
Zu erwähnen ist noch, dass man auch Arrays in Streams verwandeln kann; damit geht dann auch ein paralleles Sortieren von Arrays (siehe JEP 103). Nicht nur die Collections sind überarbeitet worden. Es gibt weitere Abstraktionen im JDK, die so erweitert worden sind, dass sie von den Lambdas profitieren (siehe JEP 109). Ein Beispiel dafür ist die Klasse java.io.BufferedReader . Sie hat jetzt eine lines -Methode, die den BufferedReader in einen Stream<String> verwandelt, sodass man z.B. eine Datei beim Einlesen als Stream von Strings betrachten und sämtliche Bulk Operations darauf anwenden kann. Das Date/Time API
Mit Java 8 gibt es im Package
java.time
und seinen Sub-Package neue Abstraktionen für Datum und Zeit, die eine
Alternative zu den bislang verfügbaren JDK-Abstraktionen
java.util.Date
and
java.util.Calendar
darstellen.
Die beiden alten Abstraktionen
Date
und
Calendar
gibt es seit Java 1.0 bzw. 1.1 und sie haben eine Reihe von bekannten Unzulänglichkeiten.
Dazu gehört die mangelhafte Benutzerfreundlichkeit. Wenn man z.B. den
24. Dezember 2013 darstellen will, dann darf man nicht etwa Folgendes tun:
Date xmas2013 = new Date(2013,12,24,0,0);
sondern man muss es so konstruieren:
int year = 2013 - 1900 ; int month = 12 - 1 ;
Date xmas2013 = new Date(year,month,24,0,0);
Des Weiteren sind Objekte vom Type
Date
und
Calendar
veränderlich.
Es gibt mit den alten Abstraktionen keine Möglichkeit, ein unveränderliches
Datum in irgendeiner Form auszudrücken, denn jedes
Date
-
oder
Calendar
-Objekt
hat
set()
-Methoden. Das hat insbesondere
den Nachteil, dass
Date
und
Calendar
nicht
thread-safe sind.
Die neuen Date/Time-Abstraktionen (siehe
/
JEP150
/) eliminieren diese und viele
andere Unzulänglichkeiten der alten Abstraktionen. Zum Beispiel sind
im
java.
time
-API
die einzelnen Aspekte von Datum und Zeit klarer definiert und voneinander
getrennt. So wird zum Beispiel unterschieden zwischen einem Zeitpunkt
in einem Kontinuum (
java.time
.Instant
)
und der für Menschen natürlichen Vorstellung von Datum (
java.
time
.LocalDate
)
und Zeit (
java.
time
.LocalTime
).
Der Zeitpunkt im Kontinuum ist üblicherweise repräsentiert durch einen
einzelnen Zähler, z.B. die Millisekunden seit dem 1.1.1970; das ist vorteilhaft
für Berechnungen, aber schwer verständlich für Menschen. Die menschliche
Vorstellung von Datum und Zeit wird hingegen dargestellt durch Felder für
Tag-Monat-Jahr Stunde-Minute-Sekunde. In den alten Abstraktionen war
alles miteinander verwoben; in den neuen Abstraktionen ist es getrennt.
Ein weiteres Beispiel für die klarere Struktur des
java.
time
-APIs
sind Klassen wie
java.time.MonthDay
.
Mit
MonthDay
kann man einen Geburtstag
oder Jahrestag ausdrücken, ohne ein bestimmtes Jahr angeben zu müssen.
Man kann also zum Beispiel "Weihnachten" ausdrücken als:
MonthDay xmas = MonthDay.of(Month.DECEMBER,24);
System.out.println("Christmas Eve 2050 will be on
a "+xmas.atYear(2050).getDayOfWeek()+".");
Im Gegensatz zu
Date
und
Calendar
sind die Abstraktionen im
java
.time
-API
unveränderlich und thread-safe. Statt bestehende Objekte zu ändern, werden
stets neue erzeugt. Hier einige Beispiele:
LocalDate xmasEve2013 = LocalDate.of(2013,Month.DECEMBER,24);
LocalDate xmasEve2014 = xmasEve2013.
withYear(2014)
;
LocalDate xmasEveThisYear = LocalDate.of(Year.now().getValue(),Month.DECEMBER,24);
LocalDate xmasEveNextYear = xmasEveThisYear.
plusYears(1)
;
Abgesehen von
with…()
-,
plus()
-
und
minus()
-Methoden unterstützen
LocalDate
und
LocalTime
auch komplexere Anpassungen über
sogenannte
TemporalAdjuster
. Es gibt
eine ganze Reihe von vordefinierten Anpassungen; man kann aber auch nach
Belieben eigene Adjuster implementieren. Hier ein Beispiel, nämlich
die Berechnung des Datums des diesjährigen ersten Advent:
LocalDate firstSundayOfAdvent = xmasEveThisYear.with(TemporalAdjuster.previousOrSame(DayOfWeek.SUNDAY))
.minusWeeks(3));
Wie man sieht, sind die Schnittstellen so
gestaltet, dass man sie im Fluent-Programming-Stil verwenden kann (so ähnlich
wie bei den
Stream
s): die
plusDays
()-
Methode
gibt wieder ein
LocalDate
zurück, auf
das man die Methode
with
()
anwenden kann, die wiederum ein
LocalDate
zurück gibt, auf dass man die nächste Operation anwenden kann.
Natürlich gibt es auch Abstraktionen für
Zeitzonen im
java.
time
-API.
Auch hier ist die Struktur klarer als bei der alten Klasse
java.util.TimeZone
.
Eine Zeitzone besteht aus einem Bezeichner (z.B. Europe/Berlin oder US/Pacific),
einem Offset, der den Unterschied zur Greenwich/UTC-Zeit beschreibt (z.B.
+02:00
oder
-07:00
) und einem Satz von Regeln,
die beschreiben, wann und wie sich der Offset für Sommer- und Winterzeit
ändert. Die alte Klasse
java.util.TimeZone
hat versucht, alle drei Eigenschaften in einer einzigen Klasse abzubilden.
Im
java.
time
-API
sind es drei Abstraktionen, nämlich die Klassen
ZoneId
,
ZoneOffset
und
ZoneRules
aus den Package
java.time
und
java.time.zone
. Das ist klarer;
oft wird ohnehin nur der aktuelle Offset gebraucht. Es ist außerdem
flexibler, nämlich wenn sich die Daylight-Savings-Regeln ändern (wie
z.B. in Russland, wo 2011 die Zeitumstellung abgeschafft wurde).
Die Featureliste ist damit keineswegs erschöpft:
Es werden verschiedene Kalendersysteme unterstützt: ISO, Japan, Minguo
(China), ThaiBuddhist, Hijrah (Islam). Es gibt Abstraktionen für Zeitspannen:
java.time.D
uration
(für z.B. 2.45 sec oder 500 ns) und
java.time.P
eriod
(für z.B. 9 Monate oder 14 Tage). Parsen und Formatieren wird mit Hilfe
der Klasse
java.time.format.DateTimeFormatter
unterstützt. Es gibt auch eine Low-Level-Schnittstelle (im Package
java.time.temporal
),
die es erlaubt, eigene Data-Time-Klassen oder eigene Kalendersysteme zu
implementieren.
Insgesamt besticht das neue
java.
time
-API
durch sein klares, konsistentes und elegantes Design. Es ist benutzerfreundlich
und ergibt gut lesbaren Code. Der maßgebliche Autor des neuen
java.
time
-API
ist Stephen Colebourne, der auch bereits an der JodaTime-Bibliothek (siehe
/
JODA
/) mitgewirkt hat. Seine jahrelange
Erfahrung mit JodaTime ist in das Design und die Implementierung des neuen
java.
time
-API
eingeflossen. Wir werden uns das
java.
time
-API
in einem der nachfolgenden Beiträge noch einmal genauer ansehen.
Neue Features im Zusammenhang mit Concurrency
Die Unterstützung für Concurrent Programming
ist in Java 8 an verschiedenen Stellen erweitert worden. Es gibt einige
neue Concurrency Utilities im Package
java.util.concurrent
.
Der Fork-Join-Pool ist überarbeitet worden. Sogar der Low-Level-Support
im Package
sun.misc
, der vorwiegend für
die Implementierung des JDK und nur selten von "normalen" Java-Entwicklern
benutzt wird, hat Ergänzungen bekommen.
Ergänzungen des Fork-Join-Framework
Der Fork-Join-Framework, der mit Java 7
in den JDK aufgenommen wurde (siehe /
KRE4
/),
hat mit Java 8 noch einmal einige Überarbeitungen und Ergänzungen erfahren.
Dabei geht es um folgende Aspekte:
•
Redesign
der
ForkJoinPool
-Implementierung
•
Common
Pool
•
eine
neue Art von
ForkJoinTask
Redesign der Implementierung
Die Überarbeitungen betreffen den
ForkJoinPool
selbst. Er wurde einem umfassenden internen Redesign und Refactoring
unterzogen. Bislang hatte der
ForkJoinPool
eine einzige SubmitQueue, in der neue externe Aufträge abgelegt wurden,
ehe sie auf die verschiedenen Worker-Threads verteilt wurden. Diese eine
SubmitQueue ist durch eine Vielzahl von SubmitQueues ersetzt worden.
Dadurch lässt sich in Anwendungsfällen mit vielen Pool-Benutzern, die
viele neue externe Aufträgen an den Pool übergeben, der Durchsatz des
ForkJoinPool
s
deutlich erhöhen. Doug Lea hat bei der Ankündigung der Änderung eine
Durchsatzsteigerung um den Faktor 60 genannt (siehe /
LEA
/).
Das Refactoring führt dazu, dass der
ForkJoinPool
nun derjenige Thread-Pool im JDK ist, der am besten auf Plattformen mit
vielen CPU-Cores skaliert. Er ist dem
ThreadPoolExecutor
- also dem Standard-Thread-Pool aus Java 5 - dabei deutlich überlegen.
Anlass für das Redesign des
ForkJoinPool
s
waren Lasttests bei der Entwicklung der Akka-Concurrency-Library. Weitere
Details zu dem Test sowie Vergleichsdaten, die das Verhalten von
ThreadPoolExecutor
und
ForkJoinPool
(Java 8) unter Last
zeigen, finden sich unter /
AKKA
/.
CommonPool
Die Ergänzungen des Fork-Join-Frameworks
betreffen die Unterstützungen der parallelen Streams (siehe JEP 107: Bulk
Data Operations for Collections). Der
ForkJoinPool
ist um eine statische Methode
commonPool()
erweitert worden. Diese Methode liefert eine Singleton-Instanz des
ForkJoinPool
s
zurück, die von allen Operationen der parallelen Streams benutzt wird.
Der CommonPool hat an einigen Stellen ein etwas anderes Verhalten als ein
explizit instanzierter
ForkJoinPool
.
Zum Beispiel haben die Methoden
shu
t
down()
und
shutdownNow()
bei ihm keinen Effekt, weil der Pool bis zur Beendigung der JVM zur Verfügung
stehen muss. Im Rahmen dieses Artikels können wir leider nicht auf alle
Änderungen im Detail eingehen, sondern verweisen auf die Javadoc des
ForkJoinPools
im JDK.
Counted Completer
Bisher gab es zwei Typen von
ForkJoinTask
,
nämlich die
RecursiveTask
und die
Rec
ursiveAction
.
Mit diesen bisherigen rekursiven Tasks wird in der Fork-Phase ein Baum
von Parent- und Child-Tasks aufgebaut, der in der Join-Phase rekursiv zwecks
Einsammeln der Ergebnisse wieder abgearbeitet wird, indem jede Child-Task
per
return
-Statement im
join()
der Parent-Task landet. Nun ist eine weitere Art von
ForkJoinTask
hinzu
gekommen, nämlich der
CountedCompleter
.
Die neue Art von Task hilft, die implizite Baumstruktur explizit als Baum
von Java-Referenzen zwischen Child- und Parent-Tasks zu implementieren.
Der Vorteil der expliziten Baumstruktur ist, dass die vom
CountedCompleter
abgeleiteten Tasks diese Struktur dann auch aus anderen Gründen als einem
join()
traversieren können, z.B. für eine rasche Cancellation. Diese neue
Möglichkeit der Traversierung wird von den Operationen der parallelen
Streams benutzt. Alle parallelen Stream-Operationen sind Tasks, die indirekt
(über die Klasse
java.util.stream.AbstractTask
)
von
CountedCompl
et
er
abgeleitet sind.
Weitere Concurrency Updates
Es gibt eine Reihe von weiteren Ergänzungen
im Package
java.util.concurrent
:
bessere Atomics (Akkumulatoren), ein optimistisches Read-Write-Lock (
StampedLock
)
und ein komfortableres Future (
CompletableFuture
).
Akkumulatoren
Die atomaren Variablen im Package
java
.util.concurrent.atomic
sind
dafür gedacht, performant ohne Synchronisation, aber dennoch thread-sicher
mit vielen Threads auf Variablen zuzugreifen. Normalerweise werden Locks
verwendet, um den konkurrierenden Zugriff auf Variablen zu sichern. Atomare
Variablen hingegen kommen ohne Locks aus. Sie bieten ununterbrechbare
("atomare") Operationen, die auf einer sogenannte CAS-Instruktion (CAS
= Compare-And-Swap) beruhen. Damit sind die atomaren Variablen in manchen
Situation performanter als normale mit Lock geschützte Variablen.
Wenn aber sehr viele Threads sehr häufig
auf ein und dieselbe atomare Variable zugreifen, dann kann auch sie zum
Engpass werden. Das passiert beispielsweise, wenn eine skalare Variable
wie ein
Atomic
Long
als Zähler verwendet wird, auf den ständig zahlreichen Threads zugreifen
wollen. Mit Java 8 hat man für solche Situationen eine besser skalierende
Alternative geschaffen: Es gibt nun die Akkumulatoren
LongAdder
,
DoubleAdder
,
LongAccumulator
und
DoubleAccumulator
.
Dabei handelt es sich um skalare atomare Variablen, die logisch betrachtet
ähnlich wie z.B. ein
AtomicLong
funktionieren und das atomare Addieren, Inkrementieren und Dekrementieren
unterstützen. Sie haben aber anders als die atomaren Variablen keine
compareAndSet
-Methode
und sie sind intern anders organisiert.
Während ein
AtomicLong
aus genau einer Speicherzelle besteht, auf die atomar per CAS zugegriffen
wird, besteht ein
LongAdder
aus mehreren Speicherzellen. Wenn Konkurrenz an einer Speicherzelle herrscht,
dann wird einfach eine andere Speicherzelle verwendet, die ohne Kollision
verfügbar ist. Der Wert von einem
LongAdder
ist also über mehrere Zellen verteilt und wird erst ausgerechnet, wenn
man ihn mit der
sum
-
oder
longValue
-Methode
anfordert. Das braucht zwar mehr Speicher, reduziert aber die Kollisionen
und ist in Situationen mit heftiger Konkurrenz schneller als eine herkömmliche
atomare Variable.
Die Akkumulatoren sind Verallgemeinerungen der Adder . Beim Adder wird addiert; beim Accumulator kann man die Akkumulierungsfunktion frei wählen. Es könnte also auch eine Multiplikation oder eine andere Art von Akkumulation sein. StampedLock
Es gibt eine neue Art von Lock, das
StampedLock
.
Es ist eine Alternative zum
ReentrantReadWriteLock
,
das es schon seit Java 5 gibt. Beim
ReentrantReadWriteLock
kann man ein Write-Lock anfordern, das exklusiven Zugriff auf die betreffenden
Daten gibt, d.h. alle anderen Reader und Writer müssen warten. Man kann
auch eine Read-Lock anfordern, das dafür sorgt, dass man sich die Daten
lediglich mit anderen Reader-Threads teilen muss und dass alle Writer-Threads
warten müssen. Sowohl das Read- als auch das Write-Lock sind pessimistisch,
d.h. andere Threads müssen warten, weil sie durch das Lock blockiert werden.
Das
StampedLock
hat nun eine optimistische Form von Read-Lock. Man versucht den lesenden
Zugriff auf die fraglichen Daten mit Hilfe der
tryOptimisticRead
-Methode.
Weil es optimistisch ist, blockiert es die anderen Threads nicht und es
kann während des Lesens konkurrierende Schreibzugriffe geben. Man
muss also nach den Lese-Operationen nachschauen, ob der Optimismus gerechtfertigt
war. Wenn während des Lesens keine Schreibzugriffe waren, dann hat man
den Lesezugriff erfolgreich erledigt. Wenn während des Lesens doch ein
konkurrierender Schreibzugriff passiert ist, dann geht man zum pessimistischen
Locking über und holt sich mit
readLock
ein "normales" pessimistisches Read-Lock. Auf ein solche Lock muss man
ggf. warten; dafür ist es dann aber sicher und blockiert konkurrierende
Writer-Threads blockiert.
Hier ist ein Code-Beispiel für das oben
beschriebene optimistische Lesen:
public class NumberRange { // INVARIANT: lower <= upper private int lower = 0; private int upper = 0; private final StampedLock lock = new StampedLock(); ... public int[] getRange() { long stamp = lock.tryOptimisticRead(); // optimistischer Leseversuch int l = lower; int u = upper; if (lock.validate(stamp)) { // falls geglückt ... return new int[] {l,u}; // ... fertig ! else { // falls missglückt ... stamp = lock.readLock(); // ... pessimistisches Lock holen try { // ... und nochmal lesen l = lower; u = upper; return new int[] {l,u}; } finally { lock.unlockRead(stamp); } } }
}
Analog kann man auch eine Art optimistischen
Upgrade von Read-Lock auf Write-Lock machen. Man kann natürlich auch
ganz normal pessimistisch "locken", wie auch mit dem alten
ReentrantReadWriteLock
.
Das optimistische Lesen lohnt sich für kurze Lese-Sequenzen, die einfach nur Daten lesen und in lokale Variablen kopieren, um sie anschließend zu verarbeiten. Bei solchen kurzen Lese-Operationen hat das optimistische Lock gute Chancen, häufig erfolgreich abzulaufen. In solchen Situation ist der Durchsatz und die Performance mit einem optimistischen Lock besser als mit einem pessimistischen. CompletionStage / CompletableFuture
Bisher gab es im JDK nur das Interface
Future<T>
,
mit dessen überladener Methode
get()
man auf Ergebnisse warten konnte. Mit Java 8 kommt nun das Interface
CompletionStage<T>
neu hinzu, das mit mehr als 35 Methoden neue Funktionalität anbietet,
um mit einem Fluent-Programming-Stil auf Ergebnisse zu reagieren. Die
neue Klasse
CompletableFuture<T>
implementiert die beiden Interfaces
Future<T>
und
CompletionStage<T>
.
Schauen wir uns an einem Beispiel an, wie man mit dem
CompletableFuture<T>
arbeitet.
ExecutorService myPool = Executors.newFixedThreadPool(4);
Supplier<String> task = () -> readString();
try { CompletableFuture.supplyAsync(task, myPool) // Zeile 5 .thenAccept(s ->System.out.println("->" + s + "<- read")) // Zeile 6 .get(); // Zeile 7 } catch (InterruptedException e) { // do nothing } catch (ExecutionException e) { System.err.println("problem: " + e.getCause());
}
myPool.shutdown();
Bisher ist es so, dass man ein
Future
zurückbekommt, wenn man mit
submit()
eine Task in Form eines
Runnable
oder
Callable
an einen
ExecutorService
übergibt. Mit diesem
Future
kann man prüfen, ob die übergebene Task erfolgreich ausgeführt worden
ist und in dem Fall, dass die Task ein
Callable
ist, zusätzlich auch noch das Ergebnis abholen.
Wie geht das Ganze nun mit einem CompletableFuture ? Erst einmal muss man sich ein CompletableFuture besorgen. Dafür hat die Klasse CompletableFuture statische Factory-Methoden wie supplyAsync() . Die Parameter dieser Factory-Methode sind die Task, die ausgeführt werden soll, und der ExecutorService , der sie ausführen soll (siehe Zeile 5).
Bemerkenswert ist dabei, dass unsere Task,
die ein Ergebnis vom Typ
String
produziert, vom Typ
Supplier<String>
ist und nicht
Callable<String>
,
wie man vielleicht erwarten würde. Der Grund dafür ist folgender: Ein
Callable
darf Checked-Exceptions werfen, aber Checked-Exceptions vertragen sich
nicht gut mit dem Fluent-Programming-Stil aus der Funktionalen Programmierung
(siehe /
CHKEXC
/).
Zurück zu der Factory-Methode
supplyAsync()
in Zeile 5, die ein
CompletableFuture<String>
zurückgibt. Statt, wie bei einem
Future
nur auf das Ergebnis zu warten und es abzuholen, hängen wir mit
thenAccept()
eine weitere Task ein, die auf das Ergebnis angewendet werden soll, wenn
es dann da ist (siehe Zeile 6). In unserem einfachen Beispiel macht
diese weitere Task nur die Ausgabe auf
System.out
.
Auch diese zweite Task wird von einem Thread aus
myPool
ausgeführt.
Das
thenAccept()
gibt wieder ein
CompletableFuture
zurück.
Diesmal ist es ein
CompletableFuture<Void>
,
da die zweite Task kein Ergebnis produziert. Auf dem
CompletableFuture
rufen wir
get()
(aus dem
Future
Interface) auf, um zu prüfen, ob eine Exception bei der Verarbeitung aufgetreten
ist (siehe Zeile 7). Falls es zu einer Exception gekommen ist, wird diese
- wie beim
Future
üblich - in einer
ExecutionException
verpackt von
get()
geworfen. Diese Fehlerprüfung bezieht sich nicht nur auf das
thenAccept()
,
sondern auch auf das vorangegangene
supplyAsync()
,
da Exceptions aus einer vorderen Stufe über alle nachfolgenden Stufen
weiterpropagiert werden.
Man sollte sich an dieser Stelle im Klaren
darüber sein, dass der Aufruf von
get()
dazu führt, dass der aufrufende Thread wartet, bis unsere beiden Tasks
im Pool ausgeführt worden sind (oder eine der Tasks mit einer Exception
abgebrochen wurde). Das ist genauso wie beim alten
Future
.
Dies war nur ein kleines Beispiel für das,
was mit
CompletableFuture
möglich ist. Wie oben schon erwähnt ist das API dieser Klasse sehr
umfangreich, so dass es Sinn macht, sich die Javadoc dieser Klasse noch
mal genauer anzusehen. Erwähnenswert ist noch, dass es auch Methoden
gibt, mit denen es möglich ist, zwei
CompletableFutures
so miteinander zu verknüpfen, dass die
Completion
eines oder beider
CompletableFutures
eine neue Task triggert.
Zu guter Letzt: ConcurrentHashMap
Die
ConcurrentHashMap
ist analog zu den "normalen" Collections um Bulk Operations erweitert worden.
Sie hat jetzt beispielsweise Methoden wie
forEachEntrySequentially
und
forEachEntryInParallel
, und viele
andere.
Neben den High-Level Concurrency Utilities im package java.util.concurrent ist auch der Low-Level Concurrency Support ergänzt worden. Neues im Low-Level Concurrency Support
Der JDK hat im Package
sun.misc
Abstraktionen, die Concurrency auf dem untersten Level unterstützen.
Diese Abstraktionen werden für die Implementierung der JDK-Klassen verwendet
und sind nicht für den täglichen Gebrauch gedacht. Sie sind plattform-spezifisch,
native, hoch effizient, aber "unsafe". Die im Zusammenhang mit Concurrency
am häufigsten verwendete Low-Level-Abstraktion ist die Klasse
sun.misc.Unsafe
.
Diese Klasse ist ein Sammelbecken für alle möglichen Low-Level-Operationen.
(Wer sich näher für
sun.misc.Unsafe
interessiert, dem sei /
UNSAFE
/ als
Einstieg empfohlen.) Java 8 liefert in diesem Low-Level-Bereich zwei
Neuerungen: die Annotation
@Contended
und Memory Fences. Wir wollen sie an dieser Stelle nicht erläutern,
weil beide Themen intensive Kenntnis des Java-Memory-Modells voraussetzen.
Wer sich für die Details interessiert, findet Informationen zu
@Contended
unter /
JEP142
/ und /
SHIPIL
/
und zu Memory Fences unter /
JEP171
/.
Annotations
Die Annotationen sind als neues Sprachmittel
in Java 5 hinzugekommen. In Java 8 hat es Syntax-Änderungen im Zusammenhang
mit der Verwendung von Annotationen im Source-Code gegeben und es gibt
Neues bei der Verarbeitung von Source-Code-Annotationen.
Type Annotations
Die Verwendung von Annotationen wird in
Java 8 gelockert, so dass Annotation als Typ-Qualifizierungen verwendet
werden können, auf deren Basis Type-Checker-Werkzeuge Überprüfungen
machen und Fehler finden können.
Bis Java 7 war die Verwendung von Annotationen
auf Deklarationen (von Typen, Methoden, Variablen, Parametern, Packages,
etc.) beschränkt. Die Verwendung von Annotation an anderen Stellen war
syntaktisch nicht zulässig. Beispielsweise war Folgendes in Java 7 illegal:
private List<
@NonNull
String> list = new ArrayList<>(); // illegal in Java 7; permitted
in Java 8
Die Annotation
@NonNul
l
bezieht
sich auf den Typ
String
und der annotierte
Typ "
@NonNull String
" wird als Typparameter
in einem generischen Typ benutzt. Das ist eine Verwendung von Annotationen,
die der Compiler in Java 7 nicht akzeptiert. Diese Restriktion entfällt
mit Java 8. Die Syntax von Java ist so geändert worden, dass Typen überall
mit Annotationen versehen werden können - egal, ob die annotierten Typen
in Deklarationen oder anderswo auftauchen. Hier sind einige Beispiele
der Verwendung von Annotationen, die in Java 7 illegal und in Java 8 erlaubt
sind:
als Typparameter : Map< @NonNull String, @NonEmpty List< @Readonly Document>> files; in throws-Klauseln: void monitorTemperature() throws @Critical TemperatureException { ... } beim Supertyp : class UnmodifiableList<T> implements @Readonly List< @Readonly T> { ... }
in
Casts
:
myString = (
@NonNull
String) myObject;
Sogar der Bounds-Typ eines Wildcards und
der Elementtyp eines Arrays können in Java 8 annotiert werden. Die Details
findet man in der Spezifikation (siehe /
ANNOT
/).
Die im obigen Beispiel verwendeten Annotationen sind im Übrigen
nicht
Bestandteil von Java 8; lediglich die Möglichkeit, beliebige Annotationen
an den gezeigten Stellen zu verwenden, ist in Java 8 (im Gegensatz zu Java
7) syntaktisch erlaubt. Es stellt sich die Frage: wofür braucht man
diese Syntax-Änderung?
Das Ziel der Syntax-Lockerung ist es - wie
eingangs schon angedeutet - Annotationen als Typ-Qualifizierungen verwenden
zu können. Annotationen wie
@NonNull
,
@Nullable
,
@Mutable
,
@Immutable
,
@ReadOnly
,
etc. beschreiben Eigenschaften von Typen, die sich mit Java-Sprachmitteln
nicht ausdrücken lassen. Mit Hilfe solcher Typ-Annotationen wird quasi
das Typsystem von Java erweitert, ohne dass die Sprache selbst geändert
werden muss. Die Idee ist, dass der Entwickler seinen Source-Code mit
Typ-Annotationen ausstattet und ein Werkzeug anschließend die zusätzliche
Typinformation verwendet, um Überprüfungen zu machen und Fehler zu finden.
Beispielsweise kann ein Tool prüfen, ob eine Methode, die ein
@NonNull
-Argument
benötigt, auch tatsächlich stets mit
@NonNull
-Referenzen
versorgt wird. Solche Überprüfungen können die IDEs machen; IntelliJ
zum Beispiel bietet schon seit Langem die Annotationen
@NotNull
und
@Nullable
für solche Zwecke an.
Typprüfungen können aber auch von anderen Type-Checker-Werkzeugen gemacht
werden. Ein Beispiel für einen frei verfügbaren TypeChecker findet
man unter /
TYPCHK
/.
Wie oben schon erwähnt: Weder die Annotationen wie @NonNull , @Nullable , @Mutable , @Immutable , @ReadOnly , etc. noch die Type-Checker-Werkzeuge sind Bestandteil von Java 8. Lediglich die Möglichkeit, Annotationen als Typ-Qualifizierungen verwenden zu können, kommt mit Java 8. Alles andere kommt von Third-Party-IDE- bzw. Werkzeug-Herstellern. Repeatable Annotations
Gelegentlich stößt man auf Annotationen,
die mehrfach an einer Klasse, Methode, etc. verwendet werden sollen.
Dieselbe Annotation mehrfach zu verwenden, wurde bislang mit einer Fehlermeldung
wegen "duplicate annotation" abgewiesen. Um dies doch zu erlauben, hat
man sich für Java 8 einen Mechanismus einfallen lassen, mit dem Duplikate
in eine Container-Annotation eingepackt werden. Hier ist ein Beispiel:
@interface Change { String date(); String reason();
}
@Change(date="April 16, 2013", reason="fixed rfe #391") @Change(date="April 15, 2013", reason="initial setup")
public class SomeClass { ... }
Wir haben eine
@Change
Annotation definiert, die mehrfach vorkommen darf. In Java 7 geht das
nicht; der Compiler meldet einen Fehler, wenn wir sie mehrfach verwenden.
In Java 8 kann man die
@Change
Annotation "repeatable" machen. Dafür gibt es eine Meta-Annotation
@Repeatable
,
in der man eine Container-Annotation spezifizieren muss. Die Container-Annotation
muss ein Array der fraglichen "repeatable" Annotation enthalten. Wir
müssten unsere
@Change
Annotation also so deklarieren:
@Repeatable(ChangeLog.class) @interface Change { String date(); String reason();
}
wobei
ChangeLog
die Container-Annotation ist:
@interface ChangeLog { Change[] value();
}
Wenn der Compiler eine wiederholbare Annotation mehrfach vorfindet, verpackt er sie in ein Array und macht aus den wiederholten Annotation eine Container-Annotation. Annotation Processing Tool (apt) fällt weg
In Java 5 wurde zusammen mit dem neuen
Sprachkonstrukt der Annotations auch ein Werkzeug für die Analyse und
Verarbeitung von Annotations im Source-Code zur Verfügung gestellt: das
Annotation Processing Tool (kurz:
apt
).
Die Funktionalität des
apt
-Tools wurde
in Java 6 in den Compiler eingebaut. Seitdem kann man sogenannte Compiler-Plugins
implementieren, die Annotations im Source-Code verarbeiten. Da das
apt
-Tool
seit Java 6 obsolet ist, wird es nun mit Java 8 aus dem JDK entfernt.
Adapter von java.lang.reflect zu javax.lang.model
Wer Annotation selber verarbeiten möchte
und entsprechende Compiler-Plugins implementiert, bedient sich des
javax.lang.model
-APIs.
Es wurde in Java 6 für den Zweck des Annotation-Processings per Compiler-Plugin
definiert. Das
javax.lang.model
-API
ist eine Schnittstelle, welche die im Source-Code definierte Typinformation
aufbereitet und (insbesondere für das Auffinden von Annotationen) zugänglich
macht. Sie liefert zum Beispiel die Information, welche Klasse in der Source-Datei
definiert wird, wie die Klasse heißt, wie sie aussieht (z.B. welche Felder
und Methoden sie hat), welche Modifier sie hat (
static
,
final
,
abstract
,
etc.), ob sie Annotationen hat und vieles mehr.
Ähnliche Information ist auch über Reflection
zugänglich. Auch per Reflection wird Information geliefert z.B. über
eine Klasse, deren Namen, Modifier, Annotationen, Felder und Methoden.
Allerdings geht es dabei nicht um statische, im Source-Code enthaltene
Typinformation, sondern um die Typen, die zur Laufzeit in die JVM geladen
wurden. Prinzipiell ist die Information aber ähnlich und man kann ähnliche
Logik darauf aufsetzen. Leider ist die dynamische Typinformation per Reflection
über eine andere Schnittstelle zugänglich, nämlich das
java.lang.reflect
-API.
Deshalb muss man eine Logik, die man wieder verwenden will, doppelt implementieren
- einmal mit dem
javax.lang.model
-API
und noch einmal mit dem
java.lang.reflect
-API
- nur weil die Schnittstellen unterschiedlich sind.
Für Java 8 wurde nun exemplarisch eine Implementierung des javax.lang.model -APIs basierend auf Reflection gemacht (siehe u.a. / MODEL /). Ob diese Beispiel-Implementierung in einer der zukünftigen Java-Version eine richtige JDK-Komponente wird, ist derzeit noch nicht klar; im Moment ist es nur ein Beispiel, das man als Vorlage für eigene Implementierungen hernehmen kann. Im Prinzip handelt es sich dabei um einen Adapter, der das java.lang.reflect -API auf das javax.lang.model -API abbildet. In diesem Zuge wurden einige Ergänzungen im Reflection-API gemacht. So gibt es zum Beispiel nun im Package java.lang.reflect eine Klasse Executable , die die Gemeinsamkeiten von Methoden und Konstruktoren abbildet - eine Abstraktion, die man schon lange vermisst hat. Garbage CollectionKeine Permanent Generation mehrDie größte Änderung, die im Bereich Garbage Collection mit Java 8 kommt, ist das Entfernen der Permanent Generation (oder kurz: Perm Generation ) aus der JVM. Die Daten, die bisher in der Perm Generation vorhanden waren, wandern in einen neuen Bereich im Native Memory: dem Metaspace . (Genau genommen gibt es einige wenige Daten aus der Perm Generation, die nicht in den Metaspace wandern, sondern auf den Java Heap kommt. Wir gehen hier nicht weiter auf sie ein.)
Bisher war die Perm Generation der Bereich,
in dem die Information über die geladenen Java Klassen (d.h. die Klassen-Meta-Daten)
sowie internalisierte
String
s
und statische Variablen gespeichert wurden. Das Problem dabei war, dass
die JVM nach internen Heuristiken eine maximal Größe für diesen Bereich
bestimmt hat. Wenn beim Ablauf des Programms diese Größe nicht ausreichte,
brach die JVM den Prozess mit einem
java.lang.OutOfMemoryError:
PermGen space
ab. Dann blieb einem nicht anderes, als mit der
JVM-Option
-XX:MaxPermSize
die Perm Generation explizit zu vergrößern, bis das Programm ohne Probleme
lief. Dies war meist aber nur eine temporäre Lösung. Denn, da Programme
im Rahmen ihrer Weiterentwicklung wuchsen und dabei neue Klassen hinzukamen
und mehr Speicher in der Perm Generation benötigt wurde, trat der
OutOfMemoryError
der Perm Generation erneut auf. Was dazu führte, dass die
MaxPermSize
wieder erhöht werden musste.
Das Entfernen der Perm Generation und die Verschiebung der Daten in den Metaspace im Native Memory schafft hier Abhilfe, denn der Metaspace kann nun beliebig wachsen. Dieses Anwachsen vergrößert natürlich den JVM-Prozess. Irgendwann wird dann der physikalische Speicher vielleicht nicht ausreichen und der Prozess teilweise herausgeswappt, was seinen Ablauf erheblich verlangsamt. Aber ein Abbrechen der JVM mit OutOfMemoryError , weil der Speicher für die Metadaten der Klassen, statische Variablen, usw. nicht ausreicht, tritt ab Java 8 nicht mehr auf.
Genau genommen benötigt der letzte Satz
noch eine Ergänzung. Es gibt jetzt nämlich mit Java 8 die JVM-Option
-XX:MaxMetaspaceSize
.
Hiermit kann die Größe des Metaspace explizit beschränkt werden. Wählt
man diese Größe nun zu klein, kommt auch weiterhin der
OutOfMemoryError
.
Das kann natürlich aber auch Absicht sein, weil man den Abbruch mit Error
einem quälend langsamen Ablauf mit Swapping vorzieht; nach dem Motto:
Besser eine Ende mit Schrecken, als ein Schrecken ohne Ende.
Sollte man weiterhin die Option
-XX:MaxPermSize
beim Start der JVM verwenden, so erhält man eine Warnung:
ignoring
option MaxPermSize=128M; support was removed in 8.0
die einen daran erinnert, dass es mit Java 8 keine Perm Generation mehr
gibt.
Natürlich gibt es auf dem Metaspace (ähnlich
wie früher auf der Perm Generation) Garbage Collection. Das heißt,
die Metadaten einer Klasse wie auch ihre statischen Variablen können aus
dem Metaspace wieder entfernt werden, wenn es keine Verweise mehr auf
die Klassen bzw. die Variablen gibt. Da Klassen von ihrem Class Loadern
referenziert werden, bedeutet dies, dass es auch auf den Class Loader keine
Referenzen mehr geben darf (d.h. der Class Loader nicht mehr genutzt wird),
damit die Metadaten zu seinen Klassen aus dem Metaspace entfernt werden
können.
In dem Garbage Collection Logging, das mit
der JVM-Option
-verbose:gc
eingeschaltet werden kann, gibt es nun auch Informationen zum Metaspace.
Verwendet man zusätzlich noch die JVM Option
-XX:+PrintGCDetails
,
so wird bei einer Full Collection die Größe des Metaspace angezeigt:
[ GC (Allocation Failure) [DefNew: 4481K->1K(4992K), 0.0002880 secs] 9800K->5320K(15936K), 0.0003200 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [DefNew: 4481K->1K(4992K), 0.0003164 secs] 9800K->5320K(15936K), 0.0003648 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation
Failure) [DefNew: 2821K->32K(4992K), 0.0002616 secs][Tenured: 5319K->818K(10944K),
0.0023344 secs] 8140K->818K(15936K),
Metaspace:
2629K->2629K(2686K/6528K)]
,
0.0026569 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Verwendet man die JVM Option -
XX:+PrintHeapAtGC
,
so wird bei jeder Garbage Collection die Größe beider Metaspace-Bereiche
(Daten und Klassen-Metadaten) getrennt ausgewiesen:
{Heap before GC invocations=394 (full 0): def new generation total 4992K, used 4480K [0x04b10000, 0x05070000, 0x0a060000) eden space 4480K, 100% used [0x04b10000, 0x04f70000, 0x04f70000) from space 512K, 0% used [0x04ff0000, 0x04ff03b8, 0x05070000) to space 512K, 0% used [0x04f70000, 0x04f70000, 0x04ff0000) tenured generation total 10944K, used 1185K [0x0a060000, 0x0ab10000, 0x14b10000) the space 10944K, 10% used [0x0a060000, 0x0a1885c0, 0x0a188600, 0x0ab10000) Metaspace total 2686K, used 2629K, reserved 6528K data space 2401K, used 2361K, reserved 4480K class space 284K, used 268K, reserved 2048K Heap after GC invocations=395 (full 0): def new generation total 4992K, used 1K [0x04b10000, 0x05070000, 0x0a060000) eden space 4480K, 0% used [0x04b10000, 0x04b10000, 0x04f70000) from space 512K, 0% used [0x04f70000, 0x04f706d8, 0x04ff0000) to space 512K, 0% used [0x04ff0000, 0x04ff0000, 0x05070000) tenured generation total 10944K, used 1185K [0x0a060000, 0x0ab10000, 0x14b10000) the space 10944K, 10% used [0x0a060000, 0x0a1885c0, 0x0a188600, 0x0ab10000) Metaspace total 2686K, used 2629K, reserved 6528K data space 2401K, used 2361K, reserved 4480K class space 284K, used 268K, reserved 2048K
}
Daneben können Informationen über den Metaspace
auch über eine MemoryPoolMXBean mit dem Namen
Metaspace
ermittelt
werden.
Bei der Garbage Collection gibt es weitere
kleinere Verbesserungen und Veränderungen. So hat sich der Logging-Output
bei allen Garbage Collectoren geändert. Neben anderen Änderungen wird
jetzt immer der Grund für das Starten des Garbage Collectors ausgegeben:
[GC ( Allocation Failure ) 5668K->1187K(15936K), 0.0002539 secs] [GC ( Allocation Failure ) 5667K->1187K(15936K), 0.0002413 secs] [GC ( Allocation Failure ) 5667K->1187K(15936K), 0.0003035 secs]
[GC (
Allocation
Failure
)
5667K->1187K(15936K), 0.0002958 secs]
Da dies in den allermeisten Fällen Allocation Failure ist, wirkt die Information ein wenig redundant. Wenig genutzte GC-Kombinationen verschwindenDrei wenig genutzte Garbage-Collector-Kombinationen werden ab Java 8 nicht mehr unterstützt:• DefNew + CMS • ParNew + SerialOld
•
Incremental
CMS
Damit soll die Entwicklungskapazität weg von der Wartung alter, wenig genutzter Features und hin zu neuen Features verschoben werden. Verbesserte Typ-Deduktion für Generische Methoden
Dabei geht es um die Deduktion der Typparameter
von generischen Methoden. Wenn eine generische Methode aufgerufen wird,
muss man - anders als bei generischen Klassen - die Typparameter in der
Regel nicht angeben. Der Compiler versucht, sie aus dem Kontext automatisch
zu deduzieren. Dazu schaut er zuerst die Typen der Argumente an, die
beim Methodenaufruf übergeben werden. Wenn er daraus keine Erkenntnisse
ziehen kann, z.B. weil die Methode gar keine Argumente hat, dann schaut
der Compiler den umgebenden Kontext an. Bislang war der einzige dafür
mögliche Kontext die Zuweisung. Hier ein Beispiel:
class Util { static <Z> List<Z> nil() { return new ArrayList<>(); };
}
List<String> list = Util.nil(); //1 fine
list = Collections.synchronizedList(Util.nil());
//2 error in Java 7, fine in Java 8
Der Aufruf in Zeile //1 funktioniert schon,
seit es generische Methoden gibt. Der Compiler schaut die linke Seite
der Zuweisung an und stellt fest, dass der Typparameter Z:=String sein
muss. Zeile //2 hat der Compiler jedoch bislang nicht gemocht. Dort
taucht der Aufruf der generischen
nil
-Methode
als Argument in einem Methodenaufruf auf und nicht als linke Seite einer
Zuweisung. Der Aufrufkontext war bislang kein Kontext, der für die Deduktion
des Typparameters verwendet wurde. Genau das hat sich geändert.
In Java 8 lässt sich jetzt auch Zeile //2 ohne Fehler übersetzen.
Dieselbe Typededuktion wird auch für den sogenannten Diamond-Operator
angewandt, sodass sich auch dort die Typdeduktion verbessert hat:
List<String> list = new ArrayList<>(); //1 fine
list = Collections.synchronizedList(new
ArrayList<>()); //2 error in Java 7, fine in Java 8
Reflection für die Namen von Methodenparametern
Bislang wurde zu den Parametern einer Methode
nur der Typ und die Position in den Bytecode übernommen. Der Name, den
der Methodenparameter im Source-Code hatte, geht dabei verloren. Das
hat sich mit Java 8 geändert. Wenn man seine Sourcen mit der Compiler-Option
-parameters
übersetzt hat, dann kann man zu den Methoden in diesen Sourcen später
per Reflection die Namen der Methodenparameter ermitteln. Hier ist ein
Beispiel:
public class MethodParameterNames { public static void main(String... whichNameDidWeUseHere) throws NoSuchMethodException { Method method = MethodParameterNames.class.getDeclaredMethod("main",String[].class); Parameter [] methodParameters = method. getParameters (); for (Parameter p : methodParameters) { System.out.println(p); } }
}
Die main-Method inspiziert sich selbst per
Reflection und gibt Informationen über ihr Argument aus. Neu sind dabei
die Methode
getParameters
in der Klasse
Method
und die Klasse
Parameter
im Package
java.lang.reflect
.
Wenn man diese Klasse mit
javac
-parameters
MethodParameterNames.java
übersetzt hat und anschließend das Programm
ausführt, macht es folgende Ausgabe:
java.lang.String... whichNameDidWeUseHere
Wie man sieht, ist zu dem Parameter neben
seinem Typ auch sein Name verfügbar. Dazu natürlich - wie früher auch
- die Modifier und die Annotations, falls vorhanden.
Zusammenfassung
Java 8 ist ein größeres Release mit zahlreichen
Neuerungen. Wir haben in diesem Beitrag die wesentlichen Neuerungen kurz
vorgestellt:
•
Lambda-Ausdrücke
und Methoden-Referenzen als Notation für anonyme Funktionen
•
Default-Methoden
für die Erweiterung von Interfaces
•
Streams
als Erweiterung des Collection-Frameworks für sequentielle und parallele
Bulk Operations
•
ein
neues Date/Time API
•
Ergänzungen
des Fork-Join-Frameworks (Redesign der Implementierung, CommonPool für
die parallelen Bulk Operations und Counted Completer als alternative ForkJoinTask)
•
neue
Concurrency Utilities (Akkumulatoren als Alternative zu AtomicInteger,
StampedLock als optimistisches ReadWriteLock und Counted Completer als
Future mit funktionaler Schnittstelle)
•
Lockerung
der Syntax für Annotations (um Typ-Qualifizierungen und Type-Checker-Tools
zu ermöglichen)
•
Garbage
Collection Änderungen (Eliminierung der Permanent Generation sowie einiger
selten genutzten GC-Kombinationen)
•
Änderungen
im javac-Compiler (verbesserte Typ-Deduktion für generische Methoden)
•
Ergänzung
der Reflection (zum Auffinden der Namen von Methodenparametern )
Diverse Literaturverweise
Die gesamte Serie über Java 8:
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
© Copyright 1995-2018 by Angelika Langer. All Rights Reserved. URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/73.Java8.Overview/73.Java8.Overview.html> last update: 26 Oct 2018 |