-1

I have an android application which is showing strange behavior. I have a method which deserializes object from file. Below is the code I'm using:

public static Object readData(Context context, String fileName)
    {
        synchronized (context) {
            ObjectInputStream input = null;
            Object object = null;
            if (fileExists(context, fileName)) {
                try {
                    input = new ObjectInputStream(context.openFileInput(fileName));
                    object = input.readObject();
                    Log.v(Constant.TAG, "Writable object has been loaded from file "+fileName);
                } catch (IOException e) {
                    Log.e(Constant.TAG, e.getMessage(), e);
                } catch (ClassNotFoundException e) {
                    Log.e(Constant.TAG, e.getMessage(), e);
                } finally {
                    try {
                        if (input != null)
                            input.close();
                    } catch (IOException e) {
                        Log.e(Constant.TAG, e.getMessage(), e);
                    }
                }
            }
            return object;
        }
    }

Normally it works well, but when someone minimize my application and after sometime reopens, it crashes. From crash report I found that it's throwing IllegalArgumentException in below line from above code

object = input.readObject();

I gone through the documentation of ObjectInputStream.readObject but it doesn't state circumstances under which it can throw IllegalArgumentException.

It is only happening when user is bringing app from background. It works perfectly well when app starts (by start I mean when app was not running, not even in background).

PS: there are some crash reports which show ClassCastException on the same line, which is even stranger as I'm not casting, only reading into an Object.

Update

Stack trace

java.lang.RuntimeException: 
  at android.app.ActivityThread.performLaunchActivity (ActivityThread.java:2423)
  at android.app.ActivityThread.handleLaunchActivity (ActivityThread.java:2483)
  at android.app.ActivityThread.access$900 (ActivityThread.java:153)
  at android.app.ActivityThread$H.handleMessage (ActivityThread.java:1349)
  at android.os.Handler.dispatchMessage (Handler.java:102)
  at android.os.Looper.loop (Looper.java:148)
  at android.app.ActivityThread.main (ActivityThread.java:5441)
  at java.lang.reflect.Method.invoke (Native Method)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run (ZygoteInit.java:738)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:628)
Caused by: java.lang.IllegalArgumentException: 
  at java.lang.reflect.Field.set (Native Method)
  at java.io.ObjectInputStream.readFieldValues (ObjectInputStream.java:1127)
  at java.io.ObjectInputStream.defaultReadObject (ObjectInputStream.java:454)
  at java.io.ObjectInputStream.readObjectForClass (ObjectInputStream.java:1345)
  at java.io.ObjectInputStream.readHierarchy (ObjectInputStream.java:1242)
  at java.io.ObjectInputStream.readNewObject (ObjectInputStream.java:1835)
  at java.io.ObjectInputStream.readNonPrimitiveContent (ObjectInputStream.java:761)
  at java.io.ObjectInputStream.readObject (ObjectInputStream.java:1983)
  at java.io.ObjectInputStream.readObject (ObjectInputStream.java:1940)
  at com.pixyfisocial.pixyfi.util.IOUtil.readData (IOUtil.java:245)
  at com.pixyfisocial.Login.getLoggedInUserInfoFromCache (Login.java:313)
  at com.pixyfisocial.Login.startApp (Login.java:124)
  at com.pixyfisocial.Login.onCreate (Login.java:98)
  at android.app.Activity.performCreate (Activity.java:6303)
  at android.app.Instrumentation.callActivityOnCreate (Instrumentation.java:1108)
  at android.app.ActivityThread.performLaunchActivity (ActivityThread.java:2376)
Kumar Gaurav
  • 1,287
  • 3
  • 19
  • 47
  • 2
    Please provide complete (unedited) exception messages and stacktraces – Stephen C Sep 04 '17 at 03:48
  • I do not understand reason to downvote the question. – Kumar Gaurav Sep 05 '17 at 10:37
  • I didn't downvote. However, I can see a variety of reasons for downvoting, including not including an MVCE, and taking a lonnnng time to post the stacktrace. – Stephen C Sep 05 '17 at 10:43
  • I dint meant you. Just wanted to know the reason – Kumar Gaurav Sep 05 '17 at 13:09
  • You asked this in a (now deleted) "answer": *"How can i control it? From my side object is being written from app. That wont change representation for sure"* – Stephen C Sep 05 '17 at 13:42
  • First of all, as we have said, the **evidence** that you have shown us points to the representation having changed. If you want further help, you are going to **have to** provide us with an MVCE; see https://stackoverflow.com/help/mcve. That will allow us to try it out for ourselves, and figure out what is actually happening. – Stephen C Sep 05 '17 at 13:46

1 Answers1

2

A cursory examination of the sourcecode indicates that IllegalArgumentException is typically thrown in ObjectInputStream when there is a mismatch between the serialized object representation and what the reading class is expecting. For example, you may have incompatible custom readObject and writeObject methods. Or you may have made a binary-incompatible1 change to the object representation without changing hard-coded serialVersionUID numbers or implementing custom methods to deal with this2.

There will be more clues in your stacktraces ... and in the source code of ObjectInputStream.

The ClassCastExceptions could be another manifestation of this.


1 - Changes leading to semantic incompatibility are a different matter. They won't lead to an IllegalArgumentException, but you should do something about them anyway.

2 - If you want to cope with incompatibilities, then you probably don't want to change the serialVerionUID. If you do, then you will need to do "clever things" with classloading and multiple versions of the class. But the flip-side is that if your code needs to cope with multiple representations that have the same serialVersionUID, the representation version must be deducible from the representation itself. This requires planning.


FOLLOW-UP

I took your stacktrace and tried to match it against the Android source code available at https://android.googlesource.com

The line numbers don't match exactly, but I thinkthe problem is happening in the method below. Specifically the line I have labeled "HERE". According to the javadoc for Field.set:

  1. If the specified object argument is not an instance of the class or interface declaring the underlying field, the method throws an IllegalArgumentException.

  2. If the underlying field is of a primitive type, an unwrapping conversion is attempted to convert the new value to a value of a primitive type. If this attempt fails, the method throws an IllegalArgumentException.

  3. If, after possible unwrapping, the new value cannot be converted to the type of the underlying field by an identity or widening conversion, the method throws an IllegalArgumentException.

One of these three things is happening. It is not possible to say which one ... unless you provide a full working MCVE (that someone can run on an Android emulator!) ... but the indications point to you having (somehow) broken the serialization compatibility rules.

Note, since the line numbers didn't match, I cannot say with certainty that the Android you are using matches this following. If you want to be sure, you need to search the history in the GIT repo to find a matching version .... or look in the vendor specific source code bundle / repo for your device.

/**
 * Reads a collection of field values for the class descriptor
 * {@code classDesc} (an {@code ObjectStreamClass}). The
 * values will be used to set instance fields in object {@code obj}.
 * This is the default mechanism, when emulated fields (an
 * {@code GetField}) are not used. Actual values to load are stored
 * directly into the object {@code obj}.
 *
 * @param obj
 *            Instance in which the fields will be set.
 * @param classDesc
 *            A class descriptor (an {@code ObjectStreamClass})
 *            defining which fields should be loaded.
 *
 * @throws IOException
 *             If an IO exception happened when reading the field values.
 * @throws InvalidClassException
 *             If an incompatible type is being assigned to an emulated
 *             field.
 * @throws OptionalDataException
 *             If optional data could not be found when reading the
 *             exception graph
 * @throws ClassNotFoundException
 *             If a class of an object being de-serialized can not be found
 *
 * @see #readFields
 * @see #readObject()
 */
private void readFieldValues(Object obj, ObjectStreamClass classDesc) throws OptionalDataException, ClassNotFoundException, IOException {
    // Now we must read all fields and assign them to the receiver
    ObjectStreamField[] fields = classDesc.getLoadFields();
    fields = (fields == null) ? ObjectStreamClass.NO_FIELDS : fields;
    Class<?> declaringClass = classDesc.forClass();
    if (declaringClass == null && mustResolve) {
        throw new ClassNotFoundException(classDesc.getName());
    }
    for (ObjectStreamField fieldDesc : fields) {
        Field field = classDesc.getReflectionField(fieldDesc);
        if (field != null && Modifier.isTransient(field.getModifiers())) {
            field = null; // No setting transient fields! (http://b/4471249)
        }
        // We may not have been able to find the field, or it may be transient, but we still
        // need to read the value and do the other checking...
        try {
            Class<?> type = fieldDesc.getTypeInternal();
            if (type == byte.class) {
                byte b = input.readByte();
                if (field != null) {
                    field.setByte(obj, b);
                }
            } else if (type == char.class) {
                char c = input.readChar();
                if (field != null) {
                    field.setChar(obj, c);
                }
            } else if (type == double.class) {
                double d = input.readDouble();
                if (field != null) {
                    field.setDouble(obj, d);
                }
            } else if (type == float.class) {
                float f = input.readFloat();
                if (field != null) {
                    field.setFloat(obj, f);
                }
            } else if (type == int.class) {
                int i = input.readInt();
                if (field != null) {
                    field.setInt(obj, i);
                }
            } else if (type == long.class) {
                long j = input.readLong();
                if (field != null) {
                    field.setLong(obj, j);
                }
            } else if (type == short.class) {
                short s = input.readShort();
                if (field != null) {
                    field.setShort(obj, s);
                }
            } else if (type == boolean.class) {
                boolean z = input.readBoolean();
                if (field != null) {
                    field.setBoolean(obj, z);
                }
            } else {
                Object toSet = fieldDesc.isUnshared() ? readUnshared() : readObject();
                if (toSet != null) {
                    // Get the field type from the local field rather than
                    // from the stream's supplied data. That's the field
                    // we'll be setting, so that's the one that needs to be
                    // validated.
                    String fieldName = fieldDesc.getName();
                    ObjectStreamField localFieldDesc = classDesc.getField(fieldName);
                    Class<?> fieldType = localFieldDesc.getTypeInternal();
                    Class<?> valueType = toSet.getClass();
                    if (!fieldType.isAssignableFrom(valueType)) {
                        throw new ClassCastException(classDesc.getName() + "." + fieldName + " - " + fieldType + " not compatible with " + valueType);
                    }
                    if (field != null) {
                        field.set(obj, toSet);  // <<< --- HERE
                    }
                }
            }
        } catch (IllegalAccessException iae) {
            // ObjectStreamField should have called setAccessible(true).
            throw new AssertionError(iae);
        } catch (NoSuchFieldError ignored) {
        }
    }
}
Community
  • 1
  • 1
Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
  • 'Or you may have changed object representations without changing hard-coded `serialVersionUID` numbers': this is what he *should* do, unless he has actually introduced a serialization-incompatibility, which is harder to do than generally thought, and if he's done that he should be writing custom `readObject()/writeObject()/readResolve()/writeReplace()` methods rather then upsetting the `serialVersionUID` if at all possible. Your answer makes it sound like the `serialVersionUID` should be updated on every object representation change, which is certainly not the case. – user207421 Sep 04 '17 at 05:12
  • I was talking in terms of the cause. I wasn't implying that you *should* change the UID. But I have clarified anyway. – Stephen C Sep 04 '17 at 05:52
  • readData method that i posted is used for reading from different files deserializing different object. When i'm within the method readData i don't know the type, hence i'm deserializing into Object. Does it really matter what object is in the file as log as I'm reading it into object of Object class? Another thing to notice, I have hardcoded serialVersionUID in every Serializable class. – Kumar Gaurav Sep 04 '17 at 09:38
  • 1) No. That doesn't matter. 2) Hard-coding the serialVersionUID is relevant, but not the (likely) cause of your problems. The likely cause is that you have made changes to the representation of one of your types that has broken serialization compatibility; see https://docs.oracle.com/javase/7/docs/platform/serialization/spec/version.html – Stephen C Sep 04 '17 at 13:02
  • But that also is highly unlikely as it is happening in android App, the same app is writing data and then reading it. How can representation of the class change in an App? – Kumar Gaurav Sep 04 '17 at 15:28
  • 1
    Because you updated the app? Another explanation is that you have implemented custom readObject / writeObject methods and they are not compatible. Anyhow, the clues are in your codebase or in the stacktraces. – Stephen C Sep 04 '17 at 15:37
  • I don't have custom implementation of readOject or writeOject. I have updated post with stacktrace – Kumar Gaurav Sep 04 '17 at 16:59
  • This also exposes a bug in Android. It should throw an `InvalidClassException`, as the Oracle JRE does in such a circumstance. – user207421 Sep 05 '17 at 03:32
  • Er um ... maybe. You could also argue that Android is not Java, and therefore differences in behavior are not (necessarily) bugs. – Stephen C Sep 05 '17 at 03:34
  • So does that mean I have to handle this gracefully as there is no way to avoid it? – Kumar Gaurav Sep 05 '17 at 05:07
  • Well not exactly. There >>is<< a way to avoid it. Two actually. 1) Don't make incompatible representation changes. 2) Implement serialized representation versioning in using writeObject / readObject methods, and .... discipline. (Or thorough testing that will alert you to the fact that you are about to break serialization for older versions ... before you publish your updated app.) – Stephen C Sep 05 '17 at 05:18