Разделить строку на пары ключ-значение

У меня есть строка вроде этого:

pet:cat::car:honda::location:Japan::food:sushi

Теперь : указывает пары ключ-значение, а :: - пары. Я хочу добавить пары ключ-значение к карте.

Я могу добиться этого, используя:

Map<String, String> map = new HashMap<String, String>();
String test = "pet:cat::car:honda::location:Japan::food:sushi";
String[] test1 = test.split("::");

for (String s : test1) {
    String[] t = s.split(":");
    map.put(t[0], t[1]);
}

for (String s : map.keySet()) {
    System.out.println(s + " is " + map.get(s));
}

Но есть ли эффективный способ сделать это?


Я чувствую, что код неэффективен, потому что я использовал 2 String[] объекта и дважды называл функцию split. Кроме того, я использую t[0] и t[1], которые могут вызывать ArrayIndexOutOfBoundsException, если значений нет.

Ответ 1

Вы можете сделать один вызов split() и один проход в строке, используя следующий код. Но он, конечно, предполагает, что String действительна в первую очередь:

    Map<String, String> map = new HashMap<String, String>();
    String test = "pet:cat::car:honda::location:Japan::food:sushi";

    // split on ':' and on '::'
    String[] parts = test.split("::?");

    for (int i = 0; i < parts.length; i += 2) {
        map.put(parts[i], parts[i + 1]);
    }

    for (String s : map.keySet()) {
        System.out.println(s + " is " + map.get(s));
    }

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

EDIT:

для тех, кто задается вопросом, что означает ::? в приведенном выше коде: String.split() принимает регулярное выражение в качестве аргумента. Разделитель - это подстрока, которая соответствует регулярному выражению. ::? - это регулярное выражение, которое означает: 1 двоеточие, за которым следует 0 или 1 двоеточие. Таким образом, это позволяет рассматривать :: и : как разделители.

Ответ 2

Использование библиотеки Guava - однострочный:

String test = "pet:cat::car:honda::location:Japan::food:sushi";
Map<String, String> map = Splitter.on( "::" ).withKeyValueSeparator( ':' ).split( test );
System.out.println(map);

Выход:

{pet=cat, car=honda, location=Japan, food=sushi}

Это также может работать быстрее, чем JDK String.split, поскольку он не создает регулярное выражение для "::".

Обновить, он даже правильно обрабатывает угловой случай из комментариев:

String test = "pet:cat::car:honda::location:Japan::food:sushi:::cool";
Map<String, String> map = Splitter.on( "::" ).withKeyValueSeparator( ':' ).split( test );
System.out.println(map);

Вывод:

{pet=cat, car=honda, location=Japan, food=sushi, =cool}

Ответ 3

Ваше решение действительно несколько неэффективно.

Человек, который дал вам строку для разбора, также является клоуном. Существуют отраслевые стандартные форматы сериализации, такие как JSON или XML, для которых существуют быстрые, эффективные анализы. Изобретение квадратного колеса никогда не является хорошей идеей.

Первый вопрос: Вам все равно? Достаточно ли достаточно, чтобы это мешало производительности вашего приложения? Скорее всего, это не так, но есть только один способ узнать. Контролируйте свой код.

Тем не менее, существуют более эффективные решения. Ниже приведен пример

public static void main (String[] args) throws java.lang.Exception
{
    String test = "pet:cat::car:honda::location:Japan::food:sushi";
    boolean stateiskey = true;

    Map<String, String> map = new HashMap<>();
    int keystart = 0;
    int keyend = 0;
    int valuestart = 0;
    int valueend = 0;

    for(int i = 0; i < test.length(); i++){
        char nextchar = test.charAt(i);
        if (stateiskey) {
            if (nextchar == ':') {
              keyend = i;           
              stateiskey = false;
              valuestart = i + 1;
            }
        } else {
            if (i == test.length() - 1 || (nextchar == ':' && test.charAt(i + 1) == ':')) {
                valueend = i;
                if (i + 1 == test.length()) valueend += 1; //compensate one for the end of the string
                String key = test.substring(keystart, keyend);
                String value = test.substring(valuestart, valueend);
                keystart = i + 2;
                map.put(key, value);
                i++;
                stateiskey = true;
            }
        }
    }

    System.out.println(map);
}

Это решение является конечным автоматом с двумя состояниями. Он просматривает каждый символ только дважды, один раз, когда он проверяет его на границе, и один раз, когда он копирует его в новую строку на вашей карте. Это минимальная сумма.

Он не создает объекты, которые не нужны, например, строкоструйные машины, строки или массивы, что снижает давление на сбор.

Он поддерживает хорошую локальность. Следующий символ, вероятно, всегда находится в кеше, поэтому поиск дешев.

Это происходит по серьезной цене, которая, вероятно, не стоит того, чтобы:

  • Это намного сложнее и менее очевидно.
  • Есть все виды движущихся частей
  • Сложнее отлаживать, когда ваша строка находится в неожиданном формате
  • Ваши коллеги будут ненавидеть вас.
  • Вы будете ненавидеть вас, когда вам нужно что-то отладить

Стоит ли? Может быть. Насколько быстро вам нужна эта строка, которая точно разбирается?

Быстрый и грязный тест в https://ideone.com/8T7twy говорит мне, что для этой строки этот метод примерно в 4 раза быстрее. Для более длинных строк разница, вероятно, несколько больше.

Но ваша версия все еще только 415 миллисекунд для 100 000 повторений, где эта составляет 99 миллисекунд.

Ответ 4

Я не знаю, что это лучший подход или нет, но я думаю, что это другой способ сделать то же самое без использования метода split дважды

Map<String, String> map = new HashMap<String, String>();
String test = "pet:cat::car:honda::location:Japan::food:sushi";
String[] test1 = test.replaceAll("::",":").split(":");
for(int i=0;i<test1.length;i=i+2)
{
     map.put(test1[i], test1[i+1]);
}

for (String s : map.keySet()) {
    System.out.println(s + " is " + map.get(s));
}

Надеюсь, это поможет:)

Ответ 5

Ваша программа абсолютно прекрасна.

Просто потому, что вы попросили более оптимальный код.

Я уменьшил вашу память, приняв несколько переменных вместо того, чтобы брать массивы и хранить в них.

Посмотрите на свою строку, это следует за patter.

key : value :: key : value ::....

Что мы можем сделать из этого?

получить ключ до :, как только он достигнет значения :, до тех пор, пока он не достигнет "::".

package qwerty7;

import java.util.HashMap;

public class Demo {
public static void main(String ar[])
{
    StringBuilder s = new StringBuilder("pet:cat::car:honda::location:Japan::food:sushi");
    boolean isKey = true;
    String key = "", value = "";
    HashMap<String, String> hm = new HashMap();
    for(int i = 0; i < s.length(); i++)
    {
        char ch = s.charAt(i);
        char nextChar = s.charAt(i+1);
        if(ch == ':' && nextChar != ':')
        {
            isKey = false;
            continue;
        }
        else if(ch == ':' && nextChar == ':')
        {
            hm.put(key, value);
            isKey = true;
            key = "";
            value = "";
            i+=1;
            continue;
        }
        if(isKey)
        {
            key += ch;
        }
        else
        {
            value += ch;
        }
         if(i == s.length() - 1)
            {
                hm.put(key, value);
            }

    }
    for (String x : hm.keySet()) {
        System.out.println(x + " is " + hm.get(x));
    }
}
}

Это не займет много итераций при расщеплении каждый раз.

Не занимает много памяти.

Сложность времени O (n)

Вывод:

car is honda
location is Japan
pet is cat
food is sushi