不区分大小写的字符串作为HashMap键


178

由于以下原因,我想使用不区分大小写的字符串作为HashMap键。

  • 在初始化期间,我的程序使用用户定义的String创建HashMap。
  • 在处理事件(在我的情况下为网络流量)时,我可能会在其他情况下收到String,但是我应该能够<key, value>忽略HashMap中的来自流量的情况而从HashMap 定位。

我遵循了这种方法

CaseInsensitiveString.java

    public final class CaseInsensitiveString {
            private String s;

            public CaseInsensitiveString(String s) {
                            if (s == null)
                            throw new NullPointerException();
                            this.s = s;
            }

            public boolean equals(Object o) {
                            return o instanceof CaseInsensitiveString &&
                            ((CaseInsensitiveString)o).s.equalsIgnoreCase(s);
            }

            private volatile int hashCode = 0;

            public int hashCode() {
                            if (hashCode == 0)
                            hashCode = s.toUpperCase().hashCode();

                            return hashCode;
            }

            public String toString() {
                            return s;
            }
    }

LookupCode.java

    node = nodeMap.get(new CaseInsensitiveString(stringFromEvent.toString()));

因此,我为每个事件创建一个CaseInsensitiveString新对象。因此,它可能会影响性能。

还有其他解决方法吗?


3
[有没有好的办法有一个地图<字符串,?> get和put忽略大小写?] [1] [1]:stackoverflow.com/questions/212562/...
博格兰瑟姆

我对以下问题进行了评论,但它们低于阈值,因此人们可能看不到它们。提防子类化HashMap。JDK8更改了实现,您现在(至少)需要覆盖putAll才能使这些建议生效。
史蒂夫·N

这应该工作正常。您可以使用flyweight摆脱新的对象实例。
topkara

Answers:


331
Map<String, String> nodeMap = 
    new TreeMap<>(String.CASE_INSENSITIVE_ORDER);

这就是您真正需要的。


6
到目前为止,这是最简单的方法,并且在遍历键时仍保留键的大小写。
2014年

这很漂亮!这是在ColdFusion中创建有序结构(保留使用点表示法的功能)的最后一个难题。您可以使用var struct = createObject('java','java.util.TreeMap')。init(createObject('java','java.lang.String' ).CASE_INSENSITIVE_ORDER); 丑陋,但可行;)
Eric Fuller

public static <K extends String, V> Map<K, V> caseInsensitiveMap() { return new TreeMap<K, V>(String.CASE_INSENSITIVE_ORDER); }
2015年

5
不需要,<K extends String>因为String是最终的决定: public static <V> Map<String, V> caseInsensitiveMap() { return new TreeMap<String, V>(String.CASE_INSENSITIVE_ORDER); }
Roel Spilker

19
请记住,TreeMap对于基本操作而言不是固定时间。对于大多数应用程序来说,这不是一个问题,但是请牢记。来自JavaDoc:“此实现为containsKey,get,put和remove操作提供了保证的log(n)时间成本。算法是对Cormen,Leiserson和Rivest的Introduction to Algorithms中算法的改编。”
James Schek

57

正如GuidoGarcía在这里的回答中所建议的:

import java.util.HashMap;

public class CaseInsensitiveMap extends HashMap<String, String> {

    @Override
    public String put(String key, String value) {
       return super.put(key.toLowerCase(), value);
    }

    // not @Override because that would require the key parameter to be of type Object
    public String get(String key) {
       return super.get(key.toLowerCase());
    }
}

要么

https://commons.apache.org/proper/commons-collections/apidocs/org/apache/commons/collections4/map/CaseInsensitiveMap.html


28
包含,putAll等如何?
assylias 2012年

14
这在某些语言(例如土耳其语)中不起作用。Google“土耳其测试”
雨果

5
@assylias:真,containsKey()并且remove()应该被覆盖在相同的方式get()。该HashMap.putAll()实现使用put(),因此这应该不成问题-只要HashMap实现保持不变即可。;)get()方法签名也接受一个Object参数,而不是String。该代码也不会测试空键:super.get(key == null ? null : key.toString().toLowercase());
sfera

请注意,如果您需要copy-constructor HashMap(<? extends String, ? extends String> anotherMap),则不应调用同一构造函数的超级实现,因为该操作不能保证您的键是小写的。您可以使用: super(anotherMap.size()); putAll(anotherMap);代替。
sfera

如果您希望地图的值不是字符串怎么办?(即CaseInsensitiveMap<String, Integer>
亚当·帕金

16

一种方法是创建Apache Commons AbstractHashedMap类的自定义子类,该类重写hashisEqualKeys方法以执行不区分大小写的哈希和键比较。(注意-我从未尝试过此方法...)

这样避免了每次需要进行地图查找或更新时创建新对象的开销。和常见的Map操作应O(1)...就像一个普通的HashMap

并且,如果您准备接受他们所做的实现选择,则Apache Commons CaseInsensitiveMapAbstractHashedMap为您进行定制/专业化工作。


但是,如果O(logN)getput操作是可接受的,TreeMap则可以选择不区分大小写的字符串比较器;例如使用String.CASE_INSENSITIVE_ORDER

而且,如果您不介意每次执行put或时都创建一个新的临时String对象get,那么Vishal的回答就很好。(不过,我注意到,如果这样做,您将不会保留键的原始大小写……)


6

子类化HashMap并创建一个版本,该版本在putget(可能还有其他面向键的方法)的。

或复合一个 HashMap到新类中,并将所有内容委托给地图,但转换键。

如果需要保留原始密钥,则可以维护双重映射,也可以将原始密钥与值一起存储。


您的意思是在查找期间执行String.toLowerCase()吗?
rs

@ user710178不只是查找过程中,但存储以及期间。
Dave Newton

@ user710178哦,对了,正如其他答案指出的那样,如果您不介意其他依赖项,则该问题已经存在。
戴夫·牛顿

@StephenC如果它满足您的需求,请确保;OP指定了一个HashMap,所以这就是我的意思:)哦,你的意思是下议院;我懂了。我想,只要您不需要它就可以泛滥(或者他们现在终于有了仿制药了吗?)
Dave Newton

1
对于JDK 8及更高版本,您还需要(至少)重写putAll,因为实现已更改。
史蒂夫·N

4

我想到两个选择:

  1. 您可以直接将s.toUpperCase().hashCode();用作键Map
  2. 您可以将a TreeMap<String>Comparator忽略大小写的自定义一起使用。

否则,如果您喜欢自己的解决方案,则与其定义一种新的String,不如使用所需的区分大小写功能实现一个新的Map。


3

“包装”字符串以记住hashCode会更好。在普通的String类中,hashCode()第一次是O(N),然后是O(1),因为它被保留以备将来使用。

public class HashWrap {
    private final String value;
    private final int hash;

    public String get() {
        return value;
    }

    public HashWrap(String value) {
        this.value = value;
        String lc = value.toLowerCase();
        this.hash = lc.hashCode();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o instanceof HashWrap) {
            HashWrap that = (HashWrap) o;
            return value.equalsIgnoreCase(that.value);
        } else {
            return false;
        }
    }

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

    //might want to implement compare too if you want to use with SortedMaps/Sets.
}

这将允许您使用Java中Hashtable的任何实现,并具有O(1)hasCode()。



2

根据其他答案,基本上有两种方法:子类化HashMap或包装String。第一个需要更多的工作。实际上,如果要正确执行操作,则必须重写几乎所有方法(containsKey, entrySet, get, put, putAll and remove)。

无论如何,这是有问题的。如果你想避免将来出现问题,你必须指定LocaleString情况下操作。因此,您将创建新的方法(get(String, Locale),...)。一切都更加容易和清晰地包装了String:

public final class CaseInsensitiveString {

    private final String s;

    public CaseInsensitiveString(String s, Locale locale) {
        this.s = s.toUpperCase(locale);
    }

    // equals, hashCode & toString, no need for memoizing hashCode
}

好吧,关于您对性能的担心:过早的优化是万恶之源 :)


2
“而且,关于您对性能的担心:过早的优化是万恶之源:)”-相反,将其作为始终编写低效率代码的借口是邪恶的。
Gordon

1
实际上,@ Gordon都一样糟糕,具体取决于上下文。标签“邪恶”是黑白思维的标志,例如“最佳实践”和许多IT人员倾向于使用的其他各种无用的短语。最好完全避免它。
Stephen C

我发现告诉人们他们没有遵循“最佳实践”的习惯往往比告诉他们有不良实践的习惯要少。
戈登

0

这是我为最近的项目实现的HashMaps适配器。其工作方式与@SandyR相似,但封装了转换逻辑,因此您不必手动将字符串转换为包装对象。

我使用了Java 8功能,但进行了一些更改,就可以使其适应以前的版本。除了新的Java 8流功能之外,我已经针对大多数常见场景进行了测试。

基本上,它包装HashMap,在将字符串转换到包装对象或从包装对象转换字符串时,将所有函数定向到它。但是我还必须改编KeySet和EntrySet,因为它们会将一些函数转发到地图本身。因此,我为键和条目返回了两个新的Set,它们实际上包装了原始的keySet()和entrySet()。

一个注意事项:Java 8更改了putAll方法的实现,我找不到一种简单的方法来覆盖它。因此,当前的实现可能会降低性能,尤其是如果您对大型数据集使用putAll()。

如果您发现错误或有改进代码的建议,请告诉我。

包webbit.collections;

import java.util.*;
import java.util.function.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;


public class CaseInsensitiveMapAdapter<T> implements Map<String,T>
{
    private Map<CaseInsensitiveMapKey,T> map;
    private KeySet keySet;
    private EntrySet entrySet;


    public CaseInsensitiveMapAdapter()
    {
    }

    public CaseInsensitiveMapAdapter(Map<String, T> map)
    {
        this.map = getMapImplementation();
        this.putAll(map);
    }

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

    @Override
    public boolean isEmpty()
    {
        return getMap().isEmpty();
    }

    @Override
    public boolean containsKey(Object key)
    {
        return getMap().containsKey(lookupKey(key));
    }

    @Override
    public boolean containsValue(Object value)
    {
        return getMap().containsValue(value);
    }

    @Override
    public T get(Object key)
    {
        return getMap().get(lookupKey(key));
    }

    @Override
    public T put(String key, T value)
    {
        return getMap().put(lookupKey(key), value);
    }

    @Override
    public T remove(Object key)
    {
        return getMap().remove(lookupKey(key));
    }

    /***
     * I completely ignore Java 8 implementation and put one by one.This will be slower.
     */
    @Override
    public void putAll(Map<? extends String, ? extends T> m)
    {
        for (String key : m.keySet()) {
            getMap().put(lookupKey(key),m.get(key));
        }
    }

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

    @Override
    public Set<String> keySet()
    {
        if (keySet == null)
            keySet = new KeySet(getMap().keySet());
        return keySet;
    }

    @Override
    public Collection<T> values()
    {
        return getMap().values();
    }

    @Override
    public Set<Entry<String, T>> entrySet()
    {
        if (entrySet == null)
            entrySet = new EntrySet(getMap().entrySet());
        return entrySet;
    }

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

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

    @Override
    public T getOrDefault(Object key, T defaultValue)
    {
        return getMap().getOrDefault(lookupKey(key), defaultValue);
    }

    @Override
    public void forEach(final BiConsumer<? super String, ? super T> action)
    {
        getMap().forEach(new BiConsumer<CaseInsensitiveMapKey, T>()
        {
            @Override
            public void accept(CaseInsensitiveMapKey lookupKey, T t)
            {
                action.accept(lookupKey.key,t);
            }
        });
    }

    @Override
    public void replaceAll(final BiFunction<? super String, ? super T, ? extends T> function)
    {
        getMap().replaceAll(new BiFunction<CaseInsensitiveMapKey, T, T>()
        {
            @Override
            public T apply(CaseInsensitiveMapKey lookupKey, T t)
            {
                return function.apply(lookupKey.key,t);
            }
        });
    }

    @Override
    public T putIfAbsent(String key, T value)
    {
        return getMap().putIfAbsent(lookupKey(key), value);
    }

    @Override
    public boolean remove(Object key, Object value)
    {
        return getMap().remove(lookupKey(key), value);
    }

    @Override
    public boolean replace(String key, T oldValue, T newValue)
    {
        return getMap().replace(lookupKey(key), oldValue, newValue);
    }

    @Override
    public T replace(String key, T value)
    {
        return getMap().replace(lookupKey(key), value);
    }

    @Override
    public T computeIfAbsent(String key, final Function<? super String, ? extends T> mappingFunction)
    {
        return getMap().computeIfAbsent(lookupKey(key), new Function<CaseInsensitiveMapKey, T>()
        {
            @Override
            public T apply(CaseInsensitiveMapKey lookupKey)
            {
                return mappingFunction.apply(lookupKey.key);
            }
        });
    }

    @Override
    public T computeIfPresent(String key, final BiFunction<? super String, ? super T, ? extends T> remappingFunction)
    {
        return getMap().computeIfPresent(lookupKey(key), new BiFunction<CaseInsensitiveMapKey, T, T>()
        {
            @Override
            public T apply(CaseInsensitiveMapKey lookupKey, T t)
            {
                return remappingFunction.apply(lookupKey.key, t);
            }
        });
    }

    @Override
    public T compute(String key, final BiFunction<? super String, ? super T, ? extends T> remappingFunction)
    {
        return getMap().compute(lookupKey(key), new BiFunction<CaseInsensitiveMapKey, T, T>()
        {
            @Override
            public T apply(CaseInsensitiveMapKey lookupKey, T t)
            {
                return remappingFunction.apply(lookupKey.key,t);
            }
        });
    }

    @Override
    public T merge(String key, T value, BiFunction<? super T, ? super T, ? extends T> remappingFunction)
    {
        return getMap().merge(lookupKey(key), value, remappingFunction);
    }

    protected  Map<CaseInsensitiveMapKey,T> getMapImplementation() {
        return new HashMap<>();
    }

    private Map<CaseInsensitiveMapKey,T> getMap() {
        if (map == null)
            map = getMapImplementation();
        return map;
    }

    private CaseInsensitiveMapKey lookupKey(Object key)
    {
        return new CaseInsensitiveMapKey((String)key);
    }

    public class CaseInsensitiveMapKey {
        private String key;
        private String lookupKey;

        public CaseInsensitiveMapKey(String key)
        {
            this.key = key;
            this.lookupKey = key.toUpperCase();
        }

        @Override
        public boolean equals(Object o)
        {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            CaseInsensitiveMapKey that = (CaseInsensitiveMapKey) o;

            return lookupKey.equals(that.lookupKey);

        }

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

    private class KeySet implements Set<String> {

        private Set<CaseInsensitiveMapKey> wrapped;

        public KeySet(Set<CaseInsensitiveMapKey> wrapped)
        {
            this.wrapped = wrapped;
        }


        private List<String> keyList() {
            return stream().collect(Collectors.toList());
        }

        private Collection<CaseInsensitiveMapKey> mapCollection(Collection<?> c) {
            return c.stream().map(it -> lookupKey(it)).collect(Collectors.toList());
        }

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

        @Override
        public boolean isEmpty()
        {
            return wrapped.isEmpty();
        }

        @Override
        public boolean contains(Object o)
        {
            return wrapped.contains(lookupKey(o));
        }

        @Override
        public Iterator<String> iterator()
        {
            return keyList().iterator();
        }

        @Override
        public Object[] toArray()
        {
            return keyList().toArray();
        }

        @Override
        public <T> T[] toArray(T[] a)
        {
            return keyList().toArray(a);
        }

        @Override
        public boolean add(String s)
        {
            return wrapped.add(lookupKey(s));
        }

        @Override
        public boolean remove(Object o)
        {
            return wrapped.remove(lookupKey(o));
        }

        @Override
        public boolean containsAll(Collection<?> c)
        {
            return keyList().containsAll(c);
        }

        @Override
        public boolean addAll(Collection<? extends String> c)
        {
            return wrapped.addAll(mapCollection(c));
        }

        @Override
        public boolean retainAll(Collection<?> c)
        {
            return wrapped.retainAll(mapCollection(c));
        }

        @Override
        public boolean removeAll(Collection<?> c)
        {
            return wrapped.removeAll(mapCollection(c));
        }

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

        @Override
        public boolean equals(Object o)
        {
            return wrapped.equals(lookupKey(o));
        }

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

        @Override
        public Spliterator<String> spliterator()
        {
            return keyList().spliterator();
        }

        @Override
        public boolean removeIf(Predicate<? super String> filter)
        {
            return wrapped.removeIf(new Predicate<CaseInsensitiveMapKey>()
            {
                @Override
                public boolean test(CaseInsensitiveMapKey lookupKey)
                {
                    return filter.test(lookupKey.key);
                }
            });
        }

        @Override
        public Stream<String> stream()
        {
            return wrapped.stream().map(it -> it.key);
        }

        @Override
        public Stream<String> parallelStream()
        {
            return wrapped.stream().map(it -> it.key).parallel();
        }

        @Override
        public void forEach(Consumer<? super String> action)
        {
            wrapped.forEach(new Consumer<CaseInsensitiveMapKey>()
            {
                @Override
                public void accept(CaseInsensitiveMapKey lookupKey)
                {
                    action.accept(lookupKey.key);
                }
            });
        }
    }

    private class EntrySet implements Set<Map.Entry<String,T>> {

        private Set<Entry<CaseInsensitiveMapKey,T>> wrapped;

        public EntrySet(Set<Entry<CaseInsensitiveMapKey,T>> wrapped)
        {
            this.wrapped = wrapped;
        }


        private List<Map.Entry<String,T>> keyList() {
            return stream().collect(Collectors.toList());
        }

        private Collection<Entry<CaseInsensitiveMapKey,T>> mapCollection(Collection<?> c) {
            return c.stream().map(it -> new CaseInsensitiveEntryAdapter((Entry<String,T>)it)).collect(Collectors.toList());
        }

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

        @Override
        public boolean isEmpty()
        {
            return wrapped.isEmpty();
        }

        @Override
        public boolean contains(Object o)
        {
            return wrapped.contains(lookupKey(o));
        }

        @Override
        public Iterator<Map.Entry<String,T>> iterator()
        {
            return keyList().iterator();
        }

        @Override
        public Object[] toArray()
        {
            return keyList().toArray();
        }

        @Override
        public <T> T[] toArray(T[] a)
        {
            return keyList().toArray(a);
        }

        @Override
        public boolean add(Entry<String,T> s)
        {
            return wrapped.add(null );
        }

        @Override
        public boolean remove(Object o)
        {
            return wrapped.remove(lookupKey(o));
        }

        @Override
        public boolean containsAll(Collection<?> c)
        {
            return keyList().containsAll(c);
        }

        @Override
        public boolean addAll(Collection<? extends Entry<String,T>> c)
        {
            return wrapped.addAll(mapCollection(c));
        }

        @Override
        public boolean retainAll(Collection<?> c)
        {
            return wrapped.retainAll(mapCollection(c));
        }

        @Override
        public boolean removeAll(Collection<?> c)
        {
            return wrapped.removeAll(mapCollection(c));
        }

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

        @Override
        public boolean equals(Object o)
        {
            return wrapped.equals(lookupKey(o));
        }

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

        @Override
        public Spliterator<Entry<String,T>> spliterator()
        {
            return keyList().spliterator();
        }

        @Override
        public boolean removeIf(Predicate<? super Entry<String, T>> filter)
        {
            return wrapped.removeIf(new Predicate<Entry<CaseInsensitiveMapKey, T>>()
            {
                @Override
                public boolean test(Entry<CaseInsensitiveMapKey, T> entry)
                {
                    return filter.test(new FromCaseInsensitiveEntryAdapter(entry));
                }
            });
        }

        @Override
        public Stream<Entry<String,T>> stream()
        {
            return wrapped.stream().map(it -> new Entry<String, T>()
            {
                @Override
                public String getKey()
                {
                    return it.getKey().key;
                }

                @Override
                public T getValue()
                {
                    return it.getValue();
                }

                @Override
                public T setValue(T value)
                {
                    return it.setValue(value);
                }
            });
        }

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

        @Override
        public void forEach(Consumer<? super Entry<String, T>> action)
        {
            wrapped.forEach(new Consumer<Entry<CaseInsensitiveMapKey, T>>()
            {
                @Override
                public void accept(Entry<CaseInsensitiveMapKey, T> entry)
                {
                    action.accept(new FromCaseInsensitiveEntryAdapter(entry));
                }
            });
        }
    }

    private class EntryAdapter implements Map.Entry<String,T> {
        private Entry<String,T> wrapped;

        public EntryAdapter(Entry<String, T> wrapped)
        {
            this.wrapped = wrapped;
        }

        @Override
        public String getKey()
        {
            return wrapped.getKey();
        }

        @Override
        public T getValue()
        {
            return wrapped.getValue();
        }

        @Override
        public T setValue(T value)
        {
            return wrapped.setValue(value);
        }

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

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


    }

    private class CaseInsensitiveEntryAdapter implements Map.Entry<CaseInsensitiveMapKey,T> {

        private Entry<String,T> wrapped;

        public CaseInsensitiveEntryAdapter(Entry<String, T> wrapped)
        {
            this.wrapped = wrapped;
        }

        @Override
        public CaseInsensitiveMapKey getKey()
        {
            return lookupKey(wrapped.getKey());
        }

        @Override
        public T getValue()
        {
            return wrapped.getValue();
        }

        @Override
        public T setValue(T value)
        {
            return wrapped.setValue(value);
        }
    }

    private class FromCaseInsensitiveEntryAdapter implements Map.Entry<String,T> {

        private Entry<CaseInsensitiveMapKey,T> wrapped;

        public FromCaseInsensitiveEntryAdapter(Entry<CaseInsensitiveMapKey, T> wrapped)
        {
            this.wrapped = wrapped;
        }

        @Override
        public String getKey()
        {
            return wrapped.getKey().key;
        }

        @Override
        public T getValue()
        {
            return wrapped.getValue();
        }

        @Override
        public T setValue(T value)
        {
            return wrapped.setValue(value);
        }
    }


}

0

因此,我为每个事件创建一个CaseInsensitiveString新对象。因此,它可能会影响性能。

在查找之前创建包装器或将键转换为小写形式都会创建新对象。编写自己的java.util.Map实现是避免这种情况的唯一方法。这并不难,海事组织值得。我发现以下哈希函数可以很好地工作,最多可以使用数百个键。

static int ciHashCode(String string)
{
    // length and the low 5 bits of hashCode() are case insensitive
    return (string.hashCode() & 0x1f)*33 + string.length();
}

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.