Java 11 Developer Certification - List and ArrayList Methods

April 03, 2021

What we are covering in this lesson

  1. List.of and List.copyOf methods
  2. ArrayList.toArray method

List.of and List.copyOf methods

Lets discuss a bit more on two special methods. List.of was introduced in Java 9 and List.copyOf was introduced in Java 10. Both these are static factory methods creating unmodifiable lists. The object return is an instantiation of a member type defined in the immutable collections class.

Some features of these methods are mentioned below -

  • They are not modifiable. Elements cannot be added, removed or replaced.
  • If the elements themselves are mutable, this may cause the list’s contents to appear to change.
  • They also disallow null elements, attempts to create them with null elements result in a null pointer exception.
  • The order of elements in the list is the same as the order of the provided arguments or of the elements in the provided array.

Now lets create a new class to review the above features.

We create an arrayList originalList, add a few items to it, copy it to copiedList and print the values.

Then we are trying to add NULL at all index except 5. The compiler is happy in such cases, but we get a NullPointerException during runtime, when we try to make a copyOf the list. Thats the reason we have added a sublist of the original list and bypassing the null values

The List.copyOf method is not overloaded, and so takes a Collection as a parameter. On the other hand, List.of has 13 overloaded versions, and we have used some of them in the below code as well.

package maxCode.online.list;

import java.util.ArrayList;
import java.util.List;

public class FactoryExamples {
	public static void main(String[] args) {
		List<String> originalList = new ArrayList<>();
		for (int i = 0; i < 10; i++) {
			originalList.add("TEST_" + (i + 1));
        }

		System.out.println(originalList);

		// List.copyOf was added in Java 10
		List<String> copiedList = List.copyOf(originalList);
		System.out.println(copiedList);
		// copiedList is immutable..

		// Let's change original list's values, and insert a null
		for (int i = 0; i < 10; i++) {
			if (i != 5)
				originalList.set(i, "TEST_" + (i + 10));
			else
				originalList.set(i, null);
		}
		System.out.println(originalList);

		// Let's try making a fresh copy
		copiedList = List.copyOf(originalList.subList(0, 5));
		System.out.println(copiedList);

		// Create some test data
		String[] stringArray = { "ABC", "DEF", "GHI", "JKL", "MNO", "PQR", "STU" };

		// List.of can accept an array..
		copiedList = List.<String>of(stringArray);
		System.out.println(copiedList);

		// List.of can accept a variable list of elements
		copiedList = List.<String>of("ABC", "DEF", "GHI", "JKL", "MNO");
		System.out.println(copiedList);

		// List.of can be called with no parameter at all
		copiedList = List.<String>of();
		System.out.println(copiedList);
	}
}

Output

[TEST_1, TEST_2, TEST_3, TEST_4, TEST_5, TEST_6, TEST_7, TEST_8, TEST_9, TEST_10]
[TEST_1, TEST_2, TEST_3, TEST_4, TEST_5, TEST_6, TEST_7, TEST_8, TEST_9, TEST_10]
[TEST_10, TEST_11, TEST_12, TEST_13, TEST_14, null, TEST_16, TEST_17, TEST_18, TEST_19]
[TEST_10, TEST_11, TEST_12, TEST_13, TEST_14]
[ABC, DEF, GHI, JKL, MNO, PQR, STU]
[ABC, DEF, GHI, JKL, MNO]
[]

ArrayList.toArray method

The below table shows three scenarios that occur when calling the ArrayList.toArray method

Parameter Input Result Notes
No parameter Array of Object returned with elements Result is same for raw ArrayList and generic ArrayList - Object[] returned whose size is list.size()
Array length >= element size, passed array elements NOT NULL Result = Parameter Array,
Elements at index < list.size() get populated with lists elements
Element at index = list.size is set to NULL
Elements at index > list.size() remain the same as they were when passed to the method
If ArrayList is raw, you must cast to a typed array if assigning output to a variable
Array length < element size Array returned is the set of values in the list, and the type of the parameter passed ArrayStoreException occurs if the type of the array of the parameter isnt compatible with the element type in the list

We will be covering these scenarios in the below code.

Lets test scenario 1 first. We are testing two types of ArrayList here, a generic one and a raw type ArrayList, both of them being populated with Integer elements.

We are saving these arrays to a var type, and printing its type. Later, we are also checking the cases when we need casting and when we dont!

For Scenario 2, code is quite similar to Scenario 1 but with some minor changes. The parameter array aIntArray that we are passing has values for all of its elements. Instead of var, we are using Integer array to assign the result of toArray. Thing to note here, we do not need to cast the generics Integer array, but we have to do it for the raw one. Also when we use individual elements, no casting in required.

What’s with the null element in each array. This is a feature of the toArray method. It sets the element that is at the index list.size to NULL, to inform the consumer where the copied element’s boundary is.

For Scenario 3, we are not passing any parameter, and it populates a newly instance object array with the lists elements, which happen to be all integer. This behaviour is the same for raw as well as generic lists. We have used an Object[] because we cannot do a Integer[] array typecast directly here, else it will throw a Runtime ClassCastException.

package maxCode.online.list;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class ToArrayExamples {
	public static void main(String[] args) {
		// testValue will be used to access a single value in returned array
		int testValue = 0;

		// This is a generically typed ArrayList, passing Integer as the parameterized type.
		ArrayList<Integer> intList = new ArrayList<>(List.of(1, 2, 3, 4, 5));

		// This is a raw type ArrayList
		ArrayList rawList = new ArrayList(List.of(1, 2, 3, 4, 5));

		// Set up the parameter we will pass to the toArray() method
		Integer[] aIntArray = new Integer[10];

		System.out.println("---- All tests executed with lists that" + " contain: " + rawList + " ----");
		System.out.println("\nScenario 1:  Parameter is reference variable" + " for Integer[10], elements all null");
		// Scenario 1: array.length >= list.size();
		// and array elements initialized to null
		// The method toArray([]<T>) populates the passed array with as
		// many elements as list contains and returns passed array back.
		// No new array is created.
		var bInt = intList.toArray(aIntArray);
		System.out.println("Result type from ArrayList<Integer> " + "assigned to var = " + bInt.getClass().getTypeName());
		System.out.println("--- Resulting array elements = " + Arrays.toString(bInt));

		// No casting required.
		testValue = bInt[0];
		System.out.println("--- No cast required at individual level:" + " the first element is " + testValue);

		// Reset the array we use as a parameter between tests, so
		// tests between raw ArrayList and generic ArrayList are same
		aIntArray = new Integer[10];

		// A raw ArrayList will still return array passed, in
		// this case aIntArray reference

		var bRaw = rawList.toArray(aIntArray);
		System.out.println("Result type from ArrayList assigned to var = " + bRaw.getClass().getTypeName());
		System.out.println("--- Resulting array elements = " + Arrays.toString(bRaw));

		// Casting required.
		testValue = (int) bRaw[0];
		System.out.println("--- Cast required at individual level: the " + "first element is " + testValue);

		System.out.println("\nScenario 2:  Parameter is reference variable" + " for Integer[10], elements all populated");
		// Scenario 2: array.length > list.size();
		// and array elements initialized (not null)
		// the method toArray([]<T>) populates passed array with as many
		// elements as list contains, and has one other, maybe unexpected
		// behavioral twist - it sets the element at list.size() to null,
		// leaving all other elements at indices > list.size() as they were.
		aIntArray = new Integer[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
		Integer[] cInt = intList.toArray(aIntArray);
		System.out.println("Result type from ArrayList<Integer> " + "assigned to Integer[] = " + cInt.getClass().getTypeName());
		System.out.println("--- Resulting array elements = " + Arrays.toString(cInt));

		// No casting required.
		testValue = cInt[0];
		System.out.println("--- No cast required at individual level:" + " the first element is " + testValue);

		// Reset the array we use as a parameter between tests, so
		// tests between raw ArrayList and generic ArrayList are same
		aIntArray = new Integer[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

		// A raw ArrayList will still return array passed, in
		// this case aIntArray reference, but to assign it to an
		// Integer[] array we must cast.
		Integer[] cRaw = (Integer[]) rawList.toArray(aIntArray);
		System.out.println("Result type from ArrayList assigned to  Integer[] " + " required casting = "
				+ cRaw.getClass().getTypeName());
		System.out.println("--- Resulting array elements = " + Arrays.toString(cRaw));

		// No Casting required.
		testValue = cRaw[0];
		System.out.println("--- No Cast required at individual level since"
				+ " we cast entire array to (Integer[]) : the first element is " + testValue);

		System.out.println("\nScenario 3:  No parameter passed");
		// Scenario 3: no parameter
		// the method toArray() populates a newly instantiated Object[]
		// array with the list's elements, which happen to all be Integer.
		// The behavior is the same for both a raw list and generic list
		Object[] dInt = intList.toArray(); // CANNOT cast to (Integer[])
		System.out.println("Result type from ArrayList<Integer> " + "assigned to Object[] = " + dInt.getClass().getTypeName());
		System.out.println("--- Resulting array elements = " + Arrays.toString(dInt));

		// Casting required.
		testValue = (int) dInt[0];
		System.out.println("--- Cast required at individual level:" + " the first element is " + testValue);

		Object[] dRaw = intList.toArray();
		System.out.println("Result type from ArrayList " + "assigned to Object[] = " + dRaw.getClass().getTypeName());
		System.out.println("--- Resulting array elements = " + Arrays.toString(dRaw));

		// Casting required.
		testValue = (int) dRaw[0];
		System.out.println("--- Cast required at individual level:" + " the first element is " + testValue);
	}
}

Output

---- All tests executed with lists that contain: [1, 2, 3, 4, 5] ----

Scenario 1:  Parameter is reference variable for Integer[10], elements all null
Result type from ArrayList<Integer> assigned to var = java.lang.Integer[]
--- Resulting array elements = [1, 2, 3, 4, 5, null, null, null, null, null]
--- No cast required at individual level: the first element is 1
Result type from ArrayList assigned to var = java.lang.Integer[]
--- Resulting array elements = [1, 2, 3, 4, 5, null, null, null, null, null]
--- Cast required at individual level: the first element is 1

Scenario 2:  Parameter is reference variable for Integer[10], elements all populated
Result type from ArrayList<Integer> assigned to Integer[] = java.lang.Integer[]
--- Resulting array elements = [1, 2, 3, 4, 5, null, 7, 8, 9, 10]
--- No cast required at individual level: the first element is 1
Result type from ArrayList assigned to  Integer[]  required casting = java.lang.Integer[]
--- Resulting array elements = [1, 2, 3, 4, 5, null, 7, 8, 9, 10]
--- No Cast required at individual level since we cast entire array to (Integer[]) : the first element is 1

Scenario 3:  No parameter passed
Result type from ArrayList<Integer> assigned to Object[] = java.lang.Object[]
--- Resulting array elements = [1, 2, 3, 4, 5]
--- Cast required at individual level: the first element is 1
Result type from ArrayList assigned to Object[] = java.lang.Object[]
--- Resulting array elements = [1, 2, 3, 4, 5]
--- Cast required at individual level: the first element is 1

The main things to remember -

  • Both list returned the array that was passed but changed its elements (copying elements from the list to the array).
  • Using the raw array list and assigning the result of the method to var required us to cast it. That’s when retrieving a single element from the returned array.
  • It doesn’t matter what type of arrayList you use when executing to array with no parameter. The result will always be an object array and casting will be required when you want to access its individual elements for that reason.
  • The elements are the elements copied from the list.

In addition to the above scenarios, lets look at some more options. The below code is in continuation with the last code. Here we have the array length less than the list.size(), and the array elements initialise to null, or otherwise the method toArray populates a new instantiated array of the same type as the parameter passed, and the size of the list itself, not the parameter passed.

The code is very similar to Scenario 1, except what we’re now using is an Integer array initialised to three elements. The difference is that the array that is returned from the method is not the array we passed, but it is the type we gave the array we passed as a parameter.

So we’re assigning the result there to our local variable type var variable, and we’re making no assumptions about what type of object the toArray methods return in there. Applies to both raw and generic one!

One final example - Scenario 5. Its just a different twist to the last scenario. So it’s the array.length less than the list size, and no reference to an array passed. The method toArray populates a newly instantiated array of the same type as the parameter itself, and the size of the list itself, not the parameter passed.

So we’re passing an referenced Number[0] array as the parameter, an empty array, at a different type than our ArrayList of integer object generic list.

        System.out.println("\nScenario 4:  Array passed is too small");
        // Scenario 4: array.length < list.size();
        //             and array elements initialized to null or otherwise
        // the method toArray() populates a newly instantiated
        // array of the same type as the parameter passed,
        // and the size of the list itself, not the parameter passed
        aIntArray = new Integer[3];

        // Let's make no assumptions about what is returned...
        var eInt = intList.toArray(aIntArray);
        System.out.println("Result type from ArrayList<Integer> " +
                "assigned to var = " + eInt.getClass().getTypeName());
        System.out.println("--- Resulting array elements = " + Arrays.toString(eInt));

        // No casting required.
        testValue = eInt[0];
        System.out.println("--- No cast required at individual level:" + " the first element is " + testValue);

        // Reset the array we use as a parameter between tests, so
        // tests between raw ArrayList and generic ArrayList are same
        aIntArray = new Integer[3];

        // A raw ArrayList will still return an Integer[] array
        var eRaw = rawList.toArray(aIntArray);
        System.out.println("Result type from ArrayList assigned to var = " + eRaw.getClass().getTypeName());
        System.out.println("--- Resulting array elements = " + Arrays.toString(eRaw));

        // Casting required.
        testValue = (int) eRaw[0];
        System.out.println("--- Cast required at individual level: the " + "first element is " + testValue);

        System.out.println("\nScenario 5:  Array passed is now Number[] and not a reference variable");
        // Scenario 5: array.length < list.size();
        //             and no reference to array passed
        // the method toArray() populates a newly instantiated
        // array of the same type as the parameter passed,
        // and the size of the list itself, not the parameter passed

        var fInt = intList.toArray(new Number[0]);
        System.out.println("Result type from ArrayList<Integer> " +
                "assigned to var = " + fInt.getClass().getTypeName());
        System.out.println("--- Resulting array elements = " + Arrays.toString(fInt));

        // casting required.
        testValue = (int) fInt[0];
        System.out.println("--- Cast required at individual level:" + " the first element is " + testValue);

        // A raw ArrayList will still return a Number[] array
        var fRaw = rawList.toArray(new Number[0]);
        System.out.println("Result type from ArrayList assigned to var = " + fRaw.getClass().getTypeName());
        System.out.println("--- Resulting array elements = " + Arrays.toString(fRaw));

        // Casting required.
        testValue = (int) fRaw[0];
        System.out.println("--- Cast required at individual level: the " + "first element is " + testValue);

Output

Scenario 4:  Array passed is too small
Result type from ArrayList<Integer> assigned to var = java.lang.Integer[]
--- Resulting array elements = [1, 2, 3, 4, 5]
--- No cast required at individual level: the first element is 1
Result type from ArrayList assigned to var = java.lang.Integer[]
--- Resulting array elements = [1, 2, 3, 4, 5]
--- Cast required at individual level: the first element is 1

Scenario 5:  Array passed is now Number[] and not a reference variable
Result type from ArrayList<Integer> assigned to var = java.lang.Number[]
--- Resulting array elements = [1, 2, 3, 4, 5]
--- Cast required at individual level: the first element is 1
Result type from ArrayList assigned to var = java.lang.Number[]
--- Resulting array elements = [1, 2, 3, 4, 5]
--- Cast required at individual level: the first element is 1

So putting it all together here -

  • The type of array returned is determined by the parameter passed, not by the type associated to the ArrayList itself.
  • Typing the ArrayList does allow you to eliminate casting in most cases, the exception is when you have no parameter, or your parameter array is a different type.