Каков наилучший способ объединить два списка в карту (Java)?

Было бы неплохо использовать for (String item: list), но он будет проходить только через один список, и для другого списка вам понадобится явный итератор. Или вы можете использовать явный итератор для обоих.

Здесь пример проблемы и решение с использованием индексированного цикла for:

import java.util.*;
public class ListsToMap {
  static public void main(String[] args) {
    List<String> names = Arrays.asList("apple,orange,pear".split(","));
    List<String> things = Arrays.asList("123,456,789".split(","));
    Map<String,String> map = new LinkedHashMap<String,String>();  // ordered

    for (int i=0; i<names.size(); i++) {
      map.put(names.get(i), things.get(i));    // is there a clearer way?
    }

    System.out.println(map);
  }
}

Вывод:

{apple=123, orange=456, pear=789}

Есть ли более четкий способ? Может быть, в API-интерфейсах коллекций?

Ответ 1

Поскольку отношение key-value неявно с помощью индекса списка, я думаю, что решение for-loop, которое использует индекс списка явно, на самом деле довольно ясное - и короткое.

Ответ 2

Прошло некоторое время с тех пор, как был задан этот вопрос, но в наши дни я частично отношусь к чему-то вроде:

public static <K, V> Map<K, V> zipToMap(List<K> keys, List<V> values) {
    return IntStream.range(0, keys.size()).boxed()
            .collect(Collectors.toMap(keys::get, values::get));
}

Для тех, кто не знаком с потоками, что делает это, получается IntStream от 0 до длины, затем он помещает его, делая его Stream<Integer>, чтобы он мог быть преобразован в объект, а затем собирает их с помощью Collectors.toMap который берет двух поставщиков, один из которых генерирует ключи, другие значения.

Это может означать некоторую проверку (например, если keys.size() меньше values.size()), но он отлично работает как простое решение.

EDIT: Вышеизложенное отлично работает для чего-либо с постоянным поиском по времени, но если вы хотите что-то, что будет работать в том же порядке (и по-прежнему использовать такой же шаблон), вы можете сделать что-то вроде:

public static <K, V> Map<K, V> zipToMap(List<K> keys, List<V> values) {
    Iterator<K> keyIter = keys.iterator();
    Iterator<V> valIter = values.iterator();
    return IntStream.range(0, keys.size()).boxed()
            .collect(Collectors.toMap(_i -> keyIter.next(), _i -> valIter.next()));
}

Результат тот же (опять же, отсутствие проверок длины и т.д.), но временная сложность не зависит от реализации метода get для любого используемого списка.

Ответ 3

Я бы часто использовал следующую идиому. Я признаю, что это спорно, является ли это понятнее.

Iterator<String> i1 = names.iterator();
Iterator<String> i2 = things.iterator();
while (i1.hasNext() && i2.hasNext()) {
    map.put(i1.next(), i2.next());
}
if (i1.hasNext() || i2.hasNext()) complainAboutSizes();

Он имеет преимущество в том, что он также работает для коллекций и подобных вещей без произвольного доступа или без эффективного произвольного доступа, таких как LinkedList, TreeSets или SQL ResultSets. Например, если вы будете использовать оригинальный алгоритм в LinkedLists, у вас будет медленный алгоритм рисования Shlemiel, который фактически нуждается в n * n операциях для списков длины n.

Как указал 13ren, вы также можете использовать тот факт, что Iterator.next выдает исключение NoSuchElementException, если вы пытаетесь читать после конца одного списка, когда длины не совпадают. Таким образом, вы получите более краткий, но, возможно, немного запутанный вариант:

Iterator<String> i1 = names.iterator();
Iterator<String> i2 = things.iterator();
while (i1.hasNext() || i2.hasNext()) map.put(i1.next(), i2.next());

Ответ 4

Ваше решение выше, конечно, правильно, но ваш вопрос был о ясности, я отвечу на это.

Самый простой способ объединить два списка - поместить комбинацию в метод с красивым ясным именем. Я только что взял ваше решение и извлек его в метод здесь:

Map<String,String> combineListsIntoOrderedMap (List<String> keys, List<String> values) {
    if (keys.size() != values.size())
        throw new IllegalArgumentException ("Cannot combine lists with dissimilar sizes");
    Map<String,String> map = new LinkedHashMap<String,String>();
    for (int i=0; i<keys.size(); i++) {
        map.put(keys.get(i), values.get(i));
    }
    return map;
}

И, конечно, ваш рефакторированный main теперь будет выглядеть так:

static public void main(String[] args) {
    List<String> names = Arrays.asList("apple,orange,pear".split(","));
    List<String> things = Arrays.asList("123,456,789".split(","));
    Map<String,String> map = combineListsIntoOrderedMap (names, things);
    System.out.println(map);
}

Я не мог устоять перед проверкой длины.

Ответ 5

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

Альтернативное решение Java 8, которое позволяет избежать вызова boxed() на IntStream, это

List<String> keys = Arrays.asList("A", "B", "C");
List<String> values = Arrays.asList("1", "2", "3");

Map<String, String> map = IntStream.range(0, keys.size())
                                   .collect(
                                        HashMap::new, 
                                        (m, i) -> m.put(keys.get(i), values.get(i)), 
                                        Map::putAll
                                   );
                          );

Ответ 6

ArrayUtils # toMap() не объединяет два списка в карту, но делает это для 2-мерного массива (так что не что вы ищете, но, может быть, интерес для будущих ссылок...)

Ответ 7

Как и ясность, я думаю, что есть другие вещи, которые стоит рассмотреть:

  • Правильное отклонение незаконных аргументов, таких как списки разных размеров и null (посмотрите, что произойдет, если things есть null в коде вопроса).
  • Возможность обработки списков, которые не имеют быстрого произвольного доступа.
  • Возможность обработки параллельных и синхронизированных коллекций.

Итак, для кода библиотеки, возможно, что-то вроде этого:

@SuppressWarnings("unchecked")
public static <K,V> Map<K,V> linkedZip(List<? extends K> keys, List<? extends V> values) {
    Object[] keyArray = keys.toArray();
    Object[] valueArray = values.toArray();
    int len = keyArray.length;
    if (len != valueArray.length) {
        throwLengthMismatch(keyArray, valueArray);
    }
    Map<K,V> map = new java.util.LinkedHashMap<K,V>((int)(len/0.75f)+1);
    for (int i=0; i<len; ++i) {
        map.put((K)keyArray[i], (V)valueArray[i]);
    }
    return map;
}

(Может захотеть проверить не ставить несколько одинаковых ключей.)

Ответ 8

Нет четкого пути. Мне все еще интересно, есть ли у Apache Commons или Guava что-то подобное. Во всяком случае, у меня была своя статическая утилита. Но этот знает о ключевых столкновениях!

public static <K, V> Map<K, V> map(Collection<K> keys, Collection<V> values) {

    Map<K, V> map = new HashMap<K, V>();
    Iterator<K> keyIt = keys.iterator();
    Iterator<V> valueIt = values.iterator();
    while (keyIt.hasNext() && valueIt.hasNext()) {
        K k = keyIt.next();
        if (null != map.put(k, valueIt.next())){
            throw new IllegalArgumentException("Keys are not unique! Key " + k + " found more then once.");
        }
    }
    if (keyIt.hasNext() || valueIt.hasNext()) {
        throw new IllegalArgumentException("Keys and values collections have not the same size");
    };

    return map;
}

Ответ 9

Вам не нужно даже ограничивать себя строками. Некоторое изменение кода у CPerkins:

Map<K, V> <K, V> combineListsIntoOrderedMap (List<K> keys, List<V> values) {
      if (keys.size() != values.size())
          throw new IllegalArgumentException ("Cannot combine lists with dissimilar sizes");
Map<K, V> map = new LinkedHashMap<K, V>();
for (int i=0; i<keys.size(); i++) {
  map.put(keys.get(i), values.get(i));
}
return map;

}

Ответ 10

С библиотекой vavr:

List.ofAll(names).zip(things).toJavaMap(Function.identity());

Ответ 11

Используйте Clojure. одна строка - все, что требуется;)

 (zipmap list1 list2)

Ответ 12

Это работает с использованием Eclipse Collections.

Map<String, String> map =
        Maps.adapt(new LinkedHashMap<String, String>())
                .withAllKeyValues(
                        Lists.mutable.of("apple,orange,pear".split(","))
                                .zip(Lists.mutable.of("123,456,789".split(","))));

System.out.println(map);

Примечание: я являюсь коммиттером для Eclipse Collections.

Ответ 13

Другое решение Java 8:

Если у вас есть доступ к библиотеке Guava (самая ранняя поддержка потоков в версии 21 [1]), вы можете сделать:

Streams.zip(keyList.stream(), valueList.stream(), Maps::immutableEntry)
       .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

Для меня преимущество этого метода просто заключается в том, что он является одним вкладышем, и я нашел это полезным для моего варианта использования.

Ответ 14

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

public static void main(String[] args) {
    List<String> names = Arrays.asList("apple,orange,pear".split(","));
    List<String> things = Arrays.asList("123,456,789".split(","));
    Map<String, String> map = new HashMap<>(4);
    for (Map.Entry<String, String> e : new DualIterator<>(names, things)) {
        map.put(e.getKey(), e.getValue());
    }
    System.out.println(map);
}

Если yes (Map.Entryвыбрано в качестве удобства), то вот полный пример (примечание: поток небезопасный):

import java.util.*;
/** <p>
    A thread unsafe iterator over two lists to convert them 
    into a map such that keys in first list at a certain
    index map onto values in the second list <b> at the same index</b>.
    </p>
    Created by kmhaswade on 5/10/16.
 */
public class DualIterator<K, V> implements Iterable<Map.Entry<K, V>> {
    private final List<K> keys;
    private final List<V> values;
    private int anchor = 0;

    public DualIterator(List<K> keys, List<V> values) {
        // do all the validations here
        this.keys = keys;
        this.values = values;
    }
    @Override
    public Iterator<Map.Entry<K, V>> iterator() {
        return new Iterator<Map.Entry<K, V>>() {
            @Override
            public boolean hasNext() {
                return keys.size() > anchor;
            }

            @Override
            public Map.Entry<K, V> next() {
                Map.Entry<K, V> e = new AbstractMap.SimpleEntry<>(keys.get(anchor), values.get(anchor));
                anchor += 1;
                return e;
            }
        };
    }

    public static void main(String[] args) {
        List<String> names = Arrays.asList("apple,orange,pear".split(","));
        List<String> things = Arrays.asList("123,456,789".split(","));
        Map<String, String> map = new LinkedHashMap<>(4);
        for (Map.Entry<String, String> e : new DualIterator<>(names, things)) {
            map.put(e.getKey(), e.getValue());
        }
        System.out.println(map);
    }
}

Он печатает (для каждого требования):

{apple=123, orange=456, pear=789}

Ответ 15

В Java 8 я просто перебрал бы и то и другое, один за другим, и заполнил карту:

public <K, V> Map<K, V> combineListsIntoOrderedMap (Iterable<K> keys, Iterable<V> values) {

    Map<K, V> map = new LinkedHashMap<>();

    Iterator<V> vit = values.iterator();
    for (K k: keys) {
        if (!vit.hasNext())
            throw new IllegalArgumentException ("Less values than keys.");

        map.put(k, vit.next());
    }

    return map;
}

Или вы можете пойти еще дальше с функциональным стилем и сделать:

/**
 * Usage:
 *
 *     Map<K, V> map2 = new LinkedHashMap<>();
 *     combineListsIntoOrderedMap(keys, values, map2::put);
 */
public <K, V> void combineListsIntoOrderedMap (Iterable<K> keys, Iterable<V> values, BiConsumer<K, V> onItem) {
    Iterator<V> vit = values.iterator();
    for (K k: keys) {
        if (!vit.hasNext())
            throw new IllegalArgumentException ("Less values than keys.");
        onItem.accept(k, vit.next());
    }
}

Ответ 16

Кредитное плечо AbstractMap и AbstractSet:

import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

public class ZippedMap<A, B> extends AbstractMap<A, B> {

  private final List<A> first;

  private final List<B> second;

  public ZippedMap(List<A> first, List<B> second) {
    if (first.size() != second.size()) {
      throw new IllegalArgumentException("Expected lists of equal size");
    }
    this.first = first;
    this.second = second;
  }

  @Override
  public Set<Entry<A, B>> entrySet() {
    return new AbstractSet<>() {
      @Override
      public Iterator<Entry<A, B>> iterator() {
        Iterator<A> i = first.iterator();
        Iterator<B> i2 = second.iterator();
        return new Iterator<>() {

          @Override
          public boolean hasNext() {
            return i.hasNext();
          }

          @Override
          public Entry<A, B> next() {
            return new SimpleImmutableEntry<>(i.next(), i2.next());
          }
        };
      }

      @Override
      public int size() {
        return first.size();
      }
    };
  }
}

Использование:

public static void main(String... args) {
  Map<Integer, Integer> zippedMap = new ZippedMap<>(List.of(1, 2, 3), List.of(1, 2, 3));
  zippedMap.forEach((k, v) -> System.out.println("key = " + k + " value = " + v));
}

Вывод:

key = 1 value = 1
key = 2 value = 2
key = 3 value = 3

Я получил эту идею от класса java.util.stream.Collectors.Partition, который делает то же самое.

Он обеспечивает инкапсуляцию и четкое намерение (объединение двух списков), а также возможность повторного использования и производительность.

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