Collecting
As the preceding examples have shown, it is possible (indeed, common) to obtain a stream from a collection. Sometimes it is desirable to obtain the opposite: to obtain a
collection from a stream. To perform such an action, the stream API provides
the collect( ) method. It has two
forms. The one we will use first is shown here:
<R, A> R
collect(Collector<? super T, A, R> collectorFunc)
Here, R specifies the type of the result, and T specifies the element type of the invoking stream. The internal
accumulated type is specified by A.
The collectorFunc specifies how the
collection process works. The collect( )
method is a terminal operation.
The Collector interface is declared in java.util.stream, as shown here: interface Collector<T, A, R>
T, A, and R have the same meanings as just
described. Collector specifies
several methods, but for the
purposes of this chapter, we won’t need to implement them. Instead, we will use
two of the predefined collectors that are provided by the Collectors class, which is packaged in java.util.stream.
The Collectors class defines a number of static collector methods that
you can use as-is. The two we will use are toList(
) and toSet( ), shown here:
static <T>
Collector<T, ?, List<T>> toList( ) static <T> Collector<T,
?, Set<T>> toSet( )
The toList( ) method returns a collector that can be used to collect
elements into a List. The toSet( ) method returns a collector
that can be used to collect elements into a
Set. For example, to collect
elements into a List, you can call collect( ) like this:
collect(Collectors.toList())
The following program puts
the preceding discussion into action. It reworks the example in the previous
section so that it collects the names and phone numbers into a List and a Set.
// Use collect() to create a List and a Set
from a stream.
import java.util.*;
import java.util.stream.*;
class NamePhoneEmail { String name;
String phonenum; String email;
NamePhoneEmail(String n, String p, String e) {
name = n;
phonenum = p; email = e;
}
}
class NamePhone { String name; String phonenum;
NamePhone(String n, String p) { name = n;
phonenum = p;
}
}
class StreamDemo7 {
public static void main(String[] args) {
// A list of names, phone numbers, and e-mail
addresses.
ArrayList<NamePhoneEmail> myList = new
ArrayList<>( );
myList.add(new
NamePhoneEmail("Larry", "555-5555",
"Larry@HerbSchildt.com"));
myList.add(new
NamePhoneEmail("James", "555-4444",
"James@HerbSchildt.com"));
myList.add(new NamePhoneEmail("Mary",
"555-3333", "Mary@HerbSchildt.com"));
//Map just the names and phone numbers to a new
stream.
Stream<NamePhone> nameAndPhone =
myList.stream().map(
-> new NamePhone(a.name,a.phonenum)
);
//Use collect to create a List of the names and
phone numbers. List<NamePhone> npList = nameAndPhone.collect(Collectors.toList());
System.out.println("Names and phone
numbers in a List:"); for(NamePhone e : npList)
System.out.println(e.name + ": " +
e.phonenum);
//Obtain another mapping of the names and phone
numbers.
nameAndPhone = myList.stream().map(
-> new NamePhone(a.name,a.phonenum)
);
//Now, create a Set by use of collect().
Set<NamePhone> npSet =
nameAndPhone.collect(Collectors.toSet());
System.out.println("\nNames and phone
numbers in a Set:"); for(NamePhone e : npSet)
System.out.println(e.name + ": " +
e.phonenum);
}
}
The output is shown here:
Names and phone numbers in a List:
Larry: 555-5555
James: 555-4444
Mary: 555-3333
Names and phone numbers in a Set:
James: 555-4444
Larry: 555-5555
Mary: 555-3333
In the program, the following
line collects the name and phone numbers into a List by
using toList( ):
List<NamePhone> npList =
nameAndPhone.collect(Collectors.toList());
After this line executes, the
collection referred to by npList can
be used like any other List
collection. For example, it can be cycled through by using a for-each for loop, as shown in the next line:
for(NamePhone e : npList)
System.out.println(e.name + ": " + e.phonenum);
The creation of a Set via collect(Collectors.toSet( )) works in the same way. The ability to
move data from a collection to a stream, and then back to a collection again is
a very powerful attribute of the stream API. It gives you the ability to
operate on a collection through a stream, but then repackage it as a
collection. Furthermore, the stream operations can, if appropriate, occur in
parallel.
The version of collect( ) used by the previous example
is quite convenient, and often the one you want, but there is a second version
that gives you more control over the collection process. It is shown here:
<R> R
collect(Supplier<R> target,
BiConsumer<R, ? super T> accumulator,
BiConsumer <R, R> combiner)
Here, target specifies how the object that holds the result is created.
For example, to use a LinkedList as
the result collection, you would specify its constructor. The accumulator function adds an element to the result
and combiner combines two partial
results. Thus, these functions work similarly to the way they do in reduce( ). For both, they must be
stateless and non-interfering. They must also be associative.
Note that the target parameter is of type Supplier. It is a functional interface
declared in java.util.function. It
specifies only the get( ) method,
which has no parameters and, in this case,
returns an object of type R. Thus,
as it relates to collect( ), get( ) returns a reference to a mutable
storage object, such as a collection.
Note also that the types of accumulator and combiner are BiConsumer.
This is a functional interface defined in java.util.function.
It specifies the abstract method accept(
) that is shown here:
void accept(T obj, U obj2)
This method performs some
type of operation on obj and obj2. As it relates to accumulator, obj specifies the target collection, and obj2 specifies the element to add to that collection. As it relates to combiner, obj and obj2
specify two collections that will be combined.
Using the version of collect( ) just described, you could
use a LinkedList as the target in
the preceding program, as shown here:
LinkedList<NamePhone> npList =
nameAndPhone.collect(
() -> new LinkedList<>(),
(list, element) -> list.add(element),
(listA,listB ) -> listA.addAll(listB));
Notice that the first
argument to collect( ) is a lambda
expression that returns a new LinkedList.
The second argument uses the standard collection method add( ) to add an element
to the list. The third element uses addAll(
) to combine two linked lists. As a point of interest, you can use any
method defined by LinkedList to add
an element to the list. For example, you could use addFirst( ) to add elements to the start of the list, as shown
here:
(list, element) -> list.addFirst(element)
As you may have guessed, it
is not always necessary to specify a lambda expression for the arguments to collect( ). Often, method and/or
constructor references will suffice. For example, again assuming the preceding
program, this statement creates a HashSet
that contains all of the elements in the nameAndPhone
stream:
HashSet<NamePhone> npSet =
nameAndPhone.collect(HashSet::new, HashSet::add, HashSet::addAll);
Notice that the first
argument specifies the HashSet
constructor reference. The second and third specify method references to HashSet’s add( ) and addAll( )
methods.
One last point: In the
language of the stream API, the collect(
) method performs what is called a mutable
reduction. This is because the result of the reduction is a mutable (i.e.,
changeable) storage object, such as a collection.
Related Topics
Privacy Policy, Terms and Conditions, DMCA Policy and Compliant
Copyright © 2018-2023 BrainKart.com; All Rights Reserved. Developed by Therithal info, Chennai.