不区分大小写的string作为HashMap键

我想使用不区分大小写的string作为HashMap键的原因如下。

  • 在初始化过程中,我的程序使用用户定义的string创buildHashMap
  • 在处理一个事件(在我的情况下是networkingstream量)的时候,我可能收到了一个不同的情况下的string,但是我应该能够从HashMap中find<key, value> ,忽略了从stream量收到的情况。

我遵循这个方法

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

因此,我为每个事件创build一个CaseInsensitiveString的新对象。 所以,这可能会打击业绩。

有没有其他办法来解决这个问题?

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

这真是你所需要的。

正如GuidoGarcía在他们的回答中所build议的那样:

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

要么

http://commons.apache.org/proper/commons-collections/javadocs/api-release/org/apache/commons/collections4/map/CaseInsensitiveMap.html

一种方法是创buildApache Commons AbstractHashedMap类的自定义子类,覆盖hashisEqualKeys方法以执行不区分大小写的散列和键比较。 (注 – 我从来没有尝试过这个…)

这可以避免每次需要执行地图查找或更新时创build新对象的开销。 而普通的Map操作应该像O(1)一样…就像一个普通的HashMap

如果您准备接受所做的实现select,Apache Commons CaseInsensitiveMap将为您定制/专门化AbstractHashedMap


但是,如果O(logN) getput操作是可以接受的,则带有不区分大小写的string比较器的TreeMap是一个选项; 例如使用String.CASE_INSENSITIVE_ORDER

如果你不介意在每次做putget时候创build一个新的临时String对象,那么Vishal的答案就好了。 (不过,我注意到,如果你这样做的话,你将不会保留原来的钥匙)

子类HashMap并创build一个低版本的关键putget (可能是其他面向关键的方法)。

或者将一个HashMap复合到新类中,并将所有内容委托给地图,但翻译这些键。

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

我有两个select:

1)你可以直接使用s.toUpperCase().hashCode(); 作为Map的关键。 2)你可以使用一个TreeMap和一个自定义的Comparator来忽略大小写。

否则,如果你喜欢你的解决scheme,而不是定义一种新的string,我宁愿实现一个新的地图与所需的不区分大小写function。

为了记住hashCode,“包装”string不是更好吗? 在正常的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()。

你可以使用Eclipse集合中的基于HashingStrategy的Map

 HashingStrategy<String> hashingStrategy = HashingStrategies.fromFunction(String::toUpperCase); MutableMap<String, String> node = HashingStrategyMaps.mutable.of(hashingStrategy); 

注意:我是Eclipse Collections的贡献者。

基于其他的答案,基本上有两种方法:子类化HashMap或包装String 。 第一个需要更多的工作。 事实上,如果你想正确地做,你必须覆盖几乎所有的方法( containsKey, entrySet, get, put, putAll and remove )。

无论如何,它有一个问题。 如果您想避免将来出现问题,则必须在String大小写操作中指定Locale 。 所以你会创build新的方法( 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 } 

还有关于你对性能的担忧: 不成熟的优化是所有邪恶的根源 🙂

对于强大的CaseInsensitiveMap / CaseInsensitiveSet实现,请查看java-util( https://github.com/jdereg/java-util )。

这些映射在标准的O(1)查找时间内执行,保留添加项目的情况,支持putAll(),retainAll(),removeAll()等所有Map API,并允许异构项放入密钥集。

此外,由.keySet()和.entrySet()返回的java.util.Set不区分大小写(许多实现不会)。 最后,如果您在迭代时从键/条目集中获取密钥,则会返回String,而不是CaseInsensitiveString包装类。

这是我为最近的项目实现的HashMaps的一个适配器。 以类似于@SandyR所做的方式工作,但是封装了转换逻辑,因此您不需要手动将string转换为包装器对象。

我使用Java 8的function,但有一些变化,你可以适应以前的版本。 我testing了它的最常见的情况,除了新的Java 8streamfunction。

基本上它包装一个HashMap,将所有的函数指向它,同时将string转换为/从一个包装对象。 但是我还必须修改KeySet和EntrySet,因为它们将一些函数转发给了地图本身。 所以我返回两个新的集合的键和条目实际上包装原始keySet()和entrySet()。

一个注意:Java 8已经改变了putAll方法的实现,我找不到一个简单的方法来覆盖。 因此,当前的实现可能会降低性能,尤其是对于大型数据集使用putAll()时。

请让我知道,如果你发现一个错误或有改善代码的build议。

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

因此,我为每个事件创build一个CaseInsensitiveString的新对象。 所以,这可能会打击业绩。

在查找之前创build包装或将键转换为小写都会创build新对象。 编写自己的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(); }