Java - 不可修改的键集映射

Java - unmodifiable key set map

我正在寻找一种方法来提供 Map 预定义(如在运行时不可变,而不是编译时常量)常量键集,但可修改值。

JDK 提供 Collections.unmodifiableMap 工厂方法,它包装 Map 并提供它的不可变视图。

是否有类似的方法来包装 Map 以便只有它的键是不可变的?例如,put(K,V) 将替换现有键的值,但如果键不存在则抛出 UnsupportedOperationException

1) 代理

我会把你的 SemiMutableMap 的范围缩小到

interface ISemiMutableMap<U, V> {

    V get(U key);
    V set(U key, V value) throws Exception; //create your own maybe ?
}

这将减少访问的可能性,但让您可以完全控制它。

然后像代理一样简单地实现它

public class SemiMutableMap<U, V> implements ISemiMutableMap<U,V>{

    private Map<U, V> map;

    public SemiMutableMap(Map<U, V> map){ //get the predefine maps
        this.map = map;
    }

    public V get(U key){
        return map.get(U);
    }

    public V set(U key, V value) throws Exception{
        if(!map.containsKey(key)){
            throw new Exception();
        }

        return map.put(key,value);
    }
}

你当然可以在其中添加你喜欢的方法。

请注意,这并不完全正确,构造函数应该克隆地图而不是使用相同的引用,但我有点懒惰;)而且我在没有 IDE

2) 实现

没有什么能阻止您简单地从 Collections 中获取 UnmodifiableMap 的代码并根据您的需要进行调整。由此,您会发现根据需要创建自己的实现非常简单。

相信我,这个 class 已经过测试和审查 ;)

您需要调整 put 才能更新现有值(与上面相同的代码)和 UnmodifiableEntry.setValue 才能接受来自条目集的更新。

我相信你想做这样的事情。

public class UnmodifiableKeyMap{

static enum MYKeySet {KEY1, KEY2, KEY3, KEY4};
private Map myUnmodifiableHashMap = new HashMap();

public boolean containsKey(Object key) {
    return this.containsKey(key);
}


public Object get(Object key) {

    if(this.containsKey(key)){
        return this.myUnmodifiableHashMap.get(key);
    }else {
        return null;
    }
}

public Object put(Object key, Object value) throws Exception {

    if(this.containsKey(key)){
        this.myUnmodifiableHashMap.put(key, value);
    }

    throw new Exception("UnsupportedOperationException");
}


public Set keySet() {

    Set mySet = new HashSet(Arrays.asList(MYKeySet.values()));
    return mySet;
}

public Collection values() {

    return this.myUnmodifiableHashMap.values();
}

}

使用枚举作为键。然后人们不必关心他们是否可以添加新密钥,因为密钥域是固定且有限的。事实上,这是 Java 提供 java.util.EnumMap<K extends Enum<K>,V> 的标准用例 http://docs.oracle.com/javase/8/docs/api/java/util/EnumMap.html

好的,这里建议的所有解决方案都是包装或扩展 Collections.UnmodifiableMap。两者都是不可能的,因为原始实现不允许覆盖 put(和 replace 等),这正是使其安全的原因...

我看到两个选项:

  1. "smelly" 选项 - 使用反射获取原始 Map<> 并直接调用它的方法。
  2. "ugly"选项-复制java.lang.Collections中一些静态类的源码并修改。

如果谁有更好的想法,请告诉我。


这是第二种解决方案的初步实施:

import java.io.Serializable;
import java.util.*;
import java.util.function.*;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import static java.util.Collections.unmodifiableCollection;
import static java.util.Collections.unmodifiableSet;

/**
 * @serial include
 */
public class UnmodifiableKeySetMap<K,V> implements Map<K,V>, Serializable {

    private final Map<K, V> m;

    /**
     * Returns a view of the specified map with unmodifiable key set. This 
     * method allows modules to provide users with "read-only" access to 
     * internal maps. Query operations on the returned map "read through"
     * to the specified map, and attempts to modify the returned
     * map, whether direct or via its collection views, result in an
     * <tt>UnsupportedOperationException</tt>.<p>
     *
     * The returned map will be serializable if the specified map
     * is serializable.
     *
     * @param <K> the class of the map keys
     * @param <V> the class of the map values
     * @param  m the map for which an unmodifiable view is to be returned.
     * @return an unmodifiable view of the specified map.
     */
    public static <K,V> Map<K,V> unmodifiableKeySetMap(Map<K, V> m) {
        return new UnmodifiableKeySetMap<>(m);
    }

    UnmodifiableKeySetMap(Map<K, V> m) {
        if (m==null)
            throw new NullPointerException();
        this.m = m;
    }

    public int size()                        {return m.size();}
    public boolean isEmpty()                 {return m.isEmpty();}
    public boolean containsKey(Object key)   {return m.containsKey(key);}
    public boolean containsValue(Object val) {return m.containsValue(val);}
    public V get(Object key)                 {return m.get(key);}

    public V put(K key, V value) {
        if (containsKey(key)) {
            return m.put(key, value);
        }
        throw new UnsupportedOperationException();
    }
    public V remove(Object key) {
        throw new UnsupportedOperationException();
    }
    public void putAll(Map<? extends K, ? extends V> m) {
        throw new UnsupportedOperationException();
    }
    public void clear() {
        throw new UnsupportedOperationException();
    }

    private transient Set<K> keySet;
    private transient Set<Map.Entry<K,V>> entrySet;
    private transient Collection<V> values;

    public Set<K> keySet() {
        if (keySet==null)
            keySet = unmodifiableSet(m.keySet());
        return keySet;
    }

    public Set<Map.Entry<K,V>> entrySet() {
        if (entrySet==null)
            entrySet = new UnmodifiableKeySetMap.UnmodifiableEntrySet<>(m.entrySet());
        return entrySet;
    }

    public Collection<V> values() {
        if (values==null)
            values = unmodifiableCollection(m.values());
        return values;
    }

    public boolean equals(Object o) {return o == this || m.equals(o);}
    public int hashCode()           {return m.hashCode();}
    public String toString()        {return m.toString();}

    // Override default methods in Map
    @Override
    @SuppressWarnings("unchecked")
    public V getOrDefault(Object k, V defaultValue) {
        // Safe cast as we don't change the value
        return ((Map<K, V>)m).getOrDefault(k, defaultValue);
    }

    @Override
    public void forEach(BiConsumer<? super K, ? super V> action) {
        m.forEach(action);
    }

    @Override
    public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
        throw new UnsupportedOperationException();
    }

    @Override
    public V putIfAbsent(K key, V value) {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean remove(Object key, Object value) {
        throw new UnsupportedOperationException();
    }

    @Override
    public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
        throw new UnsupportedOperationException();
    }

    @Override
    public V computeIfPresent(K key,
                              BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        throw new UnsupportedOperationException();
    }

    @Override
    public V compute(K key,
                     BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        throw new UnsupportedOperationException();
    }

    @Override
    public V merge(K key, V value,
                   BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
        throw new UnsupportedOperationException();
    }

    /**
     * @serial include
     */
    static class UnmodifiableSet<E> extends UnmodifiableCollection<E>
            implements Set<E>, Serializable {
        private static final long serialVersionUID = -9215047833775013803L;

        UnmodifiableSet(Set<? extends E> s)     {super(s);}
        public boolean equals(Object o) {return o == this || c.equals(o);}
        public int hashCode()           {return c.hashCode();}
    }

    /**
     * @serial include
     */
    static class UnmodifiableCollection<E> implements Collection<E>, Serializable {
        private static final long serialVersionUID = 1820017752578914078L;

        final Collection<? extends E> c;

        UnmodifiableCollection(Collection<? extends E> c) {
            if (c==null)
                throw new NullPointerException();
            this.c = c;
        }

        public int size()                   {return c.size();}
        public boolean isEmpty()            {return c.isEmpty();}
        public boolean contains(Object o)   {return c.contains(o);}
        public Object[] toArray()           {return c.toArray();}
        public <T> T[] toArray(T[] a)       {return c.toArray(a);}
        public String toString()            {return c.toString();}

        public Iterator<E> iterator() {
            return new Iterator<E>() {
                private final Iterator<? extends E> i = c.iterator();

                public boolean hasNext() {return i.hasNext();}
                public E next()          {return i.next();}
                public void remove() {
                    throw new UnsupportedOperationException();
                }
                @Override
                public void forEachRemaining(Consumer<? super E> action) {
                    // Use backing collection version
                    i.forEachRemaining(action);
                }
            };
        }

        public boolean add(E e) {
            throw new UnsupportedOperationException();
        }
        public boolean remove(Object o) {
            throw new UnsupportedOperationException();
        }

        public boolean containsAll(Collection<?> coll) {
            return c.containsAll(coll);
        }
        public boolean addAll(Collection<? extends E> coll) {
            throw new UnsupportedOperationException();
        }
        public boolean removeAll(Collection<?> coll) {
            throw new UnsupportedOperationException();
        }
        public boolean retainAll(Collection<?> coll) {
            throw new UnsupportedOperationException();
        }
        public void clear() {
            throw new UnsupportedOperationException();
        }

        // Override default methods in Collection
        @Override
        public void forEach(Consumer<? super E> action) {
            c.forEach(action);
        }
        @Override
        public boolean removeIf(Predicate<? super E> filter) {
            throw new UnsupportedOperationException();
        }
        @SuppressWarnings("unchecked")
        @Override
        public Spliterator<E> spliterator() {
            return (Spliterator<E>)c.spliterator();
        }
        @SuppressWarnings("unchecked")
        @Override
        public Stream<E> stream() {
            return (Stream<E>)c.stream();
        }
        @SuppressWarnings("unchecked")
        @Override
        public Stream<E> parallelStream() {
            return (Stream<E>)c.parallelStream();
        }
    }

    /**
     * We need this class in addition to UnmodifiableSet as
     * Map.Entries themselves permit modification of the backing Map
     * via their setValue operation.  This class is subtle: there are
     * many possible attacks that must be thwarted.
     *
     * @serial include
     */
    static class UnmodifiableEntrySet<K,V>
            extends UnmodifiableSet<Entry<K,V>> {
        private static final long serialVersionUID = 7854390611657943733L;

        @SuppressWarnings({"unchecked", "rawtypes"})
        UnmodifiableEntrySet(Set<? extends Map.Entry<? extends K, ? extends V>> s) {
            // Need to cast to raw in order to work around a limitation in the type system
            super((Set)s);
        }

        static <K, V> Consumer<Entry<K, V>> entryConsumer(Consumer<? super Entry<K, V>> action) {
            return e -> action.accept(new UnmodifiableKeySetMap.UnmodifiableEntrySet.UnmodifiableEntry<>(e));
        }

        public void forEach(Consumer<? super Entry<K, V>> action) {
            Objects.requireNonNull(action);
            c.forEach(entryConsumer(action));
        }

        static final class UnmodifiableEntrySetSpliterator<K, V>
                implements Spliterator<Entry<K,V>> {
            final Spliterator<Map.Entry<K, V>> s;

            UnmodifiableEntrySetSpliterator(Spliterator<Entry<K, V>> s) {
                this.s = s;
            }

            @Override
            public boolean tryAdvance(Consumer<? super Entry<K, V>> action) {
                Objects.requireNonNull(action);
                return s.tryAdvance(entryConsumer(action));
            }

            @Override
            public void forEachRemaining(Consumer<? super Entry<K, V>> action) {
                Objects.requireNonNull(action);
                s.forEachRemaining(entryConsumer(action));
            }

            @Override
            public Spliterator<Entry<K, V>> trySplit() {
                Spliterator<Entry<K, V>> split = s.trySplit();
                return split == null
                        ? null
                        : new UnmodifiableKeySetMap.UnmodifiableEntrySet.UnmodifiableEntrySetSpliterator<>(split);
            }

            @Override
            public long estimateSize() {
                return s.estimateSize();
            }

            @Override
            public long getExactSizeIfKnown() {
                return s.getExactSizeIfKnown();
            }

            @Override
            public int characteristics() {
                return s.characteristics();
            }

            @Override
            public boolean hasCharacteristics(int characteristics) {
                return s.hasCharacteristics(characteristics);
            }

            @Override
            public Comparator<? super Entry<K, V>> getComparator() {
                return s.getComparator();
            }
        }

        @SuppressWarnings("unchecked")
        public Spliterator<Entry<K,V>> spliterator() {
            return new UnmodifiableKeySetMap.UnmodifiableEntrySet.UnmodifiableEntrySetSpliterator<>(
                    (Spliterator<Map.Entry<K, V>>) c.spliterator());
        }

        @Override
        public Stream<Entry<K,V>> stream() {
            return StreamSupport.stream(spliterator(), false);
        }

        @Override
        public Stream<Entry<K,V>> parallelStream() {
            return StreamSupport.stream(spliterator(), true);
        }

        public Iterator<Map.Entry<K,V>> iterator() {
            return new Iterator<Map.Entry<K,V>>() {
                private final Iterator<? extends Map.Entry<? extends K, ? extends V>> i = c.iterator();

                public boolean hasNext() {
                    return i.hasNext();
                }
                public Map.Entry<K,V> next() {
                    return new UnmodifiableKeySetMap.UnmodifiableEntrySet.UnmodifiableEntry<>(i.next());
                }
                public void remove() {
                    throw new UnsupportedOperationException();
                }
            };
        }

        @SuppressWarnings("unchecked")
        public Object[] toArray() {
            Object[] a = c.toArray();
            for (int i=0; i<a.length; i++)
                a[i] = new UnmodifiableKeySetMap.UnmodifiableEntrySet.UnmodifiableEntry<>((Map.Entry<? extends K, ? extends V>)a[i]);
            return a;
        }

        @SuppressWarnings("unchecked")
        public <T> T[] toArray(T[] a) {
            // We don't pass a to c.toArray, to avoid window of
            // vulnerability wherein an unscrupulous multithreaded client
            // could get his hands on raw (unwrapped) Entries from c.
            Object[] arr = c.toArray(a.length==0 ? a : Arrays.copyOf(a, 0));

            for (int i=0; i<arr.length; i++)
                arr[i] = new UnmodifiableKeySetMap.UnmodifiableEntrySet.UnmodifiableEntry<>((Map.Entry<? extends K, ? extends V>)arr[i]);

            if (arr.length > a.length)
                return (T[])arr;

            System.arraycopy(arr, 0, a, 0, arr.length);
            if (a.length > arr.length)
                a[arr.length] = null;
            return a;
        }

        /**
         * This method is overridden to protect the backing set against
         * an object with a nefarious equals function that senses
         * that the equality-candidate is Map.Entry and calls its
         * setValue method.
         */
        public boolean contains(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            return c.contains(
                    new UnmodifiableKeySetMap.UnmodifiableEntrySet.UnmodifiableEntry<>((Map.Entry<?,?>) o));
        }

        /**
         * The next two methods are overridden to protect against
         * an unscrupulous List whose contains(Object o) method senses
         * when o is a Map.Entry, and calls o.setValue.
         */
        public boolean containsAll(Collection<?> coll) {
            for (Object e : coll) {
                if (!contains(e)) // Invokes safe contains() above
                    return false;
            }
            return true;
        }
        public boolean equals(Object o) {
            if (o == this)
                return true;

            if (!(o instanceof Set))
                return false;
            Set<?> s = (Set<?>) o;
            if (s.size() != c.size())
                return false;
            return containsAll(s); // Invokes safe containsAll() above
        }

        /**
         * This "wrapper class" serves two purposes: it prevents
         * the client from modifying the backing Map, by short-circuiting
         * the setValue method, and it protects the backing Map against
         * an ill-behaved Map.Entry that attempts to modify another
         * Map Entry when asked to perform an equality check.
         */
        private static class UnmodifiableEntry<K,V> implements Map.Entry<K,V> {
            private Map.Entry<? extends K, ? extends V> e;

            UnmodifiableEntry(Map.Entry<? extends K, ? extends V> e)
            {this.e = Objects.requireNonNull(e);}

            public K getKey()        {return e.getKey();}
            public V getValue()      {return e.getValue();}
            public V setValue(V value) {
                throw new UnsupportedOperationException();
            }
            public int hashCode()    {return e.hashCode();}
            public boolean equals(Object o) {
                if (this == o)
                    return true;
                if (!(o instanceof Map.Entry))
                    return false;
                Map.Entry<?,?> t = (Map.Entry<?,?>)o;
                return eq(e.getKey(),   t.getKey()) &&
                        eq(e.getValue(), t.getValue());
            }
            public String toString() {return e.toString();}
        }
    }

    /**
     * Returns true if the specified arguments are equal, or both null.
     *
     * NB: Do not replace with Object.equals until JDK-8015417 is resolved.
     */
    static boolean eq(Object o1, Object o2) {
        return o1==null ? o2==null : o1.equals(o2);
    }
}

我知道这并不是严格意义上的问题,但我认为这种方法值得考虑。

使用 Google Guava 的 ImmutableMap 并使您的值类型可变 - 如有必要,使用包装器类型。

在继续之前,了解这一点很重要:映射级别的不变性 意味着您不能重新分配对象。没有什么可以阻止您对地图中已有的对象调用增变器方法。有了这种洞察力,上述方法可能是显而易见的,但让我通过一个例子来解释。

对我来说,值类型是 AmtomicInteger - 已经完全按照我需要的方式完全可变,所以我不需要改变任何东西。但是,一般来说,假设您有一个类型 T,并且您想要该类型的部分可变映射。嗯,你几乎可以通过一个可变映射来做到这一点,它的值是 class 的包装类型。这是一个非常基本的包装器类型,您可能希望通过一些封装来增强它:

public class Wrapper<T>{
  public T value;
  Wrapper(T t){value = t;}
}

然后您只需以正常方式创建一个常规 ImmutableMap,除了用 Wrapper 的对象而不是 T 的对象填充它。使用值时,您需要将它们从包装器中拆箱。

同样,我知道这不是要求的内容,对于某些用例来说,从包装器中拆箱可能太痛苦了,但我想这在很多情况下都是可行的。当然对我来说,它帮助我意识到我已经有了一个可变类型,它基本上是一个 int 的包装器,所以 ImmutableMap 很好。