web-dev-qa-db-fra.com

Obtenir un élément d'un ensemble

Pourquoi Set ne fournit-il pas une opération permettant d'obtenir un élément égal à un autre élément?

Set<Foo> set = ...;
...
Foo foo = new Foo(1, 2, 3);
Foo bar = set.get(foo);   // get the Foo element from the Set that equals foo

Je peux demander si la Set contient un élément égal à bar, alors pourquoi ne puis-je pas obtenir cet élément? :(

Pour clarifier, la méthode equals est remplacée, mais elle ne vérifie qu'un seul des champs, pas tous. Deux objets Foo considérés comme égaux peuvent donc avoir des valeurs différentes, c'est pourquoi je ne peux pas simplement utiliser foo.

285
foobar

Il ne servirait à rien d'obtenir l'élément s'il est égal. Une Map convient mieux à cette utilisation.


Si vous voulez toujours trouver l'élément, vous n'avez pas d'autre choix que d'utiliser l'itérateur:

public static void main(String[] args) {

    Set<Foo> set = new HashSet<Foo>();
    set.add(new Foo("Hello"));

    for (Iterator<Foo> it = set.iterator(); it.hasNext(); ) {
        Foo f = it.next();
        if (f.equals(new Foo("Hello")))
            System.out.println("foo found");
    }
}

static class Foo {
    String string;
    Foo(String string) {
        this.string = string;
    }
    @Override
    public int hashCode() { 
        return string.hashCode(); 
    }
    @Override
    public boolean equals(Object obj) {
        return string.equals(((Foo) obj).string);
    }
}
99
dacwe

Pour répondre à la question précise "Why _ Set ne fournit pas d'opération permettant d'obtenir un élément égal à un autre élément?", La réponse serait: parce que les concepteurs du framework de collection n'étaient pas très prospectif. Ils n'ont pas anticipé votre cas d'utilisation très légitime, ils ont essayé naïvement de "modéliser l'abstraction d'un ensemble mathématique" (du javadoc) et ont simplement oublié d'ajouter la méthode utile get().

Passons maintenant à la question implicite "comment obtenez-vous l'élément alors": Je pense que la meilleure solution consiste à utiliser un Map<E,E> au lieu d'un Set<E>, pour mapper les éléments sur se. De cette façon, vous pouvez efficacement extraire un élément du "set", car la méthode get () de la Map trouvera le élément utilisant une table de hachage ou un algorithme d'arborescence efficace. Si vous le souhaitez, vous pouvez écrire votre propre implémentation de Set qui offre la méthode supplémentaire get(), encapsulant le Map.

Les réponses suivantes sont à mon avis mauvaises ou mauvaises:

"Vous n'avez pas besoin d'obtenir l'élément, car vous avez déjà un objet égal": l'assertion est fausse, comme vous l'avez déjà montré dans la question. Deux objets égaux peuvent toujours avoir un état différent qui n'est pas pertinent pour l'égalité d'objet. Le but est d’avoir accès à cet état de l’élément contenu dans la Set, et non à l’état de l’objet utilisé en tant que "requête".

"Vous n'avez pas d'autre choix que d'utiliser l'itérateur": il s'agit d'une recherche linéaire sur une collection totalement inefficace pour les grands ensembles (ironiquement, le Set est organisé en tant que carte de hachage ou arbre pouvant être interrogé efficacement) ) Ne le fais pas! En utilisant cette approche, j'ai constaté de graves problèmes de performances dans des systèmes réels. À mon avis, ce qui est terrible avec la méthode manquante get() n’est pas tellement qu’il est un peu fastidieux de la contourner, mais que la plupart des programmeurs vont utiliser la méthode de recherche linéaire sans penser aux implications.

341
jschreiner

Convertissez le jeu en liste, puis utilisez la méthode get de liste

Set<Foo> set = ...;
List<Foo> list = new ArrayList<Foo>(set);
Foo obj = list.get(0);
20
To Kra

Si vous avez un objet égal, pourquoi avez-vous besoin de celui de l'ensemble? S'il n'est "égal" que par une clé, une Map constituerait un meilleur choix.

Quoi qu'il en soit, ce qui suit le fera:

Foo getEqual(Foo sample, Set<Foo> all) {
  for (Foo one : all) {
    if (one.equals(sample)) {
      return one;
    }
  } 
  return null;
}

Avec Java 8, cela peut devenir une ligne:

return all.stream().filter(sample::equals).findAny().orElse(null);
14
Arne Burmeister

La valeur par défaut définie dans Java n'est malheureusement pas conçue pour permettre une opération "get", car jschreiner a été expliquée avec précision.

Les solutions consistant à utiliser un itérateur pour rechercher l'élément d'intérêt (suggéré par dacwe ) ou pour supprimer l'élément et le rajouter avec ses valeurs mises à jour (suggéré par KyleM ) , pourrait fonctionner, mais peut être très inefficace.

Redéfinir l'implémentation d'égaux pour que les objets non égaux soient "égaux", comme indiqué correctement par David Ogren , peut facilement entraîner des problèmes de maintenance.

Et utiliser une carte en remplacement explicite (comme le suggèrent de nombreuses personnes), à mon sens, rend le code moins élégant.

Si l'objectif est d'avoir accès à l'instance d'origine de l'élément contenu dans l'ensemble (espérons que j'ai bien compris votre cas d'utilisation), voici une autre solution possible.


J'ai personnellement eu les mêmes besoins lors du développement d'un jeu vidéo client-serveur avec Java. Dans mon cas, chaque client avait des copies des composants stockés sur le serveur et le problème était chaque fois qu'un client devait modifier un objet du serveur.

Passer un objet sur Internet signifiait que le client avait de toute façon différentes instances de cet objet. Afin de faire correspondre cette instance "copiée" avec celle d'origine, j'ai décidé d'utiliser _UUID Java.

J'ai donc créé une classe abstraite UniqueItem, qui attribue automatiquement un identifiant unique aléatoire à chaque instance de ses sous-classes.

Cet UUID est partagé entre le client et l'instance de serveur. Il est donc facile de les faire correspondre simplement en utilisant une carte.

Cependant, utiliser directement une carte dans un cas similaire était encore inélégant. Quelqu'un pourrait dire que l'utilisation d'une carte peut être plus compliquée à gérer et à gérer.

Pour ces raisons, j'ai mis en place une bibliothèque appelée MagicSet, qui rend l'utilisation d'une carte "transparente" pour le développeur.

https://github.com/ricpacca/magicset


Comme le Java HashSet d’origine, un MagicHashSet (qui est l’une des implémentations de MagicSet fournies dans la bibliothèque) utilise un hachage de sauvegarde, mais au lieu d’avoir des éléments comme clés et une valeur fictive, elle utilise UUID de l'élément en tant que clé et de l'élément lui-même en tant que valeur. Cela ne provoque pas de surcharge dans l'utilisation de la mémoire par rapport à un HashSet normal.

De plus, un MagicSet peut être utilisé exactement comme un ensemble, mais avec quelques méthodes fournissant des fonctionnalités supplémentaires, telles que getFromId (), popFromId (), removeFromId (), etc.

La seule exigence pour l'utiliser est que tout élément que vous souhaitez stocker dans un MagicSet doit étendre la classe abstraite UniqueItem.


Voici un exemple de code, imaginant de récupérer l'instance d'origine d'une ville à partir d'un MagicSet, à partir d'une autre instance de cette ville avec le même UUID (ou même simplement son UUID).

class City extends UniqueItem {

    // Somewhere in this class

    public void doSomething() {
        // Whatever
    }
}

public class GameMap {
    private MagicSet<City> cities;

    public GameMap(Collection<City> cities) {
        cities = new MagicHashSet<>(cities);
    }

    /*
     * cityId is the UUID of the city you want to retrieve.
     * If you have a copied instance of that city, you can simply 
     * call copiedCity.getId() and pass the return value to this method.
     */
    public void doSomethingInCity(UUID cityId) {
        City city = cities.getFromId(cityId);
        city.doSomething();
    }

    // Other methods can be called on a MagicSet too
}
12
ricpacca

Si votre jeu est en fait un NavigableSet<Foo> (tel qu'un TreeSet) et un Foo implements Comparable<Foo>, vous pouvez utiliser

Foo bar = set.floor(foo); // or .ceiling
if (foo.equals(bar)) {
    // use bar…
}

(Merci au commentaire de @ eliran-malka pour le conseil.)

10
Jesse Glick

Avec Java 8, vous pouvez faire:

Foo foo = set.stream().filter(item->item.equals(theItemYouAreLookingFor)).findFirst().get();

Mais soyez prudent, .get () lève une NoSuchElementException, ou vous pouvez manipuler un élément optionnel.

9
cloudy_weather
Object objectToGet = ...
Map<Object, Object> map = new HashMap<Object, Object>(set.size());
for (Object o : set) {
    map.put(o, o);
}
Object objectFromSet = map.get(objectToGet);

Si vous n'obtenez qu'un seul résultat, cela ne sera pas très performant, car vous passerez en boucle sur tous vos éléments, mais si vous effectuez plusieurs extractions sur un grand ensemble, vous remarquerez la différence.

4
Xymon

Pourquoi:

Il semble que Set joue un rôle utile en fournissant un moyen de comparaison. Il est conçu pour ne pas stocker les éléments en double.

En raison de cette intention/conception, si on devait obtenir () une référence à l'objet stocké, puis la muter, il est possible que les intentions de conception de Set puissent être contrecarrées et provoquer un comportement inattendu.

De la JavaDocs

Il faut faire très attention si des objets mutables sont utilisés comme éléments d'ensemble. Le comportement d'un ensemble n'est pas spécifié si la valeur d'un objet est modifiée de manière à affecter les comparaisons égales alors que l'objet est un élément de l'ensemble.

Comment:

Maintenant que les flux ont été introduits, on peut faire ce qui suit

mySet.stream()
.filter(object -> object.property.equals(myProperty))
.findFirst().get();
2
Jason M

Vous devriez utiliser l’objet Java HashMap à cet effet http://download.Oracle.com/javase/1,5.0/docs/api/Java/util/HashMap.html

1
nurxyz

Parce que toute implémentation particulière de Set peut être ou ne pas être accès aléatoire .

Vous pouvez toujours obtenir un itérateur et parcourir le Set en utilisant la méthode next() des itérateurs pour renvoyer le résultat souhaité une fois que vous avez trouvé l'élément égal. Cela fonctionne quelle que soit la mise en œuvre. Si l'implémentation N'EST PAS un accès aléatoire (imaginez un ensemble sauvegardé sur une liste chaînée), une méthode get(E element) dans l'interface serait trompeuse, car il faudrait itérer la collection pour trouver l'élément à renvoyer, et un get(E element) semblerait impliquer que cela serait nécessaire, que l'ensemble puisse aller directement à l'élément à obtenir.

contains() peut ou non avoir à faire la même chose, bien sûr, en fonction de la mise en œuvre, mais le nom ne semble pas se prêter au même genre de malentendus.

0
Tom Tresansky

Si vous voulez nth élément de HashSet, vous pouvez aller avec la solution ci-dessous, ici j'ai ajouté l'objet de ModelClass dans HashSet.

ModelClass m1 = null;
int nth=scanner.nextInt();
for(int index=0;index<hashset1.size();index++){
    m1 = (ModelClass) itr.next();
    if(nth == index) {
        System.out.println(m1);
        break;
    }
}
0
Hardik Patel

il semble que le bon objet à utiliser est le Interner de goyave:

Fournit un comportement équivalent à String.intern () pour d'autres types immuables. Les implémentations courantes sont disponibles dans la classe Interners .

Il comporte également quelques leviers très intéressants, tels que concurrencyLevel ou le type de références utilisé (il est intéressant de noter qu'il ne propose pas un SoftInterner que je pourrais voir plus utile qu'un WeakInterner).

0
molyss

vous pouvez utiliser la classe Iterator

import Java.util.Iterator;
import Java.util.HashSet;

public class MyClass {
 public static void main(String[ ] args) {
 HashSet<String> animals = new HashSet<String>();
animals.add("fox");
animals.add("cat");
animals.add("dog");
animals.add("rabbit");

Iterator<String> it = animals.iterator();
while(it.hasNext()) {
  String value = it.next();
  System.out.println(value);   
 }
 }
}
0
chiha asma

Si vous regardez les premières lignes de l'implémentation de Java.util.HashSet, vous verrez:

public class HashSet<E>
    ....
    private transient HashMap<E,Object> map;

Donc HashSet utilise HashMap de manière inter-nale de toute façon, ce qui signifie que si vous utilisez directement un HashMap et utilisez la même valeur que la clé et la valeur, vous obtiendrez l'effet souhaité et vous enregistrez un peu de mémoire.

0
rghome

Je sais que cela a été demandé et répondu il y a longtemps, cependant si quelqu'un est intéressé, voici ma solution - classe de définition personnalisée soutenue par HashMap:

http://Pastebin.com/Qv6S91n9

Vous pouvez facilement implémenter toutes les autres méthodes Set.

0
Lukas Z.

Oui, utilisez HashMap ... mais de manière spécialisée: le piège que je prévois en essayant d'utiliser un HashMap comme pseudo -Set est la confusion possible entre des éléments "réels" de les éléments Map/Set et "candidats", c'est-à-dire utilisés pour vérifier si un élément equal est déjà présent. C'est loin d'être infaillible, mais vous éloigne du piège:

class SelfMappingHashMap<V> extends HashMap<V, V>{
    @Override
    public String toString(){
        // otherwise you get lots of "... object1=object1, object2=object2..." stuff
        return keySet().toString();
    }

    @Override
    public V get( Object key ){
        throw new UnsupportedOperationException( "use tryToGetRealFromCandidate()");
    }

    @Override
    public V put( V key, V value ){
       // thorny issue here: if you were indavertently to `put`
       // a "candidate instance" with the element already in the `Map/Set`: 
       // these will obviously be considered equivalent 
       assert key.equals( value );
       return super.put( key, value );
    }

    public V tryToGetRealFromCandidate( V key ){
        return super.get(key);
    }
}

Alors fais ceci:

SelfMappingHashMap<SomeClass> selfMap = new SelfMappingHashMap<SomeClass>();
...
SomeClass candidate = new SomeClass();
if( selfMap.contains( candidate ) ){
    SomeClass realThing = selfMap.tryToGetRealFromCandidate( candidate );
    ...
    realThing.useInSomeWay()...
}

Mais ... vous voulez maintenant que la candidate s'autodétruise, à moins que le programmeur ne la mette immédiatement dans le Map/Set... vous voudriez que contains "altère" la candidate de sorte que toute utilisation de celle-ci à moins de joindre la Map la rende "anathème". Vous pourriez peut-être faire en sorte que SomeClass implémente une nouvelle interface Taintable.

Une solution plus satisfaisante est un GettableSet , comme ci-dessous. Cependant, pour que cela fonctionne, vous devez soit être responsable de la conception de SomeClass afin de rendre tous les constructeurs non visibles (ou ... capables et désireux de concevoir et d'utiliser une classe de wrapper pour cela):

public interface NoVisibleConstructor {
    // again, this is a "Nudge" technique, in the sense that there is no known method of 
    // making an interface enforce "no visible constructor" in its implementing classes 
    // - of course when Java finally implements full multiple inheritance some reflection 
    // technique might be used...
    NoVisibleConstructor addOrGetExisting( GettableSet<? extends NoVisibleConstructor> gettableSet );
};

public interface GettableSet<V extends NoVisibleConstructor> extends Set<V> {
    V getGenuineFromImpostor( V impostor ); // see below for naming
}

La mise en oeuvre:

public class GettableHashSet<V extends NoVisibleConstructor> implements GettableSet<V> {
    private Map<V, V> map = new HashMap<V, V>();

    @Override
    public V getGenuineFromImpostor(V impostor ) {
        return map.get( impostor );
    }

    @Override
    public int size() {
        return map.size();
    }

    @Override
    public boolean contains(Object o) {
        return map.containsKey( o );
    }

    @Override
    public boolean add(V e) {
        assert e != null;
        V result = map.put( e,  e );
        return result != null;
    }

    @Override
    public boolean remove(Object o) {
        V result = map.remove( o );
        return result != null;
    }

    @Override
    public boolean addAll(Collection<? extends V> c) {
        // for example:
        throw new UnsupportedOperationException();
    }

    @Override
    public void clear() {
        map.clear();
    }

    // implement the other methods from Set ...
}

Vos classes NoVisibleConstructor ressemblent alors à ceci:

class SomeClass implements NoVisibleConstructor {

    private SomeClass( Object param1, Object param2 ){
        // ...
    }

    static SomeClass getOrCreate( GettableSet<SomeClass> gettableSet, Object param1, Object param2 ) {
        SomeClass candidate = new SomeClass( param1, param2 );
        if (gettableSet.contains(candidate)) {
            // obviously this then means that the candidate "fails" (or is revealed
            // to be an "impostor" if you will).  Return the existing element:
            return gettableSet.getGenuineFromImpostor(candidate);
        }
        gettableSet.add( candidate );
        return candidate;
    }

    @Override
    public NoVisibleConstructor addOrGetExisting( GettableSet<? extends NoVisibleConstructor> gettableSet ){
       // more elegant implementation-hiding: see below
    }
}

PS un problème technique avec une telle classe NoVisibleConstructor: on peut objecter qu’une telle classe est intrinsèquement final, ce qui peut être indésirable. En fait, vous pouvez toujours ajouter un constructeur factice protected sans paramètre:

protected SomeClass(){
    throw new UnsupportedOperationException();
}

... qui laisserait au moins une sous-classe compiler. Vous devrez alors vous demander si vous devez inclure une autre méthode de fabrique getOrCreate() dans la sous-classe.

Dernière étape est une classe de base abstraite (NB "élément" pour une liste, "membre" pour un ensemble) comme ceci pour les membres de votre ensemble (lorsque cela est possible - encore une fois, possibilité d'utiliser un classe wrapper où la classe n'est pas sous votre contrôle ou a déjà une classe de base, etc.), pour un masquage maximal de l'implémentation:

public abstract class AbstractSetMember implements NoVisibleConstructor {
    @Override
    public NoVisibleConstructor
            addOrGetExisting(GettableSet<? extends NoVisibleConstructor> gettableSet) {
        AbstractSetMember member = this;
        @SuppressWarnings("unchecked") // unavoidable!
        GettableSet<AbstractSetMembers> set = (GettableSet<AbstractSetMember>) gettableSet;
        if (gettableSet.contains( member )) {
            member = set.getGenuineFromImpostor( member );
            cleanUpAfterFindingGenuine( set );
        } else {
            addNewToSet( set );
        }
        return member;
    }

    abstract public void addNewToSet(GettableSet<? extends AbstractSetMember> gettableSet );
    abstract public void cleanUpAfterFindingGenuine(GettableSet<? extends AbstractSetMember> gettableSet );
}

... l'utilisation est assez évidente (dans la méthode d'usine de votre SomeClassstatic):

SomeClass setMember = new SomeClass( param1, param2 ).addOrGetExisting( set );
0
mike rodent

Été fait ça !! Si vous utilisez Guava, voici un moyen rapide de le convertir en carte:

Map<Integer,Foo> map = Maps.uniqueIndex(fooSet, Foo::getKey);
0
Heisenberg