Мне нужен Map, который сравнивает ключи с startsWith(): ключи должны совпадать, если key.startsWith(entry.getKey()). Есть ли такая реализация?
Что я обнаружил: префиксное дерево (три) немного отличается от радиксное дерево (компактное префиксное дерево) и ПАТРИСИЯ дерево. Я нашел несколько реализаций, но они не определяют такие методы, как navigableMap.floorEntry(key) и navigableMap.higherEntry(key).
Сложная часть использования startsWith() для сравнения ключей - это разрешение конфликтов: если карта содержит записи как для «foo», так и для «foobar», что должно произойти?
С помощью NavigableMap легко реализовать желаемую функциональность, например TreeMap, если мы запрещаем конфликтующие ключи, что-то вроде:
private Map.Entry<String, T> findEntry(String key) {
Map.Entry<String, T> entry = map.floorEntry(key);
if (entry != null && key.startsWith(entry.getKey())) {
return entry;
}
return null;
}
Но если мы хотим поддерживать конфликтующие ключи, мы не можем полагаться на floorEntry(), как мы это делаем в приведенном выше коде: map.floorEntry("foobaz") найдет запись для "foobar", а не для "foo". Эту проблему можно решить, указав записи "foobar" указатель на запись "foo" ... И похоже, что мы заново изобретаем компактное префиксное дерево поверх TreeMap.
Так:
1) Существует ли существующая реализация дерева, которое сравнивает ключи узлов с startWith?
2) Есть ли реализация компактное префиксное дерево (радиксное дерево), которая также implements NavigableMap?
и в лучшем случае это будет:
3) некий аналог HashMap, поддерживающий совпадение startWith ()
UPD
Я понял, что это очень общая проблема: переклассификация.
Учитывая адрес дома (ну, записанный в обратном порядке, Страна-Город-Улица-Номер дома), найдите соответствующее почтовое отделение (почтовый индекс).
По IP-адресу найдите соответствующего провайдера.
В общем случае проблема заключается в следующем: по ключу объекта определить класс объекта. Объект идентифицируется ключом из M бит, но только первые N бит, N <M, определяют класс объекта; N не обязательно должно быть одинаковым для разных ключей. Узлы дерева не обязательно должны содержать ключи объектов; они содержат достаточно битов, чтобы определить класс объекта.
Поскольку ключи объекта этого типа соответствуют некоторой классификации (class-subclass-group-subgroup-id), я выбрал слово переклассификация.




Насколько я знаю, для этого нет реализации по умолчанию, но вы можете легко написать это сами:
List<String> matchingKeys = map.keySet().stream().
filter(key -> key.startsWith(prefix)).
collect(Collectors.toList());
Когда у вас есть совпадающие ключи, вы можете настроить, что делать. Может быть, примерно так:
for(Map.Entry<String, Object> entry : map.entrySet()) {
if (matchingKeys.indexOf(entry.getKey()) != -1) {
return entry;
}
}
Или другой подход:
Optional<Entry<String, T>> matchingEntry = map.entrySet().stream().
filter((k) -> k.getKey().startsWith(prefix)).findFirst();
if (matchingEntry.isPresent()) {
return matchingEntry.get();
}
return null;
Это то, что я использую на данный момент. Он не поддерживает конфликтующие ключи (при их обнаружении выдается исключение RuntimeException).
StringStartsWithKeyMap.java:
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* A map in which a key matches an entry if key.startsWith(entry.key)
* @param <T>
*/
public class StringStartsWithKeyMap<T> implements Map<String, T> {
TreeMap<String, T> map = new TreeMap<>();
@Override
public int size() {
return map.size();
}
@Override
public boolean isEmpty() {
return map.isEmpty();
}
@Override
public boolean containsKey(Object key) {
return null != findEntry((String)key);
}
@Override
public boolean containsValue(Object value) {
return map.containsValue(value);
}
@Override
public T get(Object key) {
Entry<String, T> e = findEntry((String) key);
return e == null ? null : e.getValue();
}
@Override
public T put(String key, T value) {
if (map.containsKey(key)) {
return map.put(key, value);
} else if (null == findEntry(key) && null == findHigherEntry(key)) {
return map.put(key, value);
}
throw new RuntimeException("Conflicting keys: \""+key+"\" after "+findEntry(key)+" or before "+findHigherEntry(key));
}
@Override
public T remove(Object key) {
return map.remove(key);
}
@Override
public void putAll(Map<? extends String, ? extends T> m) {
m.forEach(this::put);
}
@Override
public void clear() {
map.clear();
}
@Override
public Set<String> keySet() {
return map.keySet();
}
@Override
public Collection<T> values() {
return map.values();
}
@Override
public Set<Entry<String, T>> entrySet() {
return map.entrySet();
}
public StringStartsWithKeyMap<T> putSameValueFor(T value, String... keys) {
for (String k : keys) {
put(k, value);
}
return this;
}
public StringStartsWithKeyMap<T> putSameValueFor(T value, Iterable<String> keys) {
for (String k : keys) {
put(k, value);
}
return this;
}
private Map.Entry<String, T> findEntry(String key) {
Map.Entry<String, T> entry = map.floorEntry(key);
if (entry != null && key.startsWith(entry.getKey())) {
return entry;
}
return null;
}
private Map.Entry<String, T> findHigherEntry(String key) {
Map.Entry<String, T> entry = map.higherEntry(key);
if (entry != null && entry.getKey().startsWith(key)) {
return entry;
}
return null;
}
}
и тесты:
StringStartsWithKeyMapTest.java:
import org.junit.Test;
import java.util.Arrays;
import java.util.Map;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
public class StringStartsWithKeyMapTest {
@Test
public void sizeTest() {
Map<String, Integer> map3 = makeMap3();
Map<String, Integer> map0 = makeMap0();
assertThat(map3.size(), is(3));
assertThat(map0.size(), is(0));
}
@Test
public void isEmptyTest() {
Map<String, Integer> map3 = makeMap3();
Map<String, Integer> map0 = makeMap0();
assertThat(map3.isEmpty(), is(false));
assertThat(map0.isEmpty(), is(true));
}
@Test
public void containsKeyTest() {
Map<String, Integer> map3 = makeMap3();
Map<String, Integer> map0 = makeMap0();
assertThat(map3.containsKey("foo"), is(true));
assertThat(map3.containsKey("foobar"), is(true));
assertThat(map3.containsKey("quux"), is(false));
assertThat(map3.containsKey("quuxfoo"), is(false));
assertThat(map0.containsKey("foo"), is(false));
}
@Test
public void containsValueTest() {
Map<String, Integer> map3 = makeMap3();
Map<String, Integer> map0 = makeMap0();
assertThat(map3.containsValue(1), is(true));
assertThat(map3.containsValue(1000), is(false));
assertThat(map0.containsValue(1), is(false));
}
@Test
public void getTest() {
Map<String, Integer> map3 = makeMap3();
Map<String, Integer> map0 = makeMap0();
assertThat(map3.get("foo"), is(1));
assertThat(map3.get("foobar"), is(1));
assertThat(map3.get("bar"), is(2));
assertThat(map3.get("baraboo"), is(2));
assertThat(map3.get("baz"), is(3));
assertThat(map3.get("bah"), is((Integer) null));
assertThat(map0.get("foo"), is((Integer) null));
assertThat(map0.get(null), is((Integer) null));
}
@Test
public void putTest() {
Map<String, Integer> map3 = makeMap3();
map3.put("qux",100);
assertThat(map3.get("qux"), is(100));
map3.put("foo",200);
assertThat(map3.get("foo"), is(200));
try {
map3.put("foobar",6);
assertThat(true, is(false));
} catch (RuntimeException re) {
}
try {
map3.put("fo",60);
assertThat(true, is(false));
} catch (RuntimeException re) {
}
}
@Test
public void removeTest() {
Map<String, Integer> map3 = makeMap3();
map3.remove("foo");
map3.remove("baraboo");
assertThat(map3.size(), is(2));
assertThat(map3.get("foo"), is((Integer)null));
assertThat(map3.get("bar"), is(2));
assertThat(map3.get("baz"), is(3));
assertThat(map3.get("baraboo"), is(2)); // need exact match for removal
}
@Test
public void putAllTest() {
Map<String, Integer> map3 = makeMap3();
Map<String, Integer> map3a = makeMap3a();
map3.putAll(map3a);
assertThat(map3a.size(), is(3));
assertThat(map3.size(), is(5));
assertThat(map3.get("foo"), is(100));
assertThat(map3.get("bar"), is(2));
assertThat(map3.get("baz"), is(3));
assertThat(map3.get("corge"), is(5));
assertThat(map3.get("grault"), is(6));
assertThat(map3.get("corgegrault"), is(5));
}
@Test
public void clearTest() {
Map<String, Integer> map3 = makeMap3();
assertThat(map3.get("foo"), is(1));
assertThat(map3.get("foobar"), is(1));
map3.clear();
assertThat(map3.get("foo"), is((Integer) null));
assertThat(map3.get("foobar"), is((Integer) null));
assertThat(map3.containsKey("foo"), is(false));
assertThat(map3.containsValue(1), is(false));
assertThat(map3.size(), is(0));
}
@Test
public void keySetTest() {
Map<String, Integer> map3 = makeMap3();
Map<String, Integer> map0 = makeMap0();
assertThat(map0.keySet().isEmpty(), is(true));
assertThat(map0.keySet().contains("foo"), is(false));
assertThat(map3.keySet().size(), is(3));
assertThat(map3.keySet().contains("foo"), is(true));
}
@Test
public void valuesTest() {
Map<String, Integer> map3 = makeMap3();
Map<String, Integer> map0 = makeMap0();
assertThat(map0.values().isEmpty(), is(true));
assertThat(map3.values().size(), is(3));
assertThat(map3.values().contains(1), is(true));
assertThat(map3.values().contains(2), is(true));
assertThat(map3.values().contains(3), is(true));
}
@Test
public void entrySetTest() {
Map<String, Integer> map3 = makeMap3();
Map<String, Integer> map0 = makeMap0();
assertThat(map0.entrySet().isEmpty(), is(true));
assertThat(map3.entrySet().size(), is(3));
}
@Test
public void putSameValueForTest() {
StringStartsWithKeyMap<Integer> map3 = makeMap3();
map3.putSameValueFor(400,"foo","corge","grault");
assertThat(map3.size(), is(5));
assertThat(map3.get("foo"), is(400));
assertThat(map3.get("bar"), is(2));
assertThat(map3.get("baz"), is(3));
assertThat(map3.get("corge"), is(400));
assertThat(map3.get("grault"), is(400));
assertThat(map3.get("corgegrault"), is(400));
}
@Test
public void putSameValueForTest2() {
StringStartsWithKeyMap<Integer> map3 = makeMap3();
map3.putSameValueFor(400, Arrays.asList("foo","corge","grault"));
assertThat(map3.size(), is(5));
assertThat(map3.get("foo"), is(400));
assertThat(map3.get("bar"), is(2));
assertThat(map3.get("baz"), is(3));
assertThat(map3.get("corge"), is(400));
assertThat(map3.get("grault"), is(400));
assertThat(map3.get("corgegrault"), is(400));
}
private StringStartsWithKeyMap<Integer> makeMap3() {
StringStartsWithKeyMap<Integer> res = new StringStartsWithKeyMap<>();
res.put("foo",1);
res.put("bar",2);
res.put("baz",3);
return res;
}
private StringStartsWithKeyMap<Integer> makeMap0() {
StringStartsWithKeyMap<Integer> res = new StringStartsWithKeyMap<>();
return res;
}
private StringStartsWithKeyMap<Integer> makeMap3a() {
StringStartsWithKeyMap<Integer> res = new StringStartsWithKeyMap<>();
res.put("foo",100);
res.put("corge",5);
res.put("grault",6);
return res;
}
}
Я не уверен, правильно ли я понял вашу точку зрения относительно конфликтующих ключей. Если ваша карта содержит в качестве ключей как foo, так и foobar, и вы хотите искать 'startWith (foo)', то, как я понимаю, должны быть возвращены значения, соответствующие как foo, так и foobar. В общем, если вы выполняете поиск с использованием префикса ключа, нельзя гарантировать, что карта будет содержать только один ключ, удовлетворяющий этому префиксу.
Если вы знаете, что данные, которые вы сохранили на карте, гарантируют это, тогда вы можете просто прочитать первое значение, возвращаемое методом поиска.
Поиск с использованием startWith может быть выполнен следующим образом:
Пример кода:
public class NavigableMapExample {
public static void main(String[] args) {
NavigableMap<String, String> map = new TreeMap<>();
map.put("abc", "value0");
map.put("abcd", "value1");
map.put("abcdef", "value2");
map.put("abdfghij", "value3");
map.put("abcefgh", "value4");
map.put("abefijk", "value5");
System.out.println("Values starting from abc");
// expected output value0, value1, value2, value4
searchByPrefix(map, "abc").stream().forEach(System.out::println);
}
private static Collection<String> searchByPrefix(NavigableMap<String, String> map, String begin) {
String end = endKey(begin);
// map.subMap(fromKey, fromInclusive, toKey, toInclusive), toKey should not be inclusive for our case
return map.subMap(begin, true, end, false).values();
}
public static String endKey(String prefix) {
return prefix.substring(0, prefix.length() - 1) + (char)(prefix.charAt(prefix.length() -1) + 1);
}
}
Вышеупомянутое также может быть обобщено для ключей, не являющихся строками, при условии