La première partie de la traduction de l'article Fonctionnalités Java 8 – Le guide ULTIMATE . La deuxième partie est ici (le lien peut changer). Note de l'éditeur : Cet article a été publié alors que Java 8 était disponible au public et tout indique qu'il s'agit bien d'une version majeure. Ici, nous avons fourni en abondance des guides Java Code Geeks tels que Jouer avec Java 8 – Lambdas et concurrence , Guide de l'API de date et d'heure Java 8 : LocalDateTime et Classe abstraite par rapport à l'interface à l'ère Java 8 . Nous proposons également des liens vers 15 didacticiels Java 8 incontournables provenant d'autres sources . Bien sûr, nous examinons certains inconvénients, comme le côté obscur de Java 8 . Il est donc temps de rassembler toutes les principales fonctionnalités de Java 8 en un seul endroit pour votre commodité. Apprécier!
1. Introduction
La sortie de Java 8 est sans aucun doute le plus grand événement depuis Java 5 (sorti il y a assez longtemps, en 2004). Il a apporté de nombreuses nouvelles fonctionnalités à Java tant au niveau du langage, du compilateur, des bibliothèques, des outils que de la JVM (Java Virtual Machine). Dans ce didacticiel, nous allons examiner ces changements et démontrer différents cas d'utilisation avec des exemples réels. Le guide se compose de plusieurs parties dont chacune aborde un aspect spécifique de la plateforme :- Langue
- Compilateur
- Bibliothèques
- Outils
- Environnement d'exécution (JVM)
2. Nouvelles fonctionnalités de Java 8
Dans tous les cas, Java 8 est une version majeure. On peut dire que cela a pris beaucoup de temps en raison de la mise en œuvre des fonctionnalités que tout développeur Java recherchait. Dans cette section, nous allons aborder la plupart d’entre eux.2.1. Lambdas et interfaces fonctionnelles
Les lambdas (également connues sous le nom de méthodes privées ou anonymes) constituent le changement de langage le plus important et le plus attendu dans toute la version Java 8. Ils nous permettent de spécifier une fonctionnalité en tant qu'argument de méthode (en déclarant une fonction autour) ou de spécifier du code en tant que données. : concepts que tout développeur fonctionnel connaît . De nombreux langages sur la plateforme JVM (Groovy, Scala , ...) avaient des lambdas dès le premier jour, mais les développeurs Java n'avaient d'autre choix que de représenter les lambdas via des classes anonymes. Discuter de la conception des lambdas a demandé beaucoup de temps et d'efforts de la part du public. Mais des compromis ont finalement été trouvés, ce qui a conduit à l’émergence de nouveaux designs concis. Dans sa forme la plus simple, un lambda peut être représenté comme une liste de paramètres séparés par des virgules, un symbole -> et un corps. Par exemple:Arrays.asList( "a", "b", "d" ).forEach( e -> System.out.println( e ) )
Notez que le type de l'argument e est déterminé par le compilateur. De plus, vous pouvez spécifier explicitement le type d'un paramètre en plaçant le paramètre entre parenthèses. Par exemple:
Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.println( e ) );
Dans le cas où le corps lambda est plus complexe, il peut être entouré d'accolades, semblable à une définition de fonction normale en Java. Par exemple:
Arrays.asList( "a", "b", "d" ).forEach( e -< {
System.out.print( e );
System.out.print( e );
} );
Un lambda peut faire référence à des membres de classe et à des variables locales (ce qui rend implicitement l'appel efficace, que final
le champ soit accédé ou non). Par exemple, ces 2 extraits sont équivalents :
String separator = ",";
Arrays.asList( "a", "b", "d" ).forEach(
( String e ) -> System.out.print( e + separator ) );
ET:
final String separator = ",";
Arrays.asList( "a", "b", "d" ).forEach(
( String e ) -> System.out.print( e + separator ) );
Les Lambdas peuvent renvoyer une valeur. Le type de retour sera déterminé par le compilateur. Une déclaration return
n'est pas requise si le corps du lambda est constitué d'une seule ligne. Les deux extraits de code ci-dessous sont équivalents :
Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> e1.compareTo( e2 ) );
ET:
Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> {
int result = e1.compareTo( e2 );
return result;
} );
Les développeurs du langage ont longuement réfléchi à la manière de rendre les fonctions existantes compatibles avec Lambda. C’est ainsi qu’est né le concept d’interface fonctionnelle. Une interface fonctionnelle est une interface avec une seule méthode. En conséquence, il peut être implicitement converti en expression lambda. java.lang.Runnable
et java.util.concurrent.Callable
deux excellents exemples d'interfaces fonctionnelles. En pratique, les interfaces fonctionnelles sont très fragiles : si quelqu'un ajoute ne serait-ce qu'une autre méthode à la définition de l'interface, elle ne sera plus fonctionnelle et le processus de compilation ne se terminera pas. Pour éviter cette fragilité et définir explicitement l'intention d'une interface comme fonctionnelle, une annotation spéciale a été ajoutée dans Java 8 @FunctionalInterface
(toutes les interfaces existantes dans la bibliothèque Java ont reçu l'annotation @FunctionalInterface). Regardons cette définition simple d'une interface fonctionnelle :
@FunctionalInterface
public interface Functional {
void method();
}
Il y a une chose à garder à l'esprit : les méthodes par défaut et les méthodes statiques ne violent pas le principe d'une interface fonctionnelle et peuvent être déclarées :
@FunctionalInterface
public interface FunctionalDefaultMethods {
void method();
default void defaultMethod() {
}
}
Les Lambdas sont la fonctionnalité la plus populaire de Java 8. Ils ont tout le potentiel pour attirer davantage de développeurs vers cette merveilleuse plate-forme et fournir une prise en charge intelligente des fonctionnalités de Java pur. Pour des informations plus détaillées, veuillez vous référer à la documentation officielle .
2.2. Interfaces par défaut et méthodes statiques
Java 8 a élargi la définition des interfaces avec deux nouveaux concepts : la méthode par défaut et la méthode statique. Les méthodes par défaut rendent les interfaces quelque peu similaires aux traits, mais servent un objectif légèrement différent. Ils vous permettent d'ajouter de nouvelles méthodes aux interfaces existantes sans rompre la compatibilité ascendante pour les versions précédemment écrites de ces interfaces. La différence entre les méthodes par défaut et les méthodes abstraites est que les méthodes abstraites doivent être implémentées alors que les méthodes par défaut ne le sont pas. Au lieu de cela, chaque interface doit fournir une implémentation dite par défaut, et tous les descendants la recevront par défaut (avec la possibilité de remplacer cette implémentation par défaut si nécessaire). Regardons l'exemple ci-dessous.private interface Defaulable {
// Интерфейсы теперь разрешают методы по умолчанию,
// клиент может реализовывать (переопределять)
// or не реализовывать его
default String notRequired() {
return "Default implementation";
}
}
private static class DefaultableImpl implements Defaulable {
}
private static class OverridableImpl implements Defaulable {
@Override
public String notRequired() {
return "Overridden implementation";
}
}
L'interface Defaulable
déclare une méthode par défaut notRequired()
à l'aide d'un mot-clé default
dans le cadre de la définition de la méthode. L'une des classes, DefaultableImpl
, implémente cette interface en laissant la méthode par défaut telle quelle. Une autre classe, OverridableImpl
, remplace l'implémentation par défaut et fournit la sienne. Une autre fonctionnalité intéressante introduite dans Java 8 est que les interfaces peuvent déclarer (et proposer des implémentations) de méthodes statiques. Voici un exemple :
private interface DefaulableFactory {
// Interfaces now allow static methods
static Defaulable create( Supplier<Defaulable> supplier ) {
return supplier.get();
}
}
Un petit morceau de code combine la méthode par défaut et la méthode statique de l'exemple ci-dessus :
public static void main( String[] args ) {
Defaulable defaulable = DefaulableFactory.create( DefaultableImpl::new );
System.out.println( defaulable.notRequired() );
defaulable = DefaulableFactory.create( OverridableImpl::new );
System.out.println( defaulable.notRequired() );
}
La sortie console de ce programme ressemble à ceci :
Default implementation
Overridden implementation
L'implémentation de la méthode par défaut dans la JVM est très efficace et l'invocation de la méthode est prise en charge par les instructions de bytecode. Les méthodes par défaut ont permis aux interfaces Java existantes d'évoluer sans interrompre le processus de compilation. De bons exemples sont les nombreuses méthodes ajoutées à l'interface java.util.Collection
: stream()
, parallelStream()
, forEach()
, removeIf()
, ... Bien qu'étant puissantes, les méthodes par défaut doivent être utilisées avec prudence : avant de déclarer une méthode par défaut, vous devez réfléchir à deux fois si cela est vraiment nécessaire car cela peut conduire à aux ambiguïtés et aux erreurs de compilation dans des hiérarchies complexes. Pour des informations plus détaillées, veuillez vous référer à la documentation .
2.3. Méthodes de référence
Les méthodes de référence implémentent une syntaxe utile pour référencer des méthodes ou des constructeurs existants de classes ou d'objets Java (instances). Associées aux expressions lambda, les méthodes de référence rendent les constructions de langage compactes et concises, les rendant ainsi basées sur des modèles. Vous trouverez ci-dessous une classeCar
à titre d'exemple de différentes définitions de méthodes. Soulignons les quatre types de méthodes de référence pris en charge :
public static class Car {
public static Car create( final Supplier<Car> supplier ) {
return supplier.get();
}
public static void collide( final Car car ) {
System.out.println( "Collided " + car.toString() );
}
public void follow( final Car another ) {
System.out.println( "Following the " + another.toString() );
}
public void repair() {
System.out.println( "Repaired " + this.toString() );
}
}
La première méthode de référence est une référence à un constructeur avec une syntaxe Class::new
ou une alternative aux génériques Class< T >::new
. Notez que le constructeur n'a aucun argument.
final Car car = Car.create( Car::new );
final List<Car> cars = Arrays.asList( car );
La deuxième option est une référence à une méthode statique avec la syntaxe Class::static_method
. Notez que la méthode prend exactement un paramètre de type Car
.
cars.forEach( Car::collide );
Le troisième type est une référence à une méthode d'instance d'un objet arbitraire d'un certain type avec la syntaxe Class::method
. Notez qu'aucun argument n'est accepté par la méthode.
cars.forEach( Car::repair );
Et le dernier et quatrième type est une référence à une méthode d'instance d'une classe spécifique avec la syntaxe instance::method
. Veuillez noter que la méthode n'accepte qu'un seul paramètre de type Car
.
final Car police = Car.create( Car::new );
cars.forEach( police::follow );
L'exécution de tous ces exemples en tant que programmes Java produit la sortie de console suivante (la référence de classe Car
peut varier) :
Collided com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d
Repaired com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d
Following the com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d
Pour des informations plus détaillées et des détails sur les méthodes de référence, veuillez vous référer à la documentation officielle .
2.4. Annotations en double
Depuis que Java 5 a introduit la prise en charge des annotations , cette fonctionnalité est devenue très populaire et très largement utilisée. Cependant, l’une des limites de l’utilisation des annotations était le fait qu’une même annotation ne peut pas être déclarée plus d’une fois au même endroit. Java 8 enfreint cette règle et introduit des annotations en double. Cela permet aux mêmes annotations d'être répétées plusieurs fois à l'emplacement où elles sont déclarées. Les annotations en double doivent s'annoter elles-mêmes en utilisant annotation@Repeatable
. En fait, il ne s’agit pas tant d’un changement de langage que d’une astuce du compilateur, alors que la technique reste la même. Regardons un exemple simple :
package com.javacodegeeks.java8.repeatable.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
public class RepeatingAnnotations {
@Target( ElementType.TYPE )
@Retention( RetentionPolicy.RUNTIME )
public @interface Filters {
Filter[] value();
}
@Target( ElementType.TYPE )
@Retention( RetentionPolicy.RUNTIME )
@Repeatable( Filters.class )
public @interface Filter {
String value();
};
@Filter( "filter1" )
@Filter( "filter2" )
public interface Filterable {
}
public static void main(String[] args) {
for( Filter filter: Filterable.class.getAnnotationsByType( Filter.class ) ) {
System.out.println( filter.value() );
}
}
}
Comme nous pouvons le voir, la classe Filter
est annotée avec @Repeatable( Filters. class
). Filters
est simplement propriétaire des annotations Filter
, mais le compilateur Java essaie de cacher leur présence aux développeurs. Ainsi, l'interface Filterable
contient des annotations Filter
qui sont déclarées deux fois (sans mentionner Filters
). L'API Reflection fournit également une nouvelle méthode getAnnotationsByType()
pour renvoyer des annotations en double d'un certain type (rappelez-vous que Filterable. class
.getAnnotation( Filters. class
) renverra une instance injectée par le compilateur Filters
). Le résultat du programme ressemblera à ceci :
filter1
filter2
Pour des informations plus détaillées, veuillez vous référer à la documentation officielle .
2.5. Inférence de type améliorée
Le compilateur Java 8 a reçu de nombreuses améliorations d'inférence de type. Dans de nombreux cas, des paramètres de type explicites peuvent être définis par le compilateur, rendant ainsi le code plus propre. Regardons un exemple :package com.javacodegeeks.java8.type.inference;
public class Value<T> {
public static<T> T defaultValue() {
return null;
}
public T getOrDefault( T value, T defaultValue ) {
return ( value != null ) ? value : defaultValue;
}
}
Et voici l'utilisation avec type Value<String>
:
package com.javacodegeeks.java8.type.inference;
public class TypeInference {
public static void main(String[] args) {
final Value<String> value = new Value<>();
value.getOrDefault( "22", Value.defaultValue() );
}
}
Le paramètre type Value.defaultValue()
est déterminé automatiquement et n’a pas besoin d’être fourni explicitement. Dans Java 7, le même exemple ne sera pas compilé et doit être réécrit sous la forme <NOBR>Value.<String>defaultValue()</NOBR>.
2.6. Prise en charge étendue des annotations
Java 8 étend le contexte dans lequel les annotations peuvent être utilisées. De nos jours, presque tout peut avoir une annotation : variables locales, types génériques, superclasses et interfaces implémentées, et même les exceptions de méthode. Quelques exemples sont présentés ci-dessous :package com.javacodegeeks.java8.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Collection;
public class Annotations {
@Retention( RetentionPolicy.RUNTIME )
@Target( { ElementType.TYPE_USE, ElementType.TYPE_PARAMETER } )
public @interface NonEmpty {
}
public static class Holder<@NonEmpty T> extends @NonEmpty Object {
public void method() throws @NonEmpty Exception {
}
}
@SuppressWarnings( "unused" )
public static void main(String[] args) {
final Holder<String> holder = new @NonEmpty Holder<String>();
@NonEmpty Collection<@NonEmpty String> strings = new ArrayList<>();
}
}
ElementType.TYPE_USE
et ElementType.TYPE_PARAMETER
deux nouveaux types d'éléments pour décrire le contexte d'annotation pertinent. Annotation Processing API
a également subi des modifications mineures pour reconnaître de nouveaux types d'annotations en Java.
3. Nouvelles fonctionnalités du compilateur Java
3.1. Noms des paramètres
Au fil du temps, les développeurs Java ont inventé diverses façons de stocker les noms de paramètres de méthode dans le bytecode Java afin de les rendre disponibles au moment de l'exécution (par exemple, la bibliothèque Paranamer ). Enfin, Java 8 crée cette fonction difficile dans le langage (en utilisant l'API et la méthode ReflectionParameter.getName()
) et le bytecode (en utilisant le nouvel argument du compilateur javac
:) –parameters
.
package com.javacodegeeks.java8.parameter.names;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
public class ParameterNames {
public static void main(String[] args) throws Exception {
Method method = ParameterNames.class.getMethod( "main", String[].class );
for( final Parameter parameter: method.getParameters() ) {
System.out.println( "Parameter: " + parameter.getName() );
}
}
}
Si vous compilez cette classe sans utiliser d'argument –parameters
puis exécutez le programme, vous verrez quelque chose comme ceci :
Parameter: arg0
Avec le paramètre –parameters
passé au compilateur, la sortie du programme sera différente (le nom réel du paramètre sera affiché) :
Parameter: args
Pour les utilisateurs avancés de Maven, l'argument –parameters peut être ajouté à la compilation à l'aide de la section maven-compiler-plugin
:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<compilerArgument>-parameters</compilerArgument>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
Pour vérifier la disponibilité des noms de paramètres, il existe une isNamePresent()
méthode pratique fournie par la classe Parameter
.
4. Nouveaux outils Java
Java 8 est livré avec un nouvel ensemble d'outils de ligne de commande. Dans cette section, nous examinerons les plus intéressants d’entre eux.4.1. Moteur Nashorn : jjs
jjs est un moteur Nashorn autonome basé sur une ligne de commande. Il prend une liste de fichiers de code source JavaScript et les exécute. Par exemple, créons un fichier func.js avec le contenu suivant :function f() {
return 1;
};
print( f() + 1 );
Pour exécuter ce fichier passons-le en argument à jjs :
jjs func.js
La sortie de la console ressemblera à ceci :
2
Pour plus de détails, consultez la documentation .
4.2. Analyseur de dépendances de classe : jdeps
jdeps est un très bon outil en ligne de commande. Il affiche les dépendances au niveau du package ou de la classe pour les classes Java. Il accepte un fichier .class , un dossier ou un fichier JAR en entrée. Par défaut , jdeps affiche les dépendances sur la sortie standard (console). À titre d'exemple, regardons le rapport de dépendance de la populaire bibliothèque Spring Framework . Pour que l'exemple soit court, examinons uniquement les dépendances du fichier JARorg.springframework.core-3.0.5.RELEASE.jar
.
jdeps org.springframework.core-3.0.5.RELEASE.jar
Cette commande génère beaucoup de résultats, nous n’analyserons donc qu’une partie de la sortie. Les dépendances sont regroupées par packages. S'il n'y a pas de dépendances, not found sera affiché .
org.springframework.core-3.0.5.RELEASE.jar -> C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar
org.springframework.core (org.springframework.core-3.0.5.RELEASE.jar)
-> java.io
-> java.lang
-> java.lang.annotation
-> java.lang.ref
-> java.lang.reflect
-> java.util
-> java.util.concurrent
-> org.apache.commons.logging not found
-> org.springframework.asm not found
-> org.springframework.asm.commons not found
org.springframework.core.annotation (org.springframework.core-3.0.5.RELEASE.jar)
-> java.lang
-> java.lang.annotation
-> java.lang.reflect
-> java.util
Pour des informations plus détaillées, veuillez vous référer à la documentation officielle .
GO TO FULL VERSION