LinkedList puesto en Intent extra se redistribuye a ArrayList cuando se recupera en la siguiente actividad

Un comportamiento que estoy observando wrt pasando datos serializables como bash adicional es bastante extraño, y solo quería aclarar si hay algo que no me estoy perdiendo.

Entonces, lo que estaba tratando de hacer es que en ActivtyA puse una instancia de LinkedList en la intent que creé para comenzar la siguiente actividad: ActivityB .

 LinkedList items = (some operation); Intent intent = new Intent(this, ActivityB.class); intent.putExtra(AppConstants.KEY_ITEMS, items); 

En el onCreate de ActivityB , traté de recuperar la LinkedList extra de la siguiente manera:

 LinkedList items = (LinkedList) getIntent() .getSerializableExtra(AppConstants.KEY_ITEMS); 

Al ejecutar esto, repetidamente obtuve una ClassCastException en ActivityB , en la línea de arriba. Básicamente, la excepción decía que estaba recibiendo una ArrayList . Una vez que cambié el código de arriba para recibir una ArrayList , todo funcionó bien.

Ahora no puedo deducir de la documentación existente si este es el comportamiento esperado en Android al pasar implementaciones de listas serializables. O tal vez, hay algo fundamentalmente malo con lo que estoy haciendo.

Gracias.

Puedo decirte por qué sucede esto, pero no te va a gustar 😉

Primero un poco de información de fondo:

Los extras en un Intent son básicamente un Bundle Android que es básicamente un HashMap de pares clave / valor. Entonces cuando haces algo como

 intent.putExtra(AppConstants.KEY_ITEMS, items); 

Android crea un nuevo Bundle para los extras y agrega una entrada de mapa al Bundle donde la clave es AppConstants.KEY_ITEMS y el valor es items (que es su objeto LinkedList).

Todo está bien y bien, y si mirara el paquete de extras después de ejecutar su código, encontrará que contiene una LinkedList . Ahora viene la parte interesante…

Cuando llamas a startActivity() con Intent que contiene extras, Android necesita convertir los extras de un mapa de pares clave / valor en una secuencia de bytes. Básicamente, necesita serializar el paquete . Tiene que hacer eso porque puede iniciar la actividad en otro proceso y para hacerlo necesita serializar / deserializar los objetos en el paquete para que pueda recrearlos en el nuevo proceso. También necesita hacer esto porque Android guarda los contenidos del Intent en algunas tablas del sistema para que pueda regenerar el Intento si lo necesita más tarde.

Para serializar el Bundle en una secuencia de bytes, pasa por el mapa en el paquete y obtiene cada par de clave / valor. Luego toma cada “valor” (que es algún tipo de objeto) e intenta determinar qué tipo de objeto es para poder serializarlo de la manera más eficiente. Para hacer esto, verifica el tipo de objeto contra una lista de tipos de objetos conocidos . La lista de “tipos de objetos conocidos” contiene elementos como Integer , Long , String , Map , Bundle y desafortunadamente también List . Entonces, si el objeto es una List (de la que existen muchos tipos diferentes, incluida LinkedList ) lo serializa y lo marca como un objeto de tipo List .

Cuando el Bundle se deserializa, es decir: cuando haces esto:

 LinkedList items = (LinkedList) getIntent().getSerializableExtra(AppConstants.KEY_ITEMS); 

produce una ArrayList para todos los objetos en la lista de Bundle of type.

No hay nada que puedas hacer para cambiar este comportamiento de Android. Al menos ahora sabes por qué hace esto.

Solo para que lo sepas: de hecho, escribí un pequeño progtwig de prueba para verificar este comportamiento y he examinado el código fuente de Parcel.writeValue(Object v) que es el método que se llama desde Bundle cuando convierte el mapa en un byte. stream.

Nota importante: dado que List es una interfaz, esto significa que cualquier clase que implemente List que ponga en un Bundle aparecerá como ArrayList . También es interesante que Map también esté en la lista de “tipos de objetos conocidos”, lo que significa que no importa qué tipo de objeto Map coloques en un Bundle (por ejemplo, SortedMap , SortedMap o cualquier clase que implemente la interfaz Map ), siempre obtendrá un HashMap de él.

La respuesta de @David Wasser es correcta en términos de diagnóstico del problema. Esta publicación es para compartir cómo lo manejé.

El problema con cualquier objeto List que sale como ArrayList no es horrible, porque siempre puedes hacer algo como

 LinkedList items = new LinkedList<>( (List) intent.getSerializableExtra(KEY)); 

que agregará todos los elementos de la lista deserializada a una nueva LinkedList .

El problema es mucho peor cuando se trata de Map , porque es posible que haya tratado de serializar un LinkedHashMap y ahora haya perdido el orden de los elementos.

Afortunadamente, hay una manera (relativamente) indolora de esto: defina su propia clase de contenedor serializable. Puede hacerlo para tipos específicos o hacerlo genéricamente:

 public class Wrapper  implements Serializable { private T wrapped; public Wrapper(T wrapped) { this.wrapped = wrapped; } public T get() { return wrapped; } } 

Luego puede usar esto para ocultar su List , Map u otro tipo de datos de la verificación de tipo de Android:

 intent.putExtra(KEY, new Wrapper<>(items)); 

y después:

 items = ((Wrapper>) intent.getSerializableExtra(KEY)).get(); 

Si está utilizando la biblioteca IcePick y está teniendo este problema, puede usar la técnica de Ted Hoop con un paquete personalizado para evitar tener que lidiar con las instancias de Wrapper en su código.

 public class LinkedHashmapBundler implements Bundler { @Override public void put(String s, LinkedHashMap val, Bundle bundle) { bundle.putSerializable(s, new Wrapper<>(val)); } @SuppressWarnings("unchecked") @Override public LinkedHashMap get(String s, Bundle bundle) { return ((Wrapper) bundle.getSerializable(s)).get(); } } // Use it like this @State(LinkedHashmapBundler.class) LinkedHasMap map