Java 8 stream

Share

As a junior full stack programmer in the betting team of the MozzartBet IT sector, one of the challenges that motivate me daily is, for example, how to create betting transactions collections in order to represent the player’s calculation, and then process the whole collection to find out how much a player has spent money. For most of these problems, the use of Java 8 is most often imposed. In order to speed up processing, I rely on multi-core architecture, but writing parallel code is difficult and error-prone, and standard processing of collections is far from perfect in Java.

Java provides a new additional package in Java 8 called java.util.stream. This package consists of classes, interfaces and enum to allows functional-style operations on the elements. Java 8 introduced the concept of stream that lets the developer to process data declaratively and leverage multicore architecture without the need to write any specific code for it.  

Stream represents a sequence of objects from a source, which supports aggregate operations.

Following are the characteristics of a stream:

Sequence of elements − A stream provides a set of elements of specific type in a sequential manner. A stream gets elements on demand. It never stores the elements.

Source− Stream takes Collections, Arrays, or I/O resources as input source.

Aggregate operations− Stream supports aggregate operations like forEach, filter, map, sorted, and so on.

Pipelining− Most of the stream operations return stream itself so that their result can be pipelined. Function of these operations is to take input, process them, and return output to the target. collect() method is a terminal operation which is normally present at the end of the pipelining operation to mark the end of the stream.

Automatic iterations− Stream operations do the iterations internally over the source elements provided, in contrast to Collections where explicit iteration is required.

We can generate stream with two methods:

             stream() – Returns a sequential stream considering collection as its source.

             parallelStream() − Returns a parallel stream considering collection as its source.

When stream is generated, we can use some of the many provided methods:

– forEach to iterate each element of the stream

Random random = new Random();

random.ints().limit(15).forEach(System.out::println);

This code segment shows how to print 15 random numbers using forEach.

– map is used to map each element to its corresponding result.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7);

List<Integer> squaresList = numbers.stream().map( i -> i*i).distinct().collect(Collectors.toList())

The following code segment prints unique squares of numbers using map.

– filter to eliminate elements based on a criteria.

List<String>strings = Arrays.asList("MozzartBet", "", "Java 8", "Stream", "filter","");

int count = strings.stream().filter(string -> string.isEmpty()).count();

This code segment prints a count of empty strings using filter.

-sorted to sort the stream.

Random random = new Random();

random.ints().limit(15).sorted().forEach(System.out::println);

The following code segment shows how to print 15 random numbers in a sorted order.

List<String>strings = Arrays.asList("MozzartBet", "", "Java 8", "parallelStream", "filter","");

int count = strings.parallelStream().filter(string -> string.isEmpty()).count();

As we can see, code is much shorter and it is very easy to switch between sequential and parallel streams then the same code in Java 7. Also streams are easy and good to use with Lambda functions.

The result of the following two pieces of logic is the same

List<Integer> list = Arrays.asList(1, 2, 3);

// Old school

for (Integer i : list)

System.out.println(i);

// “Modern”

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

“Modern” approach should be used with extreme care, i.e. only if you truly benefit from the internal, functional iteration.

Here’s a short list of cons of the “modern” approach compared to the classic one:

Performance – in many cases, performance is not critical, and you shouldn’t do any optimisation. But if we are talking about 20% more CPU consumption just based on the choice of loop style, then we did something fundamentally wrong.

Here’s benchmark result on an ordinary loop, finding the max value in a list of boxed ints:

ArrayList, for-loop : 6.55 ms

ArrayList, seq. stream: 8.33 ms

Here’s benchmark result on an ordinary loop, finding the max value in an array of primitive ints:

int-array, for-loop : 0.36 ms

int-array, seq. stream: 5.35 ms

Readability

We’ll always discuss style of our code as if it really mattered. For instance whitespace, or curly braces.

The reason why we do so is because maintenance of software is hard. Especially of code written by someone else, long time ago.

Here’s some example how “old school” is much more readable then “modern” code:

List<Integer> list = Arrays.asList(1, 2, 3);

// Old school
    for (Integer i : list)
      for (int j = 0; j < i; j++)
    System.out.println(i * j);

// "Modern"
list.forEach(i -> {
   IntStream.range(0, i).forEach(j -> {
       System.out.println(i * j);
   });
});

There isn’t a black or white answer to the problem. But if the rest of the code base is imperative , then nesting range declarations and forEach() calls, and lambdas is certainly unusual, generating cognitive friction in the team

Maintainability Let’s take a look on next code segment:

Održivost – Pogledajmo sledeći segment koda:

List<Integer> list = Arrays.asList(1, 2, 3);

// Old school
    for (Integer i : list)
      for (int j = 0; j < i; j++)
    System.out.println(i / j);

// "Modern"
list.forEach(i -> {
   IntStream.range(0, i).forEach(j -> {
     System.out.println(i / j);
   });
});

Old school

Exception in thread "main" java.lang.ArithmeticException: / by zero

at Test.main(Test.java:13)

Modern

Exception in thread "main" java.lang.ArithmeticException: / by zero

at Test.lambda$1(Test.java:18)

at java.util.stream.Streams$RangeIntSpliterator.forEachRemaining(Streams.java:110)

at java.util.stream.IntPipeline$Head.forEach(IntPipeline.java:557)

at Test.lambda$0(Test.java:17)

at java.util.Arrays$ArrayList.forEach(Arrays.java:3880)

at Test.main(Test.java:16)

These are the same reasons why we’ve had performance issues in the first place. Internal iteration is just a lot more work for the JVM and the libraries.

– Note – All parallel streams which are called for the service use same common fork-join thread pool if for one of them is not created custom pool.

Conclusion

When you migrate to Java 8 and contemplate using a more functional style in your code, beware that functional programming is not always better. In fact, it is just different and allows us to reason about problems differently. Java developers will need to practice, and come up with an intuitive understanding of when to use functional programming, and when to stick with OO/imperative. With the right amount of practice, combining both will help us improve our software.

Share

Sign in to get new blogs and news first:

Leave a Reply

Mladen Branković

Junior full stack developer @ MozzartBet
mm

Born in Valjevo, graduated and completed Bachelor studies at School of Electrical Engineering in Belgrade, The Department of Computer Science and Information Technology. At the moment, working for MozzartBet as junior full stack developer. Has experience in Java development, Javascript and AngularJS. Worked on a software solution for alerting users on escalations in betting system and multimedial views of sport results showing in MozzartBet betting premises.

Sign in to get new blogs and news first.

Categories