J'ai essayé d'expliquer Effective Java 3rd Edition "presque tous les chapitres" en "japonais facile à lire".

Je pense que Effective Java est un livre incontournable pour devenir un ingénieur Java "à part entière". Surtout les ingénieurs qui sont en mesure de rendre publiques des API ne peuvent pas en créer de décentes à moins de comprendre ce qui est écrit dans ce livre.

Il va sans dire que c'est un livre merveilleux, mais d'un autre côté, j'ai aussi le sentiment que c'est un livre difficile à comprendre, "Bref, c'est ce que c'est".

La cause est probablement la suivante.

L'essence du contenu lui-même n'est pas très difficile, mais je pense que c'est dommage que le seuil de ce livre soit relevé pour cette raison.

Par conséquent, dans cet article, je voudrais expliquer (presque) tous les éléments en "japonais facile à lire" autant que possible.

Cependant, j'ai omis des explications pour les éléments qui sont trop évidents ou faciles à lire. De plus, mon opinion personnelle est mitigée. J'espère que vous pouvez le voir sur cette prémisse.

Effective Java 3rd Edition est écrit pour Java 9. Par conséquent, je publierai la documentation officielle de Java 9 au cas où (car il est difficile à atteindre de manière inattendue).

[Java 9] Haut du document officiel https://docs.oracle.com/javase/jp/9/

[Java 9] JDK Javadoc (suivi de l'écran supérieur) https://docs.oracle.com/javase/jp/9/docs/toc.htm

[Java 9] Spécifications du langage Java (suivies de l'écran supérieur) https://docs.oracle.com/javase/specs/jls/se9/html/index.html

Chapitre 1 Introduction

Il ne contient que les définitions des termes utilisés dans les livres. Vous n'êtes pas obligé de le lire. (fin)

Chapitre 2 Création et disparition d'objets

Élément 1 Considérez une méthode de fabrique statique au lieu d'un constructeur.

Par exemple, cela ressemble à ce qui suit.

Considérez d'abord la méthode de fabrique statique, et si elle est subtile, choisissez un constructeur.

◆ Avantages

** ① Vous pouvez donner un nom descriptif. ** **

Le constructeur présente les inconvénients suivants.

La méthode de l'usine statique ne présente pas cet inconvénient.

** ② Il n'est pas nécessaire de créer un nouvel objet. Réutilisez le même objet. ** **

** ③ Vous pouvez renvoyer un objet de ce sous-type au lieu du type de retour lui-même. ** **

Par exemple, java.util.Collections a une méthode de fabrique statique appelée ʻemptyList () `. Il présente les caractéristiques suivantes.

Grâce à cela, l'API Collections est très simple. En particulier···

** ④ Vous pouvez changer le sous-type à renvoyer en fonction de la situation. ** **

En ③, l'explication est basée sur l'hypothèse que le même sous-type sera toujours retourné. Ce que je veux dire en ④, c'est que vous pouvez sélectionner et renvoyer celui qui convient à votre situation à partir de plusieurs sous-types.

** ⑤ Le sous-type à renvoyer peut être décidé lors de l'exécution. (C'est OK même si ce n'est pas décidé au moment de la mise en œuvre de la méthode de l'usine statique.)

Par exemple, JDBC DriverManager.getConnection () correspond à ceci.

En conséquence, il devient plus flexible en tant qu'API.

◆ Inconvénients

** ① L'utilisateur ne peut pas créer une sous-classe du type de retour. ** **

Par exemple, ʻemptyList () `dans java.util.Collections renvoie EmptyList, mais comme EmptyList est une classe privée, les utilisateurs ne peuvent pas créer de sous-classes d'EmptyList.

Non limité à cet exemple, je ne pouvais pas penser à un cas qui me donnerait envie de créer une sous-classe. En pratique, je ne pense pas qu'il y ait de faiblesses ou de restrictions.

** ② Il est difficile pour les utilisateurs de trouver des méthodes d'usine statiques. ** **

C'est certainement le cas. Dans Javadoc, le constructeur est une section séparée, ce qui le distingue, mais les méthodes de fabrique statiques sont enterrées dans la liste des méthodes.

Essayez de rendre l'API facile à comprendre pour les utilisateurs en suivant les modèles de dénomination généraux ci-dessous.

Modèle de dénomination Exemple Sens
from Date d = Date.from(instant); Conversion de type.
of Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING); Résumer.
valueOf BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE); Il est utilisé de manière interchangeable avec from et of.
instance / getInstance StackWalker luke = StackWalker.getInstance(options); Renvoie l'instance en fonction du paramètre.
create / newInstance Object newArray = Array.newInstance(classObject, arrayLen); Renvoie une nouvelle instance pour chaque appel.
obtenir le nom du type FileStore fs = Files.getFileStore(path); Renvoyez une classe différente de la vôtre.
nouveau nom de type BufferedReader br = Files.newBufferedReader(path); Renvoyez une classe différente de la vôtre. Renvoie une nouvelle instance pour chaque appel.
Nom du modèle List<Complaint> litany = Collections.list(legacyLitany); getNom du modèle、newNom du modèleの短縮版。

référence

Vue d'ensemble du cadre des collections https://docs.oracle.com/javase/jp/9/docs/api/java/util/doc-files/coll-overview.html

Point 2 Considérez un constructeur face à de nombreux paramètres de constructeur

[NG] Modèle télescopique

public class NutritionFacts {
    //Définition de champ omise

    public NutritionFacts(int servingSize, int servings) {
        this(servingSize, servings, 0);
    }

    public NutritionFacts(int servingSize, int servings, int calories) {
        this(servingSize, servings, calories, 0);
    }

    //Le flux ci-dessus se poursuit sans fin ...
}

Point NG

[NG] Modèle Java Beans

//Ce sont simplement des Java Beans.
public class NutritionFacts {
    //Définition de champ omise

    public NutritionFats() {}
    
    public void setServingSize(int val) { //Abréviation}
    public void setServings(int val) { //Abréviation}
    public void setCalories(int val) { //Abréviation}
    //Le passeur continue ...
}

Point NG

[Modèle de constructeur]

//Code d'utilisateur
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).
    calories(100).sodium(35).carbohydrate(27).build();

Avantages

Inconvénients (triviaux)

Élément 3 Appliquer les caractéristiques de singleton avec un constructeur privé ou un type enum

Il existe trois façons de réaliser un singleton. Choisissez le meilleur pour votre situation.

Si vous implémentez une seule tonne avec le type enum, cela ressemblera à ceci.

enum type tonne unique


public enum Elvis {
    INSTANCE;
    public void someMethod();
}

Le meilleur moyen est d'utiliser enum, car d'autres méthodes risquent de ne pas être une seule tonne et prendront la peine de l'éviter. Plus précisément, c'est comme suit.

Je pense que ces risques peuvent être ignorés dans la pratique, mais il faut y réfléchir correctement.

La méthode d'utilisation du type enum n'a pas été vue dans la pratique, mais elle est rationnelle dans les points ci-dessus et devrait être activement utilisée.

Les deux méthodes d'utilisation du constructeur privé présentent les avantages suivants. Cependant, je pense qu'il n'y a qu'un nombre limité de cas où vous voulez obtenir ces avantages, donc en fin de compte, enum est le meilleur choix dans de nombreux cas.

Méthode Avantages
Définir sur le champ public et publier ・ L'API peut clairement comprendre qu'il s'agit d'une seule tonne.
· Facile
Retour avec la méthode d'usine statique ・ Vous pouvez changer plus tard s'il doit être singleton
・ Peut être une usine singleton générique
・ Vous pouvez utiliser des références de méthode

Item 4 Forcer l'impossibilité d'instanciation avec un constructeur privé

Une classe constituée uniquement de méthodes statiques et de champs statiques est communément appelée classe utilitaire.

De telles classes ne doivent pas être instanciées et utilisées, mais si vous pouvez utiliser le constructeur, vous risquez de les instancier accidentellement. Je ne pense pas qu'il y ait vraiment de mal, mais cela me rend très déçu quand il est utilisé de cette manière.

Alors implémentons un constructeur privé comme celui-ci:

public class UtilityClass {
    //Supprimez le constructeur par défaut afin qu'il ne puisse pas être instancié.
    //En laissant un commentaire comme celui-ci, transmettons l'intention d'implémenter ce constructeur à la postérité.
    private UtilityClass() {
        throw new AssertionError();
    }
}

Cela vous évitera d'être instancié ou hérité accidentellement pour créer des sous-classes.

Élément 5 Sélectionnez l'injection de dépendances plutôt que de lier directement les ressources

Prenons un correcteur orthographique comme exemple.

[NG] Implémenté en tant que classe utilitaire

public class SpellChecker{
    //Dictionnaire utilisé pour Speccheck
    private static final Lexicon dictionary = ...;

    //Supprimer l'instanciation selon la méthode de l'élément 4
    private SpellChecker() {} 

    public static boolean isValid(String word) { ... }
}

[NG] Mis en œuvre pour être une seule tonne

// SpellChecker.INSTANCE.isValid("some-word");Utilisez comme.
public class SpellChecker{
    //Dictionnaire utilisé pour Speccheck
    private final Lexicon dictionary = ...;

    //Supprimer l'instanciation selon la méthode de l'élément 4
    private SpellChecker() {} 
    public static SpellChecker INSTANCE = new SpellChecker(...); 

    public static boolean isValid(String word) { ... }
}

Étant donné qu'un seul dictionnaire peut être utilisé dans ces exemples NG, il n'est pas possible de changer de dictionnaire en fonction de la situation. Cette difficulté s'applique non seulement au code de production, mais également aux tests.

Il existe un moyen de fournir une méthode comme setDictionary (lexicon) afin que vous puissiez la changer plus tard, mais c'est difficile à comprendre pour l'utilisateur. Ce n'est pas non plus sûr pour les threads.

En premier lieu, le fait que le dictionnaire change en fonction de la situation signifie que le dictionnaire est un «état». Par conséquent, vous devez implémenter le correcteur orthographique comme une classe qui peut être instanciée et utilisée.

Plus précisément, c'est comme suit.

[OK] Implémenté dans le style de l'injection de dépendances

public class SpellChecker{
    //Dictionnaire utilisé pour Speccheck
    private final Lexicon dictionary;

    //Puisqu'il a un "état" appelé dictionnaire, il est instancié et utilisé.
    //A ce moment, une dépendance appelée dictionnaire est injectée.
    public SpellChecker(Lexicon dictionary) {
        this.dictionary = Objects.requireNonNull(dictionary);
    } 

    public static boolean isValid(String word) { ... }
}

De cette façon, vous pouvez changer de dictionnaire en fonction de la situation et ce sera thread-safe.

Élément 6 Évitez de créer des objets inutiles

La réutilisation d'objets peut minimiser le coût de création d'objets et augmenter leur vitesse.

En revanche, si vous créez beaucoup d'objets inutiles, ce sera extrêmement lent.

La nature immuable est très importante car les objets immuables (objets immuables) peuvent toujours être réutilisés en toute sécurité.

[NG Partie 1]

// new String()Donc, nous créons un objet inutile.
// "bikini"Donc, nous créons un objet inutile.
String s = new String("bikini");

【OK】

//L'objet généré est"bikini"Instance de chaîne uniquement.
//Littéraux de chaîne dans la même JVM"bikini"Les instances de sont toujours réutilisées.
String s = "bikini";

[NG Partie 2]

//Puisqu'il s'agit d'un constructeur, de nouveaux objets sont toujours créés.
new Boolean(String);

【OK】

//La méthode de fabrique statique n'a pas besoin de créer un nouvel objet.
//Un objet booléen vrai ou faux est réutilisé.
Boolean.valueOf(String);

[NG Partie 3]

//Un objet Pattern est créé à l'intérieur des correspondances.
// isRomanNumeral()À chaque appel, un objet Pattern est créé.
static boolean isRomanNumeral(String s){
    return s.matches("Expressions régulières. Le contenu est omis.");
}

【OK】

public class RomanNumerals {
    //Vous réutilisez un objet Pattern.
    private static final Pattern ROMAN = Pattern.compile("Expressions régulières. Le contenu est omis.");

    static boolean isRomanNumeral(String s) {
        return ROMAN.matcher(s).matches();
    }
}

[NG Partie 4]

//Exemple de NG en boxe automatique
private static long sum() {
    Long sum = 0L;
    for (long i = 0; i <= Integer.MAX_VALUE; i++)
        //Comme Long est immuable, une nouvelle instance sera créée à chaque fois qu'elle est ajoutée.
        sum += i;

    return sum;
}

【OK】

private static long sum() {
    //Passer au type primitif (Long)-> long)
    long sum = 0L;
    for (long i = 0; i <= Integer.MAX_VALUE; i++)
        sum += i;

    return sum;
}

À propos, par exemple, Map.keySet () renvoie une vue Set (adaptateur), mais quel que soit le nombre de fois que vous appelez keySet (), la même instance Set sera renvoyée. Il est facile de penser qu'une nouvelle instance est créée chaque fois que vous l'appelez, mais en fait, l'instance est également réutilisée à ces endroits et l'efficacité est améliorée. Même lorsque nous implémentons l'API, nous devons l'implémenter de manière optimale du point de vue «Est-il nécessaire de créer une nouvelle instance?

Élément 7 Supprimer les références d'objet obsolètes

Si votre classe gère sa propre mémoire qui échappe au contrôle du garbage collector, vous devez effacer les références aux objets dont vous n'avez plus besoin (définissez la variable sur null).

Non géré signifie que le garbage collector ne peut pas reconnaître même un objet pratiquement inutilisé.

Normalement, les variables hors de portée sont soumises au garbage collection. En revanche, si vous gérez des objets dans une classe, ils ne seront pas hors de portée et ne seront pas soumis au garbage collection. Du point de vue du garbage collector, l'objet est considéré comme étant en cours d'utilisation.

Dans le livre, ce qui précède est expliqué en utilisant une implémentation de pile simple comme exemple. Veuillez consulter le livre pour plus de détails.

Cas d'implémentation de votre propre cache

Même si vous implémentez votre propre cache, cela correspond au "cas de gestion de votre propre mémoire dans la classe" mentionné ci-dessus.

Par exemple, lors de la mise en cache de données d'image avec HashMap, HashMap doit être géré comme un champ d'un objet A, de sorte que les références sont connectées comme suit.

Source-> Objet A-> HashMap-> clé et valeur

Même si les données (clé et valeur) gérées par HashMap ne sont plus nécessaires, ces données ne seront pas collectées tant que l'objet A et HashMap continueront à être référencés. En conséquence, la mémoire augmente.

Le remède dans ce cas est le suivant. (Je ne pense pas qu'il existe de nombreuses possibilités d'implémenter le cache vous-même ...)

** Méthode: implémentez le cache avec WeakHashMap. ** **

WeakHashMap sera soumis au prochain GC pour son entrée (paire clé / valeur) lorsque la clé n'est plus référencée par quelqu'un d'autre que son objet WeakHashMap. Nous utilisons un mécanisme appelé référence faible.

Pensez à l'adopter si vous souhaitez la supprimer du cache avec l'état que "la clé n'est référencée par personne d'autre que cet objet WeakHashMap".

Comme expliqué dans ce livre, s'il est supprimé du cache si facilement, il ne peut plus être appelé cache. Pour plus de détails, voir [Voir ci-dessous. ](# Qu'est-ce qu'une référence faible référence)

** Méthode (2) Supprimez régulièrement les anciennes données du cache avec ScheduledThreadPoolExecutor. ** **

Cet article est recommandé pour une compréhension rapide de l'utilisation de ScheduledThreadPoolExecutor. https://codechacha.com/ja/java-scheduled-thread-pool-executor/

Pensez à l'utiliser si vous souhaitez supprimer quelque chose qui a été enregistré dans le cache depuis un certain temps.

** Méthode ③ Lors de l'ajout d'une nouvelle entrée dans le cache, supprimez l'ancienne. ** **

C'est simple. Similaire à (2), si vous souhaitez supprimer quelque chose qui a été enregistré dans le cache depuis un certain temps, envisagez de l'adopter.

Cas de l'enregistrement des écouteurs et des rappels en mémoire

Lors de la création d'une API qui permet aux écouteurs et aux rappels d'être enregistrés à partir du client, les écouteurs enregistrés et les rappels ne seront pas soumis à GC à moins qu'ils ne soient créés avec une considération appropriée.

C'est une bonne idée d'utiliser un mécanisme de référence faible, comme l'enregistrer en tant que clé pour WeakHashMap. S'il n'est plus utilisé par quelqu'un d'autre que WeakHashMap, il sera soumis à GC.

Référence: Qu'est-ce qu'une référence faible?

Les objets qui ne peuvent normalement pas atteindre le référent (objets qui ne sont utilisés par personne) sont soumis à GC et sont supprimés de la mémoire.

Cependant, cela peut poser problème s'il est supprimé immédiatement. Il existe un mécanisme pour empêcher les objets qui ne peuvent pas être atteints depuis la source de référence d'être immédiatement soumis à GC. C'est le package java.lang.ref.

Dans le monde de ce package, les références ordinaires sont appelées «références fortes» et leurs propres «références» sont définies comme suit.

java.lang.Types de références dans ref Difficulté à devenir une cible GC (valeur relative) La description Utilisation
Référence faible
Effacé facilement
Si seulement vous (objet WeakReference) faites référence à un objet A, vous serez soumis au prochain GC. Utilisez cette option lorsque vous souhaitez effacer immédiatement l'objet A de la mémoire lorsqu'il n'y a pas de références fortes à l'objet A. WeakHashMap utilise WeakReference dans ce but précis. Dans ce livre, il est écrit qu'il peut être utilisé comme cache, mais si la référence forte disparaît, elle disparaîtra, je pense que ce n'est plus un cache, donc je pense qu'il n'y a presque aucune possibilité de l'utiliser.
Référence souple ★★
Assez têtu
Si vous seul (objet SoftReference) faites référence à un objet A, l'objet référencé a été créé récemment./S'il est référencé, il ne sera pas soumis au prochain CG. Sinon, il sera soumis au prochain CG. Je pense que c'est le cas si vous l'utilisez à des fins de mise en cache, mais Java SE n'a pas de carte qui le prend en charge. Il semble qu'il n'y ait presque aucune chance de l'utiliser réellement.
Référence fantôme ★★★
En gros ça ne disparaît pas
Même si vous seul (objet PhantomReference) faites référence à un certain objet A, il ne sera pas la cible de GC. (inconnue)

Point 8 Évitez les finisseurs et les nettoyants

Le finaliseur ici est java.lang.Object # finalize () ou une méthode qui le remplace dans une sous-classe.

Un nettoyeur est un java.lang.ref.Cleaner.

Il y a plusieurs raisons pour lesquelles ce n'est pas bon, mais il n'est presque pas nécessaire de comprendre le contenu.

Ne l'utilisez jamais car il est dangereux de toute façon. C'est très bien.

Élément 9 Sélectionnez try-with-resources plutôt que try-finally

try-finally a les problèmes suivants:

Try-with-resources résout ces problèmes.

Si une exception se produit pendant l'essai et puis la fermeture lève également une exception, la première sera prioritaire et levée. Vous pouvez attraper cela dans la clause catfh.

Pour accéder à ce dernier, utilisez la méthode getSuppressed dans le premier objet d'exception. Cependant, dans de nombreux cas, vous voudrez connaître le premier, il semble donc que vous n'aurez pas beaucoup d'occasions de l'utiliser.

Chapitre 3 Méthodes communes à tous les objets

Point 10 Lorsque vous remplacez les égaux, suivez le contrat général

Je pense que la possibilité de passer outre la méthode des égaux est limitée en premier lieu. Si vous souhaitez remplacer, certaines conditions doivent être remplies.

Si vous devez remplacer la méthode d'égalité, respectez cette exigence. Plus précisément, les exigences sont les suivantes.

Le livre dit qu'il y a des choses à garder à l'esprit pour chaque exigence, mais il n'y a pas beaucoup d'occasions de remplacer les égaux en premier lieu, il est donc probablement moins coûteux d'en apprendre davantage lorsque vous n'en avez pas besoin.

Pour cette raison, je ne fais référence aux livres que lorsque j'en ai besoin, et je n'entrerai pas non plus dans les détails dans cet article.

Élément 11 Lorsque vous remplacez égal à, remplacez toujours hashCode

Comme vous pouvez le voir dans l'élément 10, il n'y a pas beaucoup de chances de remplacer l'égalité, donc cet élément ne semble pas non plus très important. Je vais juste vous donner un aperçu.

Les conditions requises pour remplacer la méthode hashCode sont les suivantes:

Élément 12 Toujours remplacer toString

L'avantage de la substitution de la méthode toString est qu'elle facilite le débogage pour les utilisateurs de cette classe.

Cependant, le système créé dans la pratique n'est pas une œuvre d'art et les ressources humaines et temporelles sont limitées. Par conséquent, je pense que vous devez décider de passer ou non si nécessaire.

Si vous remplacez la méthode toString, tenez compte des éléments suivants:

Point 13 Remplacer soigneusement le clone

Overriding Object.clone () est fondamentalement NG. La raison en est la suivante.

Puisqu'il est NG, vous ne devez pas remplacer Object.clone () à moins qu'il y ait une situation particulière où vous avez déjà une classe qui remplace Object.clone () et que vous devez le réparer pour la maintenance.

Utilisez plutôt les méthodes suivantes. Ceux-ci ne présentent pas les inconvénients ci-dessus.

//Copier le constructeur
public Yum(Yum yum) { ... }

//Copier l'usine
public static Yum newInstance(Yum yum) { ... }

Que vous utilisiez un constructeur de copie ou une méthode de fabrique de copie ou Object.clone (), tenez compte des points communs suivants.

En d'autres termes, lors de la copie d'un champ, il ne suffit pas de définir la référence de l'objet source de la copie sur le champ de destination de la copie. En effet, la même référence d'objet est partagée entre la source de la copie et la destination de la copie. C'est la soi-disant copie superficielle. L'idée est évidente, mais elle est assez lourde à mettre en œuvre. Il existe une exception à cette règle, et s'il s'agit d'un objet immuable, vous pouvez copier la référence.

Pour comprendre cet élément, nous vous recommandons de connaître à l'avance les éléments suivants.

Point 14 Envisager de mettre en œuvre Comparable

En implémentant Comparable.compareTo () dans la classe que vous développez, vous pouvez stocker les objets de cette classe dans une collection et utiliser l'API pratique de la collection. Par exemple, vous pourrez bien trier.

Si vous souhaitez bénéficier de cet avantage, implémentez une interface comparable.

Dans ce cas, des exigences similaires au remplacement de la méthode égale doivent être satisfaites.

Il y a des notes sur les trois premières exigences. Étant donné une classe existante qui implémente Comparable, il est pratiquement impossible de répondre à ces exigences si vous l'étendez et ajoutez de nouveaux champs. Si vous le forcez, ce n'est plus du code orienté objet. Dans un tel cas, réalisons-le avec la composition au lieu d'étendre (point 18).

Et si nous violons la quatrième exigence? Un exemple de violation est Big Decimal. new BigDecimal ("1.0") et new BigDecimal ("1.00") ne sont pas égaux dans la méthode equals et égaux dans la méthode compareTo.

Si vous les mettez dans un nouveau HashSet, ils seront comparés par la méthode equiqls, donc le nombre d'éléments sera de 2. Par contre, si vous le mettez dans un nouveau TreeSet, le nombre d'éléments sera de 1 car il sera comparé par la méthode compareTo. Si vous ne gardez pas ce comportement à l'esprit, vous n'aurez aucune idée de la cause dans l'éventualité peu probable d'un problème.

En plus des quatre exigences, notez les points suivants:

private static final Comparator<PhoneNumber> COMPARATOR = 
            comparingInt((PhoneNumber pn) -> pn.areaCode)
                .thenComparingInt(pn -> pn.prefix)
                .thenComparingInt(pn -> pn.lineNum);

public int compareTo(PhoneNumber pn) {
    return COMPARATOR.compare(this, pn);
}
//Exemple NG
static Comparator<Object> hashCodeOrder = new Comparator<>() {
    public int compare(Object o1, Object o2) {
        return o1.hashCode() - o2.hashCode();
    }
}

Chapitre 4 Classes et interfaces

Point 15 Minimiser l'accessibilité aux classes et aux membres

Qu'est-ce que le masquage et l'encapsulation d'informations?

Minimisez la partie du composant accessible de l'extérieur (API publique). Rendre les autres parties (données internes, détails d'implémentation) inaccessibles de l'extérieur.

Pour faire simple, minimisez ce que vous déclarez comme public ou protégé. Concevez soigneusement votre classe pour ce faire.

Raisons de cacher et d'encapsuler des informations (pourquoi)

Le masquage et l'encapsulation des informations permettent aux composants d'être développés individuellement et optimisés individuellement. En d'autres termes, vous pouvez réduire considérablement la crainte que cela n'endommage les autres composants.

Le but du masquage et de l'encapsulation d'informations est de viser ce qui suit.

Si vous ne pouvez pas atteindre ces objectifs, il est inutile de viser la dissimulation ou l'encapsulation d'informations. Les programmes pratiques ne sont pas des œuvres d'art, mais un moyen pour une entreprise prospère. Donc, vous ne devriez pas être satisfait d'avoir fait quelque chose de beau (je fais très attention car il est facile d'être satisfait de cette façon).

Comment faire (comment)

Plus précisément, considérez les points suivants.

Élément 16 Dans la classe publique, utilisez la méthode d'accesseur au lieu du champ public.

Il va de soi d'ajouter setter / getter.

En pratique, je pense que c'est du bon sens de faire cela, et je pense qu'il est facile d'ajouter des setters / getters sans penser à rien. Cependant, si vous ne comprenez pas la raison, ce ne sera pas une bonne conception de classe. Comprenons la raison pour laquelle nous devrions ajouter à nouveau setter / getter.

La raison de l'ajout de setter / getter est de bénéficier du masquage et de l'encapsulation des informations. Plus précisément, c'est comme suit.

Puisque le point essentiel est de réduire le nombre d'API publiques autant que possible, il est peu nécessaire de préparer les setters / getters dans les champs pour les classes privées de package et les classes internes privées. Si vous essayez de fournir un setter / getter inutilement, la mise en œuvre prendra plus de temps et le code sera difficile à lire. Ne vous sentez pas comme "ajoutez juste un setter / getter sans réfléchir".

Point 17 Minimiser la variabilité

Les objets immuables (objets immuables) présentent plusieurs avantages, notamment le fait qu'ils peuvent être utilisés en toute sécurité avec les threads. Des exemples typiques dans JDK sont des classes de données de base encadrées telles que String, Integer, BigDecimal, BigInteger.

Si vous créez votre propre classe qui a des valeurs, rendez-la immuable à moins que vous n'ayez une bonne raison de la rendre variable. Même s'il n'est pas pratique de le rendre parfaitement immuable, c'est une bonne idée de le rendre aussi immuable que possible, par exemple en rendant le champ aussi définitif que possible. En effet, si le nombre d'états possibles est réduit, il y a des avantages tels que moins de problèmes.

Avantages des objets immuables

Inconvénients des objets immuables

Comment rendre un objet immuable (conditions à remplir)

Point 18 Choisissez la composition plutôt que l'héritage

L'héritage rompt l'encapsulation. En d'autres termes, les sous-classes dépendent des détails d'implémentation des superclasses. Par conséquent, des modifications dans les détails d'implémentation des superclasses peuvent conduire à des sous-classes ne fonctionnant pas comme prévu ou à des failles de sécurité.

Vous pourriez penser que ce n'est pas grave si la sous-classe ne remplace pas les méthodes de la superclasse, mais ce n'est pas le cas. La signature de la méthode que la superclasse a ajoutée ultérieurement peut entrer en conflit avec la méthode implémentée dans la sous-classe.

En raison de ces inconvénients, ne sautez pas dans l'héritage soudainement. Les compositions n'ont pas le même inconvénient.

Cependant, l'héritage est mauvais à 100% et la composition n'est pas positive à 100%. Pouvons choisir entre composition et héritage en fonction de la situation.

Quelle est la composition

Au lieu d'hériter d'une classe existante, conservez la classe existante dans un champ privé, comme illustré ci-dessous. Étendez une classe existante en appelant les méthodes de cette classe existante.


//Classe de transfert. La composition a été appliquée dans cette classe.
//Une classe est fournie séparément de InstrumentedSet afin qu'elle puisse être réutilisée.
public class ForwardingSet<E> implements Set<E> {
    //Tenez l'objet de la classe existante que vous souhaitez étendre dans le champ.
    private final Set<E> s;

    public ForwardingSet(Set<E> s) {
        this.s = s;
    }

    //Lancez le processus sur l'objet de la classe existante que vous souhaitez étendre.
    public void clear() {
        this.s.clear();
    }

    //Omis par la suite.
}

/*
Créez votre propre classe en héritant de la classe de transfert.
Vous pourriez penser: "Quoi? L'héritage n'est pas bon, non?", Mais l'héritage ici est
C'est une bonne décision de rendre le jeu de transfert réutilisable à d'autres fins.
*/
public class InstrumentedSet<E> extends FowardingSet<E> {
    //Cette classe est responsable de la gestion du nombre de fois qu'un ensemble a été ajouté au total.
    private int addCount = 0;

    public InstrumentedSet(Set<E> s) {
        super(s);
    }

    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    //Omis par la suite.
}

En passant, la technique de l'exemple de code ci-dessus n'est pas exactement la "délégation". S'il vous plaît soyez prudente.

Quand il est normal d'hériter

Vous pouvez hériter dans les cas suivants.

En tant que prémisse majeure lors de l'adoption de l'héritage, il est nécessaire qu'une relation «est-un» soit établie entre les classes. Autrement dit, si la classe B hérite de la classe A, la réponse «Sont tous des Bs A?» Doit être oui. L'héritage est une technique pour réaliser de telles relations dans le code.

Point 19 Conception et document pour l'héritage, sinon interdire l'héritage

Créer une classe qui est censée être héritée est en fait extrêmement difficile et difficile. Plus précisément, vous devez tenir compte des éléments suivants:

Comme vous pouvez le voir, c'est extrêmement difficile. Si vous créez une classe qui est censée être héritée par tous les moyens, soyez prêt à accepter les points ci-dessus. C'est un professionnel.

Je pense qu'il y a plus de cas où ce n'est pas le cas que lors de la création d'une classe qui est censée être héritée. Dans ce cas, rendez la classe finale ou rendez le constructeur privé et préparez une méthode de fabrique statique afin que la classe créée ne soit pas héritée par erreur.

Élément 20 Sélectionnez une interface sur une classe abstraite

Cet article est extrêmement difficile à lire. J'expliquerai en priorité sur la compréhensibilité.

Il existe plusieurs implémentations d'un «type». Par exemple, il existe plusieurs implémentations d'un «type» appelé Comparable. Java fournit les deux mécanismes suivants pour réaliser un tel type «autoriser plusieurs implémentations».

Si vous voulez créer un nouveau type qui "autorise plusieurs implémentations", faites-le essentiellement avec une interface. L'interface est supérieure à la classe abstraite des manières suivantes: Il est facile à utiliser pour les utilisateurs.

Techniques de création d'interfaces compliquées "aide au montage" "montage squelette"

Si vous souhaitez créer vous-même une interface simple, c'est une technique dont vous n'avez pas à vous soucier. Veuillez ne voir que ceux qui en ont besoin.

Lorsque vous créez votre propre interface, supposons que vous définissiez plusieurs méthodes pour cette interface. Par exemple, supposons que vous définissiez la méthode A et la méthode B. Normalement, si vous savez que la méthode A appelle la méthode B, il vous est plus facile d'implémenter la logique typique de la méthode A dans votre interface. Pour les utilisateurs, c'est plus facile s'il y a peu de pièces qu'ils implémentent.

Il existe les deux modèles suivants pour y parvenir.

Point 21 Concevoir l'interface du futur

Une fois l'interface publiée, c'est la dernière. Ce n'est pas si facile de changer. Vérifions-le soigneusement avant de publier.

Si vous ajoutez une méthode à une interface ultérieurement, la classe qui implémente cette interface obtiendra une erreur de compilation. Vous pouvez utiliser default comme "astuces" pour éviter ce problème, mais c'est NG dans les points suivants. Il est important de ne pas compter sur la valeur par défaut, mais de concevoir fermement.

Rubrique 22 Utiliser l'interface uniquement pour définir le type

Il existe une méthode pour définir une constante dans une interface et implémenter cette interface afin que la classe puisse utiliser la constante. C'est NG. Le JDK a en fait une telle interface, mais vous ne devriez pas la copier.

En effet, la classe utilise les constantes d'autres composants, ce qui est un détail d'implémentation. Implémenter une interface signifie faire de cette partie une API publique. Les détails de mise en œuvre ne doivent pas être des API publiques. En premier lieu, exposer des constantes est hors de question car loin de l'essence de l'interface.

Si vous souhaitez fournir la constante à l'extérieur, effectuez l'une des opérations suivantes.

//Classe d'utilité non immuable
public class PhysicalConstatns(){
    private PhysicalConstants() {} //Empêcher l'instanciation

    public static final double AVOGADROS_NUMBER = 6.022_140_857e23;
}

En important statiquement la classe constante, l'utilisateur n'a pas à écrire le nom de la classe à chaque fois que la constante est utilisée. Cela est expliqué dans le livre, mais quand vous y reviendrez plus tard, on vous demandera "Où est définie cette constante?", Ce qui peut être difficile à lire. Tenez compte de l'équilibre lorsque vous décidez si une importation statique est appropriée.

Élément 23 Sélectionnez une hiérarchie de classes plutôt qu'une classe balisée

Supposons qu'il existe une classe dans une classe qui exprime s'il s'agit d'un cercle ou d'un rectangle. Certains d'entre eux ont des constructeurs, des champs et des méthodes qui sont utilisés pour les cercles, et certains sont également utilisés pour les rectangles.

Les classes qui tentent d'exprimer plusieurs concepts dans une classe de cette manière sont appelées «classes étiquetées» dans les livres.

Si vous souhaitez exprimer plusieurs concepts, divisez les classes avec obéissance. Plus précisément, utilisons l'héritage / sous-typage pour les organiser dans une structure hiérarchique. C'est naturel.

Élément 24 Sélectionnez une classe de membre statique sur une classe de membre non statique

Parfois, vous souhaitez déclarer une autre classe dans une classe. Dans ce contexte, la première classe est appelée "classe englobante" et la seconde classe est appelée "classe imbriquée".

Il existe plusieurs façons d'implémenter des classes imbriquées. Plus précisément, il y a les quatre suivants.

Utilisons-les correctement. Compte tenu du flux suivant, il ne devrait y avoir aucune erreur.

Examen étape 0

La "classe imbriquée" que j'essaie de créer est-elle susceptible d'être utilisée par d'autres classes, indépendamment de la classe englobante?

Si OUI, il ne doit pas être créé en tant que classe imbriquée en premier lieu. Il doit être créé comme une classe ordinaire, indépendante de la classe englobante.

Examen étape 1

Si la "classe imbriquée" que vous essayez de créer est la suivante, utilisez ** classe anonyme **.

Si une classe anonyme est déclarée dans une méthode non statique de la classe englobante, l'instance de la classe anonyme fera automatiquement référence à l'instance de la classe englobante. Dans certains cas, cela provoque une fuite de mémoire. Soyez conscient du danger avant de l'utiliser.

Cela ne se produit pas s'il est déclaré dans une méthode non statique.

Examen étape 2

Si la "classe imbriquée" que vous essayez de créer est la suivante, utilisez ** classe locale **.

Examen étape 3

Si la "classe imbriquée" que vous essayez de créer est la suivante, utilisez une ** classe membre non statique **.

Examen étape 4

Si la "classe imbriquée" que vous essayez de créer est la suivante, utilisez des ** classes membres statiques **.

Élément 25 Limiter les fichiers source à une seule classe de premier niveau

Normalement, vous n'implémentez pas plusieurs classes de niveau supérieur dans un fichier source. Cet élément explique pourquoi c'est NG, mais vous n'avez pas besoin d'en connaître la raison car vous ne le faites pas dans la pratique en premier lieu. (fin)

Chapitre 5 Génériques

Point 26: ne pas utiliser le prototype

Un prototype est une expression qui n'implique pas de paramètre de type, tel que «List» au lieu de «List ».

Il n'est plus logique de ne pas utiliser le prototype. La raison en est que vous pouvez obtenir une ClassCastException au moment de l'exécution. Si vous l'implémentez avec des paramètres de type, vous pouvez remarquer de tels risques sous la forme d'erreurs de compilation et d'avertissements au moment de la compilation. N'utilisons pas le prototype.

Cependant, il existe une situation dans laquelle vous pouvez utiliser le prototype par inadvertance. Plus précisément, c'est comme suit.

【NG】

//Ne faites pas cela simplement parce que vous ne connaissez pas les paramètres de type de Set.
static int numElementsInCommon(Set s1, Set s2) {
    int result = 0;
    for (Object o1 : s1)
        if(s2.contains(o1))
            result++;
    return result;
}

Implémentons-le comme suit.

【OK】

//Cela entraînera une erreur de compilation lors de la tentative d'ajout d'éléments à s1 ou s2.
//C'est certainement mieux que de le remarquer au moment de l'exécution. (Cependant, nul peut être ajouté)
static int numElementsInCommon(Set<?> s1, Set<?> s2) {
    //Abréviation
}

Élément 27 Supprimer l'avertissement non inspecté

Lorsqu'ils sont implémentés à l'aide de génériques, les risques menant à une exception ClassCastException au moment de l'exécution peuvent être remarqués par des erreurs de compilation et des avertissements lors de la compilation.

Vous pouvez compiler les avertissements si vous les laissez seuls, mais en règle générale, répondez à tous les avertissements et corrigez-les pour qu'ils n'apparaissent pas. Ajoutez @SuppressWarning (" décoché ") pour supprimer l'avertissement uniquement s'il n'y a vraiment pas de problème. Au fait, «non coché» est un avertissement qui signifie «Je n'ai pas vérifié le type, mais est-ce que ça va?

Si vous ne supprimez pas l'avertissement lorsqu'il est vraiment correct, vous ne remarquerez pas l'avertissement qui pose vraiment problème. Ne coupons pas les coins là non plus.

Élément 28 Sélectionnez une liste dans un tableau

En raison du contexte historique, les séquences et les génériques ont des propriétés différentes. Pour cette raison, les utiliser en combinaison pose des problèmes. À moins qu'il y ait des problèmes importants avec la simplicité du code ou les performances, implémentez-les uniformément dans Generics.

Quels types de problèmes surviendront lorsqu'ils sont utilisés en combinaison?

Si vous implémentez les deux en combinaison, vous ne comprendrez pas la signification des erreurs et des avertissements émis par le compilateur, et vous serez inutilement confus. Dans certains cas, l'avertissement est supprimé sans examen attentif, ce qui entraîne une exception ClassCastException ou demande aux membres de maintenance "Pourquoi faites-vous @ SuppressWarning ici ...?" Sera embrassé et confus.

Par exemple ...

Pourquoi cela arrive-t-il?

C'est parce qu'il existe les différences suivantes entre les deux. (Bien que cela soit expliqué dans le livre, cela n'explique pas pourquoi ces différences conduisent au problème ci-dessus ... J'ajouterai un commentaire plus tard si j'ai le temps.)

Différence ① Différence ②
Tableau Par exempleObject[] objectArray = new Long[1];PuisLong[]EstObject[]En tant que sous-type deSera traité.. Donc, ces affectations sont autorisées. Ces propriétés sont appelées covariantes. Il est bon de se souvenir avec des nuances telles que "changer de manière flexible en fonction de l'autre partie". TableauEst、À l'exécution自身がどんな型を格納できるのか、ということを知っています。なので、不適切な型のオブジェクトを代入しようとすると、À l'exécutionJe reçois une exception. Ces propriétés sont appelées «béton».
Génériques Par exempleList<Object> objectList = new ArrayList<Long>();PuisArrayList<Long>EstList<Object>En tant que sous-type deNon traité.. Par conséquent, une erreur de compilation se produira. Ces propriétés sont appelées invariantes. Ce n'est pas aussi flexible que covariant. GénériquesEstAu moment de la compilationのみ型制約を強制し、実行時にEst要素の型情報を廃棄(erase)します。こういったことを「イレイジャで実装されている」と表現します。これEst、Génériquesが導入された時に、Génériquesを利用していない既存のコードと、利用する新しいコードが共存できるようにするための措置です。これが冒頭で触れた「歴史的経緯」です。

Point 29 Utiliser le type générique

Lorsque vous créez votre propre classe, vous devez la rendre aussi générique que possible. Ce faisant, les utilisateurs bénéficieront des avantages suivants:

Dans certains cas, il peut être préférable d'utiliser des tableaux dans votre propre classe. Par exemple, si vous souhaitez créer un type générique de base comme ArrayList, ou si vous avez une raison de performances.

Dans ces cas, vous devrez faire `@SuppressWanings (" non coché ") ʻà l'intérieur de la classe pour supprimer les avertissements lors de la compilation. Bien entendu, il va sans dire que nous devons examiner attentivement s’il est vraiment approprié de le supprimer.

Point 30 Utiliser la méthode générique

Lorsque vous créez votre propre méthode, vous devez la rendre aussi générique que possible. Ce faisant, l'utilisateur bénéficiera des mêmes avantages que l'article 29.

Tout d'abord, je vais expliquer le cas de la définition d'une méthode générique normale.

[NG] Méthode non générique

//Si les types d'objets à conserver sont différents entre s1 et s2, une ClassCastException se produira lors de l'exécution.
public static Set union(Set s1, Set s2) {
    Set reslut = new HashSet(s1);
    result.addAll(s2);
    return result;
}

[OK] Méthode générique

//Les paramètres de type (liste des) utilisés dans la méthode doivent être déclarés entre le modificateur et le type de retour.
//Dans cet exemple, c'est immédiatement après static<E>C'est.
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
    Set<E> reslut = new HashSet<>(s1);
    result.addAll(s2);
    return result;
}

Vient ensuite le cas de l'implémentation d'une méthode générique avancée. Je présenterai les deux techniques suivantes.

Technique n ° 1: Usine Singleton générique

Disons que vous avez besoin d'une API qui a pour rôle de renvoyer un objet qui fonctionne avec le type que vous spécifiez. Si l'objet à créer n'a pas d'état, il est inutile de créer l'objet à chaque fois car le type spécifié par l'utilisateur est différent.

La fabrique de singleton générique est une technique qui permet à l'objet d'être singletonné (c'est-à-dire de réduire le coût de création d'une instance et la quantité de mémoire utilisée) tout en fonctionnant sur le type spécifié par l'utilisateur.

Le livre en donne un exemple avec la fonction constante. À propos, une fonction égale est une fonction qui renvoie les paramètres tels quels. À quoi cela sert-il? Vous pouvez penser qu'il apparaît comme l'une des fonctions d'activation dans le domaine de l'apprentissage automatique. Je dois spécifier quelque chose dans l'API en tant que fonction d'activation, mais je ne veux rien faire, il y a donc une utilisation telle que la spécification d'une fonction qui ne fait pratiquement rien.

//Point de cette technique ①
private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;

//Point de cette technique ②
@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identityFunction() {
    return (UnaryOperator<T>) IDENTITY_FN;
}

** Points de cette technique ① **

Étant donné que les génériques sont implémentés dans la gomme, il est possible de ne renvoyer qu'une seule instance appelée IDENTITY_FN, quel que soit le type spécifié par l'utilisateur.

D'autre part, si les génériques sont incorporés, c'est-à-dire si IDENTITY_FN se souvient du paramètre de type spécifié par l'utilisateur même au moment de l'exécution, une instance appelée IDENTITY_FN ne peut pas le gérer.

Par exemple, si un utilisateur appelle identityFunction () avec une chaîne comme paramètre de type, IDENTITY_FN doit être ʻUnaryOperator `(un monde où les génériques sont incorporés). Dans ce cas, pour correspondre à la personne qui souhaite spécifier Long comme paramètre de type, la version Long de IDENTITY_FN doit être préparée. Vous aurez également besoin d'une version longue de la méthode identityFunction () qui renvoie cet objet.

** Point de cette technique ② **

Lorsque j'essaye d'implémenter une fabrique de singleton générique, je la lance sans inspection, est-ce correct? Sera averti. Mais dans de nombreux cas, ça va. Reconnaissez la raison et supprimez l'avertissement.

Tout d'abord, que signifie l'avertissement de diffusion non inspecté dans cet exemple?

ʻUnaryOperator `est censé fonctionner avec un" type spécifique "appelé Object. L'objet est le type le plus général, mais comme il est positionné comme un type inclus dans T qui représente tous les types, il est exprimé comme "type spécifique".

D'un autre côté, le T dans ʻUnaryOperator `signifie tous les types, mais lorsque l'utilisateur l'utilise, il est fixé à un type. Il est déterminé par le type spécifié par l'utilisateur.

Si UnaryOperator <type spécifique> qui est censé fonctionner avec un type spécifique est traité comme UnaryOperator <type spécifié par l'utilisateur> du type spécifié par l'utilisateur, "type spécifié par l'utilisateur" est remplacé par "" Vous ne pourrez peut-être pas effectuer un cast vers un type spécifique et vous pourrez obtenir une ClassCastException. À partir d'un compilateur inconnu, la possibilité ne peut pas être exclue.

Ces messages sont inclus dans l'avertissement.

Cependant, dans ce cas, l'argument passé par l'utilisateur est simplement renvoyé sans aucune modification. Pour être précis, il est converti en interne du «type spécifié par l'utilisateur» en Object au moment de l'exécution, mais comme il est converti en quelque chose qui se trouve en haut de la hiérarchie de classes appelée Object, il n'y a pas de place pour ClassCastException. Pour cette raison, il n'y a aucun problème à supprimer l'avertissement avec le sentiment que "Compilateur, cette fois ça va".

** Référence: Pourquoi cette distribution ne provoque-t-elle pas une erreur de compilation? ** **

Dans la partie return (UnaryOperator <T>) IDENTITY_FN;, un avertissement de conversion non inspecté est affiché. Pourquoi ʻUnaryOperator peut-il être converti en ʻUnaryOperator <T> en premier lieu? Alors pourquoi n'obtenez-vous pas une erreur de compilation?

Les génériques sont invariants de sorte que List <Object> objectList = new ArrayList <Long> (); entraînera une erreur de compilation. Donc, à première vue, il semble qu'un tel casting n'est pas possible.

Cependant, si l'on utilise un type générique tel que T comme paramètre de type, le cast sera autorisé car le compilateur déterminera qu'il ne s'agit pas de types complètement différents. À propos, la diffusion bidirectionnelle est autorisée.

Cela s'appelle la conversion de référence restrictive et est défini par la convention du langage Java. Pour plus d'informations, ici est très utile.

Technique n ° 2: limites récursives

Il s'agit d'une technique qui impose des restrictions sur les paramètres de type pouvant être spécifiés par l'utilisateur. Il est plus facile de comprendre pourquoi nous l'appelons «récursif» en regardant un exemple.

public static <E extends Comparable<E>> E max(Collection<E> c);

Que signifie «<E étend Comparable >»?

Le type spécifié par l'utilisateur dans le paramètre type doit être comparable à d'autres objets du même type.

à propos de ça.

Pour le dire plus simplement, la collection spécifiée par l'utilisateur comme argument doit pouvoir comparer les éléments entre eux.

Avec ce genre de sentiment, vous pouvez définir des restrictions sur les paramètres de type qui peuvent être spécifiés par l'utilisateur.

Élément 31 Utiliser des caractères génériques de limite pour améliorer la flexibilité de l'API

Cet article est également assez difficile à lire. J'expliquerai en mâchant.

Disons que votre API prend un type paramétré comme argument. Par exemple, List <E>, Set <E>, ʻIterable , Collection `.

Dans de tels cas, certaines choses doivent être conçues pour rendre l'API facile à utiliser pour les utilisateurs.

Qu'est-ce que la «facilité d'utilisation» pour les utilisateurs?

Prenons d'abord le point de vue de l'utilisateur. Supposons que quelqu'un expose une classe appelée Stack en tant qu'API et que vous l'utilisez.

Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ... ;

//Puisque Integer est un sous-type de Number, pensez-vous qu'il peut être utilisé intuitivement comme ça?
numberStack.pushAll(integers);

En tant qu'utilisateur, vous pensez implicitement de cette façon. "Si nous fournissons un objet à l'API, le passage d'un objet d'un type plus spécifique fonctionnera sûrement."

Au contraire, qu'en est-il des cas suivants?

Stack<Number> numberStack = new Stack<>();
Collection<Object> objectsHolder = ... ;

//Puisque Object est un super type de Nombre, pensez-vous qu'il peut être utilisé intuitivement comme ça?
numberStack.popAll(objectsHolder);

En tant qu'utilisateur, vous pensez implicitement de cette façon. "S'il s'agit du destinataire de l'objet de l'API, il sera reçu comme un type d'objet plus abstrait."

Pour les utilisateurs, il serait utile que l'API ait ce type de flexibilité.

Que dois-je faire pour faire ça?

Revenons à la position de création de l'API.

Tout d'abord, le premier cas.

Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ... ;

//Puisque Integer est un sous-type de Number, pensez-vous qu'il peut être utilisé intuitivement comme ça?
numberStack.pushAll(integers);

Pour donner à l'API cette flexibilité, implémentez l'API comme suit:

//Le point est<? extends E>C'est la partie de. La partie logique est omise.
public <E> void pushAll(Iterable<? extends E> src) {
    ...
}

L'utilisateur pense implicitement comme ça. "Si nous fournissons un objet à l'API, le passage d'un objet d'un type plus spécifique fonctionnera sûrement."

Si les sentiments de l'utilisateur sont réalisés par l'API, ce devrait être "E lui-même ou le sous-type de E Iterable" au lieu de "E's Iterable".

Lorsqu'un paramètre fournit un objet à l'API de cette manière, il est dit producteur. Dans le cas du producteur, c'est "étend".

Notez que le type paramétré comme <? Etend E> est appelé "** type générique de limite **".

Vient ensuite le deuxième cas.

Stack<Number> numberStack = new Stack<>();
Collection<Object> objectsHolder = ... ;

//Puisque Object est un super type de Nombre, pensez-vous qu'il peut être utilisé intuitivement comme ça?
numberStack.popAll(objectsHolder);

Pour donner à l'API cette flexibilité, implémentez l'API comme suit:

//Le point est<? super E>C'est la partie de. La partie logique est omise.
public <E> void popAll(Collection<? super E> dst) {
    ...
}

L'utilisateur pense implicitement comme ça. "S'il s'agit du destinataire de l'objet de l'API, il sera reçu comme un type d'objet plus abstrait."

Si les sentiments de l'utilisateur sont réalisés par l'API, il doit s'agir de "E lui-même ou de la super collection de type E" au lieu de "E's Collection".

Lorsqu'un paramètre reçoit un objet de l'API de cette manière, on dit qu'il s'agit d'un consommateur. Dans le cas du consommateur, c'est super.

En résumé, c'est comme suit.

  • Si le paramètre est ** p ** roducer, <? ** e ** xtends E>
  • Si le paramètre est ** c ** onsumer, <? ** s ** supérieur E>

Prenez l'acronyme et souvenez-vous-en comme "PECS".

Autres conseils

Voici quelques conseils relativement diligents.

Il n'est pas normal d'appliquer le type générique de limite au type ** valeur de retour ** de l'API *. Au lieu de donner de la flexibilité à l'utilisateur, il impose des contraintes.

  • Si vous informez l'utilisateur du type générique, cela signifie que l'API est difficile à utiliser pour l'utilisateur. Passons en revue la conception de l'API.

  • Utilisez toujours «T étend Comparable <? Super T>» comme argument API, et non «T étend Comparable ». Puisque «<? Super T>» est la destination de comparaison et le côté qui reçoit T (consommateur), ajoutez «super». Cela signifie "T lui-même ou T qui peut être comparé au super type de T". En faisant cela, T lui-même n'implémente pas Comparable, mais si le supertype de T implémente Comparable, il peut être passé comme argument à l'API. Un exemple est présenté ci-dessous.

    //Exemple d'API (c'est très compliqué ... c'est un prix pour le rendre flexible.)
    public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
        ...
    }
    
    //Exemple d'utilisation de l'API
    List<ScheduledFuture<?>> scheduledFutures = ...;
    ScheduledFuture<?> max = max(scheduledFutures);
    

ScheduledFuture lui-même n'implémente pas Comparable, mais le supertype Delayed le fait. Cela signifie que vous pouvez obtenir l'aide du super type Delayed. max est une API flexible.

Point 32: Combinez soigneusement les génériques et les arguments de longueur variable

Tenter de générer un "tableau avec des génériques comme élément" tel que List <String> [] entraînera une erreur de compilation. Cela est dû au fait que si vous autorisez l'existence d'un tel tableau, une exception ClassCastException peut se produire au moment de l'exécution (élément 28).

Mais il y a des exceptions à cela. Les arguments de longueur variable sont réalisés par des tableaux, mais en spécifiant des génériques pour des arguments de longueur variable, un tableau avec des génériques comme élément est créé.

static void dangerous(List<String>... stringLists) {
    //L'identité de stringLists est List<String>C'est un "tableau" qui a.
    //Cela peut provoquer une ClassCastException quelque part.
    List<String>[] array = stringLists;
    ...
}

En raison de ces circonstances, tenez compte des points suivants lors de la création d'une API.

  • Si vous êtes à l'aise avec les performances et la redondance du code, essayez de recevoir des arguments de longueur variable dans List au lieu de spécifier des génériques pour les arguments de longueur variable. Il n'est pas nécessaire de combiner activement des arguments de longueur variable et des génériques, qui sont incompatibles. Vous vous demandez peut-être: "Est-ce gênant pour l'utilisateur de générer une liste d'arguments?", Mais il suffit que l'utilisateur utilise List.of ().

  • Si vous voulez vraiment spécifier des génériques pour les arguments de longueur variable, prenez en charge tous les éléments suivants.

  • Élimine le risque d'une ClassCastException lors de l'exécution. Dans ce but···

  • Ne pas enregistrer (ne pas écraser) les éléments dans le tableau génériques.

  • N'exposez pas (voir) une séquence de génériques à du code non approuvé.

  • Indiquons avec @ SafeVarargs qu'il n'y a aucun danger de ClassCastException. Annotez la méthode avec cette annotation. De cette façon, vous n'obtiendrez pas d'avertissements inutiles du compilateur lorsque vous appelez votre API.

Point 33: Envisagez un contenant hétérogène sûr

Personnellement, je pense que le contenu est assez avancé.

Qu'est-ce qu'un «conteneur hétérogène» en premier lieu? Quand es-tu heureux?

Prenons un exemple concret.

Si vous êtes en mesure de créer une application FW (framework) et que d'autres membres l'utilisent, vous pouvez utiliser des annotations pour contrôler des applications individuelles. Je fais une annotation et tous les membres mettent cette annotation sur leur classe. J'utilise cette annotation comme guide pour contrôler les applications (classes) créées par les membres.

Le FW obtient des annotations de la classe du membre pour déterminer comment contrôler la classe du membre.

Les informations sur le type d'annotations attachées à la classe créée par le membre sont stockées en tant que «conteneur hétérogène» dans l'objet Classe de cette classe.

FW veut savoir quelle valeur est définie pour @ MyAnnotaion1 que le membre a donné à la classe. Appelez donc ClassCreatedByMember.class.getAnnotation (MyAnnotation1.class) pour obtenir le @ MyAnnotation1 (l'objet qui représente) que le membre a attaché à cette classe.

De même, si vous voulez connaître les informations pour @ MyAnnotation2, le FW appelle ClassCreatedByMember.class.getAnnotation (MyAnnotation2.class) s.

De cette manière, vous souhaiterez peut-être utiliser un objet de classe spécifique (MyAnnotation1.class ou MyAnnotation2.class dans ce cas) comme clé pour stocker l'objet correspondant. C'est le "conteneur hétérogène" introduit dans cet article.

Un jour, vous aurez peut-être l'occasion de créer vous-même un tel «contenant hétérogène». Souvenez-vous du contenu de cet article comme technique.

Comment faire un bon contenant hétérogène

Cette section décrit comment créer un bon contenant hétérogène. Plus précisément, il explique comment le rendre sûr de type (pour éviter que ClassCastException ne se produise).

Les points sont les suivants.

public class Favorites {
    //・ Carte joker "?Utilisé pour vous donner la flexibilité d'utiliser différents types comme clés.
    //-La valeur de la carte est le type d'objet. Ce type est-il sûr? Vous pourriez penser, mais nous assurons la sécurité des moisissures ailleurs.
    private Map<Class<?>, Object> favorites = new HashMap<>();

    public <T> void putFavorite(Class<T> type, T instance) {
        //・ À titre de contre-mesure lorsque le prototype est spécifié par erreur, tapez.cast()Vérifiez le type avec.
        favorites.put(Objects.requireNonNull(type), type.cast(instance));
    }

    public <T> T getFavorite(Class<T> type) {
        //-Le type spécifié par le type d'argument est toujours retourné.
        //Par exemple, String.Un entier est retourné même si la classe est spécifiée,
        //Il n'existe pas de ClassCastException.
        //· Favoris.get()Le résultat de est de type Object, mais je le lance car c'est un problème en l'état.
        return type.cast(favorites.get(type));
    }
}

Dans l'exemple Favoris, l'utilisateur peut stocker pratiquement n'importe quel type d'objet. Cependant, dans certains cas, vous souhaiterez peut-être mettre des restrictions sur les types qui peuvent être stockés.

Par exemple, si vous souhaitez définir une restriction selon laquelle il doit s'agir d'un sous-type d'annotation

public <T extends Annotation> T getAnnotation(Class<T> annotationType);

Vous pouvez imposer des restrictions aux utilisateurs avec <T extend Annotation>, comme dans.

Énumération et annotations du chapitre 6

Item 34 Utiliser enum au lieu de la constante int

"Difficulté" lors de la déclaration d'une constante sans utiliser enum

Si vous déclarez une constante avec int sans utiliser enum ...

  • Tant que le moule correspond, il peut être utilisé à des fins qui sont loin de l'objectif initial. Je ne peux pas remarquer une telle situation avec une erreur de compilation.
  • Il n'a pas son propre espace de noms, il doit donc avoir un nom qui ne chevauche pas d'autres constantes non liées.
  • Le nom de la constante n'apparaît pas dans le journal ou le débogueur. Afficher uniquement la valeur n'aidera pas votre enquête.
  • Vous ne pouvez pas itérer des constantes ou compter le nombre de constantes.

Si vous déclarez une constante avec String ...

  • Vous ne pouvez pas forcer l'utilisation de ce champ constant. Même s'il est codé en dur et mal saisi, cela ne provoquera pas d'erreur de compilation, vous ne remarquerez donc pas une telle situation.

mécanisme et fonctionnalités de type enum

On peut dire que l'essence de la fonction appelée enum type est de donner un "type unique" aux "éléments listés" tels que les constantes. Puisqu'il s'agit d'un type unique, il n'y a pas de "difficulté lorsque vous n'utilisez pas enum" mentionné ci-dessus.

Le type enum est, après tout, une classe. C'est une forme spéciale de classe.

Vous pouvez (implicitement) avoir votre propre instance dans votre propre champ final statique public et définir vos propres champs et méthodes. Ces propriétés sont les mêmes que dans une classe normale, mais elles diffèrent d'une classe normale en ce que divers travaux sont effectués en coulisses.

Par exemple, cela ressemble à ce qui suit.

  • Les types déclarés avec enum hériteront automatiquement de java.lang.Enum. Notez que java.lang.Enum est une classe abstraite, et les fonctions que les types d'énumération devraient avoir en commun sont fermement implémentées comme indiqué ci-dessous.
  • Remplace de manière appropriée equals () et hashCode () dans la classe Object.
  • Implémente comparable.
  • Implémente sérialisable. Quelle que soit la manière dont le type enum est implémenté, il peut être pris en charge.
  • Le champ final statique public est fourni, le type enum est instancié et défini dans ce champ, etc. sont automatiquement effectués dans les coulisses.

enum a les caractéristiques suivantes.

  • Une instance du champ final statique public est affectée à une constante d'énumération définie dans le type enum. Chaque constante de dénombrement est d'une tonne unique.
  • Puisque le type enum n'a pas de constructeur accessible de l'extérieur, il a les "restrictions heureuses" suivantes.
  • Le type enum lui-même ne peut pas être instancié en externe.
  • Le type enum ne peut pas être hérité de l'extérieur.
  • Le type enum a son propre espace de noms. Vous n'avez pas à vous soucier de la collision des noms constants.
  • toString () vous permet d'afficher des informations dans le journal ou le débogueur qui vous aideront à enquêter.
  • Vous pouvez définir des méthodes et des champs pour le type enum.
  • Le type enum peut implémenter n'importe quelle interface.

Personnellement, je pense qu'il est plus difficile de comprendre le mécanisme du type enum qu'on ne le pense dans le monde. Je pense qu'il est un peu difficile d'utiliser efficacement enum sans une solide compréhension de la spécification du langage Java.

Spécifications du langage Java 8.9. Types Enum https://docs.oracle.com/javase/specs/jls/se9/html/jls-8.html#jls-8.9

Je pense que les points suivants doivent être connus car ils sont écrits dans la spécification du langage Java.

  • Les résultats suivants sont garantis pour une tonne.
  • L'appel de la méthode de clonage de type enum ne duplique pas l'instance.
  • Vous ne pouvez pas créer une instance de type enum même avec réflexion.
  • La désérialisation des instances de type enum ne crée pas d'instances dupliquées.
  • Vous pouvez définir votre propre constructeur pour le type enum, mais vous ne pouvez pas ajouter public ou protected. Il est défini sans modificateur d'accès, mais dans ce cas, il est automatiquement traité comme privé.
  • Si vous ne définissez pas votre propre constructeur pour le type enum, un constructeur par défaut privé est automatiquement fourni.
  • Le champ statique d'énumération n'est pas encore initialisé lorsque le constructeur est exécuté. Vous ne pouvez donc pas accéder aux champs statiques depuis le constructeur.
  • La définition du type enum déclare implicitement la méthode suivante:
    • public static E[] values();
    • public static E valueOf(String name);

Points lors de la création d'un type enum

Lors de la création de votre propre type d'énumération, tenez compte des points suivants.

  • Minimisez la visibilité des types enum, tout comme les classes régulières.

  • Vous pouvez souhaiter avoir le même nom de méthode mais un comportement différent pour chaque constante. C'est du polymorphisme. Vous pouvez hériter de vous-même dans le type enum. Plus précisément, vous pouvez définir une classe anonyme lors de la déclaration d'une constante d'énumération dans un type enum. Vous pouvez hériter du type enum lui-même de cette classe anonyme, remplacer la méthode absolue définie dans le type enum lui-même ou implémenter la méthode de l'interface implémentée par le type enum.

Si vous souhaitez partager du code entre des constantes, vous pouvez remplacer la méthode abstraite pour chaque constante et faire de la partie commune une méthode privée. Comme vous pouvez le voir dans le livre, je pense que vous pouvez adopter le modèle d'énumération stratégique. Dans tous les cas, ce qu'il faut, c'est "s'il faut ou non remarquer une erreur de compilation lors d'une mise en œuvre incorrecte", et le critère pour choisir lequel est "comment trouver un équilibre entre simplicité et flexibilité". C'est.

  • Le comportement peut être étendu en ajoutant une instruction switch au type enum existant. Ceci est utile dans les cas suivants.

  • Disons que vous avez un type enum existant qui se comporte différemment pour chaque constante. Vous ne pouvez pas modifier ce type d'énumération. Mais nous devons étendre ce comportement de type enum existant.

  • Il ne suffit pas de l'ajouter comme méthode au type enum existant, mais j'ai besoin d'un comportement étendu pour moi-même.

  • Si vous remplacez toString () pour renvoyer un nom unique, valueOf (String) ne prendra pas en charge ce nom unique. Il est préférable d'avoir une méthode comme fromString (String) qui peut gérer votre propre nom.

Élément 35 Utiliser des champs d'instance au lieu de numéros de commande

java.lang.Enum a une méthode appelée original (). L'appel de original () sur une instance d'une constante enum renvoie un int indiquant le numéro de la constante enum déclarée dans le type enum.

Tenter de faire quelque chose avec cette méthode échoue souvent. La logique qui dépend du nombre déclaré semble être vulnérable au changement.

N'utilisez donc pas original () sauf si c'est un très bon cas.

Si vous comprenez cet élément à ce niveau, il n'y a pas de problème dans la pratique.

Élément 36 Utiliser EnumSet au lieu du champ de bits

Il y a des moments où vous souhaitez travailler avec un ensemble de constantes.

Par exemple, supposons qu'un type d'énumération de format comporte des éléments tels que «gras», «italique» et «souligné». Dans ce cas, il est nécessaire de pouvoir exprimer une combinaison d'éléments tels que "gras" et "italique".

Avant l'avènement du type enum, cela était représenté par des bits.

//C'est NG dans les temps modernes.

//Déclaration constante
private static final int STYLE_BOLD      = 1 << 0; // 0001
private static final int STYLE_ITALIC    = 1 << 1; // 0010
private static final int STYLE_UNDERLINE = 1 << 2; // 0100

//Gras et italique
int styles = STYLE_BOLD | STYLE_ITALIC // 0011

C'est comme ça.

Si cette méthode présente les avantages d'être simple et performante, elle présente l'inconvénient de devoir déterminer le nombre de bits au début, en plus des inconvénients de la constante int décrite au point 34.

Dans les temps modernes, il existe une bonne manière d'exprimer cette «combinaison d'éléments constants». C'est EnumSet.

//Ceci est recommandé dans les temps modernes.

//Déclaration constante
enum Style {BOLD, ITALIC, UNDERLINE}

//Gras et italique
Set<Style> styles = EnumSet.of(Style.BOLD, Style.ITALIC);

C'est clairement concis. Les performances sont également bonnes car l'opération de bit est effectuée à l'intérieur de EnumSet. Bien sûr, il n'y a aucun inconvénient à la constante int.

Élément 37 Utiliser EnumMap au lieu de l'index ordinal

Vous souhaiterez peut-être créer une carte en utilisant une constante d'énumération (une instance de type enum) comme clé et d'autres données comme valeur.

Dans ce cas, utilisez EnumMap.

Comme pour l'élément 35, n'utilisez pas java.lang.Enum.ordinary () si vous faites une erreur.

(fin)

Point 38: imiter l'énumération extensible avec une interface

Les constantes d'énumération de type enum que vous exposez peuvent ne pas vous suffire.

Par exemple, si vous publiez un type d'énumération qui représente une opération à quatre règles, l'utilisateur peut penser: «Je veux également une constante d'énumération qui représente une opération d'alimentation».

Pour donner à l'API ce type de flexibilité, implémentons l'interface dans le type enum exposé. Demandez à l'utilisateur de créer son propre type d'énumération et d'implémenter l'interface. Et dans votre API, écrivez la logique de l'interface, pas de la classe d'implémentation de type enum qui représente les quatre règles. En faisant cela, le type enum étendu par l'utilisateur peut être utilisé.

Élément 39 Sélectionnez une annotation plutôt qu'un modèle de dénomination

C'est un élément paginé, mais c'est juste un long exemple de code, et il n'y a pas grand chose à apprendre. Plus précisément, c'est comme suit.

Dans l'ancien temps, il était courant de définir des règles pour les noms des éléments de programme tels que les méthodes, et les outils et les cadres contrôlent le programme en utilisant les «marqueurs» donnés selon les règles comme indices. .. Par exemple, JUnit a une règle selon laquelle les noms des méthodes de test commencent par test. Ces techniques sont appelées modèles de dénomination.

Ces techniques sont clairement vulnérables.

Si vous souhaitez contrôler le programme à partir d'un outil ou d'un framework, annotez des "indices". Vous pouvez être libre des vulnérabilités des modèles de dénomination.

Le JDK contient déjà de nombreuses annotations utiles. Faisons bon usage d'eux.

(fin)

Élément 40 Toujours utiliser l'annotation de remplacement

Assurez-vous d'ajouter «@ Override» lorsque vous remplacez les méthodes de supertype. Le compilateur vous indiquera l'erreur que vous aviez l'intention de remplacer mais pas.

(fin)

Élément 41 Utilisez l'interface des marqueurs pour définir le type

Cet article est assez difficile à lire ... Je vais mâcher et expliquer.

Par exemple, supposons que vous développiez un FW ou un outil et que vous souhaitiez contrôler les programmes individuels qui les utilisent. Dans ce cas, il est nécessaire de mettre une sorte de "marqueur" (marqueur) sur le programme individuel afin que le FW et l'outil puissent juger quelle partie du programme individuel est contrôlée et comment.

Il existe deux façons d'atteindre ces marqueurs:

  • Interface de marqueur (sérialisable, etc.)
  • Annotation de marqueur (JUnit @ Test, etc.)

Comment les utiliser correctement dans cet article? Il est expliqué que.

Avantages de l'interface de marqueur

  • L'interface de marqueur peut définir des types. Cela vous permet de remarquer des erreurs au moment de la compilation. Les annotations de marqueur, en revanche, ne le font pas.

  • L'interface du marqueur peut avoir des conditions à appliquer.

Par exemple, supposons que vous ayez une interface A et que vous souhaitiez que l'interface de marqueur soit appliquée uniquement aux classes qui implémentent cette interface A. Dans ce cas, laissez l'interface du marqueur étendre l'interface A. Ensuite, la classe qui implémente l'interface de marqueur implémentera automatiquement l'interface A également.

Vous pouvez ajouter la condition "Pour attacher cette interface de marqueur, vous devez implémenter l'interface A." (Je ne peux pas penser à un exemple concret de cette situation ...)

Les annotations de marqueur, en revanche, ne le font pas.

Avantages de l'annotation des marqueurs

  • Applicable à d'autres que les classes et interfaces. Les interfaces de marqueur, en revanche, ne peuvent être appliquées qu'aux classes et aux interfaces.

Concept d'utilisation correcte

Le message pour cet élément est quelque chose comme "Utilisez l'interface des marqueurs autant que possible, car vous remarquerez des erreurs lors de la compilation." Dans cet esprit, jetez un œil ci-dessous.

  • Si vous devez l'appliquer à autre chose qu'une classe ou une interface, vous n'avez pas d'autre choix que d'utiliser des annotations de marqueurs.

  • Si vous pensez avoir besoin d'une "méthode qui prend un objet marqué comme argument", utilisez l'interface de marqueur car vous pouvez vérifier le type au moment de la compilation. Sinon, vous pouvez utiliser des annotations de marqueur.

  • Pour les frameworks qui utilisent fortement les annotations, il peut être préférable d'utiliser des annotations de marqueur dans le but de se concentrer sur la cohérence. Vous devriez juger en regardant l'équilibre.

Chapitre 7 Lambda et Stream

Point 42 Choisissez Lambda plutôt que la classe anonyme

Dans l'ancien temps, les classes anonymes étaient utilisées pour représenter des objets de fonction.

À partir de Java 8, une interface fonctionnelle a été introduite pour faciliter la représentation des objets de fonction. En même temps, une expression lambda (ou simplement «lambda») a été introduite comme mécanisme pour représenter de manière concise une instance d'une interface fonctionnelle.

Avant d'utiliser lambda, comprenez ce qui suit.

  • L'avantage de lambda est sa simplicité, il est donc préférable d'écrire du code avec le moins de types possible.
  • Vous pouvez omettre le type car Lambda effectue l'inférence de type. Puisque l'inférence de type est effectuée en utilisant des génériques comme indice, maximiser l'utilisation des génériques est un point important pour faire ressortir la bonté de lambda.
  • Lambda n'a pas de nom et de documentation, donc si ce n'est pas une logique triviale ou une logique qui dépasse quelques lignes, elle ne devrait pas être implémentée dans lambda.
  • Les classes anonymes peuvent être plus appropriées que les lambdas. Choisissons en fonction de la situation.
  • Lambda ne peut implémenter que des interfaces fonctionnelles. Les classes anonymes peuvent implémenter des classes abstraites.
  • Les classes anonymes ne peuvent pas implémenter des interfaces avec plusieurs méthodes abstraites.
  • This dans Lambda représente une instance englobante, et this dans une classe anonyme représente une instance d'une classe anonyme.

Point 43 Sélectionnez une référence de méthode plutôt qu'un lambda

Dans certains cas, les références de méthode sont plus concises à implémenter que les lambdas. Incluez également des références de méthode comme l'une de vos options.

Cependant, les points suivants doivent être pris en compte:

  • Pour lambda, écrivez le nom du paramètre. Si ce nom de paramètre est nécessaire pour la lisibilité, vous devez choisir Lambda.
  • Pour lambda, écrivez la logique. Même s'il s'agit d'une logique fixe, si la logique est écrite et qu'elle est très facile à lire, vous devez choisir Lambda.
  • Vous avez également la possibilité d'extraire le traitement lambda dans une méthode et d'utiliser cette référence de méthode. Dans ce cas, vous pouvez écrire un document dans la méthode extraite.
  • Si le nom de la classe est très long, la référence de la méthode sera plus redondante.

Il existe cinq types de références de méthode. Le tableau du livre est très facile à comprendre, je vais donc le citer tel quel. Vous ne vous y habituerez peut-être pas au début, mais je pense que cela vaut la peine d'apprendre.

Type de référence de méthode Exemple Lambda équivalent
static Integer::parseInt str -> Integer.parseInt(str)
lié Instant.now()::isAfter Instant then = Instant.now();
t -> then.isAfter(t)
Non lié String::toLowerCase str -> str.toLowerCase()
Constructeur de classe TreeMap<K,V>::new () -> new TreeMap<K,V>()
Constructeur de tableau int[]::new len -> new int[len]

Point 44: utiliser une interface fonctionnelle standard

Avec les interfaces fonctionnelles et lambdas de Java, les meilleures pratiques de création d'API ont considérablement changé. Plus précisément, il est devenu courant de créer des constructeurs et des méthodes qui prennent des objets fonction comme arguments.

Par exemple, cela ressemble à ceci.

/**
*Ceci est un exemple d'API qui utilise un objet fonction.
* @param funcSpecifiedByUser Une fonction qui prend un sujet dans le premier argument et un objet dans le second argument et renvoie du texte. Ce résultat est affiché dans la sortie standard.
*/
public static void apiUsingFuncObj(BinaryOperator<String> funcSpecifiedByUser) {
    System.out.println(funcSpecifiedByUser.apply("I", "you"));
}

//Ceci est un exemple d'utilisation de l'API. Pour plus de clarté+Les caractères sont concaténés avec.
public static void main(String[] args) {
    apiUsingFuncObj((subjectWord, objectWord) -> subjectWord + " love " + objectWord + ".");

    // I love you.Il sera affiché.
}

De cette façon, vous pouvez adopter une interface de fonction comme argument de votre propre API. L'utilisateur peut utiliser lambda pour créer un objet fonction qui implémente l'interface de fonction et le transmettre à l'API.

À ce stade, une sorte d'interface de fonction est spécifiée pour le type d'argument de l'API que vous créez vous-même, mais dans de nombreux cas ** l'interface de fonction fournie en standard en Java est suffisante **. En tant que fournisseur d'API, vous n'avez pas besoin de définir une interface de fonction supplémentaire.

Du point de vue de l'utilisateur, il est plus facile pour l'API d'adopter l'interface de fonction standard. Si votre propre interface de fonction était définie, vous devrez comprendre ses spécifications. Avec une interface de fonction standard, c'est facile car vous pouvez utiliser vos connaissances existantes telles quelles, tout comme "Oh, c'est ça."

Ainsi, lors de l'adoption d'une interface de fonction en tant que paramètre d'API, envisagez d'abord d'utiliser l'interface de fonction standard Java.

Quelles sont les interfaces de fonction Java standard?

Il existe de nombreux articles qui présentent l'interface de fonction standard Java, je vais donc laisser les détails à cela. Ici, veuillez obtenir une vue d'ensemble en présentant les six interfaces de fonctions de base.

Interface de fonction de base

Interface de fonction Signature La description Exemple de référence de méthode
UnaryOperator<T> T apply(T t) Renvoie le même type que le type d'argument. String::toLowerCase
BinaryOperator<T> T apply(T t1, T t2) Renvoie le même type que le type d'argument. Cela prend deux arguments. BigInteger::add
Predicate<T> boolean test(T t) Prend un argument et renvoie un booléen. Collection::isEmpty
Function<T,R> R apply(T t) Renvoie un type différent de l'argument. Arrays::asList
Supplier<T> T get() Renvoie une valeur sans argument. Instant::now
Consumer<T> void accept(T t) Prend un argument mais ne renvoie rien. System.out::println

Les fonctions ne sont pas orthogonales. Vous ne devriez pas trop vous soucier de ce domaine.

Points à considérer

  • Il est mal de spécifier une classe de données de base encadrée pour le paramètre de type de l'interface de fonction de base, telle que ʻUnaryOperator . C'est parce que la boxe et le déballage sont coûteux. À la place, utilisez une interface de fonction qui prend en charge les types de données de base, tels que ʻIntUnaryOperator.

  • Dans certains cas, il est préférable de créer votre propre interface de fonction. Si l'une des conditions suivantes s'applique, vous voudrez peut-être créer la vôtre.

  • Largement utilisé et peut bénéficier de noms descriptifs.

  • Avoir un contrat solide associé à l'interface.

  • Bénéficiez d'une méthode par défaut spéciale.

  • Ajoutez @ FunctionalInterface à votre propre interface de fonction.

  • C'est une interface de fonction qui peut dire au lecteur qu'elle peut être utilisée pour lambda.

  • Si vous définissez par erreur plusieurs méthodes abstraites, vous serez averti par une erreur de compilation. C'est génial pour vous qui créez votre propre interface de fonction et pour les autres membres qui s'en occupent.

  • Si vous créez votre propre API, ne disposez pas d'une méthode avec le même nom qui reçoit différentes interfaces fonctionnelles à la même position d'argument. L'utilisateur est en difficulté. Par exemple, la méthode submit d'ExecutorService s'applique à cela.

Point 45 Utilisez le flux avec précaution

Qu'est-ce qu'un flux? Qu'est-ce que l'API Stream?

Un flux est une séquence finie ou infinie d'éléments de données. Java 8 a ajouté une API de flux pour faciliter l'utilisation de ce flux.

Dans l'API de flux, vous pouvez faire fonctionner le flux à l'aide du "pipeline de flux".

Le pipeline de flux se compose de:

  • Flux source
  • Fonctionnement intermédiaire
  • Opération de terminaison

Le pipeline est évalué paresseusement afin de pouvoir gérer une séquence infinie.

Points à noter

L'API de flux est «à la mode», mais les abus peuvent réduire sa lisibilité. Le but de l'API de flux est de "simplifier le code", il est donc NG de l'utiliser de manière à ce que l'objectif ne soit pas atteint.

Plus précisément, considérez les points suivants.

  • À l'extrême, vous ne pouvez pas dire si vous devez l'implémenter en utilisant l'API de flux ou la boucle avant de l'écrire. Cela dépend également de la familiarité des membres de l'équipe avec l'API Stream. Déterminez lequel est le plus facile à lire, selon la situation.

  • L'API Stream est susceptible de convenir dans les cas suivants.

  • Convertir la séquence d'éléments uniformément

  • Filtrer la séquence des éléments

  • Utilisez une seule opération (par exemple, ajouter, combiner, calculer le minimum) pour rassembler les éléments dans la séquence

  • Accumuler des éléments dans une séquence dans une collection, par exemple en les regroupant par attributs communs

  • Rechercher des éléments qui correspondent à une limite supérieure spécifique à partir des éléments de la séquence

  • Bien que non limités à l'API de flux, les noms de paramètres lambda ont un impact significatif sur la lisibilité. Réfléchissez bien aux noms des paramètres.

  • Les méthodes d'assistance peuvent jouer un rôle important dans l'API Stream. Si vous découpez le processus à exécuter dans le pipeline de flux en une méthode d'assistance, vous pouvez donner un nom à la méthode d'assistance. Lorsque vous appelez une méthode d'assistance à partir du pipeline de flux, vous pouvez voir ce que vous faites en regardant le nom de la méthode d'assistance, ce qui la rend plus lisible.

  • Dans les opérations intermédiaires ultérieures, vous souhaiterez peut-être accéder à des données valides dans le cadre de l'opération intermédiaire précédente. Dans ce cas, ne l'exécutez pas entre les opérations intermédiaires afin de se souvenir des données de l'opération intermédiaire précédente. C'est juste difficile à lire. À la place, calculez les données que vous recherchez en fonction des données accessibles dans le cadre d'opérations intermédiaires ultérieures.

Élément 46 Sélectionnez une fonction qui n'a pas d'effets secondaires dans le flux

Le but de cet article est "Utilisons l'API du collecteur".

L'accès «à l'extérieur» à partir du pipeline de flux est NG

Que dois-je obtenir en utilisant l'API Stream? Ce que vise l'API Stream n'est pas "en quelque sorte cool".

Le plus important est la «concision». En outre, «l'efficacité» (réduction de la charge du processeur et de la mémoire) est également importante. Dans certains cas, vous devez également viser le «parallélisme» (amélioration des performances de traitement en traitant avec plusieurs threads). Même si vous utilisez l'API de flux, cela n'a aucun sens si vous n'obtenez pas ces éléments.

Pour utiliser correctement l'API Stream, les conversions à des étapes individuelles ne doivent avoir accès qu'aux résultats de conversion de l'étape précédente.

Au contraire, vous ne devez pas accéder aux variables, etc. en dehors du pipeline de flux. Si vous faites cela, au moins vous perdez la «concision». Les lecteurs de code ne peuvent pas lire ce qui se passe à moins qu'ils ne se soucient des choses en dehors du pipeline de flux. Il sera également facile de mélanger les défauts.

Par exemple, le code suivant est NG.

Map<String, Long> freq = new HashMap<>();
try(Stream<String> words = new Scanner(file).tokens()) {
    words.forEach(word -> {
        freq.merge(word.toLowerCase(), 1L, Long::sum);
    });
}

/*
[Qu'est-ce qui ne va pas? ]

L'opération de terminaison forEach ne peut pas renvoyer le résultat final de la conversion en dehors du pipeline de flux.

Néanmoins, j'essaie d'utiliser une opération de terminaison forEach pour coordonner le résultat final de la conversion en dehors du pipeline de flux.

En conséquence, nous accédons à la variable freq en dehors du pipeline de flux, perdant ainsi la "concision".
Pour le dire clairement, c'est difficile à lire.
*/

Étant donné que le pipeline de flux constitue un processus de conversion dans son ensemble, on peut dire que le résultat final de la conversion est renvoyé à l'extérieur du pipeline de flux. En ce sens, les opportunités pour que chaque opération de terminaison soit utile sont limitées. Je pense que c'est à des fins de débogage et de sortie de journal.

Que devrais-je faire?

Alors, comment rendre chaque étape de conversion indépendante et renvoyer le résultat final de la conversion en dehors du pipeline de flux? Pour cela, un rôle appelé «collecteur» est préparé.

Plus précisément, il appelle Stream.collect (collector) comme traitement de fin. Passez l'objet du collecteur comme argument à la méthode collect. Ce collecteur collecte les éléments du flux. Souvent, vous collectez des éléments dans une collection. La méthode collect renvoie les résultats collectés par le collecteur en dehors du pipeline de flux.

Différents collecteurs sont disponibles en standard. Pour obtenir ces objets de collecteur, appelez la méthode factory dans java.util.stream.Collectors.

Ci-dessous, nous présenterons quelques-uns des collecteurs typiques disponibles en standard.

Utilisation Utilisation Comment obtenir un objet de collection Remarques
Collecter les éléments de flux dans la liste - Collectors.toList()
Collecter des éléments de flux dans l'ensemble - Collectors.toSet()
Collectez des éléments de flux dans n'importe quelle collection - Collectors.toCollection(collectionFactory)
Collecter des éléments de flux dans Map Collectez simplement Collectors.toMap(keyMapper, valueMapper)
Collecter lors de la fusion correctement lors de la duplication de clés Collectors.toMap(keyMapper, valueMapper, mergeFunction)
En plus de ↑, spécifiez d'utiliser une implémentation de Map spécifique Collectors.toMap(keyMapper, valueMapper, mergeFunction, mapFactory)
Divisez les éléments de flux en groupes et stockez la liste des éléments de chaque groupe en tant que valeurs de mappage. Collectors.groupingBy(classifier)
Presque identique à ↑, mais répertorié comme valeurs de la carteAutre queSpécifiez une collection de Collectors.groupingBy(classifier, downstream) Un collecteur en aval est un collecteur (objet de fonction) qui prend un sous-flux (un ensemble d'éléments appartenant à un groupe) comme entrée et crée une collection. Par exemple, les collectionneurs.counting()Vous pouvez utiliser le collecteur en aval obtenu dans pour compter le nombre d'observations pour chaque groupe.
En plus de ↑, spécifiez d'utiliser une implémentation de Map spécifique Collectors.groupingBy(classifier, mapFactory, downstream) L'ordre de l'aval est différent de ↑. Faisons attention.
Obtenir l'élément de valeur maximale dans un élément de flux - Collectors.maxBy(comparator) Prend un comparateur qui indique la règle de comparaison comme argument
Obtenir l'élément minimum dans un élément de flux - Collectors.minBy(comparator)
Concaténer simplement les chaînes d'éléments de flux - Collectors.joining()
Concaténer les chaînes d'éléments de flux avec un délimiteur - Collectors.joining(delimiter)

Bien sûr, il y a d'autres que ce qui précède.

Dans le tableau, j'ai écrit Collectors.toList () etc. pour une explication, mais lorsque nous l'utilisons réellement, importons statiquement tous les membres définis dans Collectors afin que Collectors. puisse être omis. .. Ce sera beaucoup plus facile à lire.

Élément 47 Sélectionnez Collection over Stream comme type de retour

Je pense qu'il est courant que votre propre API renvoie une séquence d'éléments. Dans ce cas, en fonction de l'utilisateur, vous souhaiterez peut-être traiter la valeur renvoyée comme un flux, ou vous souhaiterez peut-être la traiter comme un itérable.

Par conséquent, le type de retour qui peut gérer les deux est le meilleur. Plus précisément, Collection ou ses sous-types sont bons. Cela est dû au fait que l'interface Collection est un sous-type de Iterable et possède une méthode de flux.

  • Si le type de retour peut ** être une collection ou ses sous-types **, tenez compte des éléments suivants:

  • Si le nombre d'éléments est suffisamment petit pour être stocké en mémoire, une implémentation standard d'une collection telle que ArrayList convient.

  • Sinon, vous devez implémenter une collection spéciale qui nécessite une petite zone de mémoire.

  • Si le type de retour peut être ** not ** peut être une collection ou ses sous-types, tenez compte des éléments suivants:

  • Il est préférable de choisir Iterable ou Stream, selon ce qui est le plus naturel.

  • Parfois, la facilité de mise en œuvre détermine celui à utiliser.

  • Dans tous les cas, vous aurez besoin d'un adaptateur pour passer de l'un à l'autre. L'utilisation d'un adaptateur encombre la mise en œuvre et est lente.

Point 48 Faites attention lors de la parallélisation des flux

L'appel de Stream.parallel () dans un pipeline de flux entraîne un traitement multithread du pipeline, ce qui conduit souvent à des résultats terribles. En d'autres termes, ce qui ne va pas plus vite est catastrophiquement plus lent que de l'exécuter dans un seul thread. Il est assez difficile de comprendre pourquoi, et c'est aussi très difficile à mettre en œuvre pour être rapide.

Ne parallélisez donc pas les flux à moins d'avoir une bonne raison ou une confirmation.

Chapitre 8 Méthode

Point 49 Vérifier la validité des paramètres

Vérifions la validité des paramètres acceptés par la méthode ou le constructeur au début. Le non-respect de cette consigne peut entraîner des exceptions incompréhensibles dues à des paramètres incorrects ou des anomalies inattendues en dehors de votre code.

Considérez les points suivants:

  • S'il y a des restrictions sur les paramètres, écrivez-les dans le Javadoc @ throws.
  • La méthode Objects.requireNonNull ajoutée dans Java 7 est utile pour la vérification de null. Utilisons-le.
  • À partir de Java 9, les méthodes checkFromIndexSize, checkFromToIndex et checkIndex ont été ajoutées aux objets pour vérifier les index de liste et de tableau. C'est pratique si cela convient à votre objectif.
  • Si le coût de traitement du contrôle de validité est élevé et ** et ** le contrôle de validité est implicitement effectué au milieu du processus, le contrôle de validité ne doit pas être explicitement fourni.

Article 50 Copie défensive si nécessaire

Les classes exposées en tant qu'API doivent être considérées comme mal traitées par les utilisateurs, même si elles ne sont pas malveillantes. Plus précisément, pensez-y comme quelque chose qui briserait les invariants de cette classe.

Par conséquent, quelle que soit la façon dont l'utilisateur l'utilise, l'invariant de la classe ne doit pas être rompu. C'est une copie défensive. Plus précisément, prenez les mesures suivantes.

  • Si vous recevez un objet variable d'un utilisateur et que vous souhaitez l'enregistrer en tant qu'état, faites une copie de cet objet et enregistrez la référence. Dans ce cas, il est dangereux d'utiliser la méthode de clonage de l'objet reçu. Vous ne pouvez pas faire confiance à cette sous-classe.
  • Si vous souhaitez renvoyer un objet ou un tableau variable que la classe a comme état à l'utilisateur, faites une copie de cet objet et renvoyez-le.
  • En premier lieu, essayez d'utiliser autant que possible des classes immuables. Vous n'avez pas à vous soucier de ce qui précède. En particulier, à partir de Java 8, utilisez des classes immuables telles que java.time.Instant (une autre classe dans le même package) au lieu de java.util.Date.

Cependant, il y a des moments où vous décidez de ne pas faire de copie défensive. C'est le cas dans les cas suivants. Dans un tel cas, il est nécessaire de prendre des mesures telles que le déclarer dans Javadoc.

  • Si le coût de traitement de la copie défensive est inacceptable.
  • Si vous pouvez faire confiance à l'utilisateur pour une raison quelconque.
  • Même si l'invariant est cassé, seul l'utilisateur est en difficulté.

Point 51 Concevoir soigneusement la signature de la méthode

Cet article est une collection de conseils. Si vous suivez ces règles, votre propre API sera plus facile à apprendre et à utiliser. Et cela rend également plus difficile les erreurs.

  • Choisissez soigneusement le nom de la méthode.
  • Soyez compréhensible.
  • Doit être cohérent avec les autres noms du même package.
  • Conforme au consensus largement répandu qui existe.
  • Soyez bref.
  • Ne fournissez pas trop de méthodes utiles.
  • S'il y a trop de méthodes, ce sera difficile à la fois pour le responsable et pour l'utilisateur.
  • Fournir un processus qui peut être effectué en combinant plusieurs méthodes comme une méthode plus pratique uniquement lorsqu'il est clair qu'il sera utilisé fréquemment.
  • Gardez le nombre de paramètres à 4 ou moins.
  • Les utilisateurs ne peuvent pas se souvenir de nombreux paramètres.
  • Il est mal d'avoir plusieurs paramètres du même type. Si vous faites une erreur par inadvertance, le compilateur ne vous le dira pas.
  • Voici comment réduire les paramètres.
  • Divisez en plusieurs méthodes. Les sous-listes, indexOf et lastIndexOf de List sont des exemples.
  • Créez une classe d'assistance qui contient une collection de paramètres. Ce sera une classe membre statique.
  • Appliquez le modèle Builder aux appels de méthode. Exécutez à la fin, mais c'est une bonne idée de vérifier la validité des paramètres à ce moment.
  • Utilisez autant que possible les interfaces pour les types de paramètres.
  • Vous donne la flexibilité de passer à une implémentation différente.
  • Utilisez un type enum qui a deux éléments plutôt qu'un paramètre booléen.
  • Facile à lire et extensible.

Item 52 Utiliser avec prudence, surcharge

Lors de la création de votre propre API, il est NG de fournir plusieurs méthodes et constructeurs surchargés avec le même nombre de paramètres. Cela peut ne pas fonctionner comme prévu par l'utilisateur et peut dérouter l'utilisateur.

Par conséquent, traitons-le comme suit.

  • S'il s'agit d'une méthode, changez le nom de la méthode.
  • S'il s'agit d'un constructeur, vous ne pouvez pas changer le nom, donc implémentons-le avec une méthode de fabrique statique.
  • Si vous devez surcharger et que vous voulez qu'ils se comportent de la même manière, passez du limité au général pour vous assurer qu'ils ont tous les deux le même comportement.

Point 53: Utilisez des arguments de longueur variable avec prudence

Les méthodes d'argument de longueur variable sont utiles. Cependant, veuillez noter les points suivants.

  • Si l'argument de longueur variable contient les paramètres requis, une erreur de compilation ne se produira pas si l'utilisateur ne spécifie pas l'argument de longueur variable par erreur. N'incluez pas les paramètres requis dans les arguments de longueur variable et définissez-les séparément des arguments de longueur variable.
  • Les arguments de longueur variable sont obtenus en générant un tableau chaque fois qu'une méthode est appelée. Reconnaissez le coût de création d'un tel tableau et définissez votre API. Si ce coût est inacceptable, examinez le nombre d'arguments couramment utilisés et définissez les méthodes pour ces arguments individuellement.

Élément 54 Renvoie une collection vide ou un tableau vide au lieu de null

Certaines méthodes qui renvoient une séquence de données retournent null dans certains cas, mais c'est NG.

La raison en est la suivante.

  • Les utilisateurs devront écrire du code qui gère les valeurs nulles. En premier lieu, la correspondance avec null peut être divulguée.
  • Pour l'implémenteur d'API, le processus de retour de null encombre inutilement le code.

【NG】

public List<Cheese> getCheeses() {
    return cheesesInStock.isEmpty() ? null
        : new ArrayList<>(cheesesInStock);
}

【OK】

public List<Cheese> getCheeses() {
    //Il n'y a pas besoin de s'embarrasser du branchement conditionnel.
    //Cela renverra une liste vide.
    return new ArrayList<>(cheesesInStock);
}

Le coût de génération d'une liste vide à chaque fois est souvent négligeable. Si cela compte, essayez de renvoyer une collection vide immuable, comme avec Collections.emptyList (). Cependant, c'est rarement le cas, alors ne le faites pas à l'aveuglette.

Point 55: retourner soigneusement l'option

Avant Java 8, les étapes suivantes étaient suivies pour écrire des méthodes qui ne renvoyaient pas de valeur.

  • Renvoie null.
  • Lancez une exception.

De toute évidence, il y avait un problème avec ces méthodes, et Java 8 en a ajouté de bonnes. C'est facultatif.

En retournant Optional depuis l'API, vous pouvez informer l'utilisateur de la possibilité que la valeur de retour soit vide et forcer l'utilisateur à la gérer si elle est vide.

La méthode pour créer un objet facultatif est la suivante.

Comment générer facultatif La description
Optional.empty() Renvoie une option vide.
Optional.of(value) Renvoie un élément facultatif contenant une valeur non nulle. Si null est passé, une NullPointerException sera levée.
Optional.ofNullable(value) Accepte une valeur potentiellement nulle et renvoie une option vide si null est passé.

Les utilisateurs d'API traitent facultatif comme suit.

//S'il est vide, la valeur par défaut spécifiée par orElse est utilisée.
String lastWordInLexicon max(words).orElse("No words...");

//S'il est vide, la fabrique d'exceptions spécifiée par orElseThrow lèvera l'exception.
//La fabrique d'exceptions est maintenant spécifiée afin que le coût de génération d'une exception ne se produise que lorsque l'exception est réellement levée.
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);

//Si vous savez que l'option n'est pas vide, vous pouvez obtenir la valeur en une seule fois.
//Dans le cas peu probable où l'option est vide, une NoSuchElementException sera levée.
Element lastNobleGas = max(Elements.NOBLE_GASES).get();

Diverses méthodes utiles sont définies dans Facultatif. Par exemple, la méthode orElseGet prend un Supplier <T> et peut l'appeler lorsque Optional est vide. Cette méthode house isPresent existe parce que vous ne pouvez pas faire ce que vous vouliez faire avec d'autres méthodes, et vous ne devez pas l'utiliser de manière agressive.

Lorsque vous traitez avec des flux qui ont Optionnel comme élément, vous souhaitez souvent filtrer uniquement ceux qui ne sont pas vides. Dans ce cas, il est préférable de mettre en œuvre comme suit.

//Java 8 ou version antérieure
// Optional.isPresent()Est l'une des rares situations où
streamOfOptional
    .filter(Optional::isPresent)
    .map(Optional::get)

//Java 9 ou version ultérieure
//Stream ajouté à facultatif()Dans la méthode
//Renvoie un Stream qui contient l'élément si Facultatif a une valeur et rien s'il n'a pas de valeur.
//Profitant de cette propriété, elle peut être mise en œuvre comme suit.
streamOfOptional
    .flatMap(Optional::stream)

En ce qui concerne les options facultatives, veuillez noter ce qui suit.

  • Bien sûr, vous ne devez pas retourner la collection, etc. enveloppée dans facultatif. S'il s'agit d'une collection, renvoyez simplement une collection vide.
  • Ne pas renvoyer Facultatif pour les types de données de base encadrés. En effet, les coûts de traitement sont gaspillés, qui sont souvent non négligeables.
  • Facultatif ne doit pas être utilisé pour autre chose que renvoyer la valeur de retour de la méthode. Par exemple, conserver Facultatif dans un champ d'instance est souvent inapproprié. Il doit y avoir d'autres moyens.

Élément 56 Rédiger des commentaires de document pour tous les éléments publics de l'API

Pourquoi avez-vous besoin de commentaires sur les documents (Javadoc)? [Pourquoi]

Vous devez écrire un Javadoc pour les API publiques afin d'empêcher les utilisateurs de les utiliser à mauvais escient.

De plus, il n'est pas ouvert au public afin que les membres de la maintenance puissent maintenir l'API afin de ne pas la modifier dans le mauvais sens et que les membres de la maintenance puissent comprendre l'intention et le but de l'implémentation d'origine. Vous devez également écrire un Javadoc pour.

En pratique, vous devez rédiger le contenu minimum pouvant servir ces objectifs. Le code n'est pas une œuvre d'art, mais un moyen de gagner de l'argent. Il est également important que le coût de Javadoc vaille le profit.

Que devrais-je faire? [Comment]

C'est comme suit.

  • Spécifiez l'API et le "contrat" que l'utilisateur doit conserver.
  • Condition préalable: c'est une question qui doit être établie pour que l'utilisateur puisse appeler l'API.
  • Post-condition: Une question qui doit être remplie une fois l'appel terminé avec succès.
  • Effets secondaires: changements observables dans l'état du système. Démarrez un fil en arrière-plan, etc.
  • Paramètres: En gros, écrivez dans la nomenclature.
  • Valeur de retour: Fondamentalement, une nomenclature. Il peut être omis s'il est identique à l'explication générale de l'API.
  • Exceptions levées: écrivez si coché ou non. Écrivez le nom de la classe d'exception et les conditions à lever.
  • Exprimez le code avec «{@code}».
  • Par exemple, "this" dans "this list" représente l'objet pour lequel la méthode a été appelée.
  • Pour les classes héritées et utilisées, écrivez dans @ implSpec que vous appelez vos autres méthodes. Sinon, l'utilisateur ne l'utilisera pas correctement. Notez que cette balise n'est pas activée par défaut, du moins dans Java 9.
  • Si vous souhaitez représenter des caractères tels que «<> &», utilisez «{@literal}».
  • Écrivez une brève description au début du commentaire du document.
  • Différentes API ne doivent pas avoir la même vue d'ensemble.
  • Veuillez noter que l'explication générale se termine par un point (.). Vous devez utiliser {@literal} etc. de manière appropriée.
  • Dans la description générale, le sujet est souvent omis par souci de concision. Pour l'anglais, vous devez utiliser le formulaire présent à la troisième personne.
  • La nomenclature doit être utilisée pour les classes, les interfaces et les champs.
  • Depuis Java 9, vous pouvez effectuer une recherche par mot-clé dans le champ de recherche en haut à droite de l'écran Javadoc. Pour le moment, ce ne sont pas seulement le nom de la classe et le nom de la méthode, mais aussi les mots-clés explicitement définis par {@index} qui ont atteint la recherche.
  • Documentez tous les paramètres de type pour les types et méthodes génériques.
  • Pour le type enum, ajoutez un commentaire de document pour chacune de toutes les constantes.
  • Documentez tous les membres pour les annotations.
  • Les commentaires de documentation au niveau du package doivent être dans package-info.java.
  • Décrivons s'il est correct de fonctionner avec le multithreading.
  • Décrivez s'il peut être sérialisé et dans quel format il sera sérialisé.
  • {@ InheritDoc} est difficile à utiliser et aurait des restrictions.
  • Vous aurez peut-être besoin d'une documentation décrivant leurs relations, pas seulement des classes individuelles.
  • Les règles ci-dessus doivent être inspectées automatiquement. Dans Java 8 et au-dessus, il est coché par défaut. Vérifier le style, etc. sera inspecté plus fermement.

Chapitre 9 Programmation générale

Ce qui est écrit dans ce chapitre est "naturel". Je n'expliquerai que les choses à noter.

Point 57 Réduire la portée des variables locales

Il n'y a rien de spécial à mentionner.

Rubrique 58 Sélectionnez une boucle for-each sur la boucle for conventionnelle

C'est bien, mais rappelons-nous simplement les points suivants.

  • Collection.removeIf (filtre) vous permet de supprimer des éléments spécifiques lors de la numérisation sans recourir aux boucles for traditionnelles.

Point 59 Connaître la bibliothèque et utiliser la bibliothèque

Connaissez les fonctionnalités des principaux sous-packages, notamment java.lang, java.util, java.io, java.util.concurrent. Si vous ne savez pas, vous ne pouvez pas l'utiliser. Assurez-vous de vérifier les nouvelles fonctionnalités qui seront ajoutées dans la version.

Par exemple, à partir de Java 7, vous devez utiliser ThreadLocalRandom plutôt que Random. C'est plus rapide.

Point 60 Évitez les flotteurs et doublez si vous avez besoin d'une réponse précise

Des flotteurs et des doubles existent pour des calculs rapides qui ** se rapprochent ** de résultats précis. Il n'existe pas pour obtenir des résultats de calcul précis. Vous ne devriez donc pas les utiliser si vous voulez des résultats précis.

Utilisez plutôt Big Decimal.

Cependant, Big Decimal a l'inconvénient d'être "lent". Si la lenteur de BigDecimal est inacceptable, utilisez des entiers tels que int et long (par exemple, convertissez des dollars en cents). Vous pouvez utiliser int pour 9 chiffres maximum et 18 chiffres maximum. Si vous allez au-delà de cela, vous n'avez pas d'autre choix que d'utiliser Big Decimal.

Élément 61 Sélectionnez un type de données de base plutôt que des données de base encadrées

Il n'y a rien de spécial à mentionner.

Élément 62 Évitez les chaînes de caractères lorsque d'autres types sont appropriés

Par exemple, s'il s'agit d'une chaîne de caractères qui représente essentiellement une valeur numérique, traitez-la avec int, etc.

C'est bien, mais rappelons-nous simplement les points suivants.

  • La chaîne est immuable, donc la même instance sera utilisée dans toute la JVM tant qu'elle représente la même chaîne. Par conséquent, il ne peut pas être utilisé comme clé lorsque les threads partagent le même espace de noms.

Point 63 Méfiez-vous des performances de jointure de chaîne

Puisque String est immuable, une nouvelle instance sera créée chaque fois qu'elle est concaténée avec +.

Utilisez StringBuilder pour appliquer + plusieurs fois pour combiner des chaînes (notez que ce n'est pas thread-safe).

Cela ne veut pas dire que toutes les connexions «+» sont mauvaises. Si c'est un ou deux «+», c'est concis et ne devrait pas avoir de problèmes de performances.

Rubrique 64 Se référer à un objet dans l'interface

Il est plus flexible de s'y référer dans une interface ou une classe abstraite. En effet, la classe d'implémentation peut être remplacée ultérieurement.

Point 65 Sélectionnez une interface par réflexion

Les aspects dans lesquels la réflexion doit être utilisée sont extrêmement limités. Ne l'utilisez pas aveuglément.

Point 66: Utilisez les méthodes natives avec prudence

JNI est une fonctionnalité qui vous permet d'appeler des méthodes implémentées en C ou C ++, mais vous n'avez probablement pas à l'implémenter vous-même. Le contenu est que vous devez faire attention si vous le faites par hasard. Je pense que vous devriez lire cet article après avoir vraiment fait face à une telle situation.

Dans de rares cas, vous devrez utiliser un produit avec PJ et JNI est le seul moyen de l'utiliser. Étant donné que le processus appelé par JNI s'exécute en dehors du contrôle de JVM, il existe un risque de corruption de la mémoire. Soyez conscient de ces dangers et testez-les minutieusement.

Point 67 Optimiser soigneusement

L'optimisation est, à l'extrême, l'écriture de code qui se concentre sur les couches inférieures pour être «plus rapide». L'optimisation n'est effectuée que lorsque cela est nécessaire, pas depuis le début. En effet, l'optimisation peut encombrer votre code et peut être inefficace en premier lieu.

Voici ce qu'il faut faire:

  • Au niveau de l'architecture, il ne devrait y avoir aucun problème de performances. Concevez votre architecture avec soin. Une attention particulière doit être accordée à l'API publique, à la communication externe et à la persistance des données. Ceux-ci ont un impact significatif sur les performances et sont quasiment impossibles à remplacer ultérieurement.
  • Concevez soigneusement votre API publique.
  • Si vous créez la variable de type exposé, vous aurez besoin d'une copie défensive.
  • L'adoption incorrecte de l'héritage peut gêner les sous-classes et les empêcher d'être réglées.
  • Si vous ne l'utilisez pas pour l'interface, vous ne pourrez pas le remplacer par une implémentation efficace plus tard.
  • Si vous modifiez la conception de votre API pour de meilleures performances, vous aurez plus de mal à continuer à prendre en charge cette API.
  • Si vous pensez avoir besoin d'optimisation, utilisez des outils de profilage pour identifier les goulots d'étranglement. Par exemple, jmh est un cadre de micro-benchmark très visible.
  • Si vous voulez vraiment optimiser, mesurez pour voir si cela s'améliore vraiment. La manière dont le programme est réellement traité dépend du matériel sur lequel le bytecode s'exécute. Avec autant de matériel aujourd'hui, vous ne pouvez pas dire si l'optimisation a fonctionné sans la mesurer.

Point 68: Respectez la convention de dénomination généralement acceptée

Il n'y a rien de spécial à mentionner. C'est une évidence.

Chapitre 10 Exceptions

Point 69 Utiliser des exceptions uniquement pour des conditions exceptionnelles

Comme le dit le titre. Il n'est pas nécessaire d'expliquer la raison.

Élément 70 Utiliser les exceptions vérifiées pour les états récupérables et les exceptions d'exécution pour les erreurs de programmation

La classe qui représente une situation anormale a la structure suivante (relation d'héritage). J'ai également décrit l'utilisation appropriée et les précautions pour chacun.

La description
Throwable
Erreur et ses sous-types Il est utilisé par la JVM. Ne créez pas le vôtre.
Exception
RuntimeException et ses sous-types Il s'agit de la soi-disant "exception non contrôlée". Cela devrait être lancé si l'utilisateur fait une erreur. En effet, même si elle est gérée par l'utilisateur, elle ne sera que nuisible.
Pas un sous-type de RuntimeException Il s'agit de la soi-disant «exception vérifiée». Cela devrait être lancé si l'appelant peut récupérer correctement. Cela est dû au fait que l'utilisateur peut être forcé d'effectuer un traitement de récupération. Préparons une méthode d'acquisition d'informations dans la classe d'exception afin que l'utilisateur puisse la gérer correctement. Cependant, comme indiqué dans l'élément 71, envisagez d'abord de renvoyer Optionnel.

Point 71 Éviter d'utiliser inutilement des exceptions cochées

Si vous lancez une exception qui est vérifiée par l'API, vous devez examiner si elle est vraiment nécessaire. Même si l'utilisateur reçoit l'exception, elle ne doit pas être levée s'il ne peut pratiquement rien faire.

Les API qui lancent des exceptions vérifiées peuvent être gênantes pour les utilisateurs lorsqu'il est nécessaire et utile de forcer l'utilisateur à effectuer une récupération. C'est parce qu'il présente les inconvénients suivants.

  • Vous devez écrire un essai. C'est un problème et le code est encombré.
  • Vous ne pouvez pas utiliser cette API dans un flux. Cela est dû au fait que le flux ne peut pas gérer les exceptions vérifiées.

Afin de résoudre ou d'atténuer ces difficultés, il existe un moyen de retourner Optionnel. Au lieu de lever une exception, il renvoie une option vide. Cependant, Optional ne peut pas avoir d'informations supplémentaires telles que les classes d'exceptions. Jugons en regardant l'équilibre.

Point 72 Utiliser des exceptions standard

Comme le dit le titre. Il n'est pas nécessaire d'expliquer la raison.

Point 73 Lancez une exception adaptée au concept abstrait.

Vous pouvez propager des exceptions en Java. Si l'exception lancée dans la couche inférieure se propage à la couche supérieure via plusieurs couches, le code de la couche inférieure et le code de la couche supérieure seront combinés. En d'autres termes, il devient difficile de modifier les deux indépendamment.

Pour cette raison, gardez à l'esprit ce qui suit.

  • Ne soulevez pas d'exception en premier lieu. En d'autres termes, lors de l'appel de la couche inférieure, vérifions que les conditions préalables à l'appel de la couche inférieure sont remplies. Essayez d'éviter autant que possible la propagation.
  • Si absolument nécessaire, lancez une nouvelle exception au niveau abstrait approprié dans la couche de niveau abstrait appropriée pour séparer les couches supérieure et inférieure. À ce moment-là, l'exception d'origine doit être définie comme cause. En effet, cela facilite les recherches lorsqu'une exception se produit.

Point 74 Documenter toutes les exceptions levées par chaque méthode

Il recoupe considérablement ce qui est expliqué au point 56. Ce sera bien si vous le comprenez bien.

Élément 75 Inclure les informations d'enregistrement des erreurs dans le message détaillé

Les messages de détail d'exception jouent un rôle très important dans la recherche du moment où une exception se produit.

  • Incluez tous les paramètres et valeurs de champ qui ont causé l'exception.
  • Les journaux sont également consultés par les programmeurs généraux, donc bien sûr, n'incluent pas d'informations sécurisées.
  • Il est inutile d'inclure dans le message détaillé qu'il est facilement lisible depuis Javadoc et le code source.
  • Si vous faites votre propre exception, vous devriez être en mesure de recevoir ces informations dans l'argument constructeur de la classe d'exception afin que l'utilisateur définisse toujours les informations nécessaires à l'enquête.

Point 76: Travailler pour l'atomicité d'erreur

Supposons qu'une méthode d'un objet avec un état est appelée et que quelque chose ne va pas dans la méthode. Après cela, il est souhaitable que l'objet revienne ou reste dans l'état dans lequel il se trouvait avant l'appel de la méthode. Cette propriété est appelée "atomicité d'erreur".

Cette propriété est importante lors de la levée d'exceptions vérifiées. Une exception de vérification est levée car l'utilisateur peut effectuer une récupération. À moins qu'il ne retourne à son état d'origine, il sera impossible pour l'utilisateur d'exécuter le processus de récupération.

Pour obtenir une atomicité d'erreur, appliquez les méthodes suivantes.

  • Rendons la classe immuable en premier lieu. Si cela ne change pas, vous n'avez à penser à rien.
  • Assurez-vous que le traitement suivant est exécuté avant "Processus pour changer l'état de l'objet".
  • Validation des paramètres
  • Traitement qui peut échouer
  • Effectuez une opération sur la copie temporaire de l'objet et remplacez le contenu de l'objet par le contenu de la copie temporaire lorsque l'opération est terminée.

L'atomicité des erreurs est souhaitable, mais pas toujours réalisable, et dans certains cas, elle peut être trop coûteuse à réaliser. Si l'atomicité de l'erreur ne peut pas être obtenue, spécifiez dans la Javadoc l'état dans lequel se trouvera l'objet si la méthode échoue.

Point 77 Ne pas ignorer les exceptions

Comme le dit le titre. Il n'est pas nécessaire d'expliquer la raison.

Dans de rares cas, il peut être approprié d'ignorer l'exception. Dans ce cas, assurez-vous de laisser la raison dans les commentaires.

Chapitre 11

Rubrique 78 Synchroniser l'accès aux données variables partagées

"L'exclusion mutuelle" et la "communication inter-thread" sont réalisées en synchronisation

Lors de la lecture et de l'écriture des données d'un objet dans plusieurs threads, il est nécessaire de prêter attention aux deux points suivants.

  • Exclusion mutuelle: vous devez empêcher les autres threads de voir l'objet dans un état incohérent pendant qu'un thread modifie l'objet.
  • ** Communication inter-thread: les modifications apportées par un thread doivent être visibles de manière fiable par l'autre. ** **

Ce dernier est souvent oublié, alors soyez prudent. S'il n'est pas implémenté correctement, le compilateur peut optimiser arbitrairement les modifications apportées par un thread afin qu'elles soient à jamais invisibles pour les autres. Le résultat est des pépins ennuyeux qui peuvent ou non se produire en fonction du moment.

Le modificateur volatil se concentre sur ce dernier. Le placer dans un champ garantit que la dernière valeur écrite est visible pour le thread de lecture, car le cache par thread n'est plus utilisé. Cependant, volatile ne fait pas l'exclusion mutuelle. Dans la situation où les objets sont partagés par plusieurs threads, il est presque toujours nécessaire de satisfaire les deux points ci-dessus, il y a donc peu de situations où la volatilité est suffisante.

Afin de satisfaire les deux points ci-dessus, il est nécessaire d'effectuer une synchronisation. Plus précisément, ajoutons le modificateur synchronized à la méthode (le bloc synchronisé seul ne garantit pas la visibilité de la valeur dans d'autres threads). Dans certains cas, le package java.util.concurrent.atomic peut être approprié (comme AtomicLong).

Bonus: Qu'est-ce que «l'atomicité»?

Le terme «atomique» est souvent utilisé dans le contexte du multithreading, c'est donc une bonne idée de le comprendre. L'atomicité est la propriété que plusieurs opérations sur les données apparaissent aux autres threads comme une seule opération.

Plus précisément, je pense que vous devriez comprendre les points suivants.

  • Les variables longues et doubles ne sont généralement pas garanties atomiques. Comme résultat de deux écritures de 32 bits chacune étant effectuées séparément, un état semi-fini peut être vu à partir d'un autre thread. Même s'il est long et double, en ajoutant volatile à la variable, il sera visible par les autres threads comme une écriture 64 bits.
  • L'opérateur d'incrémentation ʻi ++ `n'est pas atomique. Il existe deux opérations, la lecture et l'écriture de variables, mais pour les autres threads, elles semblent être des opérations distinctes. Par conséquent, les lectures et les écritures à partir d'autres threads peuvent se trouver entre les lectures et les écritures dans un thread. La bibliothèque qui résout ce problème est le package java.util.concurrent.atomic.

Élément 79 Éviter une synchronisation excessive

Une synchronisation excessive peut causer de mauvaises choses.

Que signifie la surutilisation de la synchronisation?

Une utilisation excessive de la synchronisation signifie ce qui suit.

  • Utilisez la synchronisation là où elle ne devrait pas être.
  • Doit être synchronisé, mais la portée de la synchronisation est trop grande.
  • La portée est tout simplement trop grande.
  • Dans le cadre de la synchronisation, le contrôle est donné à l'utilisateur (exemple: une méthode que l'utilisateur remplace ou un objet fonction passé par l'utilisateur est appelé dans le cadre de la synchronisation).

Que se passe-t-il si vous utilisez une synchronisation excessive?

Une synchronisation excessive peut entraîner les problèmes suivants:

  • Problèmes de précision (les exemples de code dans le livre sont très utiles)

  • Un thread prendra des verrous en double, et ce thread lui-même rompra l'invariance de classe. Les verrous Java sont réentrants. En d'autres termes, si vous acquérez un verrou puis essayez à nouveau d'acquérir le même verrou, vous n'obtiendrez pas d'erreur.

  • Un blocage à mort se produira. Autrement dit, plusieurs threads attendent les uns les autres pour libérer leurs verrous.

  • Les problèmes de performance

  • Les threads autres que celui qui a acquis le verrou seront maintenus en attente plus longtemps que nécessaire.

  • Il y a un délai pour donner à tous les cœurs du processeur une vue cohérente de la mémoire. À l'ère du multicœur, ce coût est très élevé.

  • La JVM ne pourra pas optimiser pleinement l'exécution du code.

Que devrais-je faire?

C'est comme suit.

  • Essayez de ne pas synchroniser autant que possible.
  • Lors de la création d'une classe variable, vous disposez des options suivantes. Adoptons le premier autant que possible.
  • Faites synchroniser les utilisateurs. Adoptons cela autant que possible.
  • Synchronisez à l'intérieur de la classe. Si cela est adopté, il ne sera pas possible de donner à l'utilisateur la possibilité de synchroniser / ne pas synchroniser. Envisagez de faire en sorte que l'utilisateur se synchronise d'abord, et seulement si cela pose un problème, utilisez la synchronisation interne au sein de la classe.
  • Réduisez la portée si vous souhaitez synchroniser. En particulier, n'appelez pas de processus laissant le contrôle à l'utilisateur dans le cadre de la synchronisation.

Élément 80 Sélectionnez l'exécuteur, la tâche, le flux à partir du fil

Au lieu d'utiliser votre propre classe Thread, c'est dans le package java.util.concurrent Utilisez le framework exécuteur. Tel est le message de cet article.

Le contenu écrit dans cet élément est à mi-chemin. Vous devriez lire "Java Concurrency in Practice (Java Concurrency Program-Find its" Foundation "and" Latest API "-)" présenté dans cette section.

Élément 81 Sélectionnez l'utilitaire de traitement parallèle en attendant et notifiez

Ce qui était auparavant fait avec wait and notify peut désormais être facilement réalisé avec l'utilitaire d'accès concurrentiel de haut niveau du package java.util.concurrent.

Utilitaire de concurrence de haut niveau

Les utilitaires d'accès concurrentiel de haut niveau du package java.util.concurrent sont classés dans les trois catégories suivantes.

  • Cadre de l'exécuteur

  • Voir le point 81.

  • Collecte simultanée

  • Implémente des interfaces standard telles que List, Queue et Map, et effectue le traitement de synchronisation approprié en interne. Il atteint des performances élevées lors de la synchronisation.

  • Puisqu'il n'est pas possible d'intervenir dans le processus de synchronisation de l'extérieur, il n'est pas possible de combiner plusieurs opérations de base pour des collections simultanées et de les rendre atomiques. Pour ce faire, des API sont fournies pour les opérations atomiques en combinant plusieurs opérations de base. (PutIfAbsent dans la carte, etc.).

  • Le traitement qui combine plusieurs opérations de base est intégré dans des interfaces telles que Map comme implémentation par défaut, mais seule l'implémentation de collections simultanées devient atomique. L'implémentation par défaut est intégrée dans des interfaces telles que Map car elle est pratique même si elle n'est pas atomique.

  • Les collections synchronisées (telles que Collections.sysnchronizedMap) sont un produit du passé et sont lentes. Sauf si vous avez une raison spécifique, utilisez des collectes simultanées.

  • Des implémentations telles que Queue ont été étendues pour permettre aux "opérations de blocage" d'attendre la fin de l'opération. Par exemple, la méthode take de BlockingQueue attend si la file d'attente est vide et la traite lorsqu'elle est enregistrée dans la file d'attente. Le framework exécuteur utilise ce mécanisme.

  • Synchroniseur

  • Agit comme un babillard entre les fils, vous permettant de suivre le rythme entre les fils.

  • CountDownLatch est souvent utilisé. Par exemple, le thread parent crée un objet pour new CountDownLatch (3), lance les threads enfants A, B et C, appelle l'objet CountDownLatch ʻawait () et attend. Lorsque les threads A, B et C appellent countDown ()` sur cet objet CountDownLatch, le thread parent n'est pas sollicité et le traitement ultérieur du thread parent est exécuté. L'attente et la notification sont exécutées derrière la série de processus, mais CountDownLatch est en charge de toutes les parties compliquées.

  • Bien qu'il ne soit pas limité au contexte du traitement parallèle, utilisez System.nanoTime () au lieu de System.currentTimeMillis () pour mesurer l'intervalle de temps. Ce dernier est plus précis et précis et n'est pas affecté par le réglage de l'horloge en temps réel du système.

attendre et notifier

Vous pouvez également prendre soin du code en utilisant attendre et notifier pour la maintenance, etc. Dans ce cas, vous devez connaître la norme d'attente et de notification.

Cette partie est presque la même que la suivante, donc une explication détaillée est omise.

Point 82 Sécurité du fil de document

Tout ce qui est écrit est important. Il est facile de comprendre le contenu de cet élément, vous n'avez donc pas besoin de l'expliquer en particulier.

Rubrique 83: Utilisez l'initialisation du délai avec précaution

Dans de rares cas, cela peut retarder l'initialisation d'un champ jusqu'à ce que la valeur du champ soit nécessaire. C'est ce qu'on appelle l'initialisation retardée.

Le but de l'initialisation différée est le suivant:

  • Pour l'optimisation (pour "fixer")
  • Il y a une sorte de traitement cyclique dans l'initialisation, et pour interrompre la circulation

Si vous visez l'optimisation, réfléchissez à deux fois "est-ce vraiment logique? L'effet peut être très faible ou contre-productif. Il n'est pas question d'encombrer le code pour cela.

S'il n'y a pas de problème avec l'initialisation normale, ajoutez final comme indiqué ci-dessous pour initialiser.

private final FieldType field = computeFieldValue();

Il en va de même pour les champs statiques.

Méthode d'initialisation retardée

Prenons le cas où le type de champ est une référence d'objet à titre d'exemple.

Les données de base sont presque les mêmes. Dans le cas de ces données, la seule différence est qu'elles ne sont pas nulles et sont comparées à la valeur par défaut de 0.

① Lors de la rupture du cycle d'initialisation

Utilisez un "accesseur synchronisé" comme indiqué ci-dessous. C'est une méthode simple et directe.

private FieldType field;

//Par la méthode synchronisée
//L '"exclusion mutuelle" et la "communication inter-thread" sont réalisées.
private synchronized FieldType getField() {
    if (field == null) 
        field = computeFieldValue();
    return field;
}

Il en va de même pour les champs statiques.

(2) Lors du délai d'initialisation d'un champ statique pour l'optimisation

Si vous ne souhaitez pas retarder l'initialisation, procédez comme suit ...

private static final FieldType field = computeFieleValue();

Si vous souhaitez initialiser paresseusement un champ statique pour l'optimisation, utilisez «l'idiome de classe de support d'initialisation retardée» comme indiqué ci-dessous.

private static class FieldHolder {
    //Donnez à la classe membre statique les champs dont vous souhaitez retarder l'initialisation.
    static final FieldType field = computeFieleValue();
}

private static FieldType getField() {
    //· Chargez la classe de membre statique uniquement lorsque la valeur du champ est nécessaire.
    //À la suite du chargement de la classe, le processus d'initialisation du champ statique est exécuté.
    //· Une machine virtuelle Java classique synchronise l’accès aux champs lors de l’initialisation d’une classe.
    //Il n'est pas nécessaire d'écrire un processus de synchronisation explicite.
    return FieldHolder.field;
}

(3) Lors du report de l'initialisation du champ d'instance pour l'optimisation

Utilisez la "double vérification idiome" comme indiqué ci-dessous.

//Le champ initialisé ne peut pas être vu par les autres threads uniquement avec le bloc synchronisé dans la méthode getField.
//En ajoutant volatile, le champ initialisé peut être vu immédiatement par les autres threads.
private volatile FieldType field;

private FieldType getField() {
    //Pour améliorer les performances en ne chargeant le champ qu'une seule fois
    //La valeur de champ est affectée au résultat de la variable locale.
    FieldType result = field;

    //Une fois initialisé, aucun verrou n'est nécessaire,
    //Il ne verrouille pas la première inspection.
    if (result != null)
        return result;

    //Il se verrouille pour la première fois lors de la deuxième inspection.
    synchronized(this) {
        if (field == null)

            //Si non verrouillé, à ce moment (entre la décision if et l'appel à la méthode computeFieldValue)
            //Il existe un risque qu'un autre thread initialise le champ.

            field = computeFieldValue();
        return field;
    }
}

Élément 84: ne dépend pas du planificateur de threads

Pourquoi est-il gênant de s'appuyer sur le programmateur de threads?

Le planificateur de threads est l'un des composants de la JVM et, comme son nom l'indique, planifie les threads. L'un des composants de la JVM est qu'elle utilise les fonctions du système d'exploitation après tout, et son comportement dépend fortement du système d'exploitation.

Pour cette raison, nous ne savons pas exactement comment le planificateur de threads se comporte, et nous n'avons aucun contrôle sur celui-ci. Si l'exactitude et les performances du programme dépendent du planificateur de threads, il se comportera de manière erratique, parfois cela fonctionne mais parfois non, parfois c'est rapide mais parfois c'est lent. Faisons le. De plus, si vous portez vers une machine virtuelle Java avec un système d'exploitation différent, cela peut ne pas fonctionner comme avant le port.

Pour cette raison, l'exactitude et les performances de votre programme ne doivent pas dépendre du planificateur de threads.

Que devrais-je faire?

Considérez les points suivants:

  • Gardez le nombre moyen de threads RUNNABLE bas. Plus précisément, ne le faites pas beaucoup plus que le nombre de processeurs. Un grand nombre de threads RUNNABLE donne les choix du planificateur et, par conséquent, dépend du planificateur. Veuillez consulter le Document officiel ici pour les états possibles des threads. .. Pour limiter le nombre de threads RUNNABLE, tenez compte des éléments suivants:
  • Lorsque le traitement du thread est terminé, mettons le thread en état d'attente (WAITING).
  • Dans le cadre de l'exécuteur
  • Faites en sorte que le pool de threads ait la bonne taille.
  • Rendez la taille de la tâche modérée. Si la granularité de la tâche est trop petite, le coût de distribution des threads à la tâche sera élevé et lent.
  • En while (vrai), répéter le jugement de condition et réaliser "en attente" est appelé "poids occupé". Faire cela conduit aux problèmes suivants:
  • Rend le programme vulnérable aux traitements imprévisibles par le programmateur de threads.
  • Augmente la charge sur le processeur et rend le traitement moins difficile pour les autres threads.
  • N'utilisez pas Thread.yield. Cette méthode indique au planificateur que "l'utilisation du processeur allouée au propre thread peut être transférée vers un autre thread". N'utilisez pas Thread.yield car il est imprévisible et instable de savoir comment le planificateur se comportera en réponse à cela.
  • N'ajustez pas les priorités de thread. Je peux ajuster la priorité du thread avec Thread.setPriority (int newPriority), mais comme Thread.yield, je ne suis pas sûr du comportement du planificateur en réponse à cela.

Chapitre 12 Sérialisation

Point 85: Choisissez une méthode alternative à la sérialisation Java

Si vous désérialisez ne serait-ce qu'un seul endroit de votre système, vous risquez de désérialiser un objet malveillant créé par un attaquant. Lors du processus de désérialisation, un code malveillant est exécuté, ce qui entraîne des problèmes fatals tels que le blocage du système.

En raison de ces problèmes de sécurité, vous ne devez pas du tout utiliser sérialiser / désérialiser. Utilisez plutôt des technologies telles que JSON et protobuf. En adoptant ces technologies, vous pouvez éviter de nombreux problèmes de sécurité mentionnés ci-dessus et profiter des avantages du support multiplateforme, des performances élevées, d'un écosystème d'outils et du support communautaire.

Si vous n'avez pas d'autre choix que d'utiliser le mécanisme de sérialisation pour la maintenance d'un système existant, prenez les mesures suivantes.

  • Ne désérialisez pas les données non fiables.
  • Utilisez java.io.ObjectInputFilter ajouté dans Java 9 pour désérialiser uniquement les classes en liste blanche. Cependant, cela ne prend pas en charge le code d'attaque composé uniquement de classes communes.

Point 86 Mettre en œuvre sérialisable avec la dernière attention

Si vous adoptez un mécanisme de sérialisation, vous devez être conscient des coûts impliqués. Le prix est le suivant:

  • Une fois publié, vous perdez presque la flexibilité de changer l'implémentation d'une classe qui implémente Serializable.
  • Augmente la probabilité de bogues et de failles de sécurité.
  • Augmenter la charge de tests associée à la sortie d'une nouvelle version de la classe.

Notez les points suivants lors de l'implémentation de Serializable:

  • Définissez explicitement l'UID de la version série de la classe. Si vous ne le définissez pas explicitement, l'ID sera automatiquement attribué et il sera jugé incompatible même s'il s'agit d'un changement compatible.
  • En principe, les classes conçues pour l'héritage ne doivent pas implémenter Serializable. L'interface ne doit pas étendre Serializable en principe. Ceux qui utilisent ces classes et interfaces doivent implémenter Serializable.
  • Lors de l'implémentation d'une classe sérialisable, extensible et dotée de champs d'instance, tenez compte des éléments suivants:
  • Les sous-classes ne devraient pas pouvoir remplacer la méthode finalize. Empêchez les attaques du finaliseur.
  • Si vous ne parvenez pas à initialiser le champ d'instance à sa valeur par défaut, ajoutez une méthode readObjectNoData.
  • À l'exception des classes membres statiques, les classes internes ne doivent pas implémenter Serializable. En effet, il existe des éléments tels que des références à des instances englobantes dont le format de sérialisation est incertain.

Points 87-90

Je suis désolé, je n'ai plus le temps d'écrire un article, alors j'aimerais l'écrire quand j'en ai le temps. Cependant, je tiens à mentionner qu'il n'est pas trop tard pour commencer à apprendre après le réel besoin de mettre en œuvre Seirializable.

en conclusion

Si vous avez des erreurs, faites-le nous savoir!

Recommended Posts