Les génériques sont pratiques, n'est-ce pas? Mais votre tête n'est-elle pas gâchée avec beaucoup de symboles (arguments de type)? Les étapes recommandées pour généraliser l'API sont:
Prenons un exemple concret.
J'ai le code suivant.
// In: List<SalesLine>
// Out:Mapper avec le code produit comme clé et le nombre total de ventes comme valeur
Map<String, Integer> map =
list.stream().collect(
Collectors.groupingBy(SalesLine::getProductCode,
Collectors.summingInt(SalesLine::getQuantity)
));
Maintenant, disons que nous utilisons souvent le processus de "regroupement des éléments de liste avec une certaine clé, additionnant quelques valeurs numériques pour chacun et le renvoyant dans une carte", alors faisons-en une méthode utilitaire. Au lieu d'écrire en génériques en une seule fois, écrivez d'abord solidement.
public static Map<String, Integer> groupTotal(List<SalesLine> list) {
return list.stream().collect(
Collectors.groupingBy(SalesLine::getProductCode,
Collectors.summingInt(SalesLine::getQuantity)
));
}
Ensuite, dans ce processus, identifiez le type que vous souhaitez modifier.
La première chose qui est claire est le type d'élément de liste appelé SalesLine
. Remplacez-le par «T». (À ce stade, une erreur de compilation se produira)
public static <T> Map<String, Integer> groupTotal(List<T> list) {
return list.stream().collect(
Collectors.groupingBy(T::getProductCode,
Collectors.summingInt(T::getQuantity)
));
}
Ensuite, je veux pouvoir utiliser ʻIntegeret les types propriétaires sans limiter la clé à une chaîne, alors remplacez
String par
S. La valeur de la carte sera calculée comme une valeur totale, alors laissez-la comme ʻInteger
.
public static <S, T> Map<S, Integer> groupTotal(List<T> list) {
return list.stream().collect(
Collectors.groupingBy(T::getProductCode,
Collectors.summingInt(T::getQuantity)
));
}
Débarrassons-nous des T :: getProductCode
et T :: getQuantity
qui causent l'erreur de compilation.
Ceux-ci sont censés extraire des valeurs d'objets de type d'élément de liste (T
), alors passez-les comme arguments dans l'interface fonctionnelle.
Considérant que la conversion est en «T-> S» et la conversion en «T-> Integer», respectivement, c'est comme suit.
public static <S, T> Map<S, Integer> groupTotal(
List<T> list, Function<T, S> keyExtractor, Function<T, Integer> valueExtractor) {
return list.stream().collect(
Collectors.groupingBy(keyExtractor::apply,
Collectors.summingInt(valueExtractor::apply)
));
}
C'est tout. Le programme côté utilisateur est le suivant.
Map<String, Integer> map = groupTotal(list, SalesLine::getProductCode, SalesLine::getQuantity);
Considérez le programme de sortie de rapport suivant. Il s'agit du processus dit de «coupure de clé» qui est courant dans les applications d'entreprise, où une liste triée des relevés de vente est entrée et une ligne de sous-total est émise lorsque le code produit change.
ReportComponent.java
public String outputReport(List<SalesLine> sales) {
StringBuilder sb = new StringBuilder();
String currentProductCode = null;
int subtotalQty = 0;
int subtotalAmount = 0;
for (SalesLine sl: sales) {
String productCode = sl.getProductCode();
if (!productCode.equals(currentProductCode)) {
//Sortie de ligne de sous-total au moment de la pause
if (currentProductCode != null) {
sb.append(makeSubtotalLine(currentProductCode, subtotalQty, subtotalAmount)).append("\n");
}
currentProductCode = productCode;
subtotalQty = 0;
subtotalAmount = 0;
}
sb.append(makeNormalLine(sl)).append("\n");
subtotalQty += sl.getQuantity();
subtotalAmount += sl.getAmount();
}
//Traiter le dernier groupe de sous-total
sb.append(makeSubtotalLine(currentProductCode, subtotalQty, subtotalAmount)).append("\n");
return sb.toString();
}
L'exemple de sortie de ce programme est le suivant.
Détails 2020-04-01 A 2 pièces 200 yens
Détails 2020-04-01 A 3 pièces 300 yens
Détails 2020-04-02 A 1 pièce 100 yens
Détails 2020-04-02 A 1 pièce 100 yens
Sous-total A 7 pièces 700 yens
Détails 2020-04-01 B 1 pièce 150 yens
Détails 2020-04-02 B 2 pièces 300 yens
Détails 2020-04-02 B 2 pièces 300 yens
Sous-total B 5 pièces 750 yens
Détails 2020-04-01 C 2 pièces 400 yens
Sous-total C 2 pièces 400 yens
Supposons maintenant que vous souhaitiez généraliser le processus, car le traitement des sauts de clé se produit fréquemment dans les applications métier. Tout d'abord, écrivez une classe solide pour le traitement des sauts de clé. Seul le flux de traitement doit être partagé et la logique de sortie spécifique doit être passée en argument.
KeyBreakProcessor.java
public class KeyBreakProcessor {
private List<SalesLine> lines;
public KeyBreakProcessor(List<SalesLine> lines) {
this.lines = lines;
}
public void execute(Function<SalesLine, String> keyGenerator, Consumer<SalesLine> lineProcessor,
BiConsumer<String, List<SalesLine>> keyBreakLister) {
String currentKey = null;
List<SalesLine> subList = new ArrayList<>();
for (SalesLine line : lines) {
String key = keyGenerator.apply(line);
if (!key.equals(currentKey)) {
if (currentKey != null) {
keyBreakLister.accept(currentKey, subList);
subList = new ArrayList<>();
}
currentKey = key;
}
lineProcessor.accept(line);
subList.add(line);
}
keyBreakLister.accept(currentKey, subList);
}
}
Considérez le type que vous souhaitez modifier dans ce processus.
Le type SalesLine
qui représente la ligne de détail et le type String
de la clé de rupture sont ciblés, donc si vous les remplacez par L
et K
, respectivement, ce sera comme suit.
GeneralKeyBreakProcessor.java
public class GeneralKeyBreakProcessor<L, K> {
private List<L> lines;
public GeneralKeyBreakProcessor(List<L> lines) {
this.lines = lines;
}
public void execute(Function<L, K> keyGenerator, Consumer<L> lineProcessor,
BiConsumer<K, List<L>> keyBreakLister) {
K currentKey = null;
List<L> subList = new ArrayList<>();
for (L line: lines) {
K key = keyGenerator.apply(line);
if (!key.equals(currentKey)) {
if (currentKey != null) {
keyBreakLister.accept(currentKey, subList);
subList = new ArrayList<>();
}
currentKey = key;
}
lineProcessor.accept(line);
subList.add(line);
}
keyBreakLister.accept(currentKey, subList);
}
}
Ceci complète l'API générique. Le côté programme de sortie de rapport est le suivant.
ReportComponent.java
public String outputReportWithGenerics(List<SalesLine> sales) {
GeneralKeyBreakProcessor<SalesLine, String> gkbp = new GeneralKeyBreakProcessor<>(sales);
final StringBuilder sb = new StringBuilder();
//Génération de clés
Function<SalesLine, String> keyGenerator = SalesLine::getProductCode;
//Traiter 1 ligne de détail et sortie
Consumer<SalesLine> processLine = sl -> sb.append(makeNormalLine(sl)).append("\n");
//Traiter les éléments de campagne regroupés par clé et sous-total de sortie
BiConsumer<String, List<SalesLine>> subTotal = (code, lines) -> {
int qty = lines.stream().mapToInt(SalesLine::getQuantity).sum();
int amount = lines.stream().mapToInt(SalesLine::getAmount).sum();
sb.append(makeSubtotalLine(code, qty, amount)).append("\n");
};
gkbp.execute(keyGenerator, processLine, subTotal);
return sb.toString();
}
Si vous suivez les étapes, implémenter une API à l'aide de Generics n'est pas effrayant!
Il s'agit d'une version générique du programme de sortie de rapport, mais elle reste un peu difficile à lire. La révision de l'API à l'aide de la technique «API fluide» améliorera un peu la lisibilité.
ReportComponent.java
public String outputReportWithFluent(List<SalesLine> sales) {
FluentKeyBreakProcessor<SalesLine, String, String, String> processor = new FluentKeyBreakProcessor<>();
List<String> groupList =
processor.source(sales)
.key(SalesLine::getProductCode)
.eachLine(sl -> makeNormalLine(sl))
.whenKeyChanged((key, list1, list2) -> {
String lines = list2.stream().collect(Collectors.joining("\n")) + "\n";
int qty = list1.stream().mapToInt(SalesLine::getQuantity).sum();
int amount = list1.stream().mapToInt(SalesLine::getAmount).sum();
return lines + makeSubtotalLine(key, qty, amount) + "\n";
})
.execute();
return groupList.stream().collect(Collectors.joining());
}
L'explication de cette implémentation est écrite en détail dans Blog.
Recommended Posts