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
Il ne contient que les définitions des termes utilisés dans les livres. Vous n'êtes pas obligé de le lire. (fin)
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.
** ① Vous pouvez donner un nom descriptif. ** **
Le constructeur présente les inconvénients suivants.
Le nom du constructeur est limité à un (nom de classe).
La seule distinction entre les constructeurs est la différence d'arguments. En conséquence, il est difficile pour les utilisateurs de comprendre les différences entre les constructeurs.
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.
est l'interface
List`.Grâce à cela, l'API Collections est très simple. En particulier···
Le réalisateur n'a pas besoin d'expliquer à l'utilisateur comment utiliser EmptyList (API).
Les utilisateurs n'ont pas besoin d'être conscients de l'existence d'EmptyList et n'ont pas besoin de comprendre comment utiliser (API). Il vous suffit de savoir comment utiliser l'interface List
.
** ④ 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.
** ① 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の短縮版。 |
Vue d'ensemble du cadre des collections https://docs.oracle.com/javase/jp/9/docs/api/java/util/doc-files/coll-overview.html
[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
build ()
. Cet avantage est très important. En effet, si l'incohérence n'est pas détectée ici et qu'une erreur se produit dans un endroit éloigné en raison de l'incohérence, il sera difficile d'en identifier la cause.build ()
.Inconvénients (triviaux)
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 |
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.
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.
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?
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.
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.
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.
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) |
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.
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.
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.
Réflectivité: x.equals (x) renvoie vrai.
Symétrie: x.equals (y) renvoie vrai uniquement si y.equals (x) est vrai.
Transition: Si x.equals (y) et y.equals (z) sont vrais, x.equals (z) renvoie vrai.
Cohérence: Si x.equals (y) est appelé plusieurs fois, le résultat sera le même.
Non nul: x.equals (null) renvoie faux.
X, y, z ne sont pas nuls.
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.
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'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:
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.
Object.clone () a un modificateur natif, donc il est implémenté dans des langages autres que Java. En d'autres termes, il y a le contenu du processus.
Type de retour covariant https://www.slideshare.net/ryotamurohoshi/ss-38240061
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.
sgn (x.compareTo (y)) == -sgn (y.compareTo (x)). Autrement dit, lorsque la comparaison est effectuée dans l'ordre inverse, les signes des résultats sont également inversés.
Si (x.compareTo (y)> 0 && y.compareTo (z)> 0), alors x.compareTo (z)> 0. En d'autres termes, cela devient une relation de transition.
Si x.compareTo (y) == 0, alors sgn (x.compareTo (z)) == sgn (y.compareTo (y)). Autrement dit, si l'ordre de x et y est égal, le résultat sera égal lors de la comparaison d'autres objets z avec chacun de x et y.
Si x.compareTo (y) == 0, alors x.equals (y) est vrai, et vice versa ** n'est pas obligatoire, mais préférable **.
Sgn (expression) est une fonction qui renvoie -1, 0, 1 lorsque l'expression est respectivement négative, zéro et positive.
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();
}
}
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.
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).
Plus précisément, considérez les points suivants.
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".
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
Chaque instance avec une valeur différente consomme des ressources système. En conséquence, il peut consommer beaucoup de mémoire et entraîner de mauvaises performances. Par exemple, dans chacune des étapes de calcul multiples, une instance ad hoc peut être créée et détruite. La solution de contournement est la suivante.
Combinez plusieurs étapes de traitement en une seule API publique.
Fournit une classe compagnon de variable publique, telle que StringBuilder pour String.
Comment rendre un objet immuable (conditions à remplir)
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.
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.
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.
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:
Il est nécessaire de montrer à l'utilisateur quand la méthode qui peut être remplacée dans la sous-classe est appelée dans la superclasse. Ces informations sont exactement les "détails d'implémentation" de la superclasse, donc cela rompt le masquage / l'encapsulation des informations ...
Vous devrez fournir une méthode protégée (ou un champ dans certains cas) dans votre superclasse pour fournir aux sous-classes des crochets pour le comportement interne de la superclasse. Trop vous éloignera des avantages de la dissimulation / encapsulation d'informations. D'un autre côté, trop peu rendra les superclasses difficiles à utiliser pour les sous-classes. Il est extrêmement difficile de concevoir avec cet équilibre.
Les constructeurs de superclasses ne doivent pas appeler de méthodes remplaçables. Cela est dû au fait que le constructeur de la superclasse est appelé au début du constructeur de la sous-classe, vous appellerez donc la méthode remplaçable lorsque la sous-classe n'est pas prête.
Il y a certaines choses à savoir lorsque les superclasses implémentent Cloneable, Serializable. Cependant, je pense qu'une telle situation est rare, je vais donc omettre l'explication. Consultez le livre pour plus de détails.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Si la "classe imbriquée" que vous essayez de créer est la suivante, utilisez ** classe locale **.
Si la "classe imbriquée" que vous essayez de créer est la suivante, utilisez une ** classe membre non statique **.
Si la "classe imbriquée" que vous essayez de créer est la suivante, utilisez des ** classes membres statiques **.
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)
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
}
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.
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.
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 ...
Un tableau avec des génériques comme élément, tel que List <String> []
, entraînera une erreur de compilation. Si vous l'autorisez, vous pouvez obtenir une ClassCastException. Vous pourriez penser que cela n'a pas d'importance parce que vous ne le faites pas, mais ce n'est pas le cas. Par exemple, une méthode d'argument de longueur variable crée un tableau pour contenir des arguments de longueur variable. Si vous faites de l'argument un type générique, vous obtiendrez un avertissement difficile à comprendre. Si vous ne comprenez pas ce qui se passe dans les coulisses, vous ne pourrez pas le résoudre correctement. new E []
est également inutile.
Suite à son implémentation ad hoc sans comprendre la signification des erreurs de compilation et des avertissements, les classes NG suivantes sont créées.
public class Chooserr<T> {
private final T[] choiceArray;
public Chooser(Collection<T> choices) {
//les choix peuvent être du type qui contient T,
//Souvent toArray()Est-ce que T[]Il ne devrait y avoir aucune garantie qu'il reviendra.
//Vous pouvez obtenir une ClassCastException au moment de l'exécution.
//Je ne comprends pas le sens@Il est mal de supprimer les avertissements.
@SuppressWarning("unchecked")
choiceArray = (T[]) choices.toArray();
}
}
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を利用していない既存のコードと、利用する新しいコードが共存できるようにするための措置です。これが冒頭で触れた「歴史的経緯」です。 |
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.
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.
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
** 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
D'un autre côté, le T dans ʻUnaryOperator
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
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.
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.
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.
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é.
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.
Prenez l'acronyme et souvenez-vous-en comme "PECS".
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
//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.
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.
Personnellement, je pense que le contenu est assez avancé.
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.
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.
Si vous déclarez une constante avec int sans utiliser enum ...
Si vous déclarez une constante avec String ...
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.
enum a les caractéristiques suivantes.
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.
public static E[] values();
public static E valueOf(String name);
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.
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.
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.
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)
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é.
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)
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)
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:
@ Test
, etc.)Comment les utiliser correctement dans cet article? Il est expliqué que.
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.
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.
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.
This
dans Lambda représente une instance englobante, et this
dans une classe anonyme représente une instance d'une classe anonyme.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:
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] |
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.
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.
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.
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:
Le pipeline est évalué paresseusement afin de pouvoir gérer une séquence infinie.
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.
Le but de cet article est "Utilisons l'API du collecteur".
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.
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.
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.
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.
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:
@ throws
.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.
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.
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.
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.
Les méthodes d'argument de longueur variable sont utiles. Cependant, veuillez noter les points suivants.
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.
【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.
Avant Java 8, les étapes suivantes étaient suivies pour écrire des méthodes qui ne renvoyaient pas de valeur.
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.
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.
C'est comme suit.
@ 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.{@literal}
etc. de manière appropriée.{@index}
qui ont atteint la recherche.{@ InheritDoc}
est difficile à utiliser et aurait des restrictions.Ce qui est écrit dans ce chapitre est "naturel". Je n'expliquerai que les choses à noter.
Il n'y a rien de spécial à mentionner.
C'est bien, mais rappelons-nous simplement les points suivants.
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.
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.
Il n'y a rien de spécial à mentionner.
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.
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.
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.
Les aspects dans lesquels la réflexion doit être utilisée sont extrêmement limités. Ne l'utilisez pas aveuglément.
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.
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:
Il n'y a rien de spécial à mentionner. C'est une évidence.
Comme le dit le titre. Il n'est pas nécessaire d'expliquer la raison.
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. |
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.
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.
Comme le dit le titre. Il n'est pas nécessaire d'expliquer la raison.
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.
Il recoupe considérablement ce qui est expliqué au point 56. Ce sera bien si vous le comprenez bien.
Les messages de détail d'exception jouent un rôle très important dans la recherche du moment où une exception se produit.
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.
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.
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.
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.
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).
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.
Une synchronisation excessive peut causer de mauvaises choses.
Une utilisation excessive de la synchronisation signifie ce qui suit.
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.
C'est comme suit.
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.
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.
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.
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.
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.
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:
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.
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.
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.
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;
}
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;
}
}
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.
Considérez les points suivants:
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.
Si vous adoptez un mécanisme de sérialisation, vous devez être conscient des coûts impliqués. Le prix est le suivant:
Notez les points suivants lors de l'implémentation de Serializable:
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.
Si vous avez des erreurs, faites-le nous savoir!
Recommended Posts