How to properly Serialize/Deserialize an ArrayList with Custom Objects?

118 views Asked by At

I'm learning how to serialize in Java

my problem I'm having is the code works but I get warnings, I think it's the way I cast it. I'm worried it will give me a problem as I progress.

Unchecked cast from Object to ArrayList warning.

I'm trying to create a account registrations for my simple project.

public class BankAccount implements Serializable {
    
    public String username;
    public String password;
    
     public BankAccount() {
        super();
        // TODO Auto-generated constructor stub
    }

    public BankAccount(String username, String password) {
        super();
        this.username = username;
        this.password = password;
    } 
}

Serialization

BankAccount acc1 = new BankAccount("myuser", "mypass");
BankAccount acc2 = new BankAccount("abcd" , "1234");
BankAccount acc3 = new BankAccount("useracbc" , "pw1234");
ArrayList<BankAccount> accounts = new ArrayList<>();
accounts.add(acc1);
accounts.add(acc2);
accounts.add(acc3);
try {
FileOutputStream fileOut = new FileOutputStream("account.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(accounts);
out.close();
System.out.println("Account successfully serialized");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

Deserialization

ArrayList<BankAccount> myList = null;
try {
FileInputStream fileIn = new FileInputStream("account.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
myList = (ArrayList<BankAccount>) in.readObject();

fileIn.close();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
for(BankAccount list: myList) {
System.out.println(list.username);
System.out.println(list.password);
}

I tried this one from howtodoinjava but I still get the same warning from the casting.

ArrayList<Employee> employeesList = null;

try (FileInputStream fis = new FileInputStream("employeeData");
    ObjectInputStream ois = new ObjectInputStream(fis);) {

  employeesList = (ArrayList) ois.readObject();

} catch (IOException ioe) {
  ioe.printStackTrace();
} catch (ClassNotFoundException c) {
  System.out.println("Class not found");
  c.printStackTrace();
}

//Verify list data
for (Employee employee : employeesList) {
  System.out.println(employee);
}
3

There are 3 answers

9
Konrad Neitzel On

Regarding unchecked warning: One solution could be to simply do the checks first:

But with the generic type erasure, you have to check the ArrayList type and also all elements. So the code with checks could be something like:

    ArrayList<Employee> employeesList = new ArrayList<>();

    try (FileInputStream fis = new FileInputStream("employeeData");
         ObjectInputStream ois = new ObjectInputStream(fis);) {

        Object readObject = ois.readObject();
        if (readObject instanceof ArrayList readArray) {
            for (Object element: readArray) {
                if (element instanceof Employee employee) {
                    employeesList.add(employee);
                } else {
                    // Handle here, that your are loading a file that is not what expected, e.g. throw an exception.
                }
            }
        } else {
            // Handle here, that your are loading a file that is not what expected, e.g. throw an exception.
        }
    } catch (IOException ioe) {
        ioe.printStackTrace();
    } catch (ClassNotFoundException c) {
        System.out.println("Class not found");
        c.printStackTrace();
    }

That way you only do checked casts. And be aware: I did no error handling. You should handle the case that the read object is not an ArrayList or does not contain Employee elements.

So this could be valid solution to get rid of the warning. But that is not a solution I would recommend!

Instead: Create your own class Employees. That will store the List internally and will also contain all code required to access the Employees. That way you write an instance of that class and you only have to check one instanceOf instead of checking all employees.

2
Konrad Neitzel On

The warning is there because the cast cannot be verified by the compiler, and there is no runtime checks either. So the warning is valid, the only advice is:

Do not simply cast an ArrayList with a specific Generic to your target type. Because of the way Generics work in Java, you might now have a ArrayList<MyObject> but nothing guarantees that it also holds just MyObjects. It could hold any Object and you'll only notice when trying to access it.

Use encapsulation - so have your own class that keeps explicit references (without Generics). If you serialize that, you just have one check only (If you do not want the warning) or you get the ClassCastException when loading the data.

And also be aware: Serialization to JSON or XML (using libraries like Jackson, gson, ...) might be much better in most situations.

Quick test that shows, that you get an ClassCastException when accessing the elements if you simply did a cast on ArrayList. (The reason behind this is the generic type erasure.)

So a quick unit test that shows the ClassCastException when accessing the element of the ArrayList:

package de.kneitzel.so;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.awt.*;
import java.io.*;
import java.util.ArrayList;

import static org.junit.jupiter.api.Assertions.*;

public class BinarySerializationFailure {

    @Test
    public void testBinarySerialization() {
        ArrayList<String> objects = new ArrayList<>();
        objects.add("Some String");

        try ( FileOutputStream fileOut = new FileOutputStream("account.ser");
                ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
            out.writeObject(objects);
        } catch (Exception ex) {
            fail("Exception!", ex);
        }

        ArrayList<Point> loadedList = null;
        try (
                FileInputStream fileIn = new FileInputStream("account.ser");
                ObjectInputStream in = new ObjectInputStream(fileIn)) {
            loadedList = (ArrayList<Point>) in.readObject();
        } catch (Exception ex) {
            fail("Exception!", ex);
        }

        // Assert, that we was able to load the data.
        assertEquals(1, loadedList.size());

        // So whenever we try to access, we get an exception:
        ArrayList<Point> finalLoadedList = loadedList;
        assertThrows(ClassCastException.class, () -> {
            Point point = finalLoadedList.get(0);
        });
    }
}

I hope that it is now easier to understand because I didn't put important wording below the code.

0
DRIVER-LONG-Q On
  1. Custom objects implement the Serializable API

  2. Handling transient keywords: If there are properties in your class that you don't want to be serialized, you can tag them with transient keywords.

  3. Use ObjectOutputStream to serialize the ArrayList to a file and ObjectInputStream to deserialize from a file.

  4. A serialVersionUID is explicitly declared in the class, Preventing class structure changes that cause deserialization to fail

  5. About the Unchecked cast from Object to ArrayList warning, you can use the try-with-resources statement introduced in Java 7 can be combined with the instanceof check

  6. This kind of warning is acceptable, but adding type-checking makes the code more robust