Collection Interface Enhancements : What’s new in Java 8 to Java 11

1. Introduction

In this article we will see 5 different methods that were added in the Collection interface since Java 8. We will discuss methods added in Java 8 and Java 11. No new methods were added in Java 9 and Java 10 releases. If you want to take a look at new features in List interface then read it here.

2. Content

We will take a look at the methods below with examples. 

// added in Java 8
default boolean removeIf(Predicate<? super E> filter) 

// added in Java 11
default <T> T[] toArray(IntFunction<T[]> generator) 

// added in Java 8
default Spliterator<E> spliterator() 

// added in Java 8
default Stream<E> stream() 

// added in Java 8
default Stream<E> parallelStream() 

3. default boolean removeIf(Predicate<? super E> filter)

removeIf() method was added in Java 8.

removeIf() method removes all the elements from Collection for matching Predicate. But there is a secret sauce to this method. Let us understand this with an example.

Assuming that you don’t have removeIf() method, how would you still achieve the functionality?

Example : remove names that start with letter “R”

List<String> names = new ArrayList<>();
names.add("Jon");
names.add("Sansa");
names.add("Rickon");
names.add("Arya");
names.add("Robb");
names.add("Bran");

for (Iterator<String> iter = names.iterator(); iter.hasNext();) {
	if (iter.next().startsWith("R")) {
		iter.remove();
	}
}

// expected output
List<String> output = Arrays.asList("Jon", "Sansa", "Arya", "Bran");

Assert.assertEquals(output, names);

The remove method of the iterator uses implementation of ArrayList class’s remove method. remove() method works like this : When using Iterator internally it uses the index of the element it is iterating. So now remove() method will shift subsequent elements left by one index. This takes O(n) time. Iterating takes O(n) time. So the total time complexity of removing elements by using Iterator is O(n^2).

N is equal to the number of elements in the List.

This is not an optimal solution. removeIf() works in O(n) time complexity.

List<String> names = new ArrayList<>();
names.add("Jon");
names.add("Sansa");
names.add("Rickon");
names.add("Arya");
names.add("Robb");
names.add("Bran");

names.removeIf(name -> name.startsWith("R"));

List<String> output = Arrays.asList("Jon", "Sansa", "Arya", "Bran");

Assert.assertEquals(output, names);

removeIf() method does a smart work of marking elements that needs to be removed while running the Predicate on the List. Once that is done it traverses the List again and copies the elements from its old index to new index. After this, it nulls out the other objects. Below example will give you a clear picture.

Input [Jon, Sansa, Rickon, Arya, Robb, Bran]

Shifting starts

[Jon, Sansa, Arya, Arya, Robb, Bran]

[Jon, Sansa, Arya, Bran, Robb, Bran]

[Jon, Sansa, Arya, Bran, null, null]

Size of new List is 4

4. default <T> T[] toArray(IntFunction<T[]> generator)

toArray() method was added in Java 11.

There are now three different flavors of toArray method. Two of them were added prior to Java 8. toArray method is a bridge between array and collections apis.

The two flavours of toArray() method prior to Java 8 are as follows

List<String> names = new ArrayList<>();
names.add("Athos");
names.add("Porthos");
names.add("Aramis");

String[] namesArr1 = names.toArray(new String[0]);

Object[] namesArr2 = names.toArray();

The new method added in Java 11 accepts a functional interface called IntFunction. The IntFunction accepts input of type int and returns result type R. In the method IntFunction has a return type of T[].

Using the new toArray API.

List<String> names = new ArrayList<>();
names.add("Athos");
names.add("Porthos");
names.add("Aramis");

String[] usingLambda = names.toArray(val -> new String[val]);

// Use method reference instead of lambda. 
// Intention of behavior is clearer than lambda expression. 
String[] usingMethodRefs = names.toArray(String[]::new);

String[] output = names.toArray(new String[0]);

Assert.assertArrayEquals(output, usingLambda);
Assert.assertArrayEquals(output, usingMethodRefs);

5. default Spliterator<E> spliterator() 

spliterator() was added in Java 8 release.

Spliterator means a splittable iterator which is responsible for traversing and partitioning elements of the source. It is used extensively in Stream APIs. This method is an abstract implementation of Spliterator and must be implemented by a class that implements this interface. Fortunately for us this is taken care of in most common interfaces that we use like List and Set.

I would highly recommend not using this method unless you really know what you are trying to do. If you want to work with Stream API just call stream() method on the collection instance.

Example of spliterator() with List interface is as follows. Remember List interface has its own implementation of Spliterator.

List<String> names = new ArrayList<>();
names.add("Jon");
names.add("Sansa");
names.add("Rickon");
names.add("Arya");
names.add("Robb");

Spliterator<String> spliterator = names.spliterator();

Map<String, Integer> collect = 
				StreamSupport
					.stream(spliterator, false)
					.collect(
						Collectors.toMap(
									name -> name, 
									name -> name.length()));

Output
{Sansa=5, Arya=4, Jon=3, Robb=4, Rickon=6}

6. default Stream<E> stream() 

stream() was added in Java 8 release.

stream() method allows us to use a declarative way of programming in Java. We can just provide the behavior to the parameter and let the JDK figure out the details. stream() method be default operates sequentially. 

Example : Given List<String> of names get the length of every name. Return Map<String, Integer> which represents entry name : name_length.

List<String> names = new ArrayList<>();
names.add("Athos");
names.add("Porthos");
names.add("Aramis");

Map<String, Integer> nameWithLengths = 
		names
			.stream()
			.collect(Collectors.toMap(
								name -> name, 
								name -> name.length()));

// name -> name is an identity function.
// name -> name.length() can be replaced by method references

Map<String, Integer> nameWithLengths = 
		names
			.stream()
			.collect(Collectors.toMap(
								Function.identity(), 
								String::length));

7. default Stream<E> parallelStream() 

parallelStream() was added in Java 8 release.

stream() method operates sequentially that it operates on one element and then on another. parallelStream() can operate on data in parallel. It will possibly return a parallel stream as the decision is made by Streams internals.

List<String> names = new ArrayList<>();
names.add("Athos");
names.add("Porthos");
names.add("Aramis");

Map<String, Integer> nameWithLengths = 
		names
			.parallelStream()
			.collect(Collectors.toMap(
								name -> name, 
								name -> name.length()));

// name -> name is an identity function.
// name -> name.length() can be replaced by method references

Map<String, Integer> nameWithLengths = 
		names
			.parallelStream()
			.collect(Collectors.toMap(
								Function.identity(), 
								String::length));

8. Conclusion

In this article we discussed the new methods added in the Collection interface. I would highly recommend working through the examples. If you have any questions, post in the comments.

Leave a Reply

Your email address will not be published. Required fields are marked *