Java: Jak przekonwertować listę na mapę

224

Niedawno rozmawiam z kolegą na temat tego, jaki byłby optymalny sposób konwersji Listna MapJavę i czy są jakieś szczególne korzyści z tego.

Chcę poznać optymalne podejście do konwersji i naprawdę docenię, jeśli ktoś może mnie poprowadzić.

Czy to dobre podejście:

List<Object[]> results;
Map<Integer, String> resultsMap = new HashMap<Integer, String>();
for (Object[] o : results) {
    resultsMap.put((Integer) o[0], (String) o[1]);
}
Rachel
źródło
2
Jaki jest najlepszy optymalny sposób? Optymalizacja odbywa się z uwzględnieniem określonego parametru (prędkości / pamięci).
Daniel Fath,
6
Lista różni się od mapy w sposób koncepcyjny - mapa ma pojęcie pary „klucz, wartość”, natomiast lista nie. Biorąc to pod uwagę, nie jest jasne, w jaki sposób dokonasz konwersji z listy na mapę iz powrotem.
Victor Sorokin,
1
@Daniel: Przez Optimal miałem na myśli najlepszy sposób robienia tego na wiele różnych sposobów, nie jestem pewien, czy dobrze byłoby zobaczyć różne sposoby konwertowania listy na mapę.
Rachel
Przeczytaj ten artykuł: Jak przekonwertować listę na mapę w Javie
Lucky

Odpowiedzi:

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

Zakładając oczywiście, że każdy przedmiot ma getKey()metodę, która zwraca klucz odpowiedniego typu.

Jim Garrison
źródło
1
Możesz także wpisać pozycję na liście.
Jeremy,
@ Jim: Czy muszę ustawić getKey()dowolny parametr?
Rachel
Jaka byłaby wartość na mapie, czy możesz rozwinąć przykład?
Rachel
1
@Rachel - Wartość jest pozycją na liście, a klucz jest czymś, co sprawia, że ​​pozycja jest unikalna, a to zależy od ciebie. Używanie Jima getKey()było arbitralne.
Jeremy
1
Wcześniej znasz rozmiar, dzięki czemu możesz wykonać Map <Klucz, Przedmiot> mapa = nowy HashMap <Klucz, Przedmiot> (list.size ());
Víctor Romero
316

Z , będziesz mógł to zrobić w jednym wierszu za pomocą strumieni i Collectorsklasy.

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

Krótkie demo:

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 + "]";
    }
}

Wynik:

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

Jak zauważono w komentarzach, możesz użyć Function.identity()zamiast item -> item, chociaż uważam, że jest to i -> idość wyraźne.

Aby być kompletnym, pamiętaj, że możesz użyć operatora binarnego, jeśli twoja funkcja nie jest bijective. Rozważmy na przykład to Listi funkcję mapowania, która dla wartości int oblicza wynik modulo 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));

Podczas uruchamiania tego kodu pojawi się komunikat o błędzie java.lang.IllegalStateException: Duplicate key 1. Wynika to z faktu, że 1% 3 jest taki sam jak 4% 3, a zatem mają tę samą wartość klucza, biorąc pod uwagę funkcję mapowania klucza. W takim przypadku możesz podać operator scalania.

Oto suma wartości; (i1, i2) -> i1 + i2;które można zastąpić referencją do metody Integer::sum.

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

która teraz generuje:

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

Mam nadzieję, że to pomoże! :)

Alexis C.
źródło
19
lepsze wykorzystanie Function.identity()zamiastitem -> item
Emmanuel Touzery
1
@EmmanuelTouzery Cóż, Function.identity()powraca t -> t;.
Alexis C.,
2
Jasne, obie działają. Myślę, że to kwestia gustu. Funkcja Function.identity () jest bardziej natychmiast rozpoznawalna.
Emmanuel Touzery,
OP nie obsługuje żadnych pojęć, tylko ciągów i liczb całkowitych, na których nie można calować ::getKey.
phil294
@Blauhirn Wiem, mój przykład opiera się na niestandardowej klasie tuż poniżej. Możesz dowolnie używać dowolnej funkcji do generowania kluczy na podstawie wartości.
Alexis C.
115

Na wypadek, gdyby to pytanie nie zostało zamknięte jako duplikat, właściwą odpowiedzią jest skorzystanie z kolekcji Google :

Map<String,Role> mappedRoles = Maps.uniqueIndex(yourList, new Function<Role,String>() {
  public String apply(Role from) {
    return from.getName(); // or something else
  }});
ripper234
źródło
17
To powinno być na szczycie
Martin Andersson
6
Guava zawiera ściśle zgodny nadzbiór starej, przestarzałej Biblioteki Kolekcje Google . Nie należy już jej używać. ” Może być potrzebna aktualizacja.
Tiny
3
Korzystanie z zewnętrznej biblioteki do tak prostej operacji to przesada. To lub oznaka bardzo słabej standardowej biblioteki. W tym przypadku odpowiedź @ jim-garrison jest całkowicie rozsądna. To smutne, że java nie ma pomocnych metod, takich jak „mapowanie” i „zmniejszanie”, ale nie jest to całkowicie konieczne.
linuxdan
2
To używa Guawy. Niestety Guava działa bardzo wolno na Androidzie, więc tego rozwiązania nie należy używać w projekcie na Androida.
IgorGanapolsky
jeśli element na liście ma zduplikowane roleNames, powyższy kod zgłosi wyjątek,
Junchen Liu
17

Za pomocą Java 8 możesz wykonać następujące czynności:

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

Value może być dowolnym obiektem, którego używasz.

stackFan
źródło
16

Od wersji Java 8 odpowiedź @ZouZou przy użyciu modułu Collectors.toMapzbierającego jest z pewnością idiomatycznym sposobem rozwiązania tego problemu.

A ponieważ jest to tak częste zadanie, możemy przekształcić je w narzędzie statyczne.

W ten sposób rozwiązanie naprawdę staje się jednostronne.

/**
 * 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()));
}

A oto jak możesz go użyć na List<Student>:

Map<Long, Student> studentsById = toMapBy(students, Student::getId);
glts
źródło
W celu omówienia parametrów typu tej metody zobacz moje dalsze pytanie .
glts
Spowoduje to zgłoszenie wyjątku w przypadku duplikatów kluczy. Sth like: Wyjątek w wątku „main” java.lang.IllegalStateException: Duplikat klucza .... Szczegóły: patrz codecramp.com/java-8-streams-api-convert-list-map
EMM
@EMM Oczywiście, zgodnie z przeznaczeniem i udokumentowane w Javadoc.
glts
Tak, zaktualizowano odpowiedź na pytanie o duplikaty. Zapoznaj się. Dzięki
EMM,
10

A Listi Mapsą koncepcyjnie różne. A Listto uporządkowana kolekcja przedmiotów. Elementy mogą zawierać duplikaty, a element może nie mieć pojęcia unikalnego identyfikatora (klucza). A Mapma wartości odwzorowane na klucze. Każdy klucz może wskazywać tylko jedną wartość.

Dlatego, w zależności od twoich Listprzedmiotów, może być lub może nie być możliwe przekonwertowanie go na Map. Czy twoje Listprzedmioty nie mają duplikatów? Czy każdy przedmiot ma unikalny klucz? Jeśli tak, to można umieścić je w Map.

Steve Kuo
źródło
10

Alexis opublikował już odpowiedź w Javie 8 przy użyciu metody toMap(keyMapper, valueMapper). Zgodnie z dokumentacją dotyczącą implementacji tej metody:

Nie ma gwarancji na rodzaj, zmienność, serializację lub bezpieczeństwo wątków zwróconej mapy.

Jeśli więc jesteśmy zainteresowani konkretną implementacją Mapinterfejsu, np. HashMapMożemy użyć przeciążonej formy jako:

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

Chociaż użycie jednego Function.identity()lub i->ijest w porządku, ale wydaje się, że Function.identity()zamiast i -> imoże zaoszczędzić trochę pamięci zgodnie z tą pokrewną odpowiedzią .

akhil_mittal
źródło
1
Zabawny fakt, że w 2019 r. Ogromna liczba ludzi wciąż nie zdaje sobie sprawy, że nie znają prawdziwej implementacji mapy, którą otrzymują dzięki lambda! Właściwie to tylko jedna odpowiedź, którą znalazłem w lambdach Java 8, której użyłbym w produkcji.
Pavlo,
Czy istnieje sposób zbierania danych poprzez określenie typu mapy, ale bez użycia funkcji scalania?
Rosberg Linhares
8

Istnieje również prosty sposób wykonania tego za pomocą Maps.uniqueIndex (...) od Google biblioteki

Andrejs
źródło
5

Metoda uniwersalna

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);
}
xxf
źródło
Jak tego użyć? Co powinienem przekazać jako converterparametr w metodzie?
IgorGanapolsky
4

Bez java-8 będziesz mógł to zrobić w jednej linii kolekcji Commons i klasy 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);
        }
    });
}};
Witalij Oliynyk
źródło
2

Przychodzi na myśl wiele rozwiązań, w zależności od tego, co chcesz osiągnąć:

Każdy element listy jest kluczem i wartością

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

Elementy listy mają coś do sprawdzenia, może nazwę:

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

Elementy listy mają coś, by je sprawdzić, i nie ma gwarancji, że są unikalne: użyj Googles MultiMaps

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

Nadanie wszystkim elementom pozycji jako klucza:

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

...

To naprawdę zależy od tego, co chcesz osiągnąć.

Jak widać na przykładach, Mapa jest mapowaniem klucza na wartość, podczas gdy lista jest tylko serią elementów mających pozycję. Więc po prostu nie są automatycznie konwertowalne.

Daniel
źródło
Ale możemy uznać pozycję elementu listy za klucz i umieścić ich wartość na mapie, czy to dobre rozwiązanie?
Rachel,
AFAIK tak! W JDK nie ma funkcji, która robi to automatycznie, więc musisz rzucić własną.
Daniel
czy można zrobić ostatnią wersję (używając indeksu tablicy jako klucza mapy) ze strumieniami Java 8?
phil294
2

Oto mała metoda, którą napisałem właśnie w tym celu. Korzysta z Validate z Apache Commons.

Możesz go używać.

/**
 * 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;
}
Kango_V
źródło
2

Możesz wykorzystać API strumieniowe 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;
  }
}

Więcej informacji na stronie: http://codecramp.com/java-8-streams-api-convert-list-map/

EMM
źródło
1

Przykład Java 8 do konwersji List<?>obiektów na 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()));

Kod skopiowany z:
https://www.mkyong.com/java8/java-8-convert-list-to-map/

Przenocować
źródło
1

jak już powiedziano, w java-8 mamy zwięzłe rozwiązanie Kolektorów:

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

a także można zagnieździć wiele grup, przekazując inną metodę groupingBy jako drugi parametr:

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

W ten sposób uzyskamy mapę wielopoziomową, taką jak ta: Map<key, Map<key, List<Item>>>

Andrea Scarafoni
źródło
1

Korzystanie ze strumieni java-8

results.stream().collect(Collectors.toMap(e->((Integer)e[0]), e->(String)e[1]));
Ankit Sharma
źródło
0

Podoba mi się odpowiedź Kango_V, ale myślę, że jest zbyt skomplikowana. Myślę, że to jest prostsze - może zbyt proste. Jeśli jest pochylony, możesz zastąpić Łańcuch Generycznym znacznikiem i sprawić, by działał dla dowolnego typu Klucza.

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

Używany w ten sposób:

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

                    @Override
                    public String getKeyForItem(PricingPlanAttribute item) {
                        return item.getFullName();
                    }
                } );
cs94njw
źródło
0

Apache Commons MapUtils.populateMap

Jeśli nie używasz Java 8 i z jakiegoś powodu nie chcesz używać jawnej pętli, spróbuj MapUtils.populateMapz Apache Commons.

MapUtils.populateMap

Powiedz, że masz listę Pairs.

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

A teraz chcesz mapę Pairklucza do Pairobiektu.

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

daje wynik:

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

To powiedziawszy, forpętla może być łatwiejsza do zrozumienia. (To poniżej daje to samo wyjście):

Map<String, Pair<String, String>> map = new HashMap<>();
for (Pair<String, String> pair : pairs) {
  map.put(pair.getKey(), pair);
}
System.out.println(map);
typoerrpr
źródło