Как по-умному записать Java.util.Map в посылку?

У меня есть общая карта строк (ключ, значение), и это поле является частью компонента, который мне нужно разделить. Итак, я мог бы использовать метод Parcel#writeMap. Документ API говорит:

Вместо этого используйте writeBundle(Bundle). Сглаживает Map в участок в текущем dataPosition(), увеличивая dataCapacity() при необходимости. Ключи Map должны быть объектами String. Значения Map записываются с использованием writeValue(Object) и должны следовать указанной там спецификации. Настоятельно рекомендуется использовать writeBundle(Bundle) вместо этого метода, так как класс Bundle предоставляет типобезопасный API, который позволяет избежать загадочных ошибок типов в момент сортировки.

Итак, я мог бы перебрать каждую запись на моей карте и поместить ее в пакет, но я все еще ищу более разумный способ сделать это. Есть ли какой-либо метод в Android SDK, который мне не хватает?

На данный момент делаю так:

final Bundle bundle = new Bundle();
final Iterator<Entry<String, String>> iter = links.entrySet().iterator();
while(iter.hasNext())
{
    final Entry<String, String>  entry =iter.next();
    bundle.putString(entry.getKey(), entry.getValue());
}
parcel.writeBundle(bundle);

person Kitesurfer    schedule 24.11.2011    source источник
comment
Я предложил отредактировать цитату API. щас неудобно листать по горизонтали, чтобы все это прочитать.   -  person STT LCU    schedule 24.11.2011
comment
вам удалось найти решение этой проблемы? если да, пожалуйста, опубликуйте свое решение или выберите лучший ответ здесь.   -  person STT LCU    schedule 22.10.2013


Ответы (7)


В итоге сделал немного по другому. Это следует шаблону, который вы ожидаете от работы с Parcelables, поэтому он должен быть вам знаком.

public void writeToParcel(Parcel out, int flags){
  out.writeInt(map.size());
  for(Map.Entry<String,String> entry : map.entrySet()){
    out.writeString(entry.getKey());
    out.writeString(entry.getValue());
  }
}

private MyParcelable(Parcel in){
  //initialize your map before
  int size = in.readInt();
  for(int i = 0; i < size; i++){
    String key = in.readString();
    String value = in.readString();
    map.put(key,value);
  }
}

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

person Anthony Naddeo    schedule 12.01.2013
comment
Использование entrySet вместо keySet должно быть более эффективным: вы избегаете поиска значения на карте для каждой записи. - person Vincent Mimoun-Prat; 28.07.2014
comment
@MarvinLabs прав. Этот ответ можно проверить для справки. - person Sufian; 14.01.2015

можешь попробовать:

bundle.putSerializable(yourSerializableMap);

если выбранная вами карта реализует сериализуемость (например, HashMap), а затем вы можете легко использовать свой writeBundle

person STT LCU    schedule 24.11.2011
comment
это может быть не самое лучшее с точки зрения эффективности, но, конечно, это коротко, чтобы написать :) - person STT LCU; 23.04.2012

Если и key, и value карты расширяют Parcelable, у вас может быть довольно изящное универсальное решение для этого:

Код

// For writing to a Parcel
public <K extends Parcelable,V extends Parcelable> void writeParcelableMap(
        Parcel parcel, int flags, Map<K, V > map)
{
    parcel.writeInt(map.size());
    for(Map.Entry<K, V> e : map.entrySet()){
        parcel.writeParcelable(e.getKey(), flags);
        parcel.writeParcelable(e.getValue(), flags);
    }
}

// For reading from a Parcel
public <K extends Parcelable,V extends Parcelable> Map<K,V> readParcelableMap(
        Parcel parcel, Class<K> kClass, Class<V> vClass)
{
    int size = parcel.readInt();
    Map<K, V> map = new HashMap<K, V>(size);
    for(int i = 0; i < size; i++){
        map.put(kClass.cast(parcel.readParcelable(kClass.getClassLoader())),
                vClass.cast(parcel.readParcelable(vClass.getClassLoader())));
    }
    return map;
}

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

// MyClass1 and MyClass2 must extend Parcelable
Map<MyClass1, MyClass2> map;

// Writing to a parcel
writeParcelableMap(parcel, flags, map);

// Reading from a parcel
map = readParcelableMap(parcel, MyClass1.class, MyClass2.class);
person bcorso    schedule 28.08.2014
comment
Как насчет реализации Parcelable в aClass? Потому что Parcelable — это интерфейс в Android - person Jemshit Iskenderov; 20.02.2017
comment
компилятор показывает ошибку Interfered type parameter java.lang.String for type parameter K is not within its bound, should implement android.os.Parcelable - person Choletski; 05.06.2017
comment
@Choletski Эта ошибка ожидается, поскольку String не реализует Parcelable. Это решение работает, только если оба типа K и V реализуют Parcelable (см. первое предложение). - person bcorso; 10.06.2017

Хороший вопрос. В API нет никаких известных мне методов, кроме putSerializable и writeMap. Сериализация не рекомендуется по соображениям производительности, и writeMap() также не рекомендуется по несколько загадочным причинам, как вы уже указали вне.

Сегодня мне нужно было упаковать HashMap, поэтому я попробовал свои силы в написании некоторых служебных методов для отправки Map в Bundle и из Bundle рекомендуемым способом:

// Usage:

// read map into a HashMap<String,Foo>
links = readMap(parcel, Foo.class);

// another way that lets you use a different Map implementation
links = new SuperDooperMap<String, Foo>;
readMap(links, parcel, Foo.class);

// write map out
writeMap(links, parcel);

////////////////////////////////////////////////////////////////////
// Parcel methods

/**
 * Reads a Map from a Parcel that was stored using a String array and a Bundle.
 *
 * @param in   the Parcel to retrieve the map from
 * @param type the class used for the value objects in the map, equivalent to V.class before type erasure
 * @return     a map containing the items retrieved from the parcel
 */
public static <V extends Parcelable> Map<String,V> readMap(Parcel in, Class<? extends V> type) {

    Map<String,V> map = new HashMap<String,V>();
    if(in != null) {
        String[] keys = in.createStringArray();
        Bundle bundle = in.readBundle(type.getClassLoader());
        for(String key : keys)
            map.put(key, type.cast(bundle.getParcelable(key)));
    }
    return map;
}


/**
 * Reads into an existing Map from a Parcel that was stored using a String array and a Bundle.
 *
 * @param map  the Map<String,V> that will receive the items from the parcel
 * @param in   the Parcel to retrieve the map from
 * @param type the class used for the value objects in the map, equivalent to V.class before type erasure
 */
public static <V extends Parcelable> void readMap(Map<String,V> map, Parcel in, Class<V> type) {

    if(map != null) {
        map.clear();
        if(in != null) {
            String[] keys = in.createStringArray();
            Bundle bundle = in.readBundle(type.getClassLoader());
            for(String key : keys)
                map.put(key, type.cast(bundle.getParcelable(key)));
        }
    }
}


/**
 * Writes a Map to a Parcel using a String array and a Bundle.
 *
 * @param map the Map<String,V> to store in the parcel
 * @param out the Parcel to store the map in
 */
public static void writeMap(Map<String,? extends Parcelable> map, Parcel out) {

    if(map != null && map.size() > 0) {
        /*
        Set<String> keySet = map.keySet();
        Bundle b = new Bundle();
        for(String key : keySet)
            b.putParcelable(key, map.get(key));
        String[] array = keySet.toArray(new String[keySet.size()]);
        out.writeStringArray(array);
        out.writeBundle(b);
        /*/
        // alternative using an entrySet, keeping output data format the same
        // (if you don't need to preserve the data format, you might prefer to just write the key-value pairs directly to the parcel)
        Bundle bundle = new Bundle();
        for(Map.Entry<String, ? extends Parcelable> entry : map.entrySet()) {
            bundle.putParcelable(entry.getKey(), entry.getValue());
        }

        final Set<String> keySet = map.keySet();
        final String[] array = keySet.toArray(new String[keySet.size()]);
        out.writeStringArray(array);
        out.writeBundle(bundle);
        /**/
    }
    else {
        //String[] array = Collections.<String>emptySet().toArray(new String[0]);
        // you can use a static instance of String[0] here instead
        out.writeStringArray(new String[0]);
        out.writeBundle(Bundle.EMPTY);
    }
}

Редактировать: изменено writeMap для использования entrySet при сохранении того же формата данных, что и в моем исходном ответе (показан с другой стороны переключающего комментария). Если вам не нужна или вы хотите сохранить совместимость чтения, может быть проще просто хранить пары ключ-значение на каждой итерации, как в ответах @bcorso и @Anthony Naddeo.

person Lorne Laliberte    schedule 19.04.2012
comment
Вы можете улучшить это с помощью пустой проверки карты и использовать Bundle.empty в этом случае. - person Kitesurfer; 21.04.2012
comment
@Kitesurfer Да, это позволит избежать создания пустого пакета (он просто использует статический экземпляр). Есть ли какие-либо другие преимущества использования Bundle.empty по сравнению с new Bundle()? - person Lorne Laliberte; 21.04.2012
comment
это не должно. Я никогда не пробовал это, но как пустой пакет он должен быть неизменным, документы ничего не говорят об этом, так что будьте немного осторожны. Таким образом, вы немного помогаете GC/Runtime.... - person Kitesurfer; 21.04.2012
comment
@Kitesurfer спасибо за подсказку, я переписал функцию writeMap, чтобы сделать это (и в этом случае просто написал пустую строку [0] для набора ключей). - person Lorne Laliberte; 21.04.2012
comment
Использование entrySet вместо keySet должно быть более эффективным: вы избегаете поиска значения на карте для каждой записи. - person Vincent Mimoun-Prat; 28.07.2014
comment
@Vincent Да, использование entrySet было бы немного эффективнее. Я отредактировал код, чтобы показать этот подход (сохранив формат вывода, чтобы упростить совместимость чтения, если кто-то использовал исходную версию). - person Lorne Laliberte; 15.07.2015

Если ключ вашей карты - String, вы можете просто использовать Bundle, как указано в javadocs:

/**
 * Please use {@link #writeBundle} instead.  Flattens a Map into the parcel
 * at the current dataPosition(),
 * growing dataCapacity() if needed.  The Map keys must be String objects.
 * The Map values are written using {@link #writeValue} and must follow
 * the specification there.
 *
 * <p>It is strongly recommended to use {@link #writeBundle} instead of
 * this method, since the Bundle class provides a type-safe API that
 * allows you to avoid mysterious type errors at the point of marshalling.
 */
public final void writeMap(Map val) {
    writeMapInternal((Map<String, Object>) val);
}

Поэтому я написал следующий код:

private void writeMapAsBundle(Parcel dest, Map<String, Serializable> map) {
    Bundle bundle = new Bundle();
    for (Map.Entry<String, Serializable> entry : map.entrySet()) {
        bundle.putSerializable(entry.getKey(), entry.getValue());
    }
    dest.writeBundle(bundle);
}

private void readMapFromBundle(Parcel in, Map<String, Serializable> map, ClassLoader keyClassLoader) {
    Bundle bundle = in.readBundle(keyClassLoader);
    for (String key : bundle.keySet()) {
        map.put(key, bundle.getSerializable(key));
    }
}

Соответственно можно использовать Parcelable вместо Serializable

person repitch    schedule 04.04.2018

Вот моя несколько простая, но пока работающая для меня реализация в Котлине. Его можно легко изменить, если он не удовлетворяет потребности

Но не забывайте, что K,V должно быть Parcelable, если оно отличается от обычного String, Int,... и т. д.

Писать

parcel.writeMap(map)

Читать

parcel.readMap(map)

Чтение перегружено

fun<K,V> Parcel.readMap(map: MutableMap<K,V>) : MutableMap<K,V>{

    val tempMap = LinkedHashMap<Any?,Any?>()
    readMap(tempMap, map.javaClass.classLoader)

    tempMap.forEach {
        map[it.key as K] = it.value as V
    }
    /* It populates and returns the map as well
       (useful for constructor parameters inits)*/
    return map
}
person Jocky Doe    schedule 02.03.2018
comment
у вас есть расширение для writeMap? - person mochadwi; 29.05.2020

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

@FunctionalInterface
public interface ParcelWriter<T> {
    void writeToParcel(@NonNull final T value,
                       @NonNull final Parcel parcel, final int flags);
}

@FunctionalInterface
public interface ParcelReader<T> {
    T readFromParcel(@NonNull final Parcel parcel);
}

public static <K, V> void writeParcelableMap(
        @NonNull final Map<K, V> map,
        @NonNull final Parcel parcel,
        final int flags,
        @NonNull final ParcelWriter<Map.Entry<K, V>> parcelWriter) {
    parcel.writeInt(map.size());

    for (final Map.Entry<K, V> e : map.entrySet()) {
        parcelWriter.writeToParcel(e, parcel, flags);
    }
}

public static <K, V> Map<K, V> readParcelableMap(
        @NonNull final Parcel parcel,
        @NonNull final ParcelReader<Map.Entry<K, V>> parcelReader) {
    int size = parcel.readInt();
    final Map<K, V> map = new HashMap<>(size);

    for (int i = 0; i < size; i++) {
        final Map.Entry<K, V> value = parcelReader.readFromParcel(parcel);
        map.put(value.getKey(), value.getValue());
    }
    return map;
}

Он более подробный, но универсальный. Вот использование записи:

writeParcelableMap(map, dest, flags, (mapEntry, parcel, __) -> {
        parcel.write...; //key from mapEntry
        parcel.write...; //value from mapEntry
    });

и читать:

map = readParcelableMap(in, parcel ->
    new AbstractMap.SimpleEntry<>(parcel.read... /*key*/, parcel.read... /*value*/)
);
person bio007    schedule 23.04.2019