web-dev-qa-db-fra.com

Java 8 flux: recherche les éléments d'une liste qui correspondent à des conditions calculées en fonction de valeurs d'une autre liste

Avoir deux classes et deux listes correspondantes:

class Click {
   long campaignId;
   Date date;
}

class Campaign {
   long campaignId;
   Date start;
   Date end;
   String type;
}

List<Click> clicks = ..;
List<Campaign> campaigns = ..;

Et je veux trouver tous les Clicks dans clicks qui:

  1. Avoir un Campaign correspondant dans campaigns liste, c’est-à-dire Campaign avec le même campaignId ET

  2. Ce Campaign a type = "prospectif" ET

  3. Cette Campaigns.start <click.date <Campaigns.end

Jusqu'à présent, j'ai la mise en oeuvre suivante (qui me semble confuse et complexe):

clicks.
        stream().
        filter(click -> campaigns.stream().anyMatch(
                campaign -> campaign.getCampaignType().equals("prospecting") &&
                        campaign.getCampaignId().equals(click.getCampaignId()) &&
                        campaign.getStart().after(click.getDate()) &&
                        campaign.getEnd().before(click.getDate()))).
        collect(toList());

Je me demande s'il existe une solution plus simple au problème.

13
Andrey Yaskulsky

Une chose qui se démarque, c'est que votre deuxième exigence n'a rien à voir avec l'appariement, c'est une condition sur campaigns seulement. Vous devrez vérifier si cela vous convient mieux:

clicks.stream()
    .filter(click -> campaigns.stream()
        .filter(camp -> "prospecting".equals(camp.type))
        .anyMatch(camp -> 
            camp.campaignId == click.campaignId &&
            camp.end.after(click.date) &&
            camp.start.before(click.date)
        )
    )
    .collect(Collectors.toList());

Sinon, je n'ai jamais vu de solution de flux qui n'implique pas le streaming de la 2e collection à l'intérieur du prédicat du 1er, vous ne pouvez donc pas faire mieux que ce que vous avez fait. En termes de lisibilité, si cela vous semble déroutant, créez une méthode qui teste la condition booléenne et appelez-la:

clicks.stream()
    .filter(click -> campaigns.stream()
        .filter(camp -> "pre".equals(camp.type))
        .anyMatch(camp -> accept(camp, click))
    )
    .collect(Collectors.toList());

static boolean accept(Campaign camp, Click click) {
    return camp.campaignId == click.campaignId &&
            camp.end.after(click.date) &&
            camp.start.before(click.date);
}

Enfin, 2 suggestions indépendantes:

  1. N'utilisez pas l'ancienne classe Date, utilisez plutôt la nouvelle API Java.timeLocalDate .
  2. Si Campaign 'type ne peut avoir que des valeurs prédéfinies (comme "soumis", "prospection", "accepté" ...), alors un enum conviendrait mieux que un général String.
2
user1803551

Mes 2 centimes: Puisqu'il n'y a pas beaucoup de code passe-partout dans OP. Il peut donc être impossible/nécessaire de réduire les lignes/caractères dans les codes. nous pourrions le réécrire pour le rendre un peu plus clair:

Map<Long, List<Campaign>> map = campaigns.stream().filter(c -> c.type.equals("prospecting"))
                                         .collect(Collectors.groupingBy(c -> c.campaignId));

clicks.stream().filter(k -> map.containsKey(k.campaignId))
               .filter(k -> map.get(k.campaignId).stream().anyMatch(c -> c.start.before(k.date) && c.end.after(k.date)))
               .collect(Collectors.toList());

Le code n'est pas beaucoup plus court que le code original. mais cela améliorera les performances de O(nm) à O (n + m), comme @ Marco13 l’a mentionné dans les commentaires. Si vous voulez plus court, essayez StreamEx

Map<Long, List<Campaign>> map = StreamEx.of(campaigns)
                .filter(c -> c.type.equals("prospecting")).groupingBy(c -> c.campaignId);

StreamEx.of(clicks).filter(k -> map.containsKey(k.campaignId))
        .filter(k -> map.get(k.campaignId).stream().anyMatch(c -> c.start.after(k.date) && c.end.before(k.date)))
       .toList();
4
123-xyz

Eh bien, il existe un moyen très astucieux de résoudre votre problème OMI, idée originale venant de Holger (je vais trouver la question et la relier ici).

Vous pouvez définir votre méthode qui effectue les vérifications (je l'ai simplifiée un peu):

static boolean checkClick(List<Campaign> campaigns, Click click) {
    return campaigns.stream().anyMatch(camp -> camp.getCampaignId() 
               == click.getCampaignId());
}

Et définissez une fonction qui lie les paramètres:

public static <T, U> Predicate<U> bind(BiFunction<T, U, Boolean> f, T t) {
    return u -> f.apply(t, u);
}

Et l'utilisation serait:

BiFunction<List<Campaign>, Click, Boolean> biFunction = YourClass::checkClick;
Predicate<Click> predicate = bind(biFunction, campaigns);

clicks.stream()
      .filter(predicate::test)
      .collect(Collectors.toList());
3
Eugene
public List<Click> findMatchingClicks(List<Campaign> cmps, List<Click> clicks) {
    List<Campaign> cmpsProspective = cmps.stream().filter(cmp -> "prospective".equals(cmp.type)).collect(Collectors.toList());
    return clicks.stream().filter(c -> matchesAnyCmp(c, cmpsProspective).collect(Collectors.toList());
}

public boolean matchesAnyCmp(Click click, List<Campaign> cmps) {
     return cmps.stream().anyMatch(click -> cmp.start.before(click.date) && cmp.end.after(click.date));
}

Remplacez les champs pour les accesseurs, venez de l'écrire rapidement.

1
albert_nil