1. Overview

In this tutorial, we’ll learn how to create an ArrayList that can hold multiple object types in Java. We’ll also learn how to add data of multiple types into an ArrayList and then retrieve data from the ArrayList to transform it back to the original data types.

2. Background

A basic understanding of the Collection framework, especially ArrayList, is required for this article. Take a look at the corresponding articles, Java List Interface and Guide to the Java ArrayList, to gain a basic understanding of these classes.

The ArrayList class does not support primitive datatypes directly, but it does support them via wrapper classes. The ArrayList is ideally created with a reference class type. It indicates that when adding data to an ArrayList, only that reference class’s data is supported. For instance, an ArrayList<**Integer> won’t accept data from the String, Boolean, or Double classes.

Our goal is to store data from multiple types in a single ArrayList. This goes against the fundamental nature of an ArrayList with a single type parameter. However, it is still possible to achieve this with a variety of approaches. We’ll go over them in depth in the following sections of this article.

3. Raw Type vs. Parameterized Type

A raw type is a generic type without any type argument. Raw types can be used like regular types without any restrictions, except that certain uses will result in “unchecked” warnings. On the other hand, a parameterized type is an instantiation of a generic type with actual type arguments. Parameterized types can provide a generic or a concrete type argument during the instantiation of a class.

The Java specification cautions against using raw types when creating a List because type safety is lost. Casting exceptions at runtime may result from this. When declaring a list, it’s recommended to always use a type parameter:

 /* raw type */
List myList = new ArrayList<>();

 /* parameterized type */
List<Object> myList = new ArrayList<>();

4. Using Object as the Generic Type

4.1. Creating the ArrayList

Most frequently, the developer will produce an ArrayList with certain type parameters like String, Integer, or Double. This is so that a single type of data information may be represented, retrieved, and processed with ease. However, our goal is to create an ArrayList with multiple object types, thus being able to store data of many data types in a single ArrayList.

To achieve this, we can use the parent class of all Java classes: the Object class. The Object class is the topmost class in the Java class hierarchy, and all other classes are directly or indirectly derived from it.

Let’s see how to initialize an ArrayList of Object type parameter:

ArrayList<Object> multiTypeList = new ArrayList<>();

4.2. Inserting Data into the ArrayList

In this section, we’ll learn how we can insert the data into an ArrayList. We had created an ArrayList of Object element type, hence, every time we add any type of data into the ArrayList, it’ll first get auto-cast to Object type and then will get stored inside the ArrayList. We’ll attempt to insert data of various object types such as Integer, Double, String, List, and a user-defined custom object.

Furthermore, we already know that primitive data types such as int and double cannot be directly stored in an ArrayList, so we’ll convert them using their respective wrapper classes. These wrapper classes are then typecast into the Object class and stored in an ArrayList.

Other types, such as String, List, and CustomObject, are type parameters in and of themselves, so they can be added directly. However, these elements will also be typecast into the Object before being stored.

Let’s look at the code example:

multiTypeList.add(Integer.valueOf(10));
multiTypeList.add(Double.valueOf(11.5));
multiTypeList.add("String Data");
multiTypeList.add(Arrays.asList(1, 2, 3));
multiTypeList.add(new CustomObject("Class Data"));
multiTypeList.add(BigInteger.valueOf(123456789));
multiTypeList.add(LocalDate.of(2023, 9, 19));

4.3. Retrieving Data from the ArrayList

We’ll also discover how to retrieve the information from the ArrayList of Object type parameter and put it back into the various type parameters from which it was originally added. The ArrayList‘s data elements will all be of the static Object element type.

The developer will be in charge of casting it back to the appropriate data type for additional processing. We can accomplish this in two different ways: using the instanceof keyword and the getClass() method.

When we use the getClass() method, it returns the type of the data element’s class. We can compare it and then transform the data back to the appropriate data type. When we use instanceof, it compares the left-side element to the right-side type parameter and returns a boolean result.

In this tutorial, we’ll use the instanceof keyword to cast the data into the appropriate type parameter. We will then print the value of the data element to see if it was correctly transformed to the original data type.

Let’s look at the code example:

for (Object dataObj: multiTypeList) {
    if (dataObj instanceof Integer intData)
        System.out.println("Integer Data : " + intData);
    else if (dataObj instanceof Double doubleData)
        System.out.println("Double Data : " + doubleData);
    else if (dataObj instanceof String stringData)
        System.out.println("String Data : " + stringData);
    else if (dataObj instanceof List < ? > intList)
        System.out.println("List Data : " + intList);
    else if (dataObj instanceof CustomObject customObj)
        System.out.println("CustomObject Data : " + customObj.getClassData());
    else if (dataObj instanceof BigInteger bigIntData)
        System.out.println("BigInteger Data : " + bigIntData);
    else if (dataObj instanceof LocalDate localDate)
        System.out.println("LocalDate Data : " + localDate.toString());
}

Please note that we’re using pattern matching, which is a feature from JDK 16.

Let’s now check the output of this program:

// Program Output
Integer Data : 10
Double Data : 11.5
String Data : String Data
List Data : [1, 2, 3]
CustomObject Data : Class Data
BigInteger Data : 123456789
LocalDate Data : 2023-09-19

As we can see, the list elements were looped one by one, and the output was logged as per the relevant data type of each ArrayList element.

5. Alternative Approaches

It’s important to note that utilizing an ArrayList of the Object class can cause problems with data processing after retrieval if the casting or parsing of the relevant type parameter is not handled correctly. Let’s go through a few simple ways we can avoid these issues.

5.1. Using a Common Interface as the Type Parameter

One way around these potential problems is to make a list of a predefined or custom interface. This will restrict the entry of data to just those classes that implement the defined interface. As shown below, the list of Map permits data of different representations of the Map interface:

ArrayList<Map> diffMapList = new ArrayList<>();
diffMapList.add(new HashMap<>());
diffMapList.add(new TreeMap<>());
diffMapList.add(new LinkedHashMap<>());

5.2. Using a Parent Class as the Type Parameter

In a different approach, we can define a list of the parent or superclass. It will accept the values from all child class representations. Let’s create a list of Number objects that can accept Integer, Double, Float, and other numeric types:

ArrayList<Number> myList = new ArrayList<>();
myList.add(1.2);
myList.add(2);
myList.add(-3.5);

5.3. Using a Custom Wrapper Class as the Type Parameter

We can also create a custom wrapper class with the object types we want to allow. Such a class will have dedicated getter and setter methods for all elements of different types. We can then create a List with our class as a type parameter. This is a widely used approach, as it ensures type safety. The below example shows a List of custom wrapper classes for Integer and String elements:

public class CustomObject {
    String classData;
    Integer intData;
    // constructors and getters
}
ArrayList<CustomObject> objList = new ArrayList<>();
objList.add(new CustomObject("String"));
objList.add(new CustomObject(2));

5.4. Using a Functional Interface

Yet another solution is to create a list via a functional interface. This approach can be useful if we need some constraints or validation before inserting elements.

We can write a predicate that checks the List for allowed data types. This Predicate can be used by the abstract method of the functional interface to determine whether to add the data element to the list.

In the functional interface, we can then define a default method that prints the information of the allowed data type:

@FunctionalInterface
public interface UserFunctionalInterface {

    List<Object> addToList(List<Object> list, Object data);

    default void printList(List<Object> dataList) {
        for (Object data: dataList) {
            if (data instanceof String stringData)
                System.out.println("String Data: " + stringData);
            if (data instanceof Integer intData)
                System.out.println("Integer Data: " + intData);
        }
    }
}

Consider the following scenario: We create an ArrayList of objects, but only add type parameters of String and Integer. The functional interface can add the data to the list after the predicate checks the type parameter. We’ll also add a default method for printing the data:

List<Object> dataList = new ArrayList<>();

Predicate <Object> myPredicate = inputData -> (inputData instanceof String || inputData instanceof Integer);

UserFunctionalInterface myInterface = (listObj, data) -> {
    if (myPredicate.test(data))
        listObj.add(data);
    else
        System.out.println("Skipping input as data not allowed for class: " + data.getClass()
            .getSimpleName());
    return listObj;
};

myInterface.addToList(dataList, Integer.valueOf(2));
myInterface.addToList(dataList, Double.valueOf(3.33));
myInterface.addToList(dataList, "String Value");
myInterface.printList(dataList);

Let’s check the output of this approach:

//Output
Integer Data: 2
Skipping input as data is not allowed for class: Double 
String Data: String Value

6. Conclusion

In this quick article, we took a look at ArrayList’s functionality to store data of various types. We learned how we can create an ArrayList instance with an Object type parameter. In addition to this, we learned how to add or remove elements from ArrayList of multiple object types. We also learned best practices that we can adopt while dealing with the ArrayList with multiple object types.