Java: как преобразовать список в карту


224

В последнее время у меня разговор с коллегой о том , что было бы оптимальным способом обращенного Listк Mapв Java , и если есть какие - либо конкретные выгоды от этого.

Я хочу знать оптимальный подход к конверсии и был бы очень признателен, если бы кто-нибудь мог мне помочь.

Это хороший подход:

List<Object[]> results;
Map<Integer, String> resultsMap = new HashMap<Integer, String>();
for (Object[] o : results) {
    resultsMap.put((Integer) o[0], (String) o[1]);
}

2
Какой самый оптимальный способ? Оптимизация выполняется с учетом определенного параметра (скорость / память).
Даниэль Фат

6
List отличается от Map концептуальным образом - у Map есть понятие «ключ, значение», а у List нет. Учитывая это, неясно, как именно вы собираетесь конвертировать из списка в карту и обратно.
Виктор Сорокин

1
@Daniel: Оптимально, я имел в виду, что является лучшим способом сделать это среди всех различных способов, между которыми я не уверен во всех способах, и поэтому было бы хорошо увидеть некоторые различные способы преобразования списка в карту.
Рэйчел


Ответы:


188
List<Item> list;
Map<Key,Item> map = new HashMap<Key,Item>();
for (Item i : list) map.put(i.getKey(),i);

Предполагая, конечно, что у каждого элемента есть getKey()метод, который возвращает ключ правильного типа.


1
Вы также можете указать позицию в списке.
Джереми

@Jim: мне нужно установить getKey()какой-либо конкретный параметр?
Рэйчел

Кроме того, какова будет ценность на карте, можете ли вы привести пример?
Рэйчел

1
@Rachel - значение - это элемент в списке, а ключ - это то, что делает элемент уникальным, что определяется вами. Использование Джимом getKey()было произвольным.
Джереми

1
Вы заранее знаете размер, так что вы можете сделать Map <Key, Item> map = new HashMap <Key, Item> (list.size ());
Виктор Ромеро

316

С участием , вы сможете сделать это в одну строку, используя потоки и Collectorsкласс.

Map<String, Item> map = 
    list.stream().collect(Collectors.toMap(Item::getKey, item -> item));

Короткая демонстрация:

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class Test{
    public static void main (String [] args){
        List<Item> list = IntStream.rangeClosed(1, 4)
                                   .mapToObj(Item::new)
                                   .collect(Collectors.toList()); //[Item [i=1], Item [i=2], Item [i=3], Item [i=4]]

        Map<String, Item> map = 
            list.stream().collect(Collectors.toMap(Item::getKey, item -> item));

        map.forEach((k, v) -> System.out.println(k + " => " + v));
    }
}
class Item {

    private final int i;

    public Item(int i){
        this.i = i;
    }

    public String getKey(){
        return "Key-"+i;
    }

    @Override
    public String toString() {
        return "Item [i=" + i + "]";
    }
}

Вывод:

Key-1 => Item [i=1]
Key-2 => Item [i=2]
Key-3 => Item [i=3]
Key-4 => Item [i=4]

Как отмечено в комментариях, вы можете использовать Function.identity()вместо item -> item, хотя я нахожу i -> iдовольно явным.

И для полноты заметим, что вы можете использовать бинарный оператор, если ваша функция не является биективной. Например, давайте рассмотрим это Listи функцию отображения, которая для значения int вычисляет результат по модулю 3:

List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5, 6);
Map<String, Integer> map = 
    intList.stream().collect(toMap(i -> String.valueOf(i % 3), i -> i));

При запуске этого кода вы получите сообщение об ошибке java.lang.IllegalStateException: Duplicate key 1. Это связано с тем, что 1% 3 соответствует 4% 3 и, следовательно, имеет то же значение ключа, что и функция сопоставления клавиш. В этом случае вы можете предоставить оператор слияния.

Вот тот, который суммирует значения; (i1, i2) -> i1 + i2;это можно заменить ссылкой на метод Integer::sum.

Map<String, Integer> map = 
    intList.stream().collect(toMap(i -> String.valueOf(i % 3), 
                                   i -> i, 
                                   Integer::sum));

который теперь выводит:

0 => 9 (i.e 3 + 6)
1 => 5 (i.e 1 + 4)
2 => 7 (i.e 2 + 5)

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


19
лучше использовать Function.identity()вместоitem -> item
Эммануэль Тузери

1
@ EmmanuelTouzery Ну, Function.identity()возвращается t -> t;.
Алексис С.

2
Конечно, оба работают. Я думаю, это вопрос вкуса. Я нахожу Function.identity () более узнаваемым.
Эммануэль Тузери

OP не обрабатывает никаких pojos, только строки и целые числа, для которых вы не можете вызывать ::getKey.
phil294

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

115

На случай, если этот вопрос не будет закрыт как дубликат, правильный ответ - использовать Google Collections :

Map<String,Role> mappedRoles = Maps.uniqueIndex(yourList, new Function<Role,String>() {
  public String apply(Role from) {
    return from.getName(); // or something else
  }});

17
Это должно быть на вершине
Мартин Андерссон

6
« Guava содержит строго совместимый расширенный набор старой устаревшей библиотеки Google Collections . Вам больше не следует использовать эту библиотеку». Возможно, потребуется обновление.
Tiny

3
Использование внешней библиотеки для такой простой операции излишне. Это или признак очень слабой стандартной библиотеки. В этом случае ответ @ jim-garrison вполне разумен. Печально, что у java нет таких полезных методов, как «map» и «Reduce», но они не совсем необходимы.
linuxdan

2
Это использует гуаву. К сожалению, Guava очень медленный на Android, поэтому это решение не должно использоваться в проекте Android.
Игорь Ганапольский

если элемент в списке имеет повторяющиеся roleNames, вышеприведенный код выдаст исключение,
Junchen Liu

17

Используя Java 8 вы можете сделать следующее:

Map<Key, Value> result= results
                       .stream()
                       .collect(Collectors.toMap(Value::getName,Function.identity()));

Value может быть любым объектом, который вы используете.


16

Начиная с Java 8, ответ @ZouZou с использованием Collectors.toMapколлектора, безусловно, является идиоматическим способом решения этой проблемы.

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

Таким образом, решение действительно становится однострочным.

/**
 * Returns a map where each entry is an item of {@code list} mapped by the
 * key produced by applying {@code mapper} to the item.
 *
 * @param list the list to map
 * @param mapper the function to produce the key from a list item
 * @return the resulting map
 * @throws IllegalStateException on duplicate key
 */
public static <K, T> Map<K, T> toMapBy(List<T> list,
        Function<? super T, ? extends K> mapper) {
    return list.stream().collect(Collectors.toMap(mapper, Function.identity()));
}

А вот как вы могли бы использовать его на List<Student>:

Map<Long, Student> studentsById = toMapBy(students, Student::getId);

Для обсуждения параметров типа этого метода см. Мой дополнительный вопрос .
Glts

Это создаст исключение в случае, если есть дубликаты ключей. Sth like: Исключение в потоке "main" java.lang.IllegalStateException: дубликат ключа .... Подробнее см .: codecramp.com/java-8-streams-api-convert-list-map
EMM

@ EMM Конечно, как задумано и задокументировано в Javadoc.
glts

Да, обновлен ответ на случай дубликатов. Пожалуйста, просмотрите. Спасибо
EMM

10

А Listи Mapконцептуально разные. А List- это упорядоченная коллекция предметов. Элементы могут содержать дубликаты, и элемент может не иметь понятия уникального идентификатора (ключа). А Mapимеет значения, сопоставленные с ключами. Каждый ключ может указывать только на одно значение.

Поэтому, в зависимости от ваших Listпредметов, может быть или не быть возможным преобразовать его в Map. У ваших Listвещей нет дубликатов? У каждого предмета есть уникальный ключ? Если это так, то можно поместить их в Map.


10

Алексис уже опубликовал ответ в Java 8, используя метод toMap(keyMapper, valueMapper). Согласно документации для реализации этого метода:

Нет никаких гарантий относительно типа, изменчивости, сериализуемости или поточной безопасности возвращаемой карты.

Таким образом, в случае, если мы заинтересованы в конкретной реализации Mapинтерфейса, например, HashMapтогда мы можем использовать перегруженную форму как:

Map<String, Item> map2 =
                itemList.stream().collect(Collectors.toMap(Item::getKey, //key for map
                        Function.identity(),    // value for map
                        (o,n) -> o,             // merge function in case of conflict with keys
                        HashMap::new));         // map factory - we want HashMap and not any Map implementation

Хотя использование одного Function.identity()или i->iхорошо, но кажется, Function.identity()вместо того, i -> iчтобы сэкономить память согласно этому связанному ответу .


1
Забавный факт, что в 2019 году огромное количество людей все еще не осознают, что они не знают реальной реализации Карты, которую они получают с помощью лямбды! На самом деле это только один ответ, который я нашел с лямбдами Java 8, которые я бы использовал на производстве.
Павло

Есть ли способ собрать, указав тип карты, но без использования функции слияния?
Росберг


5

Универсальный метод

public static <K, V> Map<K, V> listAsMap(Collection<V> sourceList, ListToMapConverter<K, V> converter) {
    Map<K, V> newMap = new HashMap<K, V>();
    for (V item : sourceList) {
        newMap.put( converter.getKey(item), item );
    }
    return newMap;
}

public static interface ListToMapConverter<K, V> {
    public K getKey(V item);
}

Как это использовать? Что я должен передать в качестве converterпараметра в методе?
Игорь Ганапольский

4

Без java-8 вы сможете сделать это в одной строке коллекции Commons и классе Closure

List<Item> list;
@SuppressWarnings("unchecked")
Map<Key, Item> map  = new HashMap<Key, Item>>(){{
    CollectionUtils.forAllDo(list, new Closure() {
        @Override
        public void execute(Object input) {
            Item item = (Item) input;
            put(i.getKey(), item);
        }
    });
}};

2

Многие решения приходят на ум, в зависимости от того, чего вы хотите достичь:

Каждый элемент списка является ключом и значением

for( Object o : list ) {
    map.put(o,o);
}

Элементам списка есть что искать, может быть, имя:

for( MyObject o : list ) {
    map.put(o.name,o);
}

Элементам списка есть, что их искать, и нет гарантии, что они уникальны: используйте Googles MultiMaps

for( MyObject o : list ) {
    multimap.put(o.name,o);
}

Предоставление всем элементам позиции в качестве ключа:

for( int i=0; i<list.size; i++ ) {
    map.put(i,list.get(i));
}

...

Это действительно зависит от того, чего вы хотите достичь.

Как видно из примеров, карта - это отображение ключа на значение, а список - это просто последовательность элементов, каждая из которых имеет позицию. Так что они просто не конвертируются автоматически.


Но мы можем рассматривать положение элемента списка как ключевое и указывать их значение на карте, это хорошее решение?
Рэйчел

AFAIK да! В JDK нет функции, которая делает это автоматически, так что вам нужно выполнить свою собственную.
Даниил

Можно ли сделать последнюю версию (используя индекс массива в качестве ключа карты) с потоками Java 8?
phil294

2

Вот небольшой метод, который я написал именно для этой цели. Он использует Validate от Apache Commons.

Не стесняйтесь использовать его.

/**
 * Converts a <code>List</code> to a map. One of the methods of the list is called to retrive
 * the value of the key to be used and the object itself from the list entry is used as the
 * objct. An empty <code>Map</code> is returned upon null input.
 * Reflection is used to retrieve the key from the object instance and method name passed in.
 *
 * @param <K> The type of the key to be used in the map
 * @param <V> The type of value to be used in the map and the type of the elements in the
 *            collection
 * @param coll The collection to be converted.
 * @param keyType The class of key
 * @param valueType The class of the value
 * @param keyMethodName The method name to call on each instance in the collection to retrieve
 *            the key
 * @return A map of key to value instances
 * @throws IllegalArgumentException if any of the other paremeters are invalid.
 */
public static <K, V> Map<K, V> asMap(final java.util.Collection<V> coll,
        final Class<K> keyType,
        final Class<V> valueType,
        final String keyMethodName) {

    final HashMap<K, V> map = new HashMap<K, V>();
    Method method = null;

    if (isEmpty(coll)) return map;
    notNull(keyType, Messages.getString(KEY_TYPE_NOT_NULL));
    notNull(valueType, Messages.getString(VALUE_TYPE_NOT_NULL));
    notEmpty(keyMethodName, Messages.getString(KEY_METHOD_NAME_NOT_NULL));

    try {
        // return the Method to invoke to get the key for the map
        method = valueType.getMethod(keyMethodName);
    }
    catch (final NoSuchMethodException e) {
        final String message =
            String.format(
                    Messages.getString(METHOD_NOT_FOUND),
                    keyMethodName,
                    valueType);
        e.fillInStackTrace();
        logger.error(message, e);
        throw new IllegalArgumentException(message, e);
    }
    try {
        for (final V value : coll) {

            Object object;
            object = method.invoke(value);
            @SuppressWarnings("unchecked")
            final K key = (K) object;
            map.put(key, value);
        }
    }
    catch (final Exception e) {
        final String message =
            String.format(
                    Messages.getString(METHOD_CALL_FAILED),
                    method,
                    valueType);
        e.fillInStackTrace();
        logger.error(message, e);
        throw new IllegalArgumentException(message, e);
    }
    return map;
}

2

Вы можете использовать API потоков Java 8.

public class ListToMap {

  public static void main(String[] args) {
    List<User> items = Arrays.asList(new User("One"), new User("Two"), new User("Three"));

    Map<String, User> map = createHashMap(items);
    for(String key : map.keySet()) {
      System.out.println(key +" : "+map.get(key));
    }
  }

  public static Map<String, User> createHashMap(List<User> items) {
    Map<String, User> map = items.stream().collect(Collectors.toMap(User::getId, Function.identity()));
    return map;
  }
}

Для получения более подробной информации посетите: http://codecramp.com/java-8-streams-api-convert-list-map/


1

Пример Java 8 для преобразования List<?>объектов в Map<k, v>:

List<Hosting> list = new ArrayList<>();
list.add(new Hosting(1, "liquidweb.com", new Date()));
list.add(new Hosting(2, "linode.com", new Date()));
list.add(new Hosting(3, "digitalocean.com", new Date()));

//example 1
Map<Integer, String> result1 = list.stream().collect(
    Collectors.toMap(Hosting::getId, Hosting::getName));

System.out.println("Result 1 : " + result1);

//example 2
Map<Integer, String> result2 = list.stream().collect(
    Collectors.toMap(x -> x.getId(), x -> x.getName()));

Код скопирован с:
https://www.mkyong.com/java8/java-8-convert-list-to-map/


1

Как уже говорилось, в Java-8 у нас есть краткое решение от коллекционеров:

  list.stream().collect(
         groupingBy(Item::getKey)
        )

а также вы можете вкладывать несколько групп, передавая другой метод groupingBy в качестве второго параметра:

  list.stream().collect(
         groupingBy(Item::getKey, groupingBy(Item::getOtherKey))
        )

Таким образом, у нас будет многоуровневая карта, например: Map<key, Map<key, List<Item>>>



0

Мне нравится ответ Kango_V, но я думаю, что он слишком сложный. Я думаю, что это проще - может быть, слишком просто. Если вы склонны, вы можете заменить String на общий маркер и заставить его работать для любого типа ключа.

public static <E> Map<String, E> convertListToMap(Collection<E> sourceList, ListToMapConverterInterface<E> converterInterface) {
    Map<String, E> newMap = new HashMap<String, E>();
    for( E item : sourceList ) {
        newMap.put( converterInterface.getKeyForItem( item ), item );
    }
    return newMap;
}

public interface ListToMapConverterInterface<E> {
    public String getKeyForItem(E item);
}

Используется так:

        Map<String, PricingPlanAttribute> pricingPlanAttributeMap = convertListToMap( pricingPlanAttributeList,
                new ListToMapConverterInterface<PricingPlanAttribute>() {

                    @Override
                    public String getKeyForItem(PricingPlanAttribute item) {
                        return item.getFullName();
                    }
                } );

0

Apache Commons MapUtils.populateMap

Если вы не используете Java 8 и по какой-то причине не хотите использовать явный цикл, попробуйте MapUtils.populateMapApache Commons.

MapUtils.populateMap

Скажем, у вас есть список Pairс.

List<ImmutablePair<String, String>> pairs = ImmutableList.of(
    new ImmutablePair<>("A", "aaa"),
    new ImmutablePair<>("B", "bbb")
);

И теперь вы хотите карту Pairключа для Pairобъекта.

Map<String, Pair<String, String>> map = new HashMap<>();
MapUtils.populateMap(map, pairs, new Transformer<Pair<String, String>, String>() {

  @Override
  public String transform(Pair<String, String> input) {
    return input.getKey();
  }
});

System.out.println(map);

дает вывод:

{A=(A,aaa), B=(B,bbb)}

При этом, forцикл, возможно, легче понять. (Это ниже дает тот же результат):

Map<String, Pair<String, String>> map = new HashMap<>();
for (Pair<String, String> pair : pairs) {
  map.put(pair.getKey(), pair);
}
System.out.println(map);
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.