Sitelet https://howtodoinjava.com/java/collections/arraylist/serialize-deserialize-arraylist/

Serialize and Deserialize an ArrayList in Java (Examples)

Serialize an ArrayList to a file or a byte array with ObjectOutputStream, read it back with ObjectInputStream, handle the unchecked cast, and see when JSON with Jackson is the better choice.

ArrayList

To serialize an ArrayList in Java, we pass the list to ObjectOutputStream.writeObject(). To read the list back, we cast the result of ObjectInputStream.readObject() to a List. Serialization turns an object into bytes, and deserialization reads the bytes and builds a new object from them.

We serialize a list when we want to store it in a file or a cache, or send it to another Java program, and read the same list back later. ArrayList already implements Serializable (the interface that allows Java to write an object as bytes), so the only requirement is that every element is Serializable too.

The following example writes a list of two employees to a file and reads the list back, with the result of each line as a comment.

ArrayList<Employee> employees = new ArrayList<>();     // Employee implements Serializable
employees.add(new Employee(1L, "lokesh", "gupta"));
employees.add(new Employee(2L, "brian", "motto"));

// 1. Serialize: list -> bytes in a file
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("employeeData"))) {
  oos.writeObject(employees);                             // employeeData = 327 bytes
}

// 2. Deserialize: bytes -> a new list
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("employeeData"))) {
  @SuppressWarnings("unchecked")
  List<Employee> copy = (List<Employee>) ois.readObject();
  // [Employee(id=1, firstName=lokesh, lastName=gupta), Employee(id=2, firstName=brian, lastName=motto)]

  boolean equal = copy.equals(employees);                 // true
  boolean sameObject = copy == employees;                 // false (a new list)
}

Notice that the copy is equal to the original list, but it is a different object.

Next, we see what goes wrong when an element is not serializable and how to handle the unchecked cast. After that, we look at List.of() and subList(), byte arrays for caches, JSON with Jackson and the safe reading of lists from untrusted sources.

1. Serialization

In Java, the ArrayList class implements the Serializable interface, so we write no extra code for the list itself. We write the list with ObjectOutputStream and read it back with ObjectInputStream. Both streams wrap another stream, such as a file stream, so the same code can write to a file, a network socket, a byte array or any other OutputStream.

The snippets open the streams in a try-with-resources block, which closes the file for us even when an exception occurs. The enclosing method declares throws IOException, and a method that reads the list also declares ClassNotFoundException.

1.1. Serialize ArrayList of Strings

String is serializable, so a list of strings needs no extra code, and we write a list of three names to the file listData.

ArrayList<String> namesList = new ArrayList<>(List.of("alex", "brian", "charles"));

try (FileOutputStream fos = new FileOutputStream("listData");
     ObjectOutputStream oos = new ObjectOutputStream(fos)) {

  oos.writeObject(namesList);              // listData = 83 bytes
}

Java creates listData in the project root folder, because that folder is the working directory of the program. To write the file somewhere else, we pass a full path to the FileOutputStream constructor.

The listData file created in the project root folder after serializing the ArrayList
The serialized list is a binary file named listData in the working directory.

1.2. Serialize ArrayList of Objects

To serialize a list of our own objects, the element class must implement Serializable. Our Employee class uses Lombok (a library that generates code at compile time) to write the getters, setters, equals() and toString() for us. The class also declares a serialVersionUID, which is a version number that Java compares when reading the class back.

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Employee implements Serializable {

  @Serial
  private static final long serialVersionUID = 1L;

  private Long id;
  private String firstName;
  private String lastName;
}

The serialization code is the same as for strings, and only the element type changes.

ArrayList<Employee> employees = new ArrayList<>();
employees.add(new Employee(1L, "lokesh", "gupta"));
employees.add(new Employee(2L, "brian", "motto"));

try (FileOutputStream fos = new FileOutputStream("employeeData");
     ObjectOutputStream oos = new ObjectOutputStream(fos)) {

  oos.writeObject(employees);              // employeeData = 327 bytes
}

1.3. NotSerializableException When an Element Is Not Serializable

ArrayList first writes its own fields and then calls writeObject() for every element. If an element class does not implement Serializable, the write throws NotSerializableException at runtime, not at compile time. For example, our Contractor class does not implement Serializable, so writing a list of contractors fails.

List<Contractor> contractors = new ArrayList<>(List.of(new Contractor("alex")));
oos.writeObject(contractors);
Exception in thread "main" java.io.NotSerializableException: com.howtodoinjava.core.collections.list.Contractor
	at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1085)
	at java.base/java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:325)
	at java.base/java.util.ArrayList.writeObject(ArrayList.java:949)

The frame ArrayList.writeObject in the trace tells us that the list itself was fine and one of its elements failed. When the failing object sits deep inside other objects, for example in a field of a field, we start the JVM with the option -Dsun.io.serialization.extendedDebugInfo=true, and Java prints the path to the failing object.

java.io.NotSerializableException: com.howtodoinjava.core.collections.list.Contractor
	- custom writeObject data (class "java.util.ArrayList")
	- root object (class "java.util.ArrayList", [com.howtodoinjava.core.collections.list.Contractor@28d93b30])

The fix is to implement Serializable in the element class and in every class its fields refer to. Another option is to mark a field transient, so Java skips the field, which works when we can rebuild the value after reading.

1.4. What ArrayList Writes to the Stream

ArrayList keeps its elements in an internal array called elementData, whose length is the capacity of the list and is often larger than the list size. The elementData field is transient, so the default mechanism skips it, and ArrayList uses its own writeObject() method to write only the size and the elements that are in use. The capacity of the list is never written, so a list created with new ArrayList<>(1000) and the same two employees also produces 327 bytes.

Three columns. Left: the original ArrayList with size 2, capacity 10 and a transient elementData array of 10 slots. Only e1 and e2 are filled. Middle: the 327-byte stream with the ArrayList class descriptor and size 2, the element count, the Employee class descriptor with e1 values, and e2 values that reuse the class descriptor. Right: the new ArrayList with size 2 and an array of length 2 holding new Employee objects. copy.equals(list) is true and copy == list is false
ArrayList writes only its size and the used elements. readObject() then creates a new list with an array of that size.

2. Deserialization

ObjectInputStream.readObject() reads the bytes, creates a new ArrayList and a new object for every element, and returns the list as a plain Object, so we cast the result. The method readObject() throws IOException when the stream is broken or incompatible, and ClassNotFoundException when the reading program cannot find the element class on its classpath.

2.1. Deserialize List of Strings

We read back the list that we wrote in section 1.1 and check that the content is the same.

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

  @SuppressWarnings("unchecked")
  List<String> names = (List<String>) ois.readObject();

  System.out.println(names);                    // [alex, brian, charles]
  String className = names.getClass().getName();  // "java.util.ArrayList"
  boolean equal = names.equals(namesList);        // true
}

2.2. Deserialize a List of Objects

Reading the list of employees from section 1.2 works the same way, but the reading program needs the Employee class on its classpath with the same serialVersionUID. Otherwise, readObject() throws ClassNotFoundException or InvalidClassException.

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

  @SuppressWarnings("unchecked")
  List<Employee> employeeList = (List<Employee>) ois.readObject();

  for (Employee employee : employeeList) {
    System.out.println(employee);
  }
}
Employee(id=1, firstName=lokesh, lastName=gupta)
Employee(id=2, firstName=brian, lastName=motto)

2.3. Handling the Unchecked Cast Warning

The method readObject() returns a plain Object, and at runtime Java also forgets the type inside the angle brackets (type erasure). The JVM sees List, not List<Employee>, so the cast can check only that the result is a List. Without the annotation, the compiler warns about the cast.

Warn.java:8: warning: [unchecked] unchecked cast
      List<String> names = (List<String>) ois.readObject();
                                                        ^
  required: List<String>
  found:    Object

The cast does not fail when the element type is wrong. For example, a file that holds a list of strings can be read as a List<Employee> and the cast passes, so the ClassCastException appears later, at the first element access.

@SuppressWarnings("unchecked")
List<Employee> wrong = (List<Employee>) ois.readObject();    // a list of strings, no error
int size = wrong.size();                                     // 3
Employee first = wrong.get(0);
// ClassCastException: class java.lang.String cannot be cast to class com.howtodoinjava.core.collections.list.Employee

We handle the warning in one of two ways.

  • When our own code wrote the file and we know the type, we put @SuppressWarnings(“unchecked”) on the local variable, not on the whole method.
  • When the data can come from somewhere else, we check every element with Class.cast(), which fails at once, inside the reading code, with a clear message.
static <T> List<T> readList(ObjectInputStream ois, Class<T> type)
    throws IOException, ClassNotFoundException {

  if (!(ois.readObject() instanceof List<?> raw)) {
    throw new ClassCastException("Expected a List");
  }
  List<T> result = new ArrayList<>(raw.size());
  for (Object item : raw) {
    result.add(type.cast(item));
  }
  return result;
}

List<Employee> checked = readList(ois, Employee.class);   // [Employee(id=1, ...), Employee(id=2, ...)]
List<Employee> fromStrings = readList(ois, Employee.class);   // a list of strings:
// ClassCastException: Cannot cast java.lang.String to com.howtodoinjava.core.collections.list.Employee

The helper needs no annotation, because both instanceof List<?> and Class.cast() are checked at runtime.

3. Serializing an ArrayList to a Byte Array

A file is not always the target. For example, a web app that keeps the employee list in a shared cache or in a database column stores the list as a byte[]. We serialize the list with ByteArrayOutputStream and read the bytes back with ByteArrayInputStream.

static byte[] toBytes(Object obj) throws IOException {
  ByteArrayOutputStream bos = new ByteArrayOutputStream();
  try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
    oos.writeObject(obj);
  }
  return bos.toByteArray();
}

static Object fromBytes(byte[] bytes) throws IOException, ClassNotFoundException {
  try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
    return ois.readObject();
  }
}

byte[] bytes = toBytes(employees);                       // bytes.length = 327
List<Employee> fromCache = (List<Employee>) fromBytes(bytes);
// [Employee(id=1, firstName=lokesh, lastName=gupta), Employee(id=2, firstName=brian, lastName=motto)]

We call toByteArray() after the ObjectOutputStream is closed, because at that point the last buffered bytes are already in the array. The same two helpers also make a deep copy of a list (a copy that holds new element objects, not references to the old ones), so fromBytes(toBytes(list)) returns a deep copy.

4. Serializing List.of() and Other List Types

Every common List class in the JDK is serializable, and most of them come back as the same class with the same behavior. We serialized each list in the table with toBytes(), then checked the class of the copy and whether set() still works on it.

List created withClass after deserializationModifiable copy?
new ArrayList<>(…)java.util.ArrayListYes
List.of(“alex”, “brian”)ImmutableCollections$List12No, UnsupportedOperationException
List.of(a, b, c) or stream().toList()ImmutableCollections$ListNNo, UnsupportedOperationException
Arrays.asList(…)java.util.Arrays$ArrayListset() works, add() does not
Collections.unmodifiableList(…)Collections$UnmodifiableRandomAccessListNo, UnsupportedOperationException
Collections.synchronizedList(…)Collections$SynchronizedRandomAccessListYes, still synchronized
new LinkedList<>(…)java.util.LinkedListYes
new CopyOnWriteArrayList<>(…)java.util.concurrent.CopyOnWriteArrayListYes
arrayList.subList(0, 2)Not serializableNotSerializableException

An unmodifiable list (a list that rejects changes) stays unmodifiable after deserialization. If our code reads a List.of() list from a file and then calls add(), the call throws the same UnsupportedOperationException as on the original list. When we need a list we can change, we copy the result into a new ArrayList.

List<String> copy = (List<String>) fromBytes(toBytes(List.of("alex", "brian")));
String className = copy.getClass().getName();   // "java.util.ImmutableCollections$List12"
String old = copy.set(0, "zed");                // UnsupportedOperationException

List<String> editable = new ArrayList<>(copy);
editable.set(0, "zed");               // [zed, brian]

The toList() method of a stream and the toUnmodifiable collectors also return immutable collections, so the same rule applies to their results.

5. Storing an ArrayList as JSON With Jackson

Java serialization writes a binary format that only Java can read, and the bytes break when the element class changes in an incompatible way. When other services, or other versions of our application, read the same files, caches or messages, JSON is the better format, and the element class does not need Serializable for it.

Jackson 3 uses a new group id, tools.jackson.core, so the Maven coordinates differ from Jackson 2.

<dependency>
  <groupId>tools.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>3.2.3</version>
</dependency>

A TypeReference tells Jackson the element type, so Jackson creates Employee objects, not maps.

JsonMapper mapper = JsonMapper.builder().build();

String json = mapper.writeValueAsString(employees);
List<Employee> copy = mapper.readValue(json, new TypeReference<List<Employee>>() {});

String className = copy.getClass().getName();   // "java.util.ArrayList"
boolean equal = copy.equals(employees);         // true
[{"firstName":"lokesh","id":1,"lastName":"gupta"},{"firstName":"brian","id":2,"lastName":"motto"}]

The JSON text is 98 characters, whereas Java serialization needed 327 bytes for the same list, and any language can read the JSON. By default, Jackson 3 sorts the properties alphabetically, and its exceptions are unchecked, so we need no try-catch. Gson can also convert a JSON array to a list with its own TypeToken.

6. Deserializing a List From Untrusted Data

We never call readObject() on bytes that come from an untrusted source, such as an HTTP request, a shared cache or an uploaded file. The method readObject() can create objects of any serializable class on the classpath, and it runs the readObject() methods of those classes before our code sees the result.

Attackers send bytes that chain existing library classes together (a “gadget chain”), and the chain can run the attacker’s code on our server, which is called remote code execution. A crafted list can also allocate huge arrays and use up all the memory.

When we cannot replace Java serialization, an ObjectInputFilter (Java 9+) limits what readObject() may create. The filter pattern lists the allowed classes and limits, separated by semicolons, and the last entry, an exclamation mark followed by an asterisk, rejects every other class.

ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
    "java.util.ArrayList;java.lang.*;com.howtodoinjava.core.collections.list.Employee;maxdepth=5;!*");

ois.setObjectInputFilter(filter);
Object allowed = ois.readObject();      // ArrayList of employees: allowed
Object rejected = ois.readObject();     // a LinkedList of employees:
// java.io.InvalidClassException: filter status: REJECTED

The pattern allows every class of the java.lang package because of the Long id, since Java also reads Long and its parent class Number from the stream. We can set the same pattern for the whole JVM with the system property -Djdk.serialFilter, which sets a JVM-wide serialization filter. For new code, JSON with a fixed target type, as in section 5, avoids the problem.

7. ArrayList Serialization FAQs

7.1. Is ArrayList Serializable in Java?

Yes. ArrayList implements List, RandomAccess, Cloneable and Serializable, but the List interface itself does not extend Serializable. So a field declared as List is serializable only when the object stored in the field is serializable. For example, a List field that holds a subList() view throws NotSerializableException at runtime, because the field type looks fine but the object inside is not serializable.

7.2. Why Does subList() Throw NotSerializableException?

The view that subList() returns is not serializable. The method subList() returns a view of the original list (part of the list, without copying the elements), and the view is an object of the inner class ArrayList$SubList, which does not implement Serializable. So we copy the view into a new ArrayList before writing.

oos.writeObject(namesList.subList(0, 2));
// java.io.NotSerializableException: java.util.ArrayList$SubList

oos.writeObject(new ArrayList<>(namesList.subList(0, 2)));    // read back as [alex, brian]

7.3. How Do We Serialize an ArrayList of Objects That Are Not Serializable?

We have three options, and the choice depends on whether we can change the element class.

  • Implement Serializable in the element class and in the classes of its fields.
  • Copy the elements into a serializable class, for example a record that implements Serializable, and serialize the list of copies.
  • Use JSON with Jackson (section 5). Jackson needs only getters or record components, not Serializable.

7.4. Does Deserialization Keep the Order and Duplicates of the List?

Yes. ArrayList writes the elements in index order and readObject() adds them back in the same order, keeping duplicates and null elements too. The copy is equal to the original, so equals() returns true, but the copy is a different object, and so are its elements. Fields marked transient and changes to serialVersionUID follow the general rules of Java serialization.

8. Conclusion

We serialize an ArrayList with ObjectOutputStream.writeObject() and deserialize it with ObjectInputStream.readObject(), and the only condition is that every element is serializable. We keep the unchecked cast local to one variable, and when the data does not come from our own code, we check each element with Class.cast(). For caches, a byte[] works the same way as a file.

When other programs read the data, or the data comes from outside, JSON with Jackson is the safer and more portable choice. When Java serialization must stay, ObjectInputFilter limits the risk.

9. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.