Set Interface Enhancements from Java 8 to Java 10

1. Introduction

In the previous article, we saw the new additions in Set interface, Collection interface and Map.Entry since Java 8 release. In this article, we will look at different methods added in Set interface since Java 8 with a lot of examples. The new methods were added to the Set interface for Java 8, 9 and 10 version. For Java 11 and 12, no new methods were added to Set interface.

2. Content

Below are the methods that we will look at. Few methods are added as part of Collection interface too. So we will use them using our Set implementation. All the examples will use HashSet as an implementation of Set.

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

default void forEach(Consumer<? super T> action)

default Stream<E> stream()

default Stream<E> parallelStream()

default Spliterator<E> spliterator()

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

static <E> Set<E> copyOf(Collection<? extends E> coll)

static <E> Set<E> of()

static <E> Set<E> of(E e1)

static <E> Set<E> of(E e1, E e2)

static <E> Set<E> of(E e1, E e2, E e3)

static <E> Set<E> of(E e1, E e2, E e3, E e4)

static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5)

static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5, E e6)

static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7)

static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8)

static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, 
					E e9)

static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, 
					E e9, E e10)

static <E> Set<E> of(E... elements)

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

removeIf method was added to Collection interface in Java 8. 

removeIf method is a bulk in place modification API. So the backing collection will be modified because of the call made to removeIf method.

removeIf method accepts a Predicate. Predicate is a boolean valued function which returns either true or false. If the Predicate passed in parameter is null, then NullPointerException is thrown.

The elements from the Collection are removed only if the Predicate returns true.

Let us take an example. Given names as Set<String> remove all the names that start with the capital letter “W”.

Set<String> names = new HashSet<>();
names.add("Woolfield");
names.add("Locke");
names.add("Waterman");
names.add("Lightfoot");
names.add("Moss");
names.add("Wells");

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

Output : After removeIf is executed.
[Moss, Locke, Lightfoot]

We can achieve same in pre Java 8 world using below code : 

Set<String> names = new HashSet<>();
names.add("Woolfield");
names.add("Locke");
names.add("Waterman");
names.add("Lightfoot");
names.add("Moss");
names.add("Wells");

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

4. default void forEach(Consumer<? super T> action)

forEach method was added to the Collection interface in Java 8. 

forEach method is used to iterate over the Collection using for-each iterator. The good part about this method is it uses internal iteration. So we don’t need to use an Iterator. Let the JDK figure out the iteration details. If we iterate it using Iterator, that kind of iterator is called External Iterator.

I passed Consumer functional interface as a parameter to forEach method. Consumer interface accepts an argument of type T and returns void. It can be very useful while logging.

This method will throw a NullPointerException if the Consumer is null.

forEach method using Lambda Expression :

Set<String> names = new HashSet<>();
names.add("Woolfield");
names.add("Locke");
names.add("Waterman");
names.add("Lightfoot");
names.add("Moss");
names.add("Wells");

names.forEach(name -> System.out.println(name));

Output : 
Woolfield
Moss
Locke
Waterman
Lightfoot
Wells

forEach method using Method Reference : 

Set<String> names = new HashSet<>();
names.add("Woolfield");
names.add("Locke");
names.add("Waterman");
names.add("Lightfoot");
names.add("Moss");
names.add("Wells");

names.forEach(System.out::println);

Output : 
Woolfield
Moss
Locke
Waterman
Lightfoot
Wells

5. default Stream<E> stream()

stream() method was added to the Collection interface in Java 8. 

Streams API enables us to use declarative programming. Which means we can provide the behaviour that we want to the methods and let the library figure out the rest. 

Example: Given names as Set<String>, print names that don’t start with the letter “W”.

Set<String> names = new HashSet<>();
names.add("Woolfield");
names.add("Locke");
names.add("Waterman");
names.add("Lightfoot");
names.add("Moss");
names.add("Wells");

names
	.stream()
	.filter(name -> !name.startsWith("W"))
	.forEach(System.out::println);

Output : 
Moss
Locke
Lightfoot

Let us take a slightly complicated example : Given names as Set<String>, return Map<String, Integer> where entry is defined as string-string_length.

Using Lambda Expression:

Set<String> names = new HashSet<>();
names.add("Woolfield");
names.add("Locke");
names.add("Waterman");
names.add("Lightfoot");
names.add("Moss");
names.add("Wells");

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

Output : 
{Woolfield=9, Moss=4, Locke=5, Waterman=8, Lightfoot=9, Wells=5}

Using Method Reference: 

Set<String> names = new HashSet<>();
names.add("Woolfield");
names.add("Locke");
names.add("Waterman");
names.add("Lightfoot");
names.add("Moss");
names.add("Wells");

// 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));

Output
{Woolfield=9, Moss=4, Locke=5, Waterman=8, Lightfoot=9, Wells=5}

6. default Stream<E> parallelStream()

parallelStream() method was added to the Collection interface in Java 8. 

stream() method operates in a sequential manner, whereas parallelStream() may operate in a parallel mode. There is no guarantee if the parallelStream() method will indeed return a parallel stream. Stream API internals take this decision.

Example: Given names as Set<String>, print names that don’t start with the letter “W”.

Set<String> names = new HashSet<>();
names.add("Woolfield");
names.add("Locke");
names.add("Waterman");
names.add("Lightfoot");
names.add("Moss");
names.add("Wells");

names
	.parallelStream()
	.filter(name -> !name.startsWith("W"))
	.forEach(System.out::println);

Output : 
Moss
Locke
Lightfoot

Given names as Set<String>, return Map<String, Integer> where entry is defined as string-string_length.

Using Lambda expression : 

Set<String> names = new HashSet<>();
names.add("Woolfield");
names.add("Locke");
names.add("Waterman");
names.add("Lightfoot");
names.add("Moss");
names.add("Wells");

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

System.out.println(nameWithLengths);

Using Method Reference : 

Set<String> names = new HashSet<>();
names.add("Woolfield");
names.add("Locke");
names.add("Waterman");
names.add("Lightfoot");
names.add("Moss");
names.add("Wells");

// 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));

System.out.println(nameWithLengths);

7. default Spliterator<E> spliterator()

spliterator() was added in Collection and Set interface in Java 8.

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. Set interface has its own implementation of spliterator.

Don’t use this API unless you know what you are doing.

Given names as Set<String>, return Map<String, Integer> where entry is defined as string-string_length.

Set<String> names = new HashSet<>();
names.add("Woolfield");
names.add("Locke");
names.add("Waterman");
names.add("Lightfoot");
names.add("Moss");
names.add("Wells");

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

System.out.println(nameWithLengths);

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

toArray() method added to Collection interface in Java 8.

toArray() method acts as a bridge between arrays as collections APIs. Prior to Java 8 two different toArray() methods were exposed.

Using toArray() method prior to Java 8.

Set<String> names = new HashSet<>();
names.add("Woolfield");
names.add("Locke");
names.add("Waterman");
names.add("Lightfoot");
names.add("Moss");
names.add("Wells");

String[] namesArr1 = names.toArray(new String[0]);
System.out.println(Arrays.toString(namesArr1));

Object[] namesArr2 = names.toArray();
System.out.println(Arrays.toString(namesArr2));

Output 
[Woolfield, Moss, Locke, Waterman, Lightfoot, Wells]
[Woolfield, Moss, Locke, Waterman, Lightfoot, Wells]

Using toArray() method in Java 8.

Set<String> names = new HashSet<>();
names.add("Woolfield");
names.add("Locke");
names.add("Waterman");
names.add("Lightfoot");
names.add("Moss");
names.add("Wells");

String[] usingLambda = names.toArray(val -> new String[val]);
System.out.println(Arrays.toString(usingLambda));

// Use method reference instead of lambda.
// Intention of behavior is clearer than lambda expression.
String[] usingMethodRefs = names.toArray(String[]::new);
System.out.println(Arrays.toString(usingMethodRefs));
	
Output
[Woolfield, Moss, Locke, Waterman, Lightfoot, Wells]
[Woolfield, Moss, Locke, Waterman, Lightfoot, Wells]

9. Static factories

Static factories i.e of() and copyOf() methods were added in the Set interface in Java 9 and Java 10 respectively.

Several static factory methods are provided as part of the Set interface. The returned instances are immutable and cannot be changed structurally. Custom implementation is done for the returned Set object, it is not HashSet. Take a look at the java.util.ImmutableCollections class code, it is really good.

null elements are disallowed in static factories. This is a big improvement. While iterating we usually have to make sure that the element we are processing is not null. If we are using these static factories then there won’t be any null elements in Set.

9.1 copyOf static factories

copyOf method accepts Collection as an input. If the parameter i.e. Collection is null or any element in this set is null then NullPointerException is thrown.

Set<String> names = new HashSet<>();
names.add("Woolfield");
names.add("Locke");
names.add("Waterman");
names.add("Lightfoot");
names.add("Moss");
names.add("Wells");

Set<String> copyOf = Set.copyOf(names);

Assert.assertEquals(names, copyOf);

9.2 of static factories

of() method will throw NullPointerException if an element is set is null. of() method will throw IllegalArgumentException if the input contains duplicate elements.

MethodWhat does it do?
static <E> Set<E> of()Returns unmodifiable empty Set
static <E> Set<E> of(E e1)Returns an unmodifiable Set with one element
static <E> Set<E> of(E e1, E e2)Returns an unmodifiable Set with two elements
static <E> Set<E> of(E e1, E e2, E e3)Returns an unmodifiable Set with three elements
static <E> Set<E> of(E e1, E e2, E e3, E e4)Returns an unmodifiable Set with four elements
static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5)Returns an unmodifiable Set with five elements
static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5, E e6)Returns an unmodifiable Set with six elements
static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7)Returns an unmodifiable Set with seven elements
static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8)Returns an unmodifiable Set with eight elements
static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9)Returns an unmodifiable Set with nine elements
static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9, E e10)Returns an unmodifiable Set with 10 elements
static <E> Set<E> of(E… elements)Returns an unmodifiable Set with arbitrary numbers of elements
// creates empty Set<String>
Set.of();

// creates Set<String> with 1 element
Set.of("Liam");

// creates Set<String> with 2 element
Set.of("Liam", "Emma");

// creates Set<String> with 3 element
Set.of("Liam", "Emma", "Noah");

// creates Set<String> with 4 element
Set.of("Liam", "Emma", "Noah", "Olivia");

// creates Set<String> with 5 element
Set.of("Liam", "Emma", "Noah", "Olivia", "William");

// creates Set<String> with 6 element
Set.of("Liam", "Emma", "Noah", "Olivia", "William", "Ava");

// creates Set<String> with 7 element
Set.of("Liam", "Emma", "Noah", "Olivia", "William", "Ava", "James");

// creates Set<String> with 8 element
Set.of("Liam", "Emma", "Noah", "Olivia", "William", "Ava", "James", 
		"Isabella");

// creates Set<String> with 9 element
Set.of("Liam", "Emma", "Noah", "Olivia", "William", "Ava", "James", 
		"Isabella", "Oliver");

// creates Set<String> with 10 element
Set.of("Liam", "Emma", "Noah", "Olivia", "William", "Ava", "James", 
		"Isabella", "Oliver", "Sophia");

// creates Set<String> with varargs elements
Set.of("Liam", "Emma", "Noah", "Olivia", "William", "Ava", "James", 
		"Isabella", "Oliver", "Sophia", "Benjamin");

10. Conclusion

That is all on the new features of Set interface. The new methods added in the Set and Collection interface are extremely useful and simple of use. 

Leave a Reply

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