Java-stream List to String

Introduction

Streams don't hold any data by themselves - they just stream it from a source. Yet, common code routines expect some sort of a structure to hold results after processing data. That is why, after [optional] intermediate operations, the Stream API provides for ways to convert the elements that it may have acted on into collections - like lists, that you can further use in your code.

These ways include applying:

  • Pre-defined or custom collectors:
R collect[Collector collector];

This is the most common, cleanest and simple approach you can utilize, and we'll be covering that first.

  • Suppliers, accumulators, and combiners [separating a Collector into its constituent parts]:
R collect[Supplier supplier, BiConsumer accumulator, BiConsumer combiner];

Or, you could terminate a stream by turning it into an array. Then, turn that array into a list. This is because the API already has two methods for producing arrays. They include:

Object[] toArray[];

Which returns an array containing the elements of a stream.

A[] toArray[IntFunction generator];

Where, the generator is a function which produces a new array of the desired type and the provided length

These array-producing methods are bound to make code extra verbose. And, that may make your code less readable. Yet in the end, they will still help you to convert a stream to a list.

If you'd like to read more about array to list conversion, read up on How to Convert Java Array to ArrayList.

Otherwise, this guide will look into how all these approaches work in detail. It will also throw in a few dirty hacks which will help you to convert too. Be careful with them, though - such tinkerings are bound to hurt your code's performance.

How to Convert a Stream to List using Collectors

The official documentation defines a collector as an implementation which is:

  1. Mutable;
  2. A reduction operation;

And:

[3] that accumulates input elements into a mutable result container, [4] optionally transforming the accumulated result into a final representation after all input elements have been processed.

Note how these 4 conditions seem like a mouthful. But, as we will see next, they are not as hard to fulfill.

Pre-defined Collectors

The Java 8 Stream API works in tandem with the Collectors API. The Collectors class offers ready-made collectors that apply the supplier-accumulator-combiner in their implementations.

Hence, using facilities from the Collectors utility class will clean up your code significantly.

The method we can use from Collectors class is Collectors.toList[].

To convert a stream into a list using pre-built Collectors, we simply collect[] it into a list:

List list = Stream.of["David", "Scott", "Hiram"].collect[Collectors.toList[]]; System.out.println[String.format["Class: %s\nList: %s", list.getClass[], list]];

This example is rather simple and just deals with Strings:

Class: class java.util.ArrayList List: [David, Scott, Hiram]

Though, if you're not working with Strings or simpler types, you'll likely have to map[] your objects before collecting them, which is more often the case than not. Let's define a simple Donor object, and a BloodBank that keeps track of them, and convert a Stream of Donors into a List.

Convert Stream to List with map[] and collect[]

Let us start by declaring a Donor class to model a blood donor:

It is advisable to implement the Comparable interface here since it facilitates the ordering and sorting of the Donor objects in collections. You can always supply custom Comparators instead, though, a Comparable entity is simply easier and cleaner to work with.

Then, we define a BloodBank interface, which specifies that blood banks can receive a donation from a Donor, as well as return all the available types:

public interface BloodBank { void receiveDonationFrom[Donor donor]; List getAvailableTypes[]; }

The next step is to create a concrete implementation of a BloodBank. Since all concrete implementations will accept donors, and only the approach to getting the available types will be implementation-dependent - let's create an abstract class as a middleman:

Finally, we can go ahead and create a concrete implementation and map[] the Donor list to their blood type, within a Stream and collect[] it back into a list, returning the available blood types:

public class CollectorsBloodBank extends AbstractBloodBank { @Override public List getAvailableTypes[] { return donors.stream[].map[Donor::getBloodGroup].collect[Collectors.toList[]]; } }

You can map[] the donors to any of the fields in the object and return a list of those fields, such as the amountDonated or name as well. Having a comparable field also makes it possible to sort them via sorted[].

If you'd like to read more about the sorted[] method, read our How to Sort a List with Stream.sorted[].

You could return all of the Donor instances instead, by simply calling collect[] on their Stream:

@Override public List getAvailableDonors[] { return donors.stream[].collect[Collectors.toList[]]; }

Though, you're not limited to just collecting a stream into a list - this is where the collectingAndThen[] method comes into play.

Convert Stream to List with Collectors.collectingAndThen[]

Earlier on we consulted the official documentation and, it stated that collectors have the capacity of:

optionally transforming the accumulated result into a final representation after all input elements have been processed.

The accumulated result in CollectorsBloodBank, for example, is represented by Collectors.toList[]. We can transform this result further using the method Collectors.collectingAndThen[].

Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!

Good practice requires one to return immutable collection objects. So, if we were to stick to this practice, a finisher step can be added to the conversion of stream to list:

Alternatively, you can put any Function as a finisher here as well.

If you'd like to read more, you can also read our detailed guide on the Collectors.collectingAndThen[] method [coming soon!]

Convert Stream to List with Suppliers, Accumulators, and Combiners

Instead of using pre-defined collectors, you can use separate Suppliers, Accumulators and Combiners instead. These are implemented as a Suplier, BiConsumer and BiConsumer, which all fit snuggly into a collect[] instead of a pre-defined Collector.

Let's take a look at how you can utilize this flexibility to return all the available types:

The implementation above applies the requisite supplier-accumulator-combiner pattern in a few steps:

Firstly, it turns the donors list field into a stream of Donor elements.

Remember, the LambdaBloodBank can access the donors field because it extends AbstractBloodBank. And, the donors field has protected access in the AbstractBloodBank class.

Then, an intermediate map operation is performed on the stream of Donors. The operation creates a new stream containing the String values that represent the donors' blood group types. Then. a result container that is mutable - i.e., the collector's supplier is created. This supplier container will be henceforth known as bloodGroups.

We add each blood group type [named bloodgroup in this step] from the stream into the mutable container: bloodGroups. In other words, the accumulation is occurring at this step.

The mutable, supplier container bloodGroups is added into the result container known as the resultList in this step. This is thus the combiner step.

We can improve the LambdaBloodBank's getAvailableTypes[] method further by using method references instead of lambdas:

public class MembersBloodBank extends AbstractBloodBank { @Override public List getAvailableTypes[] { return donors.stream[] .map[Donor::getBloodGroup] .collect[ ArrayList::new, ArrayList::add, ArrayList::addAll ]; } }

Creating Custom Collectors for Java 8 Streams

When you pass:

Collectors.collect[Supplier supplier, BiConsumer accumulator, BiConsumer combiner];

You are providing the arguments that the Collectors utility class will use to create a custom collector for you, implicitly. Otherwise, the starting point for creating a custom collector is the implementation of the Collector interface.

In our case, a collector that accumulates the blood group types would look like this CustomCollector class:

The CustomCollector class can then help you to convert a stream to a list like in this CustomCollectorBloodBank class:

Note: If you were to go all out with this - you can have multiple methods, such as toList[], toMap[], etc. that return different collections, using this same class.

How to Convert a Stream to List using Arrays

The Stream API offers a way of collecting elements from a stream pipeline into arrays. And because the Arrays utility class has methods that transform arrays into lists, this is a route you can opt for. Albeit, this approach is verbose, code-wise, and it's recommended to utilize either pre-built collectors, or to define your own if the standard ones don't fit your use-case.

Arrays of Objects

Using the Stream.toArray[] method, transform a stream into an array of objects. [That is, elements of the base Object class]. This may turn too verbose, depending on your use case and, it risks lowering your code's readability to a considerable extent.

Take this ArrayOfObjectsBloodBank class, for example:

This approach is fickle, requires classic for loops and iteration, manual casting and is considerably less readable than previous approaches - but it works.

Arrays Requiring an IntFunction Generator

Another way that the Stream API offers for turning a stream of elements into an array is the Stream.toArray[IntFunction generator] method. Whereas the preceding tactic of deriving an array of objects demanded the use of considerably many lines of code, the generator approach is quite succinct:

This is much better than the previous approach, and actually isn't all that bad - though, there's still a simply redundant conversion between an array and list here.

Other [Discouraged] Tactics of Converting Streams to Lists

The Stream API discourages the introduction of side effects into the stream pipeline. Because streams may be exposed to parallel threads, it is dangerous to attempt to modify an externally declared source container.

Thus, the two following examples of using Stream.forEach[] and Stream.reduce[] when you want to convert a stream to list are bad hacks.

Piggybacking on Stream.forEach[]

Without parallelism, this works just fine and the code will produce the results you want but it's not future-proof and is best avoided.

Convert a Stream to List using Stream.reduce[]

Conclusion

The Stream API introduced multiple ways of making Java more functional in nature. Because streams help operations to run in parallel, it is important that optional intermediate and terminal operations uphold the principles of:

  • Non-interference
  • Minimizing side effects
  • Keeping operation behaviors stateless

Among the tactics that this article has explored, the use of collectors is the one that promises to help you achieve all the three principles. It is thus important that as you continue working with streams, you improve your skills of handling both pre-defined and custom collectors.

The source code for this guide is available on GitHub.

Video liên quan

Chủ Đề