Angelika Langer - Training & Consulting
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | Twitter | Lanyrd | Linkedin
 
HOME 

  OVERVIEW

  BY TOPIC
    JAVA
    C++

  BY COLUMN
    EFFECTIVE JAVA
    EFFECTIVE STDLIB

  BY MAGAZINE
    JAVA MAGAZIN
    JAVA SPEKTRUM
    JAVA WORLD
    JAVA SOLUTIONS
    JAVA PRO
    C++ REPORT
    CUJ
    OTHER
 

GENERICS 
LAMBDAS 
IOSTREAMS 
ABOUT 
CONTACT 
Effective Java

Effective Java
Java 8
Übersicht über die Neuerungen in Java 8
 

Java Magazin, März 2014
Klaus Kreft & Angelika Langer

Dies ist die Überarbeitung eines Manuskripts für einen Artikel, der im Rahmen einer Kolumne mit dem Titel "Effective Java" im Java Magazin erschienen ist.  Die übrigen Artikel dieser Serie sind ebenfalls verfügbar ( click here ).

 

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.
 
 
 
Java 8 Features
 
Lambdas & Lambdafication
JEP 126 
Lambda Expressions & Virtual Extension Methods
JEP 160
Lambda-Form Representation for Method Handles
JEP 103 
Parallel Array Sorting
JEP 107 
Bulk Data Operations for Collections
JEP 109
Enhance Core Libraries with Lambda
 
 
Date/Time
JEP 150
Date & Time API
 
 
Concurrency
JEP 155 
Concurrency Updates
JEP 142
Reduce Cache Contention on Specified Fields
JEP 171
Fence Intrinsics  
 
 
Annotations
JEP 104
Annotations on Java Types
JEP 120
Repeating Annotations
JEP 117
Remove the Annotation-Processing Tool (apt)
JEP 119
javax.lang.model Implementation Backed by Core Reflection
 
 
Garbage Collection
JEP 122
Remove the Permanent Generation
JEP 173
Retire Some Rarely-Used GC Combinations
 
 
Language & Type System
JEP 101
Generalized Target-Type Inference
 
 
Reflection
JEP 118
Access to Parameter Names at Runtime
 
 
Compiler (javac )
JEP 138
Autoconf-Based Build System
JEP 139
Enhance javac to Improve Build Speed
Modularization
JEP 161
Compact Profiles
JEP 162
Prepare for Modularization
JEP 148
Small VM
JEP 179
Document JDK API Support and Stability
Runtime
JEP 136
Enhanced Verification Errors
JEP 147
Reduce Class Metadata Footprint
JEP 149
Reduce Core-Library Memory Usage
JEP 178
Statically-Linked JNI Libraries
JavaScript
JEP 174
Nashorn JavaScript Engine
Library
JEP 177
Optimize java.text.DecimalFormat.format
JEP 180
Handle Frequent HashMap Collisions with Balanced Trees
JEP 184
HTTP URL Permissions
JEP 170
JDBC 4.2
JEP 185
JAXP 1.5: Restrict Fetching of External Resources
Internationalization
JEP 127
Improve Locale Data Packaging and Adopt Unicode CLDR Data
JEP 128
BCP 47 Locale Matching
JEP 133
Unicode 6.2
JEP 112
Charset Implementation Improvements
Security
JEP 164
Leverage CPU Instructions for AES Cryptography
JEP 113
MS-SFU Kerberos 5 Extensions
JEP 114
TLS Server Name Indication (SNI) Extension
JEP 115
AEAD CipherSuites
JEP 121
Stronger Algorithms for Password-Based Encryption
JEP 123
Configurable Secure Random-Number Generation
JEP 124
Enhance the Certificate Revocation-Checking API
JEP 129
NSA Suite B Cryptographic Algorithms
JEP 130
SHA-224 Message Digests
JEP 131
PKCS#11 Crypto Provider for 64-bit Windows
JEP 135
Base64 Encoding & Decoding
JEP 140
Limited doPrivileged
JEP 166
Overhaul JKS-JCEKS-PKCS12 Keystores
JEP 176
Mechanical Checking of Caller-Sensitive Methods
JavaFX
JEP 153
Launch JavaFX Applications
JavaDoc
JEP 105
DocTree API  
JEP 106
Add Javadoc to javax.tools
JEP 172
DocLint

 

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 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 Collection

Keine Permanent Generation mehr

Die 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 verschwinden

Drei 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:

/JAV8-0/ Neue Features in Java 8 - Überblick
Klaus Kreft & Angelika Langer, Java Magazin, März 2014
URL: http://www.angelikalanger.com/Articles/EffectiveJava/73.Java8.Overview/73.Java8.Overview.html
/JAV8-1/ Funktionale Programmierung in Java
Klaus Kreft & Angelika Langer, Java Magazin, September 2013
URL: http://www.angelikalanger.com/Articles/EffectiveJava/70.Java8.FunctionalProg/70.Java8.FunctionalProg.html
/JAV8-2/ Lambda-Ausdrücke und Methoden-Referenzen
Klaus Kreft & Angelika Langer, Java Magazin, Dezember 2013
URL: http://www.angelikalanger.com/Articles/EffectiveJava/71.Java8.Lambdas/71.Java8.Lambdas.html
/JAV8-3/ Default-Methoden und statische Methoden in Interfaces
Klaus Kreft & Angelika Langer, Java Magazin, Februar 2014
URL: http://www.angelikalanger.com/Articles/EffectiveJava/72.Java8.DefaultMethods/72.Java8.DefaultMethods.html
/JAV8-4/ Übersicht über das Stream API in Java 8
Klaus Kreft & Angelika Langer, Java Magazin, Mai 2014
URL: http://www.angelikalanger.com/Articles/EffectiveJava/74.Java8.Streams-Overview/74.Java8.Streams-Overview.html
/JAV8-5/ Stream-Erzeugung und Stream-Operationen
Klaus Kreft & Angelika Langer, Java Magazin, Juli 2014
URL: http://www.angelikalanger.com/Articles/EffectiveJava/75.Java8.Fundamental-Stream-Operations/75.Java8.Fundamental-Stream-Operations.html
/JAV8-6/ Stream-Kollektoren und die Stream-Operation collect()
Klaus Kreft & Angelika Langer, Java Magazin, September 2014
URL: http://www.angelikalanger.com/Articles/EffectiveJava/76.Java8.Stream-Collectors/76.Java8.Stream-Collectors.html
/JAV8-7/ Stateful Lambdas - Regeln für die Seiteneffekte in Lambda-Ausdrücken, die an Stream-Operationen übergeben werden
Klaus Kreft & Angelika Langer, Java Magazin, November 2014
URL: http://www.angelikalanger.com/Articles/EffectiveJava/77.Java8.Streams-and-Statefulness/77.Java8.Streams-and-Statefulness.html
/JAV8-8/ Das Date/Time API
Klaus Kreft & Angelika Langer, Java Magazin, Januar 2015
URL: http://www.angelikalanger.com/Articles/EffectiveJava/78.Java8.Date-Time-API/78.Java8.Date-Time-API.html
/JAV8-9/ CompletableFuture
Klaus Kreft & Angelika Langer, Java Magazin, März 2015
URL: http://www.angelikalanger.com/Articles/EffectiveJava/79.Java8.CompletableFuture/79.Java8.CompletableFuture.html
/JAV8-10/ Optional<T>
Klaus Kreft & Angelika Langer, Java Magazin, Mai 2015
URL: http://www.angelikalanger.com/Articles/EffectiveJava/80.Java8.Optional-Result/80.Java8.Optional-Result.html
/JAV8-11/ Parallel Streams
Klaus Kreft & Angelika Langer, Java Magazin, Juli 2015
URL: http://www.angelikalanger.com/Articles/EffectiveJava/81.Java8.Parallel-Streams/81.Java8.Parallel-Streams.html
/JAV8-12/ Das Performance-Modell der Streams
Klaus Kreft & Angelika Langer, Java Magazin, September 2015
URL: http://www.angelikalanger.com/Articles/EffectiveJava/82.Java8.Performance-Model-of-Streams/82.Java8.Performance-Model-of-Streams.html
/JAV8-13/ reduce() vs. collect()
Klaus Kreft & Angelika Langer, Java Magazin, November 2015
URL: http://www.angelikalanger.com/Articles/EffectiveJava/83.Java8.Reduce-vs-Collect-Stream-Operations/83.Java8.Reduce-vs-Collect-Stream-Operations.html
/JAV8-14/ User-Defined Collectors
Klaus Kreft & Angelika Langer, Java Magazin, Januar 2016
URL: http://www.angelikalanger.com/Articles/EffectiveJava/84.Java8.User-Defined-Stream-Collectors/84.Java8.User-Defined-Stream-Collectors.html
/JAV8-15/ Parallele Streams und Blockierende Funktionalität
Klaus Kreft & Angelika Langer, Java Magazin, März 2016
URL: http://www.angelikalanger.com/Articles/EffectiveJava/85.Java8.Streams-and-Blocking-Functionality/85.Java8.Streams-and-Blocking-Functionality.html
/JAV8-16/ API-Design mit Lambdas
Klaus Kreft & Angelika Langer, Java Magazin, Mai 2016
URL: http://www.angelikalanger.com/Articles/EffectiveJava/86.Java8.API-Design-With-Lambdas/86.Java8.API-Design-With-Lambdas.html
/JAV8-17/ Low-Level-Aspekte beim API Design mit Lambdas
Klaus Kreft & Angelika Langer, Java Magazin, Juli 2016
URL: http://www.angelikalanger.com/Articles/EffectiveJava/87.Java8.Programming-With-Lambdas/87.Java8.Programming-With-Lambdas.html
/JAV8-18/ Benutzer-definierte Stream-Sourcen und Spliteratoren
Klaus Kreft & Angelika Langer, Java Magazin, September 2016
URL: http://www.angelikalanger.com/Articles/EffectiveJava/88.Java8.User-Defined-Stream-Sources-And-Spliterators/88.Java8.User-Defined-Stream-Sources-And-Spliterators.html

 
 

If you are interested to hear more about this and related topics you might want to check out the following seminar:
Seminar
Lambdas & Streams - Java 8 Language Features and Stream API & Internals
3 day seminar ( open enrollment and on-site)
Java 8 - Lambdas & Stream, New Concurrency Utilities, Date/Time API
4 day seminar ( open enrollment and on-site)
Effective Java - Advanced Java Programming Idioms 
4 day seminar ( open enrollment and on-site)
 
Related Reading
Lambda & Streams Tutorial & Reference
In-Depth Coverage of all aspects of lambdas & streams
Lambdas in Java 8
Conference Presentation at JFokus 2012 (slides)
Lambdas in Java 8
Conference Presentation at JavaZone 2012 (video)
 

 
  © 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