次の理由で、大文字と小文字を区別しない文字列をHashMapキーとして使用したいと思います。
<key, value>
を見つけることができるはずです。トラフィックから受け取ったケースを無視します。私はこのアプローチに従いました
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の新しいオブジェクトを作成しています。そのため、パフォーマンスが低下する可能性があります。
この問題を解決する他の方法はありますか?
Map<String, String> nodeMap =
new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
本当に必要なのはそれだけです。
ここでの回答 で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());
}
}
または
1つのアプローチは、Apache Commons AbstractHashedMap
クラスのカスタムサブクラスを作成し、hash
およびisEqualKeys
メソッドをオーバーライドして、大文字と小文字を区別しないハッシュとキーの比較を実行することです。 (注-私はこれを自分で試したことはありません...)
これにより、マップの検索または更新が必要になるたびに新しいオブジェクトを作成するオーバーヘッドが回避されます。そして、一般的なMap
操作はO(1) ...通常のHashMap
と同じようにすべきです。
また、実装の選択を受け入れる準備ができている場合、Apache Commons CaseInsensitiveMap
はAbstractHashedMap
のカスタマイズ/特化の作業を行います。
ただし、O(logN) get
およびput
操作が許容される場合、大文字と小文字を区別しない文字列コンパレーターを使用するTreeMap
はオプションです。例えば String.CASE_INSENSITIVE_ORDER
を使用します。
また、put
またはget
を実行するたびに新しい一時Stringオブジェクトを作成してもかまわない場合は、Vishalの答えは問題ありません。 (ただし、キーの元のケースを保存しないとしたら...)
HashMap
をサブクラス化し、put
およびget
(およびおそらく他のキー指向のメソッド)のキーを小文字にするバージョンを作成します。
または、HashMap
を新しいクラスに合成し、すべてをマップに委任しますが、キーを変換します。
元のキーを保持する必要がある場合は、デュアルマップを維持するか、元のキーと値を保存できます。
2つの選択肢が思い浮かびます。
s.toUpperCase().hashCode();
をMap
のキーとして直接使用できます。Comparator
でTreeMap<String>
を使用できます。それ以外の場合、新しい種類の文字列を定義するのではなく、ソリューションを希望する場合は、必要な大文字と小文字を区別しない機能を備えた新しいMapを実装します。
Eclipse Collections から HashingStrategy ベースのMap
を使用できます
HashingStrategy<String> hashingStrategy =
HashingStrategies.fromFunction(String::toUpperCase);
MutableMap<String, String> node = HashingStrategyMaps.mutable.of(hashingStrategy);
注:私はEclipse Collectionsの寄稿者です。
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()を使用できます。
堅牢なCaseInsensitiveMap/CaseInsensitiveSetの実装については、Java-util( https://github.com/jdereg/Java-util )を確認してください。
これらのマップは標準のO(1)ルックアップ時間で実行され、追加されたアイテムの大文字小文字を保持し、putAll()、retainAll()、removeAll()などのすべてのMap APIをサポートし、異種アイテムの配置を許可しますキーセットに。
さらに、.keySet()および.entrySet()によって返されるJava.util.Setは、大文字と小文字を区別しません(多くの実装はそうではありません)。最後に、反復中にキー/エントリセットからキーをフェッチすると、CaseInsensitiveStringラッパークラスではなく、文字列が返されます。
他の答えに基づいて、基本的に2つのアプローチがあります:HashMap
のサブクラス化またはString
のラップ。最初のものはもう少し作業が必要です。実際、正しく実行したい場合は、ほぼすべてのメソッド(containsKey, entrySet, get, put, putAll and remove
)をオーバーライドする必要があります。
とにかく、問題があります。将来の問題を回避する場合は、Locale
ケース操作でString
を指定する必要があります。したがって、新しいメソッド(get(String, Locale)
、...)を作成します。すべてが文字列のラッピングをより簡単かつ明確に:
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
}
そしてまあ、パフォーマンスに関するあなたの心配について:早すぎる最適化はすべての悪の根源です :)
このため、イベントごとにCaseInsensitiveStringの新しいオブジェクトを作成しています。そのため、パフォーマンスが低下する可能性があります。
ルックアップの前にラッパーを作成するか、キーを小文字に変換すると、両方とも新しいオブジェクトが作成されます。これを回避する唯一の方法は、独自のJava.util.Map実装を作成することです。それほど難しくはありません。IMOには価値があります。次のハッシュ関数が非常にうまく機能することがわかりました。最大数百のキーです。
static int ciHashCode(String string)
{
// length and the low 5 bits of hashCode() are case insensitive
return (string.hashCode() & 0x1f)*33 + string.length();
}
これは、最近のプロジェクト用に実装したHashMapsのアダプターです。 @SandyRと同様の方法で機能しますが、変換ロジックをカプセル化するため、文字列を手動でラッパーオブジェクトに変換することはありません。
Java 8の機能を使用しましたが、いくつかの変更を加えることで、以前のバージョンに適合させることができます。新しいJava 8ストリーム関数を除き、ほとんどの一般的なシナリオでテストしました。
基本的に、HashMapをラップし、文字列をラッパーオブジェクトに変換したり、ラッパーオブジェクトから変換したりしながら、すべての機能をHashMapに向けます。しかし、KeySetとEntrySetも適応させる必要がありました。これらは、いくつかの機能をマップ自体に転送するためです。そこで、元のkeySet()とentrySet()を実際にラップするキーとエントリの2つの新しいセットを返します。
注: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);
}
}
}