Un objeto de lista no pudo desduplicarse, lo que provocó el pensamiento sobre distinto () en Java8

Otra conjetura de convertir una lista en un mapa

Java8 usa expresiones lambda para programación funcional para realizar operaciones muy convenientes en colecciones. Una operación más común es convertir una lista en un mapa, generalmente usando el método toMap () de Collectors para la conversión. Un problema más común es que cuando la lista contiene los mismos elementos, si no especifica cuál elegir, se lanzará una excepción. Por tanto, esta designación es necesaria. Versión completa de PDF de la colección de entrevistas de Java

Por supuesto, se puede especificar directamente otro método sobrecargado que usa toMap (). Aquí, queremos discutir otro método: ¿podemos usar different () para filtrar los elementos duplicados de la lista antes de la operación de conversión a mapa, y luego no necesitamos considerar el problema de elementos duplicados al convertir a mapa?

Utilice distinto () para anular la duplicación de la lista

Usa distinto () directamente, falla

package example.mystream;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class ListToMap {

    @AllArgsConstructor
    @NoArgsConstructor
    @ToString
    private static class VideoInfo {
        @Getter
        String id;
        int width;
        int height;
    }

    public static void main(String [] args) {
        List<VideoInfo> list = Arrays.asList(new VideoInfo("123", 1, 2),
                new VideoInfo("456", 4, 5), new VideoInfo("123", 1, 2));

        // preferred: handle duplicated data when toMap()
        Map<String, VideoInfo> id2VideoInfo = list.stream().collect(
                Collectors.toMap(VideoInfo::getId, x -> x,
                        (oldValue, newValue) -> newValue)
        );

        System.out.println("No Duplicated1: ");
        id2VideoInfo.forEach((x, y) -> System.out.println("<" + x + ", " + y + ">"));

        // handle duplicated data using distinct(), before toMap()
        Map<String, VideoInfo> id2VideoInfo2 = list.stream().distinct().collect(
                Collectors.toMap(VideoInfo::getId, x -> x)
        );

        System.out.println("No Duplicated2: ");
        id2VideoInfo2.forEach((x, y) -> System.out.println("<" + x + ", " + y + ">"));
    }
}

Hay un total de tres elementos en la lista, dos de los cuales consideramos duplicados. La primera conversión es usar toMap () para especificar directamente el procesamiento de claves duplicadas, por lo que se puede convertir a mapa normalmente. La segunda conversión es desduplicar la lista primero y luego convertirla en un mapa. El resultado aún falla, arrojando una IllegalStateException, por lo que distinto () debería haber fallado.

No Duplicated1: 
<123, ListToMap.VideoInfo(id=123, width=1, height=2)>
<456, ListToMap.VideoInfo(id=456, width=4, height=5)>
Exception in thread "main" java.lang.IllegalStateException: Duplicate key ListToMap.VideoInfo(id=123, width=1, height=2)
    at java.util.stream.Collectors.lambda$throwingMerger$0(Collectors.java:133)
    at java.util.HashMap.merge(HashMap.java:1253)
    at java.util.stream.Collectors.lambda$toMap$58(Collectors.java:1320)
    at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
    at java.util.stream.DistinctOps$1$2.accept(DistinctOps.java:175)
    at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
    at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
    at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
    at example.mystream.ListToMap.main(ListToMap.java:79)

Razón: distinto () depende de iguales ()

Verifique la API de different (), puede ver la siguiente introducción:

Devuelve una transmisión que consta de elementos distintos (según {@link Object # equals (Object)}) de esta transmisión.

Obviamente, cuando different () deduplica un objeto, se procesa de acuerdo con el método equals () del objeto. Si nuestra clase VideoInfo no anula el método equals () de la superclase Object, se utilizará Object.

Pero el método equals () de Object devuelve verdadero solo cuando los dos objetos son exactamente iguales. El efecto que queremos es que siempre que id / width / height de VideoInfo sean iguales, los dos objetos videoInfo se consideran iguales. Entonces, por ejemplo, reescribimos el método equals () que pertenece a videoInfo.

Notas sobre la anulación de iguales ()

Diseñamos los iguales () de VideoInfo de la siguiente manera:

@Override
public boolean equals(Object obj) {
    if (!(obj instanceof VideoInfo)) {
        return false;
    }
    VideoInfo vi = (VideoInfo) obj;
    return this.id.equals(vi.id)
          && this.width == vi.width
          && this.height == vi.height;
}

De esta forma, siempre que las tres propiedades de los dos objetos videoInfo sean iguales, los dos objetos serán iguales. Ir a ejecutar el programa felizmente, ¡todavía falla! ¿por qué?

"Effective Java" es un buen libro, incluso James Gosling, el padre de Java, dijo que este es un tutorial de Java que incluso él necesita. En este libro, el autor señaló que si se anula el método equals () de una clase, ¡entonces su método hashCode () debe anularse juntos! ¡Tiene que! ¡No hay espacio para la negociación!

El igual () reescrito debe cumplir las siguientes condiciones:

  • Según equals (), el valor de hashCode () debe ser el mismo para dos objetos que son iguales;

  • Según equals (), el valor de hashCode () puede ser el mismo o diferente para dos objetos que no son iguales;

Debido a que estas son regulaciones de Java, violar estas regulaciones hará que los programas de Java ya no se ejecuten normalmente.

Para más detalles, le sugiero que lea el libro original, que definitivamente se beneficiará mucho. ¡muy recomendable!

Al final, diseñé el método hashCode () de VideoInfo de acuerdo con la guía del Libro Sagrado de la siguiente manera:

@Override
public int hashCode() {
   int n = 31;
   n = n * 31 + this.id.hashCode();
   n = n * 31 + this.height;
   n = n * 31 + this.width;
   return n;
}

Finalmente, different () filtró con éxito los elementos repetidos en la lista. En este momento, no es ningún problema usar dos toMap () para convertir la lista en un mapa:

No Duplicated1: 
<123, ListToMap.VideoInfo(id=123, width=1, height=2)>
<456, ListToMap.VideoInfo(id=456, width=4, height=5)>
No Duplicated2: 
<123, ListToMap.VideoInfo(id=123, width=1, height=2)>
<456, ListToMap.VideoInfo(id=456, width=4, height=5)>

Ampliar

Dado que distinto () se llama igual () para la comparación, según tengo entendido, los 3 elementos de la lista deben compararse al menos 3 veces. ¿Es igual a () llamado tres veces?

Agregue una oración de impresión en igual () para que pueda saberlo. La suma igual a () es la siguiente:

@Override 
public boolean equals(Object obj) {
    if (! (obj instanceof VideoInfo)) {
        return false;
    }
    VideoInfo vi = (VideoInfo) obj;

    System.out.println("<===> Invoke equals() ==> " + this.toString() + " vs. " + vi.toString());

    return this.id.equals(vi.id) && this.width == vi.width && this.height == vi.height;
}

resultado:

No Duplicated1: 
<123, ListToMap.VideoInfo(id=123, width=1, height=2)>
<456, ListToMap.VideoInfo(id=456, width=4, height=5)>
<===> Invoke equals() ==> ListToMap.VideoInfo(id=123, width=1, height=2) vs. ListToMap.VideoInfo(id=123, width=1, height=2)
No Duplicated2: 
<123, ListToMap.VideoInfo(id=123, width=1, height=2)>
<456, ListToMap.VideoInfo(id=456, width=4, height=5)>

Resulta que equals () se llamó solo una vez. ¿Por qué no 3 veces? Piénselo detenidamente. Según la comparación de hashCode (), hashCode () es el mismo una vez, es decir, el primer elemento y el tercer elemento de la lista (ambos son VideoInfo (id = 123, width = 1, height = 2)) Aparecerá igual que hashCode ().

Entonces, podemos adivinar así: solo cuando el hashCode devuelto por hashCode () sea el mismo, se llamará a equals () para un juicio adicional. Si incluso el hashCode devuelto por hashCode () es diferente, entonces se puede considerar que los dos objetos deben ser diferentes.

Verifica la conjetura:

Cambie hashCode () de la siguiente manera:

@Override
public int hashCode() {
   return 1;
}

De esta forma, el valor de retorno hashCode () de todos los objetos es el mismo. Por supuesto, esto está en línea con la especificación de Java, porque Java solo estipula que el hashCode del mismo objeto de equals () debe ser el mismo, pero el hashCode de diferentes objetos puede no ser diferente.

resultado:

No Duplicated1: 
<123, ListToMap.VideoInfo(id=123, width=1, height=2)>
<456, ListToMap.VideoInfo(id=456, width=4, height=5)>
<===> Invoke equals() ==> ListToMap.VideoInfo(id=456, width=4, height=5) vs. ListToMap.VideoInfo(id=123, width=1, height=2)
<===> Invoke equals() ==> ListToMap.VideoInfo(id=456, width=4, height=5) vs. ListToMap.VideoInfo(id=123, width=1, height=2)
<===> Invoke equals() ==> ListToMap.VideoInfo(id=123, width=1, height=2) vs. ListToMap.VideoInfo(id=123, width=1, height=2)
No Duplicated2: 
<123, ListToMap.VideoInfo(id=123, width=1, height=2)>
<456, ListToMap.VideoInfo(id=456, width=4, height=5)>

Efectivamente, ¡equals () fue llamado tres veces! Parece que solo cuando el hashCode es el mismo, se llamará a equal () para determinar si los dos objetos son iguales; si el hashCode no es el mismo, los dos objetos son obviamente diferentes. La suposición es correcta.

En conclusión

  1. Se recomienda usar toMap () para la conversión de la lista a mapa, y no importa si habrá problemas de repetición, debe especificar las reglas de selección después de la repetición, lo que no requiere mucho esfuerzo pero tiene infinitos beneficios;

  2. Use different () en una clase personalizada, recuerde anular el método equals ();

  3. Anular equals (), debe anular hashCode ();

  4. Aunque diseñar un hashCode () simplemente puede hacer que devuelva 1, lo que no violará las regulaciones de Java, pero hacerlo conducirá a muchos males. Por ejemplo, cuando un objeto de este tipo se almacena en un hashMap, todos los objetos tienen el mismo hashCode y, finalmente, todos los objetos se almacenan en el mismo depósito del hashMap, lo que deteriora directamente el hashMap en una lista vinculada. Por lo tanto, la complejidad de O (1) se reduce a O (n) y, naturalmente, el rendimiento se reduce considerablemente.

  5. Los buenos libros son la escalera del progreso de los programadores. -Gorky. Por ejemplo, "Effecctive Java".

Programa de referencia final:

package example.mystream;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class ListToMap {

    @AllArgsConstructor
    @NoArgsConstructor
    @ToString
    private static class VideoInfo {
        @Getter
        String id;
        int width;
        int height;

        public static void main(String [] args) {
            System.out.println(new VideoInfo("123", 1, 2).equals(new VideoInfo("123", 1, 2)));
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof VideoInfo)) {
                return false;
            }
            VideoInfo vi = (VideoInfo) obj;
            return this.id.equals(vi.id)
                    && this.width == vi.width
                    && this.height == vi.height;
        }

        /**
         * If equals() is override, hashCode() must be override, too.
         * 1\. if a equals b, they must have the same hashCode;
         * 2\. if a doesn't equals b, they may have the same hashCode;
         * 3\. hashCode written in this way can be affected by sequence of the fields;
         * 3\. 2^5 - 1 = 31\. So 31 will be faster when do the multiplication,
         *      because it can be replaced by bit-shifting: 31 * i = (i << 5) - i.
         * @return
         */
        @Override
        public int hashCode() {
            int n = 31;
            n = n * 31 + this.id.hashCode();
            n = n * 31 + this.height;
            n = n * 31 + this.width;
            return n;
        }
    }

    public static void main(String [] args) {
        List<VideoInfo> list = Arrays.asList(new VideoInfo("123", 1, 2),
                new VideoInfo("456", 4, 5), new VideoInfo("123", 1, 2));

        // preferred: handle duplicated data when toMap()
        Map<String, VideoInfo> id2VideoInfo = list.stream().collect(
                Collectors.toMap(VideoInfo::getId, x -> x,
                        (oldValue, newValue) -> newValue)
        );

        System.out.println("No Duplicated1: ");
        id2VideoInfo.forEach((x, y) -> System.out.println("<" + x + ", " + y + ">"));

        // handle duplicated data using distinct(), before toMap()
        // Note that distinct() relies on equals() in the object
        // if you override equals(), hashCode() must be override together
        Map<String, VideoInfo> id2VideoInfo2 = list.stream().distinct().collect(
                Collectors.toMap(VideoInfo::getId, x -> x)
        );

        System.out.println("No Duplicated2: ");
        id2VideoInfo2.forEach((x, y) -> System.out.println("<" + x + ", " + y + ">"));
    }
}

Expandir

Suponiendo que la clase pertenece a otra persona y no se puede modificar

Arriba, VideoInfo es una clase que escribimos nosotros mismos, y podemos agregarle métodos equals () y hashCode (). Si VideoInfo es una clase en la dependencia a la que hacemos referencia y no tenemos el derecho de modificarla, ¿es imposible usar different () para personalizar el filtrado de objetos según ciertos elementos sean iguales?

Usar envoltorio

En una respuesta sobre stackoverflow, podemos encontrar un método factible: use wrapper.

Suponiendo que en una dependencia (no tenemos derecho a modificar esta clase), VideoInfo se define de la siguiente manera:

@AllArgsConstructor
@NoArgsConstructor
@ToString
public class VideoInfo {
    @Getter
    String id;
    int width;
    int height;
}

Usando la idea de envoltorio justo ahora, escriba el programa de la siguiente manera (por supuesto, para la operatividad del programa, el VideoInfo todavía se coloca, asumiendo que no se puede modificar y no se pueden agregar métodos):

package example.mystream;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class DistinctByWrapper {

    private static class VideoInfoWrapper {

        private final VideoInfo videoInfo;

        public VideoInfoWrapper(VideoInfo videoInfo) {
            this.videoInfo = videoInfo;
        }

        public VideoInfo unwrap() {
            return videoInfo;
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof VideoInfo)) {
                return false;
            }
            VideoInfo vi = (VideoInfo) obj;
            return videoInfo.id.equals(vi.id)
                    && videoInfo.width == vi.width
                    && videoInfo.height == vi.height;
        }

        @Override
        public int hashCode() {
            int n = 31;
            n = n * 31 + videoInfo.id.hashCode();
            n = n * 31 + videoInfo.height;
            n = n * 31 + videoInfo.width;
            return n;
        }
    }

    public static void main(String [] args) {
        List<VideoInfo> list = Arrays.asList(new VideoInfo("123", 1, 2),
                new VideoInfo("456", 4, 5), new VideoInfo("123", 1, 2));

        // VideoInfo --map()--> VideoInfoWrapper ----> distinct(): VideoInfoWrapper --map()--> VideoInfo
        Map<String, VideoInfo> id2VideoInfo = list.stream()
                .map(VideoInfoWrapper::new).distinct().map(VideoInfoWrapper::unwrap)
                .collect(
                Collectors.toMap(VideoInfo::getId, x -> x,
                        (oldValue, newValue) -> newValue)
        );

        id2VideoInfo.forEach((x, y) -> System.out.println("<" + x + ", " + y + ">"));
    }

}

/**
 * Assume that VideoInfo is a class that we can't modify
 */
@AllArgsConstructor
@NoArgsConstructor
@ToString
class VideoInfo {
    @Getter
    String id;
    int width;
    int height;
}

La idea completa del contenedor no es más que construir otra clase VideoInfoWrapper, agregando hashCode () y equals () al contenedor, para que el objeto contenedor se pueda personalizar de acuerdo con las reglas personalizadas.

Busque la cuenta pública de Java PhoenixMiles, responda a la "entrevista de back-end" y envíele una copia de Java Interview Questions.pdf

No podemos personalizar y filtrar VideoInfo, pero podemos personalizar y filtrar VideoInfoWrapper.

Lo que tenemos que hacer después de eso es convertir todo VideoInfo a VideoInfoWrapper, luego filtrar algunos VideoInfoWrapper, y luego transferir el VideoInfoWrapper restante de nuevo a VideoInfo para lograr el propósito de filtrar VideoInfo. ¡Muy inteligente!

Utilice "filtro () + función personalizada" en lugar de distinto ()

Otra forma más sutil es personalizar una función:

    private static <T> Predicate<T> distinctByKey(Function<? super T, Object> keyExtractor) {
        Map<Object, Boolean> map = new ConcurrentHashMap<>();
        return t -> map.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
    }

(El tipo del elemento de entrada es T y su clase principal. KeyExtracctor es una función de mapeo y devuelve Object. La función de toda la función pasada debe ser extraer la clave. La función differenceByKey devuelve la función Predicate y el tipo es T.)

Esta función pasa una función (lambda), extrae la clave del objeto pasado y luego intenta poner la clave en el concurrentHashMap. Si se puede ingresar, significa que la clave no ha aparecido antes y la función regresa falso; si no se puede poner, significa esto Si la clave está duplicada con una clave anterior, la función devuelve verdadero.

Esta función se utiliza finalmente como parámetro de entrada de la función filter (). Según la API de Java, las reglas de filtrado de filter (func) son: si func es verdadero, entonces filtrar, de lo contrario no filtrar. Por lo tanto, a través de "filter () + función personalizada", todas las claves repetidas devuelven verdadero y son filtradas por filter (), dejando atrás todas las claves no repetidas. Versión completa de PDF de la colección de entrevistas de Java

El programa final es el siguiente

package example.mystream;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class DistinctByFilterAndLambda {

    public static void main(String[] args) {
        List<VideoInfo> list = Arrays.asList(new VideoInfo("123", 1, 2),
                new VideoInfo("456", 4, 5), new VideoInfo("123", 1, 2));

        // Get distinct only
        Map<String, VideoInfo> id2VideoInfo = list.stream().filter(distinctByKey(vi -> vi.getId())).collect(
                Collectors.toMap(VideoInfo::getId, x -> x,
                        (oldValue, newValue) -> newValue)
        );

        id2VideoInfo.forEach((x, y) -> System.out.println("<" + x + ", " + y + ">"));
    }

    /**
     * If a key could not be put into ConcurrentHashMap, that means the key is duplicated
     * @param keyExtractor a mapping function to produce keys
     * @param <T> the type of the input elements
     * @return true if key is duplicated; else return false
     */
    private static <T> Predicate<T> distinctByKey(Function<? super T, Object> keyExtractor) {
        Map<Object, Boolean> map = new ConcurrentHashMap<>();
        return t -> map.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
    }
}

/**
 * Assume that VideoInfo is a class that we can't modify
 */
@AllArgsConstructor
@NoArgsConstructor
@ToString
class VideoInfo {
    @Getter
    String id;
    int width;
    int height;
}

Supongo que te gusta

Origin blog.51cto.com/14975073/2607545
Recomendado
Clasificación