'JavaFX: ObsevableMap keySet as an ObservableSet

I want to transform an ObservableMap's keySet to a read only ObservableSet. I don't want to copy the value, any modification to the ObservableMap must affect the Observable keySet. If i bind another set to the observable key set content, its content is automatically updated.

This is what i would like to write.

ObservableMap<String, Object> map = FXCollections.observableHashMap();
ObservableSet<String> keySet = FXCollections.observableKeySet(map);
Set<String> boundSet = new HashSet<String>();
Bindings.bindContent(boundSet, keySet);
map.put("v", new Object());
assert boundSet.contains("v");

Is there this functionality in the JavaFX SDK ?



Solution 1:[1]

The feature you request does not need a special ObservableSet. It’s already part of the Map interface contract:

ObservableMap<String, Object> map = FXCollections.observableHashMap();
Set<String> keySet = map.keySet();
map.put("v", new Object());
assert keySet.contains("v");

A Map’s keyset always reflects the changes made to the backing map.

http://docs.oracle.com/javase/8/docs/api/java/util/Map.html#keySet--

Returns a Set view of the keys contained in this map. The set is backed by the map, so changes to the map are reflected in the set, and vice-versa.

Solution 2:[2]

As far as I know, there's no built-in way to do it.

Here's a utility method that I made to handle this:

    /**
     * Builds and returns an observable keyset of the specified map. Note that the resulting
     * keyset is a new set that is guaranteed to have the same items as the map's true
     * keyset, but does not make any guarantees about iteration order or implementation
     * details of the keyset (for example, if the Map is a SortedMap, the keyset 
     * will not necessarily maintain keys in sorted order).
     * @param <K> Map's key type
     * @param <V> Map's value type
     * @param map the ObservableMap to which the set should be bound
     * @return a new observable set reflecting the map's keys 
     */
    public static <K, V> ObservableSet<K> getObservableKeySet(ObservableMap<K, V> map) {
        
        ObservableSet<K> set = FXCollections.observableSet(new HashSet<>());
        
        map.addListener(
                (MapChangeListener<K, V>)(
                        change -> {
                            if(change.wasAdded() && !change.wasRemoved()) {
                                set.add(change.getKey());
                            }
                            if(change.wasRemoved() && !change.wasAdded()) {
                                set.remove(change.getKey());
                            }
                            //Note that if change was added and removed, that means that
                            //the key was unchanged and a value was just replaced. That
                            //shouldn't affect the keyset so we do nothing
                            
                        })
                );
        
        return set;
        
    }

Update:

I decided that I didn't like how the iteration order of the resulting set wouldn't match the original map's iteration order, so instead I made a new class that acts as an observable wrapper around a map's keyset. This is more like Map's built-in keySet() method where it returns a view of the actual set. This just adds in the listeners that make it observable:

/**
 * Observable view of an ObservableMap's keyset
 */
public class ObservableKeySet<K, V> implements ObservableSet<K> {

    /**
     * The map's actual keyset object that gets wrapped
     */
    private Set<K> wrappedSet;
    
    /**
     * Invalidation listeners to be notified when the set changes. Note that we end 
     * up calling these more than we should since invalidation listeners should only 
     * be called if the observable value is observed between changes and we're going 
     * to call them on every change. However, the ObservableSet returned from 
     * FxCollectionUtilities.observableSet actually also does that too, so I feel 
     * like we can get away with it.
     */
    private Collection<InvalidationListener> invalidationListeners = new ArrayList<>();
    
    /**
     * Change listeners to be notified when the set changes
     */
    private Collection<SetChangeListener<? super K>> changeListeners = new ArrayList<>();
    
    /**
     * Creates an Observable Set view of the specified map's keyset
     * @param map ObservableMap
     */
    public ObservableKeySet(ObservableMap<K, V> map) {
        this.wrappedSet = map.keySet();
        
        map.addListener((MapChangeListener<K,V>)this::onMapChange);
        
    }
    
    /**
     * Code to be executed on any match change. It will determine if there is a resulting
     * set change and trigger listeners as appropriate
     * @param change
     */
    private void onMapChange(MapChangeListener.Change<? extends K, ? extends V> change) {
        
        SetChangeListener.Change<K> setChange = null;
        
        //Note that if the map change says that there was an add and removal, then
        //that means a value was getting replaced, which doesn't result in a keySet
        //change
        if(change.wasAdded() && ! change.wasRemoved()) {
            setChange = new BasicSetChange(true, change.getKey());
        }
        else if(change.wasRemoved() && ! change.wasAdded()) {
            setChange = new BasicSetChange(false, change.getKey());
        }
        
        if(setChange != null) {
            
            invalidationListeners.forEach(listener -> listener.invalidated(this));
            
            final SetChangeListener.Change<K> finalChange = setChange;
            changeListeners.forEach(listener -> listener.onChanged(finalChange));
        }
        
    }

    @Override
    public void addListener(InvalidationListener listener) {
        invalidationListeners.add(listener);
    }
    
    @Override
    public void removeListener(InvalidationListener listener) {
        invalidationListeners.remove(listener);
    }

    @Override
    public void addListener(SetChangeListener<? super K> listener) {
        changeListeners.add(listener);
    }

    @Override
    public void removeListener(SetChangeListener<? super K> listener) {
        changeListeners.remove(listener);
    }
    
    //Simple wrapper method that either just pass through to the wrapped set or 
    //throw an unsupported operation exception
    @Override public int size() {return wrappedSet.size();}
    @Override public boolean isEmpty() {return wrappedSet.isEmpty();}
    @Override public boolean contains(Object o) {return wrappedSet.contains(o);}
    @Override public Iterator<K> iterator() {return wrappedSet.iterator();}
    @Override public Object[] toArray() {return wrappedSet.toArray();}
    @Override public <T> T[] toArray(T[] a) {return wrappedSet.toArray(a);}
    @Override public boolean containsAll(Collection<?> c) {return wrappedSet.containsAll(c);}
    
    @Override public boolean add(K e) {throw new UnsupportedOperationException();}
    @Override public boolean remove(Object o) {throw new UnsupportedOperationException();}
    @Override public boolean addAll(Collection<? extends K> c) {throw new UnsupportedOperationException();}
    @Override public boolean retainAll(Collection<?> c) {throw new UnsupportedOperationException();}
    @Override public boolean removeAll(Collection<?> c) {throw new UnsupportedOperationException();}
    @Override public void clear() {throw new UnsupportedOperationException();}
    
    
    /**
     * Simple implementation of {@link SetChangeListener.Change}
     */
    private class BasicSetChange extends SetChangeListener.Change<K> {
        
        /** If true, it is an add change, otherwise it is a remove change*/
        private final boolean isAdd;
        
        /** Value that was added or removed */
        private final K value;
        
        /**
         * @param isAdd {@link #isAdd}
         * @param value {@link #value}
         */
        public BasicSetChange(boolean isAdd, K value) {
            super(ObservableKeySet.this);
            this.isAdd = isAdd;
            this.value = value;
        }
        

        @Override
        public boolean wasAdded() {
            return isAdd;
        }

        @Override
        public boolean wasRemoved() {
            return !isAdd;
        }

        @Override
        public K getElementAdded() {
            return isAdd ? value : null;
        }

        @Override
        public K getElementRemoved() {
            return isAdd ? null : value;
        }
    }

}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Holger
Solution 2