Java 8 lambda obtener y eliminar elemento de la lista

88

Dada una lista de elementos, quiero obtener el elemento con una propiedad determinada y eliminarlo de la lista. La mejor solución que encontré es:

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

¿Es posible combinar get y remove en una expresión lambda?

Marco Stramezzi
fuente
9
Esto realmente parece un caso clásico de cuándo simplemente usar un bucle y un iterador en su lugar.
chrylis -cautelyoptimistic-
1
@chrylis No estoy de acuerdo amablemente;) Estamos tan acostumbrados a la programación imperativa, que cualquier otra forma suena demasiado exótica. Imagínense si la realidad fuera al revés: estamos muy acostumbrados a la programación funcional y se agrega un nuevo paradigma imperativo a Java. ¿Diría que este sería el caso clásico de los flujos, predicados y opcionales?
fps
9
¡No llames get()aquí! No tienes idea de si está vacío o no. Lanzará una excepción si el elemento no estaba allí. En su lugar, utilice uno de los métodos seguros como ifPresent, orElse, orElseGet, oElseThrow.
Brian Goetz
@FedericoPeraltaSchaffner Probablemente sea cuestión de gustos. Pero también propondría no combinar ambos. Cuando veo que algún código obtiene un valor usando transmisiones, normalmente asumo que las operaciones en la transmisión están libres de efectos secundarios. Mezclar la eliminación del elemento puede resultar en un código que puede confundir a los lectores.
Matthias Wimmer
Solo para aclarar: ¿Desea eliminar todos los elementos en los listque el Predicatees verdadero o solo el primero (de un posiblemente cero, uno o muchos elementos)?
Kedar Mhaswade

Respuestas:

148

Para eliminar elemento de la lista

objectA.removeIf(x -> conditions);

p.ej:

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);

SALIDA: A B C

uma shankar
fuente
Sugeriría esta como la mejor respuesta
Sergey Pauk
1
Esto está muy bien. Es posible que deba implementar equals / hashCode si esto no se hace para sus propios objetos. (En este ejemplo se utilizan cadenas que las tienen por defecto).
bram000
15
En mi humilde opinión, la respuesta no considera la parte "obtener": removeIfes una solución elegante para eliminar elementos de una colección, pero no devuelve el elemento eliminado.
Marco Stramezzi
Es un modo muy simple para excluir un objeto de java ArrayList. Thkns mucho. Funciona bien para mí.
Marcelo Rebouças
2
Esto no responde a la pregunta. El requisito es eliminar el elemento de la lista y obtener el elemento / elementos eliminados en una nueva lista.
Shanika Ediriweera
34

Aunque el hilo es bastante antiguo, todavía se cree que proporciona una solución: el uso Java8.

Haz uso de la removeIffunción. La complejidad del tiempo esO(n)

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

Referencia de API: removeIf docs

Supuesto: producersProcedureActivees unList

NOTA: Con este enfoque, no podrá retener el elemento eliminado.

asifsid88
fuente
Además de eliminar el elemento de la lista, el OP todavía quiere una referencia al elemento.
eee
@eee: Muchas gracias por señalar eso. Me perdí esa parte de la pregunta original de OP.
asifsid88
solo para tener en cuenta que esto eliminará todos los elementos que coincidan con la condición. Pero OP parece necesitar eliminar solo el primer elemento (OP usó findFirst ())
nantitv
17

Considere usar iteradores de Java vainilla para realizar la tarea:

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;
}

Ventajas :

  1. Es simple y obvio.
  2. Atraviesa solo una vez y solo hasta el elemento correspondiente.
  3. Puede hacerlo en cualquiera Iterableincluso sin stream()soporte (al menos aquellos que implementan remove()en su iterador) .

Desventajas :

  1. No puede hacerlo en su lugar como una sola expresión (se requiere método auxiliar o variable)

En cuanto a

¿Es posible combinar get y remove en una expresión lambda?

otras respuestas muestran claramente que es posible, pero debe tener en cuenta

  1. La búsqueda y la eliminación pueden recorrer la lista dos veces
  2. ConcurrentModificationException se puede lanzar al eliminar un elemento de la lista que se está iterando
Vasily Liaskovsky
fuente
4
Me gusta esta solución, pero tenga en cuenta que tiene una seria desventaja que se perdió: muchas implementaciones iterables tienen remove()métodos que arrojan UOE. (No los de las colecciones de JDK, por supuesto, pero creo que es injusto decir "funciona en cualquier Iterable".)
Brian Goetz
Creo que podríamos asumir que si un elemento se puede eliminar en general , se puede eliminar mediante un iterador
Vasily Liaskovsky
5
Podría suponer eso, pero habiendo analizado cientos de implementaciones de iteradores, sería una mala suposición. (Todavía me gusta el enfoque; lo estás vendiendo en exceso).
Brian Goetz
2
@Brian Goetz: la defaultimplementación de removeIfhace la misma suposición, pero, por supuesto, se define en Collectionlugar de Iterable
Holger
13

La solución directa sería invocar ifPresent(consumer)el Opcional devuelto por findFirst(). Este consumidor se invocará cuando el opcional no esté vacío. El beneficio también es que no lanzará una excepción si la operación de búsqueda devuelve un opcional vacío, como lo haría su código actual; en cambio, no pasará nada.

Si desea devolver el valor eliminadas, podrá mapel Optionalque el resultado de la llamada remove:

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

Pero tenga en cuenta que la remove(Object)operación volverá a recorrer la lista para encontrar el elemento a eliminar. Si tiene una lista con acceso aleatorio, como una ArrayList, sería mejor hacer una secuencia sobre los índices de la lista y encontrar el primer índice que coincida con el predicado:

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

Con esta solución, la remove(int)operación opera directamente sobre el índice.

Atún
fuente
3
Esto es patológico para una lista vinculada.
chrylis -cautelyoptimistic-
1
@chrylis La solución de índice sería de hecho. Dependiendo de la implementación de la lista, uno preferiría uno sobre el otro. Hizo una pequeña edición.
Tunaki
1
@chrylis: en el caso de una LinkedList, quizás no debería usar la API de transmisión, ya que no hay solución sin atravesar al menos dos veces. Pero no conozco ningún escenario de la vida real en el que la ventaja académica de una lista vinculada pueda compensar su sobrecarga real. Entonces, la solución simple es no usar nunca LinkedList.
Holger
2
Oh, tantas ediciones ... ahora la primera solución no proporciona el elemento eliminado, ya que remove(Object)solo devuelve un booleanindicador de si había un elemento para eliminar o no.
Holger
3
@Marco Stramezzi: desafortunadamente, el comentario que lo explica fue eliminado. Sin boxed()se obtiene un OptionalIntque sólo puede mapdesde inthasta int. A diferencia IntStream, no existe un mapToObjmétodo. Con boxed(), obtendrá un Optional<Integer>que permite mapa un objeto arbitrario, es decir, el ProducerDTOdevuelto por remove(int). El elenco de Integera intes necesario para desambiguar entre remove(int)y remove(Object).
Holger
9

Puede usar el filtro de Java 8 y crear otra lista si no desea cambiar la lista anterior:

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

Estoy seguro de que esta será una respuesta impopular, pero funciona ...

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

p[0] contendrá el elemento encontrado o será nulo.

El "truco" aquí es eludir el problema "efectivamente final" utilizando una referencia de matriz que sea efectivamente final, pero estableciendo su primer elemento.

Bohemio
fuente
1
En este caso, no es tan malo, pero no mejora la posibilidad de simplemente llamar .orElse(null)para obtener el ProducerDTOo null
Holger
En ese caso, podría ser más fácil tener .orElse(null)y tener un if, ¿no?
Tunaki
@Holger, pero ¿cómo puedes invocar remove()también usando orElse(null)?
Bohemio
1
Solo usa el resultado. if(p!=null) producersProcedureActive.remove(p);eso es aún más corto que la expresión lambda en su ifPresentllamada.
Holger
@holger interpreté que el objetivo de la pregunta es evitar declaraciones múltiples, es decir, una solución de una línea
Bohemio
4

Con las colecciones de Eclipse se puede utilizar detectIndexjunto con remove(int)en cualquier 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 usa el MutableListtipo de Eclipse Collections, puede llamar al detectIndexmétodo directamente en la lista.

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);

Nota: soy un comprometido con las colecciones de Eclipse

Donald Raab
fuente
2

Cuando queremos obtener varios elementos de una lista en una nueva lista (filtrar usando un predicado) y eliminarlos de la lista existente , no pude encontrar una respuesta adecuada en ninguna parte.

Así es como podemos hacerlo usando el particionamiento de la API de 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 esta manera, elimina de manera efectiva los elementos filtrados de la lista original y los agrega a una nueva lista.

Consulte el 5.2. Collectors.partitioningBy sección de este artículo .

Shanika Ediriweera
fuente
1

Como han sugerido otros, este podría ser un caso de uso para bucles e iterables. En mi opinión, este es el enfoque más simple. Si desea modificar la lista en el lugar, no puede considerarse programación funcional "real" de todos modos. Pero podría usar Collectors.partitioningBy()para obtener una nueva lista con elementos que satisfacen su condición, y una nueva lista de aquellos que no. Por supuesto, con este enfoque, si tiene varios elementos que satisfacen la condición, todos estarán en esa lista y no solo el primero.

usuario140547
fuente
Es mucho mejor filtrar la transmisión y recopilar resultados en una nueva lista
fps
1

La siguiente lógica es la solución sin modificar la lista original

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
fuente
0

Combinando mi idea inicial y sus respuestas llegué a lo que parece ser la solución a mi propia pregunta:

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;
}

Permite eliminar el objeto usando remove(int)que no recorra nuevamente la lista (como lo sugiere @Tunaki) y permite devolver el objeto eliminado al llamador de la función.

Leí sus respuestas que me sugieren que elija métodos seguros como en ifPresentlugar deget pero no encuentro la manera de usarlos en este escenario.

¿Existe algún inconveniente importante en este tipo de solución?

Edite siguiendo los consejos de @Holger

Esta debería ser la función que necesitaba

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
fuente
2
No debe usar gety capturar la excepción. Eso no solo es un mal estilo, sino que también puede causar un mal rendimiento. La solución limpia es aún más 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 tarea es: obtener ✶ y ✶ eliminar elemento de la lista

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
fuente