web-dev-qa-db-fra.com

HashSet vs TreeSet vs LinkedHashSet sur la base de l'ajout d'une valeur en double

J'apprends le cœur de Java, c'est-à-dire Collections. J'aimerais savoir ce qui se passe en interne lorsque nous ajoutons un élément dupliqué dans HashSet, TreeSet, LinkedHashSet.

L'entrée météo est remplacée, ignorée ou une exception est levée et le programme se termine . Et une sous question est, Lequel a la complexité de temps identique ou moyenne pour toutes ses opérations

Votre réponse sera grandement appréciée.

21
Prashant Shilimkar

TreeSet, LinkedHashSet et HashSet en Java sont trois implémentations de Set dans une structure de collection et, comme beaucoup d’autres, ils sont également utilisés pour stocker des objets. La principale caractéristique de TreeSet est le tri, LinkedHashSet est l'ordre d'insertion et HashSet est simplement une collection à usage général pour le stockage d'objets. HashSet est implémenté à l'aide de HashMap en Java, tandis que TreeSet est implémenté à l'aide de TreeMap. TreeSet est une implémentation de SortedSet qui lui permet de conserver les éléments dans l'ordre de tri défini par l'interface Comparable ou Comparator. Comparable est utilisé pour le tri par ordre naturel et Comparator pour le tri par ordre personnalisé des objets, qui peut être fourni lors de la création d'une instance de TreeSet. Quoi qu'il en soit, avant de voir la différence entre TreeSet, LinkedHashSet et HashSet, voyons quelques similitudes entre eux:

1) Doublons: les trois implémentations Ensemble, l’interface signifie qu’ils ne sont pas autorisés à stocker les doublons.

2) Sécurité des threads: HashSet, TreeSet et LinkedHashSet ne sont pas thread-safe, si vous les utilisez dans un environnement multithread où au moins un Thread Set modifié vous devez les synchroniser en externe.

3) Itérateur à grande vitesse: L'itérateur renvoyé par TreeSet, LinkedHashSet et HashSet est un itérateur à sécurité rapide. En d'autres termes, si Iterator est modifié après sa création par un autre moyen que la méthode Iterator remove (), il lançera ConcurrentModificationException du mieux possible. En savoir plus sur Iterator fail-fast vs fail-safe ici

Voyons maintenant la différence entre HashSet, LinkedHashSet et TreeSet en Java:

Performance et vitesse: La première différence entre eux vient en termes de vitesse. HashSet est le plus rapide, LinkedHashSet est le deuxième en performances ou presque similaire à HashSet mais TreeSet est un peu plus lent en raison de l'opération de tri qu'il doit effectuer à chaque insertion. TreeSet fournit une durée garantie O(log(n)) pour les opérations courantes telles que l'ajout, la suppression et le contenu, tandis que HashSet et LinkedHashSet offrent des performances à temps constant, par ex. O(1) pour ajouter, contenir et supprimer une fonction de hachage donnée répartit uniformément les éléments dans le compartiment.

Commande: HashSet ne conserve aucun ordre tandis que LinkedHashSet conserve l'ordre d'insertion des éléments, à l'instar de l'interface de liste, tandis que TreeSet conserve l'ordre de tri ou les éléments.

Implémentation interne: HashSet est sauvegardé par une instance HashMap, LinkedHashSet est implémenté à l'aide de HashSet et LinkedList tandis que TreeSet est sauvegardé par NavigableMap en Java et utilise par défaut TreeMap.

null: HashSet et LinkedHashSet autorisent tous deux null, mais TreeSet n'autorise pas null et jette Java.lang.NullPointerException lors de l'insertion de null dans TreeSet. TreeSet utilise la méthode compareTo () des éléments respectifs pour les comparer, générant une exception NullPointerException lors de la comparaison avec null. Voici un exemple:

TreeSet cities
Exception in thread "main" Java.lang.NullPointerException
        at Java.lang.String.compareTo(String.Java:1167)
        at Java.lang.String.compareTo(String.Java:92)
        at Java.util.TreeMap.put(TreeMap.Java:545)
        at Java.util.TreeSet.add(TreeSet.Java:238)

Comparaison: HashSet et LinkedHashSet utilisent la méthode equals () en Java à des fins de comparaison, mais TreeSet utilise la méthode compareTo () pour gérer les commandes. C'est pourquoi compareTo () doit être cohérent avec égal à Java. à défaut, rompre le contact général de l'interface Set, c'est-à-dire qu'il peut permettre des doublons.

Utilisez peut utiliser le lien ci-dessous pour voir la mise en oeuvre interne http://grepcode.com/file/repository.grepcode.com/Java/root/jdk/openjdk/6-b14/Java/util/HashSet. Java # HashSet.add% 28Java.lang.Object% 29

From the source code 
Hashset hases Hashmap to store the data and LinkedHashSet extends Hashset and hence uses same add method of Hashset But TreeSet uses NavigableMap to store the data

Source: http://javarevisited.blogspot.com/2012/11/difference-between-treeset-hashset-vs-linkedshshsh-Java.html#ixzz2lGo6Y9mm

41
constantlearner
7
user2485429

Je n'ai pas trouvé beaucoup de données concrètes sur les différences, alors j'ai fait un point de repère pour les 3 cas.

Il semble que HashSet soit environ 4 fois plus rapide que TreeSet lors de l'ajout (dans certaines circonstances, cela variera probablement en fonction des caractéristiques exactes de vos données, etc.).

# Run complete. Total time: 00:22:47

Benchmark                                                     Mode  Cnt  Score   Error  Units
DeduplicationWithSetsBenchmark.deduplicateWithHashSet        thrpt  200  7.734 ▒ 0.133  ops/s
DeduplicationWithSetsBenchmark.deduplicateWithLinkedHashSet  thrpt  200  7.100 ▒ 0.171  ops/s
DeduplicationWithSetsBenchmark.deduplicateWithTreeSet        thrpt  200  1.983 ▒ 0.032  ops/s

Voici le code de référence:

package my.app;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import Java.util.Comparator;
import Java.util.HashSet;
import Java.util.LinkedHashSet;
import Java.util.Random;
import Java.util.Set;
import Java.util.TreeSet;

public class DeduplicationWithSetsBenchmark {

    static Item[] inputData = makeInputData();

    @Benchmark
    public int deduplicateWithHashSet() {
        return deduplicate(new HashSet<>());
    }

    @Benchmark
    public int deduplicateWithLinkedHashSet() {
        return deduplicate(new LinkedHashSet<>());
    }

    @Benchmark
    public int deduplicateWithTreeSet() {
        return deduplicate(new TreeSet<>(Item.comparator()));
    }

    private int deduplicate(Set<Item> set) {
        for (Item i : inputData) {
            set.add(i);
        }
        return set.size();
    }

    public static void main(String[] args) throws RunnerException {

        // Verify that all 3 methods give the same answers:
        DeduplicationWithSetsBenchmark x = new DeduplicationWithSetsBenchmark();
        int count = x.deduplicateWithHashSet();
        assert(count < inputData.length);
        assert(count == x.deduplicateWithLinkedHashSet());
        assert(count == x.deduplicateWithTreeSet());


        Options opt = new OptionsBuilder()
            .include(DeduplicationWithSetsBenchmark.class.getSimpleName())
            .forks(1)
            .build();

        new Runner(opt).run();
    }

    private static Item[] makeInputData() {
        int count = 1000000;
        Item[] acc = new Item[count];
        Random rnd = new Random();

        for (int i=0; i<count; i++) {
            Item item = new Item();
            // We are looking to include a few collisions, so restrict the space of the values
            item.name = "the item name " + rnd.nextInt(100);
            item.id = rnd.nextInt(100);
            acc[i] = item;
        }
        return acc;
    }

    private static class Item {
        public String name;
        public int id;

        public String getName() {
            return name;
        }

        public int getId() {
            return id;
        }

        @Override
        public boolean equals(Object obj) {
            Item other = (Item) obj;

            return name.equals(other.name) && id == other.id;
        }

        @Override
        public int hashCode() {
            return name.hashCode() * 13 + id;
        }

        static Comparator<Item> comparator() {
            return Comparator.comparing(Item::getName, Comparator.naturalOrder())
                .thenComparing(Item::getId, Comparator.naturalOrder());
        }
    }
}
1
Rich

tldr: les valeurs de répétition sont ignorées par ces collections.

Je n'ai pas vu de réponse complète à la partie en gras de la question, que se passe-t-il EXACTEMENT des doublons? Est-ce qu'il écrase l'ancien objet ou ignore le nouvel objet? Prenons cet exemple d'objet où un champ détermine l'égalité mais où il existe des données supplémentaires susceptibles de varier:

public class MyData implements Comparable {
    public final Integer valueDeterminingEquality;
    public final String extraData;

    public MyData(Integer valueDeterminingEquality, String extraData) {
        this.valueDeterminingEquality = valueDeterminingEquality;
        this.extraData = extraData;
    }

    @Override
    public boolean equals(Object o) {
        return valueDeterminingEquality.equals(((MyData) o).valueDeterminingEquality);
    }

    @Override
    public int hashCode() {
        return valueDeterminingEquality.hashCode();
    }

    @Override
    public int compareTo(Object o) {
        return valueDeterminingEquality.compareTo(((MyData)o).valueDeterminingEquality);
    }
}

Ce test unitaire montre que les valeurs en double sont ignorées par les 3 collections:

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

import Java.util.*;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

@RunWith(Parameterized.class)
public class SetRepeatedItemTest {
    private final Set<MyData> testSet;

    public SetRepeatedItemTest(Set<MyData> testSet) {
        this.testSet = testSet;
    }

    @Parameterized.Parameters
    public static Collection<Object[]> data() {
        return Arrays.asList(new Object[][] {
                { new TreeSet() }, { new HashSet() }, { new LinkedHashSet()}
        });
    }

    @Test
    public void testTreeSet() throws Exception {
        testSet.add(new MyData(1, "object1"));
        testSet.add(new MyData(1, "object2"));
        assertThat(testSet.size(), is(1));
        assertThat(testSet.iterator().next().extraData, is("object1"));
    }
}

J'ai aussi examiné l'implémentation de TreeSet, qui, nous le savons, utilise TreeMap ... Dans TreeSet.Java:

public boolean add(E var1) {
    return this.m.put(var1, PRESENT) == null;
}

Au lieu de montrer la méthode de vente complète de TreeMap, voici la boucle de recherche pertinente:

parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
        t = t.left;
else if (cmp > 0)
        t = t.right;
else
    return t.setValue(value);
} while (t != null);

donc si cmp == 0, c'est-à-dire que nous avons trouvé une entrée en double, nous retournons plus tôt au lieu d'ajouter un enfant à la fin de la boucle. L'appel à setValue ne fait en réalité rien, car TreeSet utilise des données factices pour la valeur ici, l'important est que la clé ne change pas. Si vous regardez dans HashMap, vous verrez le même comportement.

0
Ben B