Строка без учета регистра в качестве ключа HashMap
Я хотел бы использовать строку без учета регистра в качестве ключа HashMap по следующим причинам.
- Во время инициализации моя программа создает HashMap с пользовательской строкой
- При обработке события (сетевой трафик в моем случае) я мог получить строку в другом случае, но я должен быть в состоянии найти
<key, value>
из 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 для каждого события. Таким образом, это может повлиять на производительность.
Есть ли другой способ решить эту проблему?
16 ответов
Map<String, String> nodeMap =
new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
Это действительно все, что вам нужно.
Как предполагает Гвидо Гарсия в своем ответе здесь:
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());
}
}
Или же
Одним из подходов является создание собственного подкласса Apache Commons AbstractHashedMap
класс, переопределяя hash
а также isEqualKeys
методы выполнения хэширования без учета регистра и сравнения ключей. (Примечание - я никогда не пробовал это сам...)
Это позволяет избежать затрат на создание новых объектов каждый раз, когда вам нужно выполнить поиск или обновление карты. И общее Map
операции должны O(1) ... так же, как обычный HashMap
,
И если вы готовы принять выбранный ими вариант реализации, Apache Commons CaseInsensitiveMap
выполняет работу по настройке / специализации AbstractHashedMap
для тебя.
Но если O(logN) get
а также put
операции приемлемы, а TreeMap
с нечувствительным к регистру символом сравнения строк; например, используя String.CASE_INSENSITIVE_ORDER
,
И если вы не против создания нового временного объекта String каждый раз, когда вы делаете put
или же get
тогда ответ Вишала просто в порядке. (Хотя я отмечаю, что вы бы не сохранили оригинальный регистр ключей, если бы сделали это...)
Подкласс HashMap
и создайте версию, в которой строчные клавиши put
а также get
(и, вероятно, другие ключевые методы).
Или составной HashMap
в новый класс и делегировать все на карту, но перевести ключи.
Если вам нужно сохранить исходный ключ, вы можете либо сохранить двойные карты, либо сохранить исходный ключ вместе со значением.
Мне на ум приходят два варианта:
- Вы можете использовать непосредственно
s.toUpperCase().hashCode();
в качестве ключаMap
, - Вы могли бы использовать
TreeMap<String>
с обычаемComparator
это игнорировать дело.
В противном случае, если вы предпочитаете свое решение, вместо определения нового типа 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.
}
Это позволит вам использовать любую реализацию Hashtable в Java и иметь O(1) hasCode().
Вы можете использовать HashingStrategy на основе Map
из коллекций Eclipse
HashingStrategy<String> hashingStrategy =
HashingStrategies.fromFunction(String::toUpperCase);
MutableMap<String, String> node = HashingStrategyMaps.mutable.of(hashingStrategy);
Примечание: я участвую в коллекциях Eclipse.
Основываясь на других ответах, существует два основных подхода: создание подклассов 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
}
И хорошо, о ваших заботах о производительности: преждевременная оптимизация - корень всего зла:)
Вы можете использовать объекты вместо строк:
Locale locale = ...;
Collator collator = Collator.getInstance(locale);
collator.setStrength(Collator.SECONDARY); // Case-insensitive.
collator.setDecomposition(Collator.FULL_DECOMPOSITION);
CollationKey collationKey = collator.getCollationKey(stringKey);
hashMap.put(collationKey, value);
hashMap.get(collationKey);
Использовать
Collator.PRIMARY
чтобы игнорировать разницу акцентов.
The
CollationKey
API не гарантирует, что
hashCode()
а также
equals()
реализованы, но на практике вы будете использовать
RuleBasedCollationKey
, который их реализует. Если вы параноик, вы можете использовать
TreeMap
вместо этого, который гарантированно будет работать за время O(log n) вместо O(1).
Я нахожу решения, которые требуют от вас смены ключа (например,
toLowerCase
) очень нежелательны и решения, которые требуют, также нежелательны.
С
TreeMap
изменяет временную сложность (по сравнению с другими
HashMap
s), я думаю, что более целесообразно просто использовать служебный метод O(n):
public static <T> T getIgnoreCase(Map<String, T> map, String key) {
for(Entry<String, T> entry : map.entrySet()) {
if(entry.getKey().equalsIgnoreCase(key))
return entry.getValue();
}
return null;
}
Это тот метод. Поскольку жертва производительностью (сложность времени) кажется неизбежной, по крайней мере, это не требует от вас изменения базовой карты в соответствии с поиском.
Для надежной реализации CaseInsensitiveMap / CaseInsensitiveSet проверьте java-util ( https://github.com/jdereg/java-util).
Эти Карты работают в стандартном времени поиска O(1), сохраняют регистр добавленных элементов, поддерживают все API-интерфейсы Карт, такие как putAll(), retainAll(), removeAll(), и позволяют размещать гетерогенные элементы в наборе ключей.
Кроме того, java.util.Set, возвращаемый.keySet() и.entrySet(), учитывает регистр нечувствительности (многие реализации этого не делают). Наконец, если вы извлекаете ключ из набора ключ / запись во время итерации, вы получаете String, а не класс оболочки CaseInsensitiveString.
Вместо создания собственного класса для проверки и хранения нечувствительной к регистру строки в качестве ключа HashMap вы можете использовать:
- LinkedCaseInsensitiveMap является оболочкой для LinkedHashMap, которая представляет собой карту на основе хэш-таблицы и связанного списка. В отличие от LinkedHashMap, он не позволяет вставлять нулевой ключ. LinkedCaseInsensitiveMap сохраняет исходный порядок, а также исходный регистр клавиш, позволяя вызывать такие функции, как получение и удаление, в любом регистре.
Например:
Map<String, Integer> linkedHashMap = new LinkedCaseInsensitiveMap<>();
linkedHashMap.put("abc", 1);
linkedHashMap.put("AbC", 2);
System.out.println(linkedHashMap);
Вывод: {AbC=2}
Зависимость Mvn:
Spring Core - это модуль Spring Framework, который также предоставляет служебные классы, включая LinkedCaseInsensitiveMap.
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
- CaseInsensitiveMap - это карта на основе хешей, которая преобразует ключи в нижний регистр до того, как они будут добавлены или получены. В отличие от TreeMap, CaseInsensitiveMap позволяет вставку нулевого ключа.
Например:
Map<String, Integer> commonsHashMap = new CaseInsensitiveMap<>();
commonsHashMap.put("ABC", 1);
commonsHashMap.put("abc", 2);
commonsHashMap.put("aBc", 3);
System.out.println(commonsHashMap);
Вывод: {abc=3}
Зависимость:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
- TreeMap - это реализация NavigableMap, что означает, что он всегда сортирует записи после вставки на основе заданного компаратора. Кроме того, TreeMap использует компаратор, чтобы определить, является ли вставленный ключ дубликатом или новым.
Следовательно, если мы предоставим String Comparator без учета регистра, мы получим TreeMap без учета регистра.
Например:
Map<String, Integer> treeMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
treeMap.put("ABC", 1);
treeMap.put("ABc", 2);
treeMap.put("cde", 1);
System.out.println(treeMap);
Выход: {ABC=2, cde=1}
Из-за этого я создаю новый объект 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();
}
I like using ICU4J’s CaseInsensitiveString wrap of the Map key because it takes care of the hash\equals and issue and it works for unicode\i18n.
HashMap<CaseInsensitiveString, String> caseInsensitiveMap = new HashMap<>();
caseInsensitiveMap.put("tschüß", "bye");
caseInsensitiveMap.containsKey("TSCHÜSS"); # true
Как насчет использования потоков Java 8.
nodeMap.entrySet().stream().filter(x->x.getKey().equalsIgnoreCase(stringfromEven.toString()).collect(Collectors.toList())
Это адаптер для HashMaps, который я реализовал для недавнего проекта. Работает аналогично тому, что делает @SandyR, но инкапсулирует логику преобразования, поэтому вы не можете вручную преобразовывать строки в объект-оболочку.
Я использовал функции Java 8, но с некоторыми изменениями вы можете адаптировать его к предыдущим версиям. Я протестировал его для большинства распространенных сценариев, кроме новых потоковых функций Java 8.
По сути, он оборачивает HashMap, направляет все функции к нему при преобразовании строк в / из объекта-оболочки. Но мне также пришлось адаптировать KeySet и EntrySet, потому что они перенаправляют некоторые функции на саму карту. Поэтому я возвращаю два новых набора для ключей и записей, которые фактически обертывают исходные 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);
}
}
}