441 Stimmen

Abrufen eines Elements aus einer Menge

Warum ist die Set eine Operation anbieten, um ein Element zu erhalten, das gleich einem anderen Element ist?

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

Ich kann fragen, ob die Set ein Element enthält, das gleich ist mit bar Warum kann ich dieses Element nicht bekommen? :(

Zur Klarstellung: Die equals wird überschrieben, aber es wird nur eines der Felder überprüft, nicht alle. Also zwei Foo Objekte, die als gleich angesehen werden, können tatsächlich unterschiedliche Werte haben, deshalb kann ich nicht einfach foo .

0voto

John McClane Punkte 3338

Das können Sie tun, wenn Sie ein NavigableSet (z.B. eine TreeSet ):

public static <E> E get(NavigableSet<E> set, E key) {
    return set.tailSet(key, true).floor(key);
}

Etwas kniffliger sind die Dinge für HashSet und seine Abkömmlinge wie LinkedHashSet :

import java.util.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Test {
    private static final Field mapField;
    private static final Method hashMethod;
    private static final Method getNodeMethod;
    private static final Field keyField;
    static {
        try {
            mapField = HashSet.class.getDeclaredField("map");
            mapField.setAccessible(true);
            hashMethod = HashMap.class.getDeclaredMethod("hash", Object.class);
            hashMethod.setAccessible(true);
            getNodeMethod = HashMap.class.getDeclaredMethod("getNode",
                    Integer.TYPE, Object.class);
            getNodeMethod.setAccessible(true);
            keyField = Class.forName("java.util.HashMap$Node").getDeclaredField("key");
            keyField.setAccessible(true);
        } catch (ReflectiveOperationException e) {
            throw new RuntimeException(e);
        }
    }

    public static <E> E get(HashSet<E> set, E key) {
        try {
            Object map = mapField.get(set);
            Object hash = hashMethod.invoke(null, key);
            Object node = getNodeMethod.invoke(map, hash, key);
            if (node == null)
                return null;
            @SuppressWarnings("unchecked")
            E result = (E)keyField.get(node);
            return result;
        } catch (ReflectiveOperationException e) {
            throw new RuntimeException(e);
        }
    }

    public static <E> E get(NavigableSet<E> set, E key) {
        return set.tailSet(key, true).floor(key);
    }

    public static void main(String[] args) {
        HashSet<Integer> s = new HashSet<>();
//      HashSet<Integer> s = new LinkedHashSet<>();
//      TreeSet<Integer> s = new TreeSet<>();
        for (int i = 0; i < 100_000; i++)
            s.add(i);
        Integer key = java.awt.event.KeyEvent.VK_FIND;
        Integer hidden = get(s, key);
        System.out.println(key);
        System.out.println(hidden);
        System.out.println(key.equals(hidden));
        System.out.println(key == hidden);
    }
}

0voto

jfajunior Punkte 1105

Der Vertrag des Hash-Codes macht das deutlich:

"Wenn zwei Objekte gemäß der Object-Methode gleich sind, muss der Aufruf der hashCode-Methode für jedes der beiden Objekte das gleiche ganzzahlige Ergebnis liefern."

Ihre Vermutung also:

"Um das klarzustellen, die Methode equals wird überschrieben, aber sie überprüft nur eines der der Felder, nicht alle. Zwei Foo-Objekte, die als gleich angesehen werden, können also tatsächlich unterschiedliche Werte haben, deshalb kann ich nicht einfach foo verwenden."

ist falsch und Sie brechen den Vertrag. Wenn wir uns die Methode "contains" der Schnittstelle "Set" ansehen, haben wir das:

boolean contains(Object o);
Gibt true zurück, wenn diese Menge das angegebene Element enthält. Mehr formell, gibt true zurück, wenn und nur wenn diese Menge ein Element enthält "e" enthält, so dass o==null ? e==null : o.equals(e)

Um das zu erreichen, was Sie wollen, können Sie eine Map verwenden, in der Sie den Schlüssel definieren und Ihr Element mit dem Schlüssel speichern, der definiert, wie sich die Objekte voneinander unterscheiden oder gleich sind.

0voto

mike rodent Punkte 12041

Ja, verwenden Sie HashMap ... aber auf eine spezielle Art und Weise: Die Falle, die ich sehe, wenn ich versuche, eine HashMap als ein Pseudo- Set ist die mögliche Verwechslung zwischen "tatsächlichen" Elementen der Map/Set und "Kandidaten"-Elemente, d. h. Elemente, mit denen geprüft werden kann, ob ein equal Element bereits vorhanden ist. Das ist bei weitem nicht idiotensicher, aber es hilft Ihnen, die Falle zu umgehen:

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

Dann tun Sie dies:

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

Aber... Sie wollen jetzt die candidate auf irgendeine Art und Weise selbst zerstören, es sei denn, der Programmierer stellt sie sofort in den Map/Set ... würden Sie wollen contains zu "beflecken" die candidate so dass jede Verwendung, sofern sie nicht mit dem Map macht es zu einem "Anathema". Vielleicht könnten Sie SomeClass eine neue Taintable Schnittstelle.

Eine zufriedenstellendere Lösung ist eine GettableSet wie unten. Damit dies funktioniert, müssen Sie jedoch entweder für die Gestaltung von SomeClass um alle Konstruktoren unsichtbar zu machen (oder... fähig und bereit, eine Wrapper-Klasse dafür zu entwerfen und zu verwenden):

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
}

Umsetzung:

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

Ihr NoVisibleConstructor Klassen sehen dann wie folgt aus:

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 ein technisches Problem mit einem solchen NoVisibleConstructor Klasse: Es kann eingewendet werden, dass eine solche Klasse von Natur aus final was unerwünscht sein kann. Eigentlich könnte man immer einen Dummy ohne Parameter hinzufügen protected Konstrukteur:

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

... was zumindest die Kompilierung einer Unterklasse ermöglichen würde. Sie müssen sich dann überlegen, ob Sie eine weitere getOrCreate() Fabrikmethode in der Unterklasse.

Letzter Schritt ist eine abstrakte Basisklasse (NB "element" für eine Liste, "member" für eine Menge) wie diese für die Mitglieder Ihrer Menge (wenn möglich - auch hier ist die Verwendung einer Wrapper-Klasse wenn die Klasse nicht unter Ihrer Kontrolle steht oder bereits eine Basisklasse hat usw.), um die Implementierung möglichst gut zu verbergen:

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

... die Verwendung ist ziemlich offensichtlich (innerhalb Ihrer SomeClass 's static Fabrikmethode):

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

0voto

Tom Tresansky Punkte 18658

Da eine bestimmte Implementierung von Set nicht unbedingt Zufallszugriff .

Sie können jederzeit eine Iterator und schrittweise durch die Menge, wobei die Iteratoren next() Methode, um das gewünschte Ergebnis zurückzugeben, sobald Sie das gleiche Element gefunden haben. Dies funktioniert unabhängig von der Implementierung. Wenn die Implementierung NICHT zufällig ist (z.B. bei einem Set mit verknüpften Listen), kann eine get(E element) Methode in der Schnittstelle wäre trügerisch, da sie die Sammlung iterieren müsste, um das zurückzugebende Element zu finden, und eine get(E element) scheint darauf hinzudeuten, dass dies notwendig wäre, damit das Set direkt zu dem zu erhaltenden Element springen kann.

contains() muss natürlich je nach Implementierung das Gleiche tun oder auch nicht, aber der Name scheint nicht zu den gleichen Missverständnissen zu führen.

-2voto

Chris Willmore Punkte 564

Schnelle Hilfsmethode, die diese Situation lösen könnte:

<T> T onlyItem(Collection<T> items) {
    if (items.size() != 1)
        throw new IllegalArgumentException("Collection must have single item; instead it has " + items.size());

    return items.iterator().next();
}

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X