Cet article a été initialement publié sur mon blog.
Vous devriez également lire mon didacticiel Java 11 (y compris les nouvelles fonctionnalités de langage et d'API de Java 9, 10 et 11).
Bienvenue dans mon introduction à Java 8. Ce didacticiel vous guide étape par étape à travers toutes les nouvelles fonctionnalités du langage. À l'aide d'exemples de code courts et simples, vous apprendrez à utiliser les méthodes d'interface par défaut, les expressions lambda, les références de méthodes et les annotations répétables. À la fin de l'article, vous serez familiarisé avec les modifications les plus récentes de l'API telles que les flux, les interfaces fonctionnelles, les extensions de carte et la nouvelle API Date. Pas de murs de texte, juste un tas d'extraits de code commentés. Apprécier!
★★★ Vous aimez ce projet ? Laissez une étoile, suivez sur Twitter ou faites un don pour soutenir mon travail. Merci! ★★★
Java 8 nous permet d'ajouter des implémentations de méthodes non abstraites aux interfaces en utilisant le mot-clé default
. Cette fonctionnalité est également connue sous le nom de méthodes d'extension virtuelle.
Voici notre premier exemple :
interface Formula {
double calculate ( int a );
default double sqrt ( int a ) {
return Math . sqrt ( a );
}
}
Outre la méthode abstraite calculate
l'interface, Formula
définit également la méthode par défaut sqrt
. Les classes concrètes n'ont qu'à implémenter la méthode abstraite calculate
. La méthode par défaut sqrt
peut être utilisée immédiatement.
Formula formula = new Formula () {
@ Override
public double calculate ( int a ) {
return sqrt ( a * 100 );
}
};
formula . calculate ( 100 ); // 100.0
formula . sqrt ( 16 ); // 4.0
La formule est implémentée comme un objet anonyme. Le code est assez verbeux : 6 lignes de code pour un calcul aussi simple de sqrt(a * 100)
. Comme nous le verrons dans la section suivante, il existe une manière bien plus simple d'implémenter des objets à méthode unique dans Java 8.
Commençons par un exemple simple illustrant comment trier une liste de chaînes dans les versions antérieures de Java :
List < String > names = Arrays . asList ( "peter" , "anna" , "mike" , "xenia" );
Collections . sort ( names , new Comparator < String >() {
@ Override
public int compare ( String a , String b ) {
return b . compareTo ( a );
}
});
La méthode utilitaire statique Collections.sort
accepte une liste et un comparateur afin de trier les éléments de la liste donnée. Vous vous retrouvez souvent à créer des comparateurs anonymes et à les transmettre à la méthode de tri.
Au lieu de créer des objets anonymes à longueur de journée, Java 8 est livré avec une syntaxe beaucoup plus courte, les expressions lambda :
Collections . sort ( names , ( String a , String b ) -> {
return b . compareTo ( a );
});
Comme vous pouvez le constater, le code est beaucoup plus court et plus facile à lire. Mais cela devient encore plus court :
Collections . sort ( names , ( String a , String b ) -> b . compareTo ( a ));
Pour les corps de méthode sur une seule ligne, vous pouvez ignorer les accolades {}
et le mot-clé return
. Mais cela devient encore plus court :
names . sort (( a , b ) -> b . compareTo ( a ));
La liste a désormais une méthode sort
. Le compilateur Java connaît également les types de paramètres, vous pouvez donc également les ignorer. Examinons plus en détail comment les expressions lambda peuvent être utilisées dans la nature.
Comment les expressions lambda s'intègrent-elles dans le système de types Java ? Chaque lambda correspond à un type donné, spécifié par une interface. Une interface dite fonctionnelle doit contenir exactement une déclaration de méthode abstraite . Chaque expression lambda de ce type sera mise en correspondance avec cette méthode abstraite. Étant donné que les méthodes par défaut ne sont pas abstraites, vous êtes libre d'ajouter des méthodes par défaut à votre interface fonctionnelle.
Nous pouvons utiliser des interfaces arbitraires comme expressions lambda tant que l'interface ne contient qu'une seule méthode abstraite. Pour vous assurer que votre interface répond aux exigences, vous devez ajouter l'annotation @FunctionalInterface
. Le compilateur est conscient de cette annotation et génère une erreur du compilateur dès que vous essayez d'ajouter une deuxième déclaration de méthode abstraite à l'interface.
Exemple:
@ FunctionalInterface
interface Converter < F , T > {
T convert ( F from );
}
Converter < String , Integer > converter = ( from ) -> Integer . valueOf ( from );
Integer converted = converter . convert ( "123" );
System . out . println ( converted ); // 123
Gardez à l'esprit que le code est également valide si l'annotation @FunctionalInterface
est omise.
L'exemple de code ci-dessus peut être encore simplifié en utilisant des références de méthodes statiques :
Converter < String , Integer > converter = Integer :: valueOf ;
Integer converted = converter . convert ( "123" );
System . out . println ( converted ); // 123
Java 8 vous permet de transmettre des références de méthodes ou de constructeurs via le mot-clé ::
. L'exemple ci-dessus montre comment référencer une méthode statique. Mais on peut aussi référencer des méthodes objets :
class Something {
String startsWith ( String s ) {
return String . valueOf ( s . charAt ( 0 ));
}
}
Something something = new Something ();
Converter < String , String > converter = something :: startsWith ;
String converted = converter . convert ( "Java" );
System . out . println ( converted ); // "J"
Voyons comment le mot-clé ::
fonctionne pour les constructeurs. Nous définissons d’abord un exemple de classe avec différents constructeurs :
class Person {
String firstName ;
String lastName ;
Person () {}
Person ( String firstName , String lastName ) {
this . firstName = firstName ;
this . lastName = lastName ;
}
}
Nous spécifions ensuite une interface de fabrique de personnes à utiliser pour créer de nouvelles personnes :
interface PersonFactory < P extends Person > {
P create ( String firstName , String lastName );
}
Au lieu d'implémenter l'usine manuellement, nous collons le tout via des références de constructeur :
PersonFactory < Person > personFactory = Person :: new ;
Person person = personFactory . create ( "Peter" , "Parker" );
Nous créons une référence au constructeur Person via Person::new
. Le compilateur Java choisit automatiquement le bon constructeur en faisant correspondre la signature de PersonFactory.create
.
L'accès aux variables de portée externe à partir d'expressions lambda est très similaire aux objets anonymes. Vous pouvez accéder aux variables finales à partir de la portée externe locale ainsi qu'aux champs d'instance et aux variables statiques.
Nous pouvons lire les variables locales finales à partir de la portée externe des expressions lambda :
final int num = 1 ;
Converter < Integer , String > stringConverter =
( from ) -> String . valueOf ( from + num );
stringConverter . convert ( 2 ); // 3
Mais contrairement aux objets anonymes, la variable num
n'a pas besoin d'être déclarée finale. Ce code est également valable :
int num = 1 ;
Converter < Integer , String > stringConverter =
( from ) -> String . valueOf ( from + num );
stringConverter . convert ( 2 ); // 3
Cependant, num
doit être implicitement final pour que le code soit compilé. Le code suivant ne compile pas :
int num = 1 ;
Converter < Integer , String > stringConverter =
( from ) -> String . valueOf ( from + num );
num = 3 ;
L'écriture dans num
à partir de l'expression lambda est également interdite.
Contrairement aux variables locales, nous avons un accès en lecture et en écriture aux champs d'instance et aux variables statiques à partir des expressions lambda. Ce comportement est bien connu des objets anonymes.
class Lambda4 {
static int outerStaticNum ;
int outerNum ;
void testScopes () {
Converter < Integer , String > stringConverter1 = ( from ) -> {
outerNum = 23 ;
return String . valueOf ( from );
};
Converter < Integer , String > stringConverter2 = ( from ) -> {
outerStaticNum = 72 ;
return String . valueOf ( from );
};
}
}
Vous vous souvenez de l'exemple de formule de la première section ? Interface Formula
définit une méthode par défaut sqrt
accessible à partir de chaque instance de formule, y compris les objets anonymes. Cela ne fonctionne pas avec les expressions lambda.
Les méthodes par défaut ne sont pas accessibles à partir des expressions lambda. Le code suivant ne compile pas :
Formula formula = ( a ) -> sqrt ( a * 100 );
L'API JDK 1.8 contient de nombreuses interfaces fonctionnelles intégrées. Certains d’entre eux sont bien connus des anciennes versions de Java comme Comparator
ou Runnable
. Ces interfaces existantes sont étendues pour activer la prise en charge de Lambda via l'annotation @FunctionalInterface
.
Mais l’API Java 8 regorge également de nouvelles interfaces fonctionnelles pour vous faciliter la vie. Certaines de ces nouvelles interfaces sont bien connues de la bibliothèque Google Guava. Même si vous connaissez cette bibliothèque, vous devez garder un œil attentif sur la façon dont ces interfaces sont étendues par certaines extensions de méthodes utiles.
Les prédicats sont des fonctions booléennes d'un argument. L'interface contient diverses méthodes par défaut pour composer des prédicats en termes logiques complexes (et, ou, nier)
Predicate < String > predicate = ( s ) -> s . length () > 0 ;
predicate . test ( "foo" ); // true
predicate . negate (). test ( "foo" ); // false
Predicate < Boolean > nonNull = Objects :: nonNull ;
Predicate < Boolean > isNull = Objects :: isNull ;
Predicate < String > isEmpty = String :: isEmpty ;
Predicate < String > isNotEmpty = isEmpty . negate ();
Les fonctions acceptent un argument et produisent un résultat. Les méthodes par défaut peuvent être utilisées pour enchaîner plusieurs fonctions (compose, andThen).
Function < String , Integer > toInteger = Integer :: valueOf ;
Function < String , String > backToString = toInteger . andThen ( String :: valueOf );
backToString . apply ( "123" ); // "123"
Les fournisseurs produisent un résultat d'un type générique donné. Contrairement aux fonctions, les fournisseurs n'acceptent pas les arguments.
Supplier < Person > personSupplier = Person :: new ;
personSupplier . get (); // new Person
Les consommateurs représentent les opérations à effectuer sur un seul argument d'entrée.
Consumer < Person > greeter = ( p ) -> System . out . println ( "Hello, " + p . firstName );
greeter . accept ( new Person ( "Luke" , "Skywalker" ));
Les comparateurs sont bien connus dans les anciennes versions de Java. Java 8 ajoute diverses méthodes par défaut à l'interface.
Comparator < Person > comparator = ( p1 , p2 ) -> p1 . firstName . compareTo ( p2 . firstName );
Person p1 = new Person ( "John" , "Doe" );
Person p2 = new Person ( "Alice" , "Wonderland" );
comparator . compare ( p1 , p2 ); // > 0
comparator . reversed (). compare ( p1 , p2 ); // < 0
Les options ne sont pas des interfaces fonctionnelles, mais des utilitaires astucieux pour empêcher NullPointerException
. C'est un concept important pour la section suivante, alors jetons un coup d'œil rapide au fonctionnement des options.
Facultatif est un simple conteneur pour une valeur qui peut être nulle ou non nulle. Pensez à une méthode qui peut renvoyer un résultat non nul mais qui parfois ne renvoie rien. Au lieu de renvoyer null
vous renvoyez un Optional
dans Java 8.
Optional < String > optional = Optional . of ( "bam" );
optional . isPresent (); // true
optional . get (); // "bam"
optional . orElse ( "fallback" ); // "bam"
optional . ifPresent (( s ) -> System . out . println ( s . charAt ( 0 ))); // "b"
Un java.util.Stream
représente une séquence d'éléments sur lesquels une ou plusieurs opérations peuvent être effectuées. Les opérations de flux sont soit intermédiaires , soit terminales . Alors que les opérations du terminal renvoient un résultat d'un certain type, les opérations intermédiaires renvoient le flux lui-même afin que vous puissiez enchaîner plusieurs appels de méthode d'affilée. Les flux sont créés sur une source, par exemple un java.util.Collection
comme des listes ou des ensembles (les cartes ne sont pas prises en charge). Les opérations de flux peuvent être exécutées séquentiellement ou parallèlement.
Les flux sont extrêmement puissants, j'ai donc écrit un didacticiel Java 8 Streams distinct. Vous devriez également consulter Sequency en tant que bibliothèque similaire pour le Web.
Voyons d'abord comment fonctionnent les flux séquentiels. Nous créons d’abord un exemple de source sous la forme d’une liste de chaînes :
List < String > stringCollection = new ArrayList <>();
stringCollection . add ( "ddd2" );
stringCollection . add ( "aaa2" );
stringCollection . add ( "bbb1" );
stringCollection . add ( "aaa1" );
stringCollection . add ( "bbb3" );
stringCollection . add ( "ccc" );
stringCollection . add ( "bbb2" );
stringCollection . add ( "ddd1" );
Les collections dans Java 8 sont étendues afin que vous puissiez simplement créer des flux en appelant Collection.stream()
ou Collection.parallelStream()
. Les sections suivantes expliquent les opérations de flux les plus courantes.
Filter accepte un prédicat pour filtrer tous les éléments du flux. Cette opération est intermédiaire ce qui nous permet d'appeler une autre opération de flux ( forEach
) sur le résultat. ForEach accepte qu'un consommateur soit exécuté pour chaque élément du flux filtré. ForEach est une opération de terminal. C'est void
, nous ne pouvons donc pas appeler une autre opération de flux.
stringCollection
. stream ()
. filter (( s ) -> s . startsWith ( "a" ))
. forEach ( System . out :: println );
// "aaa2", "aaa1"
Sorted est une opération intermédiaire qui renvoie une vue triée du flux. Les éléments sont triés dans l'ordre naturel, sauf si vous transmettez un Comparator
personnalisé.
stringCollection
. stream ()
. sorted ()
. filter (( s ) -> s . startsWith ( "a" ))
. forEach ( System . out :: println );
// "aaa1", "aaa2"
Gardez à l’esprit que sorted
crée uniquement une vue triée du flux sans manipuler l’ordre de la collection sauvegardée. L’ordre de stringCollection
n’est pas modifié :
System . out . println ( stringCollection );
// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
La map
d'opération intermédiaire convertit chaque élément en un autre objet via la fonction donnée. L'exemple suivant convertit chaque chaîne en chaîne en majuscules. Mais vous pouvez également utiliser map
pour transformer chaque objet en un autre type. Le type générique du flux résultant dépend du type générique de la fonction que vous transmettez à map
.
stringCollection
. stream ()
. map ( String :: toUpperCase )
. sorted (( a , b ) -> b . compareTo ( a ))
. forEach ( System . out :: println );
// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
Diverses opérations de correspondance peuvent être utilisées pour vérifier si un certain prédicat correspond au flux. Toutes ces opérations sont terminales et renvoient un résultat booléen.
boolean anyStartsWithA =
stringCollection
. stream ()
. anyMatch (( s ) -> s . startsWith ( "a" ));
System . out . println ( anyStartsWithA ); // true
boolean allStartsWithA =
stringCollection
. stream ()
. allMatch (( s ) -> s . startsWith ( "a" ));
System . out . println ( allStartsWithA ); // false
boolean noneStartsWithZ =
stringCollection
. stream ()
. noneMatch (( s ) -> s . startsWith ( "z" ));
System . out . println ( noneStartsWithZ ); // true
Count est une opération de terminal renvoyant le nombre d'éléments dans le flux sous forme de fichier long
.
long startsWithB =
stringCollection
. stream ()
. filter (( s ) -> s . startsWith ( "b" ))
. count ();
System . out . println ( startsWithB ); // 3
Cette opération de terminal effectue une réduction sur les éléments du flux avec la fonction donnée. Le résultat est un Optional
contenant la valeur réduite.
Optional < String > reduced =
stringCollection
. stream ()
. sorted ()
. reduce (( s1 , s2 ) -> s1 + "#" + s2 );
reduced . ifPresent ( System . out :: println );
// "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"
Comme mentionné ci-dessus, les flux peuvent être séquentiels ou parallèles. Les opérations sur les flux séquentiels sont effectuées sur un seul thread tandis que les opérations sur les flux parallèles sont effectuées simultanément sur plusieurs threads.
L'exemple suivant montre à quel point il est facile d'augmenter les performances en utilisant des flux parallèles.
Nous créons d’abord une grande liste d’éléments uniques :
int max = 1000000 ;
List < String > values = new ArrayList <>( max );
for ( int i = 0 ; i < max ; i ++) {
UUID uuid = UUID . randomUUID ();
values . add ( uuid . toString ());
}
Nous mesurons maintenant le temps nécessaire pour trier un flux de cette collection.
long t0 = System . nanoTime ();
long count = values . stream (). sorted (). count ();
System . out . println ( count );
long t1 = System . nanoTime ();
long millis = TimeUnit . NANOSECONDS . toMillis ( t1 - t0 );
System . out . println ( String . format ( "sequential sort took: %d ms" , millis ));
// sequential sort took: 899 ms
long t0 = System . nanoTime ();
long count = values . parallelStream (). sorted (). count ();
System . out . println ( count );
long t1 = System . nanoTime ();
long millis = TimeUnit . NANOSECONDS . toMillis ( t1 - t0 );
System . out . println ( String . format ( "parallel sort took: %d ms" , millis ));
// parallel sort took: 472 ms
Comme vous pouvez le constater, les deux extraits de code sont presque identiques, mais le tri parallèle est environ 50 % plus rapide. Tout ce que vous avez à faire est de changer stream()
en parallelStream()
.
Comme déjà mentionné, les cartes ne prennent pas directement en charge les flux. Aucune méthode stream()
n'est disponible sur l'interface Map
elle-même, mais vous pouvez créer des flux spécialisés sur les clés, valeurs ou entrées d'une carte via map.keySet().stream()
, map.values().stream()
et map.entrySet().stream()
.
De plus, les cartes prennent en charge diverses méthodes nouvelles et utiles pour effectuer des tâches courantes.
Map < Integer , String > map = new HashMap <>();
for ( int i = 0 ; i < 10 ; i ++) {
map . putIfAbsent ( i , "val" + i );
}
map . forEach (( id , val ) -> System . out . println ( val ));
Le code ci-dessus devrait être explicite : putIfAbsent
nous empêche d'écrire des vérifications if null supplémentaires ; forEach
accepte un consommateur pour effectuer des opérations pour chaque valeur de la carte.
Cet exemple montre comment calculer du code sur la carte en utilisant des fonctions :
map . computeIfPresent ( 3 , ( num , val ) -> val + num );
map . get ( 3 ); // val33
map . computeIfPresent ( 9 , ( num , val ) -> null );
map . containsKey ( 9 ); // false
map . computeIfAbsent ( 23 , num -> "val" + num );
map . containsKey ( 23 ); // true
map . computeIfAbsent ( 3 , num -> "bam" );
map . get ( 3 ); // val33
Ensuite, nous apprenons comment supprimer des entrées pour une clé donnée, uniquement si elle est actuellement mappée à une valeur donnée :
map . remove ( 3 , "val3" );
map . get ( 3 ); // val33
map . remove ( 3 , "val33" );
map . get ( 3 ); // null
Une autre méthode utile :
map . getOrDefault ( 42 , "not found" ); // not found
Fusionner les entrées d'une carte est assez simple :
map . merge ( 9 , "val9" , ( value , newValue ) -> value . concat ( newValue ));
map . get ( 9 ); // val9
map . merge ( 9 , "concat" , ( value , newValue ) -> value . concat ( newValue ));
map . get ( 9 ); // val9concat
La fusion place la clé/valeur dans la carte si aucune entrée pour la clé n'existe, ou la fonction de fusion sera appelée pour modifier la valeur existante.
Java 8 contient une toute nouvelle API de date et d'heure sous le package java.time
. La nouvelle API Date est comparable à la bibliothèque Joda-Time, mais ce n'est pas la même chose. Les exemples suivants couvrent les parties les plus importantes de cette nouvelle API.
L'horloge donne accès à la date et à l'heure actuelles. Les horloges connaissent un fuseau horaire et peuvent être utilisées à la place de System.currentTimeMillis()
pour récupérer l'heure actuelle en millisecondes depuis Unix EPOCH. Un tel point instantané sur la chronologie est également représenté par la classe Instant
. Les instants peuvent être utilisés pour créer des objets java.util.Date
hérités.
Clock clock = Clock . systemDefaultZone ();
long millis = clock . millis ();
Instant instant = clock . instant ();
Date legacyDate = Date . from ( instant ); // legacy java.util.Date
Les fuseaux horaires sont représentés par un ZoneId
. Ils sont facilement accessibles via des méthodes d’usine statiques. Les fuseaux horaires définissent les décalages qu'il est important de convertir entre les instants et les dates et heures locales.
System . out . println ( ZoneId . getAvailableZoneIds ());
// prints all available timezone ids
ZoneId zone1 = ZoneId . of ( "Europe/Berlin" );
ZoneId zone2 = ZoneId . of ( "Brazil/East" );
System . out . println ( zone1 . getRules ());
System . out . println ( zone2 . getRules ());
// ZoneRules[currentStandardOffset=+01:00]
// ZoneRules[currentStandardOffset=-03:00]
LocalTime représente une heure sans fuseau horaire, par exemple 22h ou 17:30:15. L'exemple suivant crée deux heures locales pour les fuseaux horaires définis ci-dessus. Ensuite, nous comparons les deux heures et calculons la différence en heures et en minutes entre les deux heures.
LocalTime now1 = LocalTime . now ( zone1 );
LocalTime now2 = LocalTime . now ( zone2 );
System . out . println ( now1 . isBefore ( now2 )); // false
long hoursBetween = ChronoUnit . HOURS . between ( now1 , now2 );
long minutesBetween = ChronoUnit . MINUTES . between ( now1 , now2 );
System . out . println ( hoursBetween ); // -3
System . out . println ( minutesBetween ); // -239
LocalTime est livré avec diverses méthodes d'usine pour simplifier la création de nouvelles instances, y compris l'analyse des chaînes temporelles.
LocalTime late = LocalTime . of ( 23 , 59 , 59 );
System . out . println ( late ); // 23:59:59
DateTimeFormatter germanFormatter =
DateTimeFormatter
. ofLocalizedTime ( FormatStyle . SHORT )
. withLocale ( Locale . GERMAN );
LocalTime leetTime = LocalTime . parse ( "13:37" , germanFormatter );
System . out . println ( leetTime ); // 13:37
LocalDate représente une date distincte, par exemple 2014-03-11. Il est immuable et fonctionne exactement de manière analogue à LocalTime. L'exemple montre comment calculer de nouvelles dates en ajoutant ou en soustrayant des jours, des mois ou des années. Gardez à l'esprit que chaque manipulation renvoie une nouvelle instance.
LocalDate today = LocalDate . now ();
LocalDate tomorrow = today . plus ( 1 , ChronoUnit . DAYS );
LocalDate yesterday = tomorrow . minusDays ( 2 );
LocalDate independenceDay = LocalDate . of ( 2014 , Month . JULY , 4 );
DayOfWeek dayOfWeek = independenceDay . getDayOfWeek ();
System . out . println ( dayOfWeek ); // FRIDAY
Analyser un LocalDate à partir d'une chaîne est aussi simple que d'analyser un LocalTime :
DateTimeFormatter germanFormatter =
DateTimeFormatter
. ofLocalizedDate ( FormatStyle . MEDIUM )
. withLocale ( Locale . GERMAN );
LocalDate xmas = LocalDate . parse ( "24.12.2014" , germanFormatter );
System . out . println ( xmas ); // 2014-12-24
LocalDateTime représente une date-heure. Il combine la date et l'heure comme indiqué dans les sections ci-dessus en une seule instance. LocalDateTime
est immuable et fonctionne de manière similaire à LocalTime et LocalDate. Nous pouvons utiliser des méthodes pour récupérer certains champs à partir d'une date-heure :
LocalDateTime sylvester = LocalDateTime . of ( 2014 , Month . DECEMBER , 31 , 23 , 59 , 59 );
DayOfWeek dayOfWeek = sylvester . getDayOfWeek ();
System . out . println ( dayOfWeek ); // WEDNESDAY
Month month = sylvester . getMonth ();
System . out . println ( month ); // DECEMBER
long minuteOfDay = sylvester . getLong ( ChronoField . MINUTE_OF_DAY );
System . out . println ( minuteOfDay ); // 1439
Avec les informations supplémentaires d'un fuseau horaire, il peut être converti en un instant. Les instants peuvent facilement être convertis en dates héritées de type java.util.Date
.
Instant instant = sylvester
. atZone ( ZoneId . systemDefault ())
. toInstant ();
Date legacyDate = Date . from ( instant );
System . out . println ( legacyDate ); // Wed Dec 31 23:59:59 CET 2014
Le formatage des dates et heures fonctionne de la même manière que le formatage des dates ou des heures. Au lieu d'utiliser des formats prédéfinis, nous pouvons créer des formateurs à partir de modèles personnalisés.
DateTimeFormatter formatter =
DateTimeFormatter
. ofPattern ( "MMM dd, yyyy - HH:mm" );
LocalDateTime parsed = LocalDateTime . parse ( "Nov 03, 2014 - 07:13" , formatter );
String string = formatter . format ( parsed );
System . out . println ( string ); // Nov 03, 2014 - 07:13
Contrairement à java.text.NumberFormat
le nouveau DateTimeFormatter
est immuable et thread-safe .
Pour plus de détails sur la syntaxe du modèle, lisez ici.
Les annotations dans Java 8 sont répétables. Passons directement à un exemple pour comprendre cela.
Tout d’abord, nous définissons une annotation wrapper qui contient un tableau des annotations réelles :
@interface Hints {
Hint [] value ();
}
@ Repeatable ( Hints . class )
@interface Hint {
String value ();
}
Java 8 nous permet d'utiliser plusieurs annotations du même type en déclarant l'annotation @Repeatable
.
@ Hints ({ @ Hint ( "hint1" ), @ Hint ( "hint2" )})
class Person {}
@ Hint ( "hint1" )
@ Hint ( "hint2" )
class Person {}
En utilisant la variante 2, le compilateur Java configure implicitement l'annotation @Hints
sous le capot. C'est important pour lire les informations d'annotation via la réflexion.
Hint hint = Person . class . getAnnotation ( Hint . class );
System . out . println ( hint ); // null
Hints hints1 = Person . class . getAnnotation ( Hints . class );
System . out . println ( hints1 . value (). length ); // 2
Hint [] hints2 = Person . class . getAnnotationsByType ( Hint . class );
System . out . println ( hints2 . length ); // 2
Bien que nous n'ayons jamais déclaré l'annotation @Hints
sur la classe Person
, elle est toujours lisible via getAnnotation(Hints.class)
. Cependant, la méthode la plus pratique est getAnnotationsByType
, qui accorde un accès direct à toutes les annotations @Hint
annotées.
De plus, l'utilisation des annotations dans Java 8 est étendue à deux nouvelles cibles :
@ Target ({ ElementType . TYPE_PARAMETER , ElementType . TYPE_USE })
@interface MyAnnotation {}
Mon guide de programmation pour Java 8 se termine ici. Si vous souhaitez en savoir plus sur toutes les nouvelles classes et fonctionnalités de l'API JDK 8, consultez mon explorateur d'API JDK8. Il vous aide à découvrir toutes les nouvelles classes et joyaux cachés du JDK 8, comme Arrays.parallelSort
, StampedLock
et CompletableFuture
- pour n'en nommer que quelques-uns.
J'ai également publié un certain nombre d'articles de suivi sur mon blog qui pourraient vous intéresser :
Vous devriez me suivre sur Twitter. Merci d'avoir lu!