StringStartsWithKeyMap <T> в Java? (соответствует, если ключ начинается с entry.key)

Мне нужен 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), я выбрал слово переклассификация.

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
2
0
321
3

Ответы 3

Насколько я знаю, для этого нет реализации по умолчанию, но вы можете легко написать это сами:

    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 может быть выполнен следующим образом:

  1. NavigableMap позволяет получить подкарту с помощью fromKey и toKey.
  2. Префикс, по которому вы хотите выполнить поиск, является обязательным fromKey. toKey может быть вычислен из префикса, так что мы получаем только те значения, которые удовлетворяют условию префикса

Пример кода:

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

Вышеупомянутое также может быть обобщено для ключей, не являющихся строками, при условии

  1. Вселенную ключей можно сортировать (на самом деле это основное требование для NavigableMap, например, TreeMap)
  2. Набор ключей поддерживает понятие префикса, т.е. префикс (k) <k в порядке сортировки
  3. Мы можем вычислить endKey по заданному префиксу p, так что
  • пусть набор ключей K = все ключи k на карте, где prefix (k) == p
  • endKey> k для всех ключей в K
  • AND endKey <= k ', где k' наименьший ключ> все ключи в K

Другие вопросы по теме