Java 8 lambda obtient et supprime un élément de la liste

88

Étant donné une liste d'éléments, je veux obtenir l'élément avec une propriété donnée et le supprimer de la liste. La meilleure solution que j'ai trouvée est:

ProducerDTO p = producersProcedureActive
                .stream()
                .filter(producer -> producer.getPod().equals(pod))
                .findFirst()
                .get();
producersProcedureActive.remove(p);

Est-il possible de combiner get et remove dans une expression lambda?

Marco Stramezzi
la source
9
Cela semble vraiment être un cas classique de quand utiliser simplement une boucle et un itérateur à la place.
chrylis -cautouslyoptimistic-
1
@chrylis Je ne suis pas d'accord;) Nous sommes tellement habitués à la programmation impérative, que toute autre manière semble trop exotique. Imaginez si la réalité était l'inverse: nous sommes très habitués à la programmation fonctionnelle et un nouveau paradigme impératif est ajouté à Java. Diriez-vous que ce serait le cas classique pour les flux, les prédicats et les options?
fps
9
N'appelez pas get()ici! Vous ne savez pas si c'est vide ou non. Vous lancerez une exception si l'élément n'était pas là. À la place, utilisez l'une des méthodes sûres comme ifPresent, orElse, orElseGet ou orElseThrow.
Brian Goetz le
@FedericoPeraltaSchaffner Probablement une question de goût. Mais je proposerais également de ne pas combiner les deux. Quand je vois du code obtenir une valeur à l'aide de flux, je suppose normalement que les opérations dans le flux sont exemptes d'effets secondaires. Mélanger la suppression de l'élément peut entraîner un code qui peut induire en erreur les lecteurs.
Matthias Wimmer
Juste pour clarifier: Voulez-vous supprimer tous les éléments dans le listpour lesquels le Predicateest vrai ou seulement le premier (d'un éventuellement zéro, un ou plusieurs éléments)?
Kedar Mhaswade

Réponses:

148

Pour supprimer un élément de la liste

objectA.removeIf(x -> conditions);

par exemple:

objectA.removeIf(x -> blockedWorkerIds.contains(x));

List<String> str1 = new ArrayList<String>();
str1.add("A");
str1.add("B");
str1.add("C");
str1.add("D");

List<String> str2 = new ArrayList<String>();
str2.add("D");
str2.add("E");

str1.removeIf(x -> str2.contains(x)); 

str1.forEach(System.out::println);

SORTIE: A B C

uma shankar
la source
Je suggérerais ceci comme la meilleure réponse
Sergey Pauk
1
C'est très chouette. Vous devrez peut-être implémenter equals / hashCode si cela n'est pas fait pour vos propres objets. (Dans cet exemple, String est utilisé qui les a par défaut).
bram000
15
IMHO la réponse ne considère pas la partie «obtenir»: removeIfest une solution élégante pour supprimer des éléments d'une collection, mais elle ne renvoie pas l'élément supprimé.
Marco Stramezzi
C'est un mode très simple pour exclure un objet de java ArrayList. Thkns beaucoup. Fonctionne bien pour moi.
Marcelo Rebouças le
2
Cela ne répond pas à la question. La condition est de supprimer l'élément de la liste et d'obtenir l'élément / les éléments supprimés dans une nouvelle liste.
Shanika Ediriweera
34

Bien que le fil soit assez ancien, il est toujours pensé pour fournir une solution Java8.

Utilisez la removeIffonction. La complexité du temps estO(n)

producersProcedureActive.removeIf(producer -> producer.getPod().equals(pod));

Référence API: removeIf docs

Hypothèse: producersProcedureActiveest unList

REMARQUE: Avec cette approche, vous ne serez pas en mesure de récupérer l'élément supprimé.

asifsid88
la source
En plus de supprimer l'élément de la liste, l'OP souhaite toujours une référence à l'élément.
eee
@eee: Merci beaucoup de l'avoir signalé. J'ai manqué cette partie de la question initiale d'OP.
asifsid88
juste pour noter que cela supprimera tous les éléments qui correspondent à la condition. Mais OP semble devoir ne supprimer que le premier élément (OP utilisé findFirst ())
nantitv
17

Pensez à utiliser des itérateurs Java vanilla pour effectuer la tâche:

public static <T> T findAndRemoveFirst(Iterable<? extends T> collection, Predicate<? super T> test) {
    T value = null;
    for (Iterator<? extends T> it = collection.iterator(); it.hasNext();)
        if (test.test(value = it.next())) {
            it.remove();
            return value;
        }
    return null;
}

Avantages :

  1. C'est clair et évident.
  2. Il ne traverse qu'une seule fois et seulement jusqu'à l'élément correspondant.
  3. Vous pouvez le faire sur n'importe quel Iterablemême sans stream()support (au moins ceux qui implémentent remove()sur leur itérateur) .

Inconvénients :

  1. Vous ne pouvez pas le faire sur place en tant qu'expression unique (méthode auxiliaire ou variable requise)

En ce qui concerne la

Est-il possible de combiner get et remove dans une expression lambda?

d'autres réponses montrent clairement que c'est possible, mais vous devez être conscient

  1. La recherche et la suppression peuvent parcourir la liste deux fois
  2. ConcurrentModificationException peut être lancé lors de la suppression d'un élément de la liste en cours d'itération
Vasily Liaskovsky
la source
4
J'aime cette solution, mais notez qu'elle présente un sérieux inconvénient que vous avez manqué: de nombreuses implémentations Iterable ont des remove()méthodes qui lancent UOE. (Pas ceux pour les collections JDK, bien sûr, mais je pense qu'il est injuste de dire "fonctionne sur n'importe quel Iterable".)
Brian Goetz
Je pense que nous pourrions supposer que si un élément peut être supprimé en général , il peut être supprimé par itérateur
Vasily Liaskovsky
5
Vous pouvez supposer que, mais après avoir examiné des centaines d'implémentations d'itérateurs, ce serait une mauvaise hypothèse. (J'aime toujours l'approche; vous la vendez juste trop.)
Brian Goetz
2
@Brian Goetz: la defaultmise en œuvre de removeIffait la même hypothèse, mais, bien sûr, elle est définie sur Collectionplutôt que Iterable
Holger
13

La solution directe serait d'appeler ifPresent(consumer)sur l'Optionnel renvoyé par findFirst(). Ce consommateur sera appelé lorsque l'option facultative n'est pas vide. L'avantage est également qu'il ne lèvera pas d'exception si l'opération de recherche retournait une option vide, comme le ferait votre code actuel; au lieu de cela, rien ne se passera.

Si vous souhaitez renvoyer la valeur supprimée, vous pouvez mapaccéder Optionalau résultat de l'appel remove:

producersProcedureActive.stream()
                        .filter(producer -> producer.getPod().equals(pod))
                        .findFirst()
                        .map(p -> {
                            producersProcedureActive.remove(p);
                            return p;
                        });

Mais notez que l' remove(Object)opération parcourra à nouveau la liste pour trouver l'élément à supprimer. Si vous avez une liste à accès aléatoire, comme un ArrayList, il serait préférable de faire un Stream sur les index de la liste et de trouver le premier index correspondant au prédicat:

IntStream.range(0, producersProcedureActive.size())
         .filter(i -> producersProcedureActive.get(i).getPod().equals(pod))
         .boxed()
         .findFirst()
         .map(i -> producersProcedureActive.remove((int) i));

Avec cette solution, l' remove(int)opération opère directement sur l'index.

Tunaki
la source
3
C'est pathologique pour une liste chaînée.
chrylis -cautouslyoptimistic-
1
@chrylis La solution d'index serait en effet. En fonction de la mise en œuvre de la liste, l'un préférerait l'un à l'autre. Fait une petite modification.
Tunaki le
1
@chrylis: dans le cas d'un, LinkedListvous ne devriez peut-être pas utiliser l'API de flux car il n'y a pas de solution sans traverser au moins deux fois. Mais je ne connais aucun scénario réel dans lequel l'avantage académique d'une liste chaînée peut compenser ses frais généraux réels. La solution simple est donc de ne jamais utiliser LinkedList.
Holger
2
Oh tant de modifications ... maintenant, la première solution ne fournit pas l'élément supprimé car elle remove(Object)ne fait que booleansavoir s'il y avait un élément à supprimer ou non.
Holger le
3
@Marco Stramezzi: malheureusement, le commentaire expliquant cela a été supprimé. Sans boxed()vous obtenez un OptionalIntqui ne peut que mapde intà int. Contrairement à IntStream, il n'y a pas de mapToObjméthode. Avec boxed(), vous obtiendrez un Optional<Integer>qui permet d' accéder mapà un objet arbitraire, c'est à dire le ProducerDTOrenvoyé par remove(int). La distribution de Integerà intest nécessaire pour lever l'ambiguïté entre remove(int)et remove(Object).
Holger
9

Utilisez peut utiliser le filtre de Java 8, et créer une autre liste si vous ne voulez pas changer l'ancienne liste:

List<ProducerDTO> result = producersProcedureActive
                            .stream()
                            .filter(producer -> producer.getPod().equals(pod))
                            .collect(Collectors.toList());
Toi Nguyen
la source
5

Je suis sûr que ce sera une réponse impopulaire, mais cela fonctionne ...

ProducerDTO[] p = new ProducerDTO[1];
producersProcedureActive
            .stream()
            .filter(producer -> producer.getPod().equals(pod))
            .findFirst()
            .ifPresent(producer -> {producersProcedureActive.remove(producer); p[0] = producer;}

p[0] contiendra l'élément trouvé ou sera nul.

Le "truc" ici est de contourner le problème "effectivement final" en utilisant une référence de tableau qui est effectivement finale, mais en définissant son premier élément.

Bohème
la source
1
Dans ce cas, ce n'est pas si mal, mais pas une amélioration par rapport à la possibilité de simplement appeler .orElse(null)pour obtenir le ProducerDTOou null
Holger
Dans ce cas, il pourrait être plus facile d'avoir .orElse(null)et d'avoir un if, non?
Tunaki
@Holger mais comment pouvez-vous l'invoquer remove()aussi en utilisant orElse(null)?
Bohème
1
Utilisez simplement le résultat. if(p!=null) producersProcedureActive.remove(p);c'est encore plus court que l'expression lambda de votre ifPresentappel.
Holger
@holger J'ai interprété le but de la question comme étant d'éviter les déclarations multiples - c'est-à-dire une solution sur une ligne
Bohème
4

Avec les collections Eclipse, vous pouvez utiliser detectIndexavec remove(int)sur n'importe quel java.util.List.

List<Integer> integers = Lists.mutable.with(1, 2, 3, 4, 5);
int index = Iterate.detectIndex(integers, i -> i > 2);
if (index > -1) {
    integers.remove(index);
}

Assert.assertEquals(Lists.mutable.with(1, 2, 4, 5), integers);

Si vous utilisez le MutableListtype d'Eclipse Collections, vous pouvez appeler la detectIndexméthode directement dans la liste.

MutableList<Integer> integers = Lists.mutable.with(1, 2, 3, 4, 5);
int index = integers.detectIndex(i -> i > 2);
if (index > -1) {
    integers.remove(index);
}

Assert.assertEquals(Lists.mutable.with(1, 2, 4, 5), integers);

Remarque: je suis un committer pour les collections Eclipse

Donald Raab
la source
2

Lorsque nous voulons obtenir plusieurs éléments d'une liste dans une nouvelle liste (filtre à l'aide d'un prédicat) et les supprimer de la liste existante , je ne trouve nulle part de réponse correcte.

Voici comment nous pouvons le faire en utilisant le partitionnement de l'API Java Streaming.

Map<Boolean, List<ProducerDTO>> classifiedElements = producersProcedureActive
    .stream()
    .collect(Collectors.partitioningBy(producer -> producer.getPod().equals(pod)));

// get two new lists 
List<ProducerDTO> matching = classifiedElements.get(true);
List<ProducerDTO> nonMatching = classifiedElements.get(false);

// OR get non-matching elements to the existing list
producersProcedureActive = classifiedElements.get(false);

De cette façon, vous supprimez efficacement les éléments filtrés de la liste d'origine et les ajoutez à une nouvelle liste.

Reportez-vous au 5.2. Collectors.partitioningBy section de cet article .

Shanika Ediriweera
la source
1

Comme d'autres l'ont suggéré, cela pourrait être un cas d'utilisation pour les boucles et les itérables. À mon avis, c'est l'approche la plus simple. Si vous souhaitez modifier la liste sur place, elle ne peut de toute façon pas être considérée comme une programmation fonctionnelle "réelle". Mais vous pouvez utiliser Collectors.partitioningBy()pour obtenir une nouvelle liste avec des éléments qui satisfont votre condition, et une nouvelle liste de ceux qui ne le font pas. Bien sûr, avec cette approche, si vous avez plusieurs éléments satisfaisant à la condition, tous seront dans cette liste et pas seulement le premier.

user140547
la source
Il est préférable de filtrer le flux et de collecter les résultats dans une nouvelle liste
fps
1

La logique ci-dessous est la solution sans modifier la liste d'origine

List<String> str1 = new ArrayList<String>();
str1.add("A");
str1.add("B");
str1.add("C");
str1.add("D");

List<String> str2 = new ArrayList<String>();
str2.add("D");
str2.add("E");

List<String> str3 = str1.stream()
                        .filter(item -> !str2.contains(item))
                        .collect(Collectors.toList());

str1 // ["A", "B", "C", "D"]
str2 // ["D", "E"]
str3 // ["A", "B", "C"]
KimchiMan
la source
0

En combinant mon idée initiale et vos réponses, j'ai atteint ce qui semble être la solution à ma propre question:

public ProducerDTO findAndRemove(String pod) {
    ProducerDTO p = null;
    try {
        p = IntStream.range(0, producersProcedureActive.size())
             .filter(i -> producersProcedureActive.get(i).getPod().equals(pod))
             .boxed()
             .findFirst()
             .map(i -> producersProcedureActive.remove((int)i))
             .get();
        logger.debug(p);
    } catch (NoSuchElementException e) {
        logger.error("No producer found with POD [" + pod + "]");
    }
    return p;
}

Il permet de supprimer l'objet en utilisant remove(int)qui ne parcourt pas à nouveau la liste (comme suggéré par @Tunaki) et il permet de renvoyer l'objet supprimé à l'appelant de la fonction.

J'ai lu vos réponses qui me suggèrent de choisir des méthodes sûres comme ifPresentau lieu de getmais je ne trouve pas de moyen de les utiliser dans ce scénario.

Y a-t-il un inconvénient important dans ce type de solution?

Modifier en suivant les conseils de @Holger

Cela devrait être la fonction dont j'avais besoin

public ProducerDTO findAndRemove(String pod) {
    return IntStream.range(0, producersProcedureActive.size())
            .filter(i -> producersProcedureActive.get(i).getPod().equals(pod))      
            .boxed()                                                                
            .findFirst()
            .map(i -> producersProcedureActive.remove((int)i))
            .orElseGet(() -> {
                logger.error("No producer found with POD [" + pod + "]"); 
                return null; 
            });
}
Marco Stramezzi
la source
2
Vous ne devez pas utiliser getet intercepter l'exception. Ce n'est pas seulement un mauvais style, mais cela peut également entraîner de mauvaises performances. La solution propre est encore plus simple,return /* stream operation*/.findFirst() .map(i -> producersProcedureActive.remove((int)i)) .orElseGet(() -> { logger.error("No producer found with POD [" + pod + "]"); return null; });
Holger
0

la tâche est: obtenir get et ✶ supprimer l'élément de la liste

p.stream().collect( Collectors.collectingAndThen( Collector.of(
    ArrayDeque::new,
    (a, producer) -> {
      if( producer.getPod().equals( pod ) )
        a.addLast( producer );
    },
    (a1, a2) -> {
      return( a1 );
    },
    rslt -> rslt.pollFirst()
  ),
  (e) -> {
    if( e != null )
      p.remove( e );  // remove
    return( e );    // get
  } ) );
Kaplan
la source