Flowing Through the Tubes of the Stream API (Java 8)

Datetime:2016-08-23 00:38:43          Topic: Java8           Share

The target of this article is to show some features of Java 8 by example . In special, the features of  Stream API , Lambda Expressions, and Functional Interfaces .

Functional Interface

A Funcional Interface is an interface with a single abstract method.

@FunctionalInterface
public interface EventHandler<E extends Event> extends EventListener {
    void handle(E e);
}
@FunctionalInterface
public interface Comparator<T> {
    int compare(T obj1, T obj2);
}
@FunctionalInterface
public interface Runnable {
    void run();
}

Functional Interface Types

Supplier

@FunctionalInterface
public interface Supplier<T> {
    T get();
}
new Date() // Date::new
() -> "Oops..."

Consumer

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}
x -> System.out.println(x) // System.out::println
x -> x.doSomething()

Predicate

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}
x -> x.isDirectory() // File::isDirectory
x -> x % 2 == 0

Function

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
x -> x.length() // File::length
x -> x.getId()
FUNCTIONAL INTERFACE PARAMETER TYPES RETURN TYPE DESCRIPTION
Supplier <T> NONE T Supplies a value of type T
Consumer <T> T void Consumes a value of type T
Predicate <T> T boolean A boolean-valued function
Function <T, R> T R A function with argument T and return R
ToIntFunction <T> T int An int-valued function
ToLongFunction <T> T long A long-valued function
ToDoubleFunction <T> T double A double-valued function
IntFunction <R> int R A function with argument of type int
LongFunction <R> long R A function with argument of type long
DoubleFunction <R> double R A function with argument of type double
UnaryOperator <T> T T A unary operator of type T

Functional Interface and Lambda Expressions

Given a method that takes a functional interface as parameter:

public abstract class AbstractButton extends JComponent implements ItemSelectable, SwingConstants {
    /* ... */
    public void addActionListener(ActionListener l) { /* ... */ }
    /* ... */
}

And a functional interface:

public interface ActionListener extends EventListener {
    void actionPerformed(ActionEvent e);
}

You can code in the old-fashioned way using an anonymous class:

JButton button = new JButton("DON'T PANIC");
button.addActionListener(
   new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
         System.out.println(e);
      }
   }
);

Or you can code in a new way using a lambda expressions:

JButton button = new JButton("DON'T PANIC");
// LAMBDA EXPRESSION
button.addActionListener(e -> System.out.println(e));

Or even more concise using a method reference:

JButton button = new JButton("DON'T PANIC");
// LAMBDA EXPRESSION WITH METHOD REFERENCE
button.addActionListener(System.out::println);

Method Reference

object::instanceMethod
Class::instanceMethod
Class::staticMethod

Constructor Reference

Class::new

Closure

CLOSURE = PARAMETERS + BLOCK OF CODE + FREE VARIABLES

final double discount = 50.0; // FREE VARIABLES MUST BE FINAL
Function<Double, Double> blackFridayBrazil = price -> (2 * price) * (1 - (discount / 100.0));
System.out.println(blackFridayBrazil.apply(30.0));
// 30.0

Streams

  • All in one package: java.util.stream

  • Supports functional-style operations

  • All stream operations take lambda expressions as arguments

  • Integrated into the Collections API (but streams are not collections)

  • Enables bulk operations on collections through convenient methods

  • Enables filter and map-reduce transformations

  • Enables lazy evaluation and short-circuit operations

  • Never modify the underlying data structure

  • Supports infinite (unbounded) streams

  • Supports parallel execution

Working With Streams

  • Create streams from collections, arrays, generators or iterators

  • Collect streams results in collections, arrays, strings, or maps

  • Work with null values using Optional type

  • Use specialized streams for primitive types

  • Use filter operation to select

  • Use map operation to transform

Pipeline of Operations

  1. Create a stream

  2. Specify intermediate operations (for transforming the initial stream into others)

  3. Apply a terminal operation to produce a result

Streams Methods

  • Intermediate Methods

    These methods produce other streams and don't get processed until some terminal method is called.

map filter distinct sorted peek limit substream parallel sequential unordered
  • Terminate Methods

    After one of these methods is invoked, the stream is considered consumed and no more operations can be performed on it.

forEach forEachOrdered toArray reduce collect min max count anyMatch allMatch noneMatch findFirst findAny iterator
  • Short-circuit Methods

    These methods process one element at a time and stop stream processing as soon as their conditions are satisfied.

anyMatch allMatch noneMatch findFirst findAny limit substream

Short-Circuit Methods

stream.map(...).filter(...).findFirst().get()
// DOES THE map AND filter OPERATIONS ONE ELEMENT AT TIME
// CONTINUES UNTIL FIRST MATCH ON THE FILTER TEST
stream.map(...).filter(...).filter(...).allMatch(...)
// DOES ONE map, TWO filter OPERATIONS, AND ONE allMatch TEST ONE ELEMENT AT TIME
// THE FIRST TIME IT GETS false FOR allMatch TEST, IT STOPS

Creating Streams

  • From values

Stream.of(...);
  • From arrays

Stream.of(someArray);
  • From collections

someCollection.stream();
  • From functions

Stream.generate(...);
Stream.iterate(...);
  • From another stream

someStream.distinct();
someStream.limit(...);
someStream.map(...);

Collecting From Stream

  • To collections

someStream.collect(Collectors.toList());
someStream.collect(Collectors.toSet());
someStream.collect(Collectors.groupingBy(...)); 
  • To array

someStream.toArray(SomeClass[]::new);
  • To string

someStream.collect(Collectors.joining(...));

Streams

List<String> characters = Arrays.asList("mario", "Mario", "luigi", "Luigi", "Yoshi", "Toad", "Toad");

System.out.println(characters); 
// [mario, Mario, luigi, Luigi, Yoshi, Toad, Toad] 

System.out.println(characters.stream().count()); 
// 7

System.out.println(characters.stream().distinct().count()); 
// 6

System.out.println(characters.stream().map(String::toLowerCase).distinct().count());
// 4

Collecting in String

List<String> characters = Arrays.asList("Mario", "Luigi", "Yoshi", "Toad");

String result1 = characters.stream().collect(Collectors.joining());
System.out.println(result1); 
// MarioLuigiYoshiToad

String result2 = characters.stream().collect(Collectors.joining(", "));
System.out.println(result2); 
// Mario, Luigi, Yoshi, Toad 

// IF STREAM CONTAINS OBJECTS OTHER THAN STRING, FIRST CONVERT THEM TO STRINGS
String result3 = characters.stream().map(Object::toString).collect(Collectors.joining(", "));
System.out.println(result3); 
// Mario, Luigi, Yoshi, Toad 

Collecting in Collection

In order to collect the result in a HashSet use collect method. It takes three arguments:

  1. A supplier that creates new instances of the target object

  2. An accumulator that adds an element to the target

  3. A combiner that merges two objects into one

List<String> characters = Arrays.asList("Mario", "Mario", "Luigi", "Yoshi", "Toad", "Toad");

// SUPPLIER, ACCUMULATOR, COMBINER
Set<String> result1 = characters.stream().collect(HashSet::new, HashSet::add, HashSet::addAll);
System.out.println(result1); 
// [Yoshi, Luigi, Toad, Mario] 

Set<String> result2 = characters.stream().collect(Collectors.toSet());
System.out.println(result2); 
// [Yoshi, Luigi, Toad, Mario]

List<String> result3 = characters.stream().collect(Collectors.toList());
System.out.println(result3); 
// [Mario, Mario, Luigi, Yoshi, Toad, Toad] 

TreeSet<String> result4 = characters.stream().collect(Collectors.toCollection(TreeSet::new));
System.out.println(result4);
// [Luigi, Mario, Toad, Yoshi] 

Collecting in Map

This is going to be our POJO for the following examples:

public class City {
   private long id;
   private String state;
   private String name;
   private long population;
   private List<String> streets;
   /* CONSTRUCTOR, GETTERS AND SETTERS */
   @Override
   public String toString() {
      return name + "/" + state;
   }
}

And this is going to be our test dataset:

City city1 = new City(1L, "SP", "Sao Paulo", 11316149L, Arrays.asList("Av. Paulista", "Av. Reboucas"));
City city2 = new City(2L, "RJ", "Rio de Janeiro", 6323037L, Arrays.asList("Av. Brasil"));
City city3 = new City(3L, "SP", "Campinas", 1098630L, Arrays.asList("Av. Brasil"));
City city4 = new City(4L, "MG", "Uberaba", 302623L, Arrays.asList("Av. Leopoldina"));

List<City> cities = Arrays.asList(city1, city2, city3, city4);
System.out.println(cities.stream().map(Object::toString).collect(Collectors.joining(", "))); 
// Sao Paulo/SP, Rio de Janeiro/RJ, Campinas/SP, Uberaba/MG 

Map<Long, String> nameById = cities.stream().collect(Collectors.toMap(City::getId, City::getName)); 
System.out.println(nameById); 
// {1=Sao Paulo, 2=Rio de Janeiro, 3=Campinas, 4=Uberaba} 

Map<Long, City> cityByIdMap = cities.stream().collect(Collectors.toMap(City::getId, Function.identity())); 
System.out.println(cityByIdMap); 
// {1=Sao Paulo/SP, 2=Rio de Janeiro/RJ, 3=Campinas/SP, 4=Uberaba/MG}
// PROVIDE A MERGE FUNCTION TO RESOLVE COLLISIONS BETWEEN KEYS
// IF WE RETURN null FROM OUR MERGE FUNCTION, THE LATEST VALUE IS KEPT
// IN THIS EXAMPLE WE ARE GOING TO KEEP THE FIRST VALUE
TreeMap<Long, City> cityByIdTreeMap = cities.stream().collect(Collectors.toMap(City::getId, Function.identity(), (v1, v2) -> v1, TreeMap::new)); 
System.out.println(cityByIdTreeMap); 
// {1=Sao Paulo/SP, 2=Rio de Janeiro/RJ, 3=Campinas/SP, 4=Uberaba/MG} 

Map<String, List<City>> listOfCitiesByState = cities.stream().collect(Collectors.groupingBy(City::getState));
System.out.println(listOfCitiesByState);
// {RJ=[Rio de Janeiro/RJ], MG=[Uberaba/MG], SP=[Sao Paulo/SP, Campinas/SP]} 

Map<String, Set<City>> setOfCitiesByState = cities.stream().collect(Collectors.groupingBy(City::getState, Collectors.toSet()));
System.out.println(setOfCitiesByState);
// {RJ=[Rio de Janeiro/RJ], MG=[Uberaba/MG], SP=[Campinas/SP, Sao Paulo/SP]} 

Map<Boolean, List<City>> spCitiesAndOtherCities = cities.stream().collect(Collectors.partitioningBy(e -> e.getState().equals("SP")));
System.out.println(spCitiesAndOtherCities);
// {false=[Rio de Janeiro/RJ, Uberaba/MG], true=[Sao Paulo/SP, Campinas/SP]} 

List<City> spCities = spCitiesAndOtherCities.get(true);
System.out.println(spCities);
// [Sao Paulo/SP, Campinas/SP] 

List<City> otherCities = spCitiesAndOtherCities.get(false);
System.out.println(otherCities);
// [Rio de Janeiro/RJ, Uberaba/MG] 
// OTHER DOWNSTREAM PROCESSING

Map<String, Long> cityCountByState = cities.stream().collect(Collectors.groupingBy(City::getState, Collectors.counting()));
System.out.println(cityCountByState);
// {RJ=1, MG=1, SP=2} 

Map<String, Long> populationByState = cities.stream().collect(Collectors.groupingBy(City::getState, Collectors.summingLong(City::getPopulation)));
System.out.println(populationByState);
// {RJ=6323037, MG=302623, SP=12414779} 

Map<String, Optional<City>> largestCityByState = cities.stream().collect(Collectors.groupingBy(City::getState, Collectors.maxBy(Comparator.comparing(City::getPopulation))));
System.out.println(largestCityByState);
// {RJ=Optional[Rio de Janeiro/RJ], MG=Optional[Uberaba/MG], SP=Optional[Sao Paulo/SP]}

Map<String, Optional<City>> smallestCityByState = cities.stream().collect(Collectors.groupingBy(City::getState, Collectors.minBy(Comparator.comparing(City::getPopulation))));
System.out.println(smallestCityByState);
// {RJ=Optional[Rio de Janeiro/RJ], MG=Optional[Uberaba/MG], SP=Optional[Campinas/SP]} 

Map<String, Optional<String>> longestCityNameByState = cities.stream().collect(Collectors.groupingBy(City::getState, Collectors.mapping(City::getName, Collectors.maxBy(Comparator.comparing(String::length)))));
System.out.println(longestCityNameByState);
// {RJ=Optional[Rio de Janeiro], MG=Optional[Uberaba], SP=Optional[Sao Paulo]} 

Map<String, Set<String>> setOfCityNamesByState = cities.stream().collect(Collectors.groupingBy(City::getState, Collectors.mapping(City::getName, Collectors.toSet())));
System.out.println(setOfCityNamesByState);
// {RJ=[Rio de Janeiro], MG=[Uberaba], SP=[Sao Paulo, Campinas]} 

Map<String, String> cityNamesByState = cities.stream().collect(Collectors.groupingBy(City::getState, Collectors.mapping(City::getName, Collectors.joining(", "))));
System.out.println(cityNamesByState);
// {RJ=Rio de Janeiro, MG=Uberaba, SP=Sao Paulo, Campinas} 

Map<String, LongSummaryStatistics> populationSummaryByState = cities.stream().collect(Collectors.groupingBy(City::getState, Collectors.summarizingLong(City::getPopulation)));
System.out.println(populationSummaryByState);
// {RJ=LongSummaryStatistics{count=1, sum=6323037, min=6323037, average=6323037.000000, max=6323037}, MG=LongSummaryStatistics{count=1, sum=302623, min=302623, average=302623.000000, max=302623}, SP=LongSummaryStatistics{count=2, sum=12414779, min=1098630, average=6207389.500000, max=11316149}} 

Statistics

// IntSummaryStatistics, LongSummaryStatistics, DoubleSummaryStatistics
LongSummaryStatistics summary = cities.stream().collect(Collectors.summarizingLong(City::getPopulation));

System.out.println(summary.getCount());
// 4

System.out.println(summary.getSum());
// 19040439 

System.out.println(summary.getMin());
// 302623

System.out.println(summary.getMax());
// 11316149

System.out.println(summary.getAverage());
// 4760109.75 

Flattened Maps

The flatMap operation works with sub-streams of mapped elements.

All these sub-streams are flattened into a single stream of elements:

Supplier<Stream<City>> supplier = () -> cities.stream(); 

List<List<String>> mappedStreets = supplier.get().map(city -> city.getStreets()).collect(Collectors.toList());
System.out.println(mappedStreets); 
// [[Av. Paulista, Av. Reboucas], [Av. Brasil], [Av. Brasil], [Av. Leopoldina]] 

Set<String> flattenedStreets = supplier.get().flatMap(city -> city.getStreets().stream()).collect(Collectors.toSet());
System.out.println(flattenedStreets);
// [Av. Reboucas, Av. Paulista, Av. Leopoldina, Av. Brasil] 

Peeking

Use peek to debug.

Use peek to perform non-interfering action on elements:

Stream<String> stream = Stream.of("Mario", "Luigi", "Yoshi", "Toad");

stream.forEach(e -> System.out.print(e.toLowerCase() + " "));
// mario luigi yoshi toad 

stream.forEach(e -> System.out.print(e.toUpperCase() + " "));
// java.lang.IllegalStateException: stream has already been operated upon or closed 

List<String> characters = Arrays.asList("Mario", "Luigi", "Yoshi", "Toad");

characters
   .stream()
   .peek(e -> System.out.print(e.toLowerCase() + " "))
   .peek(e -> System.out.print(e.toUpperCase() + " "))
   .forEach(e -> System.out.print("(" + e + ") ")); 
// mario MARIO (Mario) luigi LUIGI (Luigi) yoshi YOSHI (Yoshi) toad TOAD (Toad) 

characters
   .stream()
   .filter(e -> e.length() > 4)
   .peek(e -> System.out.print("f(" + e + ") "))
   .map(String::toUpperCase)
   .peek(e -> System.out.print("m(" + e + ") "))
   .collect(Collectors.toList());
// f(Mario) m(MARIO) f(Luigi) m(LUIGI) f(Yoshi) m(YOSHI) 

Reducing

Reducing operation reduces the entire stream into a single value.

The reduce is similar to the fold operation in functional languages.

Reduce function:

reduce(identity, accumulator)

  1. Combines an identity with the first element of the stream

  2. The result is then combined with the second element of the stream

  3. The result is then combined with the third element of the stream

  4. And so on...

An accumulator provides how the elements are combined together:

Optional<Integer> sum = Stream.of(1, 2, 3, 4).reduce((x, y) -> x + y);
// e1 + e2 + e3 + ...

System.out.println(sum.orElse(0)); 
// 10

Integer sum = Stream.of(1, 2, 3, 4).reduce(0, (x, y) -> x + y);
// 0 + e1 + e2 + e3 + ...

System.out.println(sum); 
// 10

Integer product = Stream.of(1, 2, 3, 4).reduce(1, (x, y) -> x * y);
// 1 * e1 * e2 * e3 * ...

System.out.println(product); 
// 24

Associative Operations

Reducing works with associative operations.

Operation op

v1 op v2 op v3 op . . .

Associative operations

(x op y) op z = x op (y op z)

Addition is an associative operation

(1 + 2) + 3 = 1 + (2 + 3)

Subtraction is not an associative operation

(1 - 2) - 3 <> 1 - (2 - 3)

Identity

x op x = x

for example, 0 (zero) is the identity for the addition operation.

Reducing Properties

// REDUCING A PROPERTY WITHIN THE ELEMENT
long population = cities.stream().parallel().reduce(
   0L,
   (total, city) -> total + city.getPopulation(), // ACCUMULATOR
   (total1, total2) -> total1 + total2); // COMBINER FOR PARALLEL OPERATIONS
System.out.println(population); 
// 19040439 

long population = cities.stream().mapToLong(City::getPopulation).sum();
System.out.println(population); 
// 19040439

Parallelizing

Set<String> statesWithLargestCities =
   cities.stream()
   .parallel().filter(e -> e.getPopulation() > 1000000) // FILTERING WILL BE PERFORMED CONCURRENTLY
   .sequential().map(City::getState).collect(Collectors.toSet()); // MAPPING WILL BE PERFORMED SEQUENTIALLY
System.out.println(statesWithLargestCities); 
// [RJ, SP] 

Infinite Streams

// INFINITE SEQUENCE, VALUES WILL BE GENERATED ON THE FLY
Stream<BigInteger> integers = Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE));
System.out.println(integers.map(BigInteger::toString).collect(Collectors.joining(" "))); 
// java.lang.OutOfMemoryError: Java heap space

// INFINITE SEQUENCE, VALUES WILL BE GENERATED ON THE FLY
Stream<BigInteger> integers = Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE));
System.out.println(integers.limit(10).map(BigInteger::toString).collect(Collectors.joining(" "))); 
// 0 1 2 3 4 5 6 7 8 9 

Optional Type

Optional type handles null values.

Store T or nothing:

List<String> characters = Arrays.asList("Mario", "Luigi", "Yoshi", "Toad");

Optional<String> nameStartingWithL = characters.stream().filter(e -> e.startsWith("L")).findFirst();
nameStartingWithL.ifPresent(System.out::println); 
// Luigi

// WHENEVER YOU USE findAny YOU BETTER USE parallel
Optional<String> nameStartingWithY = characters.stream().parallel().filter(e -> e.startsWith("Y")).findAny();
nameStartingWithY.ifPresent(System.out::println); 
// Yoshi 

String nameStartingWithX = characters.stream().parallel().filter(e -> e.startsWith("X")).findAny().orElse("Oops...");
System.out.println(nameStartingWithX);
// Oops...

// NO NEED TO USE filter
boolean hasNameStartingWithM = characters.stream().parallel().anyMatch(e -> e.startsWith("M"));
System.out.println(hasNameStartingWithM); 
// true

Primitive Type Streams

Store primitive values directly without using wrappers.

IntStream byte, short, int, char, boolean
LongStream long
DoubleStream float, double
IntStream primes = IntStream.of(1, 2, 3, 5, 7);
System.out.println(Arrays.toString(primes1.toArray())); 
// [1, 2, 3, 5, 7]

int[] values = {1, 2, 3, 5, 7};
IntStream primes = Arrays.stream(values, 0, 3);
System.out.println(Arrays.toString(primes.toArray())); 
// [1, 2, 3] 

// UPPER BOUND IS EXCLUDED
IntStream zeroToNine = IntStream.range(0, 10);
zeroToNine.forEach(n -> System.out.print(n + " ")); 
// 0 1 2 3 4 5 6 7 8 9 

// UPPER BOUND IS INCLUDED
IntStream zeroToTen = IntStream.rangeClosed(0, 10);
zeroToTen.forEach(n -> System.out.print(n + " ")); 
// 0 1 2 3 4 5 6 7 8 9 10 
IntStream primes = IntStream.of(1, 2, 3, 5, 7);

// STREAM OF PRIMITIVE TYPES TO STREAM OF OBJECTS
Stream<Integer> boxedPrimes = primes.boxed();

// STREAM OF OBJECTS TO STREAM OF PRIMITIVE TYPES (USE mapToInt, mapToLong, mapToDouble)
IntStream unboxedPrimes = boxedPrimes.mapToInt(Integer::intValue); 

// STRING TO STREAM OF UNICODE CODE POINTS
IntStream codes = "A B C".codePoints();
System.out.println(Arrays.toString(codes.toArray())); 
// [65, 32, 66, 32, 67] 

Stream<String> stream = Stream.of("Mario", "Luigi", "Yoshi", "Toad");
IntStream lengths = stream.mapToInt(String::length);
System.out.println(Arrays.toString(lengths.toArray())); 
// [5, 5, 5, 4] 

Warnings

Stream<String> stream = Stream.of("Mario", "Luigi", "Yoshi", "Toad");

String[] result = stream.toArray(String[]::new);
// stream.toArray() RETURNS AN ARRAY OF TYPE Object[] 
List<String> characters = new ArrayList<String>();
characters.addAll(Arrays.asList("Mario", "Luigi", "Yoshi", "Toad"));

Stream<String> stream1 = characters.stream().filter(e -> e.startsWith("M"));
characters.add("Magikoopa"); // OK, INTERMEDIATE OPERATIONS ARE LAZY
System.out.println(stream1.collect(Collectors.toList())); 
// [Mario, Magikoopa] 

Stream<String> stream2 = characters.stream(); 
stream2.forEach(e -> {if (e.startsWith("M")) characters.remove(e);}); // ERROR, INTERFERENCE 
// java.util.ConcurrentModificationException 
List<Integer> integers = IntStream.range(0, 10).boxed().collect(Collectors.toCollection(ArrayList::new));
System.out.println(integers);
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 

System.out.println(integers.stream().peek(integers::remove).map(Object::toString).collect(Collectors.joining(", "))); 
// java.lang.NullPointerException 
List<Integer> integers = IntStream.range(0, 10).boxed().collect(Collectors.toList());
System.out.println(integers);
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 

// THE sorted OPERATION IS A "STATEFUL INTERMEDIATE OPERATION" 
System.out.println(integers.stream().sorted().peek(integers::remove).map(Object::toString).collect(Collectors.joining(", "))); 
// 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 

System.out.println(integers); 
// []
List<Integer> integers = IntStream.range(0, 10).boxed().collect(Collectors.toList());
System.out.println(integers); 
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 

System.out.println(integers.stream().parallel().sorted().peek(integers::remove).map(Object::toString).collect(Collectors.joining(", "))); 
// 0, 1, 2, 3, 4, 5, 6, 7, 8, 9

System.out.println(integers); // BAZINGA! 
// [8] 
Stream<Integer> stream1 = Stream.of(1, 2, 3, 5, 7);
System.out.println(stream1.map(Object::toString).collect(Collectors.joining(", "))); 
// 1, 2, 3, 5, 7 

String[] stringArray = {"A", "B", "C"};
Stream<String> stream2 = Stream.of(stringArray);
System.out.println(stream2.map(Object::toString).collect(Collectors.joining(", "))); 
// A, B, C 

int[] intArray = {1, 2, 3, 5, 7};
Stream<int[]> stream3 = Stream.of(intArray); // USE Arrays.stream(intArray)
System.out.println(stream3.map(Object::toString).collect(Collectors.joining(", "))); 
// [I@5caf905d ]

References

[book] Java 8 for the Really Impatient - Cay S. Horstmann





About List