5 secrets cachés en Java

Bonjour, Habr! Je vous présente la traduction de l'article " 5 secrets cachés en Java " de Justin Albano .

Vous voulez devenir un Java Jedi? Découvrez les anciens secrets de Java. Nous nous concentrerons sur l'extension des annotations, l'initialisation, les commentaires et les interfaces d'énumération.

Avec le développement des langages de programmation, des fonctions cachées commencent également à apparaître, et les constructions auxquelles les fondateurs n'ont jamais pensé sont de plus en plus répandues pour un usage général. Certaines de ces fonctions sont généralement acceptées dans la langue, tandis que d'autres se déplacent dans les coins les plus sombres de la communauté linguistique. Dans cet article, nous examinerons cinq secrets qui sont souvent négligés par de nombreux développeurs Java (pour être honnête, certains d'entre eux ont de bonnes raisons à cela). Nous examinerons à la fois les options pour leur utilisation et les raisons qui ont conduit à l'apparition de chaque fonction, ainsi que quelques exemples qui montrent quand il est conseillé d'utiliser ces fonctions.

Le lecteur doit comprendre que toutes ces fonctions ne sont pas réellement cachées, elles ne sont tout simplement pas souvent utilisées dans la programmation quotidienne. Certains d'entre eux peuvent être très utiles au bon moment, alors que l'utilisation d'autres est presque toujours une mauvaise idée, et ils sont présentés dans cet article pour intéresser le lecteur (et éventuellement le faire rire). Le lecteur doit également décider quand utiliser les fonctions décrites dans cet article: "Le fait que cela puisse être fait ne signifie pas qu'il doit être fait."

1. Mettre en œuvre des annotations


À partir du kit de développement Java (JDK) 5, les annotations font partie intégrante de nombreuses applications et environnements Java. Dans la grande majorité des cas, les annotations s'appliquent aux constructions telles que les classes, les champs, les méthodes, etc. Cependant, ils peuvent également être utilisés comme interfaces implémentées. Par exemple, supposons que nous ayons la définition d'annotation suivante:

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Test { String name(); } 

Nous appliquons généralement cette annotation à une méthode comme indiqué ci-dessous:

 public class MyTestFixure { @Test public void givenFooWhenBarThenBaz() { // ... } } 

Ensuite, nous pouvons traiter cette annotation comme décrit dans Création d'annotations en Java . Si nous voulions également créer une interface qui nous permette de créer des tests en tant qu'objets, nous devrions créer une nouvelle interface, en l'appelant quelque chose d'autre, et non pas Test:

 public interface TestInstance { public String getName(); } 

Ensuite, nous pouvons créer une instance de l'objet TestInstance:

 public class FooTestInstance implements TestInstance { @Override public String getName() { return "Foo"; } } TestInstance myTest = new FooTestInstance(); 

Bien que notre annotation et notre interface soient presque identiques, avec une duplication très notable, il semble qu'il n'y ait aucun moyen de combiner ces deux constructions. Heureusement, l'apparence est trompeuse et il existe une méthode pour combiner ces deux constructions: Implémentation des annotations:

 public class FooTest implements Test { @Override public String name() { return "Foo"; } @Override public Class<? extends Annotation> annotationType() { return Test.class; } } 

Notez que nous devons implémenter la méthode annotationType et également retourner le type d'annotation, car il s'agit d'une partie implicite de l'interface Annotation. Bien que dans presque tous les cas, l'implémentation d'annotations ne soit pas la bonne décision de conception (le compilateur Java affichera un avertissement lors de l'implémentation de l'interface), cela peut être utile dans certains cas, par exemple, dans le cadre des annotations.

2. Blocs d'initialisation non statiques.


En Java, comme dans la plupart des langages de programmation orientés objet, les objets sont créés exclusivement à l'aide du constructeur (à quelques exceptions près, comme la désérialisation des objets Java). Même lorsque nous créons des méthodes d'usine statiques pour créer des objets, nous enfermons simplement un appel dans le constructeur de l'objet pour l'instancier. Par exemple:

 public class Foo { private final String name; private Foo(String name) { this.name = name; } public static Foo withName(String name) { return new Foo(name); } } Foo foo = Foo.withName("Bar"); 

Par conséquent, lorsque nous voulons initialiser un objet, nous combinons la logique d'initialisation dans le constructeur de l'objet. Par exemple, nous définissons le champ de nom de la classe Foo dans son constructeur paramétré. Bien qu'il puisse sembler raisonnable de supposer que toute la logique d'initialisation se trouve dans le constructeur ou l'ensemble de constructeurs de la classe, ce n'est pas le cas en Java. Au lieu de cela, nous pouvons utiliser des blocs d'initialisation non statiques pour exécuter du code lors de la création d'un objet:

 public class Foo { { System.out.println("Foo:instance 1"); } public Foo() { System.out.println("Foo:constructor"); } } 

Les blocs d'initialisation non statiques sont spécifiés en ajoutant une logique d'initialisation à un ensemble d'accolades dans la définition de classe. Lorsqu'un objet est créé, les premiers blocs d'initialisation non statiques sont appelés, puis les constructeurs de l'objet. Notez que vous pouvez spécifier plusieurs blocs d'initialisation non statiques, auquel cas chacun est appelé dans l'ordre dans lequel il est spécifié dans la définition de classe. En plus des blocs d'initialisation non statiques, nous pouvons également créer des blocs statiques qui sont exécutés lorsque la classe est chargée en mémoire. Pour créer un bloc d'initialisation statique, nous ajoutons simplement le mot-clé statique:

 public class Foo { { System.out.println("Foo:instance 1"); } static { System.out.println("Foo:static 1"); } public Foo() { System.out.println("Foo:constructor"); } } 

Lorsque les trois méthodes d'initialisation sont présentes dans la classe (constructeurs, blocs d'initialisation non statiques et blocs d'initialisation statiques), les méthodes statiques sont toujours exécutées en premier (lorsque la classe est chargée en mémoire) dans l'ordre de leur déclaration, puis les blocs d'initialisation non statiques sont exécutés dans l'ordre dans lequel ils sont déclarés, et après eux - les concepteurs. Lorsqu'une superclasse est introduite, l'ordre d'exécution change un peu:

  1. Blocs d'initialisation de superclasse statique, dans l'ordre de leur déclaration
  2. Blocs d'initialisation de sous-classe statiques, dans l'ordre de leur déclaration
  3. Blocs d'initialisation de superclasse non statiques, dans l'ordre où ils sont déclarés
  4. Constructeur de superclasse
  5. Blocs d'initialisation de sous-classe non statiques, dans l'ordre dans lequel ils sont déclarés
  6. Constructeur de sous-classe

Par exemple, nous pouvons créer l'application suivante:

 public abstract class Bar { private String name; static { System.out.println("Bar:static 1"); } { System.out.println("Bar:instance 1"); } static { System.out.println("Bar:static 2"); } public Bar() { System.out.println("Bar:constructor"); } { System.out.println("Bar:instance 2"); } public Bar(String name) { this.name = name; System.out.println("Bar:name-constructor"); } } public class Foo extends Bar { static { System.out.println("Foo:static 1"); } { System.out.println("Foo:instance 1"); } static { System.out.println("Foo:static 2"); } public Foo() { System.out.println("Foo:constructor"); } public Foo(String name) { super(name); System.out.println("Foo:name-constructor"); } { System.out.println("Foo:instance 2"); } public static void main(String... args) { new Foo(); System.out.println(); new Foo("Baz"); } } 

Si nous exécutons ce code, nous obtenons la sortie suivante:

 Bar:static 1 Bar:static 2 Foo:static 1 Foo:static 2 Bar:instance 1 Bar:instance 2 Bar:constructor Foo:instance 1 Foo:instance 2 Foo:constructor Bar:instance 1 Bar:instance 2 Bar:name-constructor Foo:instance 1 Foo:instance 2 Foo:name-constructor 

Notez que les blocs d'initialisation statiques n'ont été exécutés qu'une seule fois, même si deux objets Foo ont été créés. Bien que des blocs d'initialisation non statistiques et statiques puissent être utiles, la logique d'initialisation doit être placée dans les constructeurs et des méthodes (ou méthodes statiques) doivent être utilisées dans les cas où une logique complexe nécessite l'initialisation de l'état de l'objet.

3. Initialisation double parenthèse


De nombreux langages de programmation incluent une sorte de mécanisme de syntaxe pour créer rapidement et brièvement une liste ou une carte (ou un dictionnaire) sans utiliser de code de modèle détaillé. Par exemple, C ++ inclut l' initialisation entre parenthèses , qui permet aux développeurs de créer rapidement une liste de valeurs énumérées ou même d'initialiser des objets entiers si le constructeur de l'objet prend en charge cette fonction. Malheureusement, avant JDK 9, une telle fonction n'était pas implémentée (plus à ce sujet plus tard). Pour créer simplement une liste d'objets, nous procédons comme suit:

 List<Integer> myInts = new ArrayList<>(); myInts.add(1); myInts.add(2); myInts.add(3); 

Bien que cela remplisse notre objectif de créer une nouvelle liste initialisée avec trois valeurs, elle est trop verbeuse, obligeant le développeur à répéter le nom de la variable de liste pour chaque ajout. Pour raccourcir ce code, nous pouvons utiliser une double initialisation des crochets :

 List < Integer >List<Integer> myInts = new ArrayList<>() {{ add(1); add(2); add(3); }}; 

Une initialisation à deux crochets, qui tire son nom d'un ensemble de deux accolades ouvertes et fermées, est en fait une collection de plusieurs éléments de syntaxe. Tout d'abord, nous créons une classe interne anonyme qui étend la classe ArrayList. Comme ArrayList n'a pas de méthodes abstraites, nous pouvons créer un corps vide pour une implémentation anonyme:

 List<Integer> myInts = new ArrayList<>() {}; 

En utilisant ce code, nous créons essentiellement une sous-classe anonyme, ArrayList est exactement la même que la ArrayList d'origine. L'une des principales différences est que notre classe interne a une référence implicite à la classe contenante (sous la forme d'une variable capturée par celle-ci), car nous créons une classe interne non statique. Cela nous permet d'écrire une logique intéressante, sinon confuse. Par exemple, ajouter cette variable à une classe interne anonyme initialisée avec un double crochet:

 public class Foo { public List<Foo> getListWithMeIncluded() { return new ArrayList<Foo>() {{ add(Foo.this); }}; } public static void main(String... args) { Foo foo = new Foo(); List<Foo> fooList = foo.getListWithMeIncluded(); System.out.println(foo.equals(fooList.get(0))); } } 

Si cette classe interne était définie comme statique, nous n'aurions pas accès à Foo.this. Par exemple, le code suivant qui crée une classe interne FooArrayList statique n'a pas accès au lien Foo.this et ne compile donc pas:

 public class Foo { public List<Foo> getListWithMeIncluded() { return new FooArrayList(); } private static class FooArrayList extends ArrayList<Foo> {{ add(Foo.this); }} } 

En reprenant la construction avec notre ArrayList initialisé entre crochets, une fois qu'une classe interne non statique a été créée, nous utilisons des blocs d'initialisation non statiques, comme décrit ci-dessus, pour ajouter les trois éléments initiaux lors de l'instanciation d'une classe interne anonyme. Lorsqu'une classe interne anonyme est créée et lorsqu'il n'y a qu'un seul objet d'une classe interne anonyme, nous pouvons dire que nous avons créé un objet interne non statique qui ajoute trois éléments initiaux lors de sa création. Cela se verra si nous séparons une paire d'accolades, où une accolade représente la définition d'une classe interne anonyme et l'autre marque le début de la logique d'initialisation de l'instance:

 List<Integer> myInts = new ArrayList<>() { { add(1); add(2); add(3); } }; 

Bien que cette astuce puisse être utile, JDK 9 ( JEP 269 ) a remplacé l'utilité de cette astuce par un ensemble de méthodes d'usine statiques pour List (ainsi que de nombreux autres types de collections). Par exemple, nous pourrions créer une liste plus tôt en utilisant ces méthodes d'usine statiques, comme indiqué ci-dessous:

 List<Integer> myInts = List.of(1, 2, 3); 

Cette technique d'usine statique est utilisée pour deux raisons principales: (1) une classe interne anonyme n'est pas créée et (2) pour réduire le code standard nécessaire pour créer une liste. Il faut se rappeler que dans ce cas, le résultat de List est inchangé et ne peut pas être modifié après sa création. Pour créer un fichier List mutable avec tous les éléments initiaux, nous devons utiliser une méthode régulière ou une méthode avec une parenthèse d'initialisation double.

Notez que l'initialisation simple, le double crochet et les méthodes d'usine statique JDK 9 ne sont pas uniquement disponibles pour List. Ils sont disponibles pour les objets Set et Map, comme illustré dans l'extrait de code suivant:

 //   Map<String, Integer> myMap = new HashMap<>(); myMap.put("Foo", 10); myMap.put("Bar", 15); //     Map<String, Integer> myMap = new HashMap<>() {{ put("Foo", 10); put("Bar", 15); }}; //    Map<String, Integer> myMap = Map.of("Foo", 10, "Bar", 15); 

Il est important de comprendre comment le double support est initialisé avant de décider de son utilisation. Cela améliore la lisibilité du code, mais certains effets secondaires peuvent apparaître.

4. Commentaires exécutables


Les commentaires font partie intégrante de presque tous les programmes, et le principal avantage des commentaires est qu'ils ne sont pas exécutés. Cela devient encore plus évident lorsque nous commentons une ligne de code dans notre programme: nous voulons enregistrer le code dans notre application, mais nous ne voulons pas qu'il s'exécute. Par exemple, le programme suivant affiche «5» en conséquence:

 public static void main(String args[]) { int value = 5; // value = 8; System.out.println(value); } 

Beaucoup de gens pensent que les commentaires ne sont jamais exécutés, mais ce n'est pas entièrement vrai. Par exemple, que produira l'extrait de code suivant?

 public static void main(String args[]) { int value = 5; // \u000dvalue = 8; System.out.println(value); } 

Vous pouvez supposer qu'il s'agit à nouveau de 5, mais si nous exécutons le code ci-dessus, nous verrons 8 à la sortie. La raison de cette «erreur» est le caractère Unicode \ u000d; ce caractère est en fait un retour chariot Unicode et le code source Java est utilisé par le compilateur sous forme de fichiers texte au format Unicode. Son ajout au code définit la valeur = 8 dans la ligne suivant le commentaire, assurant son exécution. Cela signifie que le fragment de code ci-dessus est en fait égal à ce qui suit:

 public static void main(String args[]) { int value = 5; // value = 8; System.out.println(value); } 

Bien que cela ressemble à un bogue Java, il s'agit en fait d'une fonctionnalité spécialement ajoutée au langage. L'objectif initial était de créer un langage indépendant de la plate-forme (d'où la création d'une machine virtuelle Java ou JVM), et l'interopérabilité du code source est un aspect clé de cet objectif. En permettant au code source Java de contenir des caractères Unicode, nous pouvons utiliser des caractères non latins de manière universelle. Cela garantit que le code écrit dans une région du monde (qui peut contenir des caractères non latins, comme dans les commentaires), peut être exécuté dans n'importe quelle autre. Voir Section 3.3 Spécifications du langage Java ou JLS pour plus d'informations.

Nous pouvons pousser cela à l'extrême et même écrire une application entière en Unicode. Par exemple, que fait le programme suivant (code source, dérivé de Java: exécution de code dans les commentaires?! )?

 \u0070\u0075\u0062\u006c\u0069\u0063\u0020\u0020\u0020\u0020 \u0063\u006c\u0061\u0073\u0073\u0020\u0055\u0067\u006c\u0079 \u007b\u0070\u0075\u0062\u006c\u0069\u0063\u0020\u0020\u0020 \u0020\u0020\u0020\u0020\u0073\u0074\u0061\u0074\u0069\u0063 \u0076\u006f\u0069\u0064\u0020\u006d\u0061\u0069\u006e\u0028 \u0053\u0074\u0072\u0069\u006e\u0067\u005b\u005d\u0020\u0020 \u0020\u0020\u0020\u0020\u0061\u0072\u0067\u0073\u0029\u007b \u0053\u0079\u0073\u0074\u0065\u006d\u002e\u006f\u0075\u0074 \u002e\u0070\u0072\u0069\u006e\u0074\u006c\u006e\u0028\u0020 \u0022\u0048\u0065\u006c\u006c\u006f\u0020\u0077\u0022\u002b \u0022\u006f\u0072\u006c\u0064\u0022\u0029\u003b\u007d\u007d 

Si vous placez le code ci-dessus dans un fichier appelé Ugly.java et l'exécutez, Hello world sera imprimé sur la sortie standard. Si nous convertissons ces caractères Unicode en caractères du code standard américain pour l'échange d'informations (ASCII) , nous obtenons le programme suivant:

 public class Ugly { public static void main(String[] args){ System.out.println("Hello w"+"orld"); } } 

Ainsi, les caractères Unicode peuvent être inclus dans le code source Java, mais s'ils ne sont pas requis, il est fortement recommandé de ne pas les utiliser (par exemple, pour inclure des caractères non latins dans les commentaires). S'ils sont néanmoins requis, assurez-vous qu'ils n'incluent pas de caractères, tels que les retours chariot, qui modifient le comportement attendu du code source.

5. Implémentation de l'interface Enum


L'une des limites des énumérations (une liste d'énumération) par rapport aux autres classes de Java est que les énumérations ne peuvent pas étendre une autre classe ou les énumérations elles-mêmes. Par exemple, vous ne pouvez pas effectuer les opérations suivantes:

 public class Speaker { public void speak() { System.out.println("Hi"); } } public enum Person extends Speaker { JOE("Joseph"), JIM("James"); private final String name; private Person(String name) { this.name = name; } } Person.JOE.speak(); 

Cependant, nous pouvons forcer nos énumérations à implémenter l'interface et fournir une implémentation pour ses méthodes abstraites comme suit:

 public interface Speaker { public void speak(); } public enum Person implements Speaker { JOE("Joseph"), JIM("James"); private final String name; private Person(String name) { this.name = name; } @Override public void speak() { System.out.println("Hi"); } } Person.JOE.speak(); 

Nous pouvons maintenant utiliser une instance de Person partout où un objet Speaker est requis. De plus, nous pouvons également assurer la mise en œuvre de méthodes d'interface abstraites sur une base continue (les méthodes dites spécifiques aux constantes):

 public interface Speaker { public void speak(); } public enum Person implements Speaker { JOE("Joseph") { public void speak() { System.out.println("Hi, my name is Joseph"); } }, JIM("James"){ public void speak() { System.out.println("Hey, what's up?"); } }; private final String name; private Person(String name) { this.name = name; } @Override public void speak() { System.out.println("Hi"); } } Person.JOE.speak(); 

Contrairement à certains des autres secrets de cet article, cette technique ne doit être utilisée que lorsque cela est nécessaire. Par exemple, si une constante d'énumération, telle que JOE ou JIM, peut être utilisée à la place d'une interface, telle que Speaker, alors l'énumération définissant une constante doit implémenter ce type d'interface. Voir Paragraphe 38 (p. 176-9) Java effectif, 3e édition pour plus d'informations.

Conclusion


Dans cet article, nous avons examiné cinq secrets cachés en Java, à savoir: (1) les annotations peuvent être étendues, (2) les blocs d'initialisation non statiques peuvent être utilisés pour configurer un objet lors de sa création, (3) l'initialisation avec des crochets doubles peut être utilisée pour exécuter des instructions lors de la création une classe interne anonyme, (4) des commentaires peuvent parfois être exécutés et (5) des énumérations peuvent implémenter des interfaces. Bien que ces fonctions soient utilisées par un certain type de tâche, certaines doivent être évitées (par exemple, créer des commentaires exécutables). Lorsque vous décidez d'utiliser ces secrets, veillez à respecter la règle: "Le fait que cela puisse être fait ne signifie pas que cela doit être fait."

Source: https://habr.com/ru/post/fr480544/


All Articles