如何以巧妙的方式将Java.util.Map写入包裹?

65
我有一个字符串的通用映射(键,值),这个字段是Bean的一部分,我需要使其可被分解。 因此,我可以使用Parcel#writeMap方法。API文档说: 请改用writeBundle(Bundle)。将Map展开为当前dataPosition()处的包裹,如果需要则增加dataCapacity()。 Map键必须是String对象。 Map值使用writeValue(Object)写入,必须遵循该规范。 强烈建议使用writeBundle(Bundle)而不是此方法,因为Bundle类提供了类型安全的API,允许您在编组点避免神秘的类型错误。 那么,我可以迭代每个Map条目,并将其放入Bundle中,但我仍在寻找更智能的方法来实现。 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);

我已经为 API 引用提出了一个编辑建议。现在横向滚动阅读它非常不舒适。 - STT LCU
3
你是否已经成功解决了这个问题?如果是的话,请发布你的解决方案或在这里选择最佳答案。 - STT LCU
7个回答

90

我的做法有点不同。它遵循处理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中提取之后键将按相同的顺序出现。


6
使用entrySet而不是keySet应该更高效:这样你就可以避免在映射中每个条目中查找值。 - Vincent Mimoun-Prat
@MarvinLabs 是正确的。可以查看这个答案作为参考。 - Sufian

28

你可以尝试:

bundle.putSerializable(yourSerializableMap);

如果你选择的地图实现了Serializable接口(例如HashMap),那么你可以轻松使用writeBundle方法。


5
可能在效率方面不是最佳的选择,但是写起来确实很简短 :) - STT LCU

25

如果 map 的 keyvalue 都扩展自 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);

一个类实现Parcelable接口怎么样?因为Parcelable是Android中的接口。 - Jemshit Iskenderov
编译器显示错误:“干扰类型参数java.lang.String对于类型参数K不在其范围内,应该实现android.os.Parcelable”。 - Choletski
@Choletski 这个错误是可以预料的,因为String没有实现Parcelable接口。只有在K和V类型都实现了Parcelable接口时,这个解决方案才有效(请参见第一句话)。 - bcorso

12

很好的问题。除了putSerializable和writeMap之外,我不知道API中是否存在其他方法。由于性能原因,不建议使用序列化,详情请参见此处。正如您指出的那样,writeMap()也不被推荐,原因有些神秘。

今天我需要将一个HashMap打包,所以尝试编写了一些实用程序方法,以推荐的方式将Map打包到Bundle中,从而实现Map与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的答案中那样。


在这种情况下,您可以通过对映射进行空检查并在空的情况下使用Bundle.empty来改进它。 - Kitesurfer
@Kitesurfer 是的,这样可以避免构建空包(它只会使用静态实例)。在使用 Bundle.empty 和 new Bundle() 之间还有其他优点吗? - Lorne Laliberte
它应该不会这样做。我从未尝试过,但作为空Bundle时,它应该是不可变的,文档没有提到任何关于此的内容,所以要小心一点。这样可以帮助GC/Runtime稍微减轻一些负担... - Kitesurfer
@Kitesurfer 感谢你的建议,我已经重写了writeMap函数来实现这一点(在这种情况下,也将只写入一个空的String[0]作为keyset)。 - Lorne Laliberte
使用entrySet而不是keySet应该更有效率:您避免了每个条目在映射中的值查找。 - Vincent Mimoun-Prat
@Vincent 是的,使用 entrySet 会更有效率一些。我编辑了代码以展示这种方法(保留输出格式以简化读取兼容性,如果有人使用原始版本)。 - Lorne Laliberte

1
如果您的地图密钥是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。

0

这是我在 Kotlin 中实现的一个相对简单但目前为止运行良好的代码。如果不满足需求,它可以很容易地进行修改。

但不要忘记,如果 K,V 不同于通常的 String, Int,... 等类型,则必须是 Parcelable

写吧。

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
}

1
你也有 writeMap 的扩展吗? - mochadwi

0
这里提到的所有解决方案都是有效的,但没有一个是普适的。通常情况下,您会有包含字符串、整数、浮点数等值和/或键的映射。在这种情况下,您不能使用<... 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*/)
);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接