DEV Community

Hùng Trần
Hùng Trần

Posted on • Originally published at Medium on

Best practices for using Java Stream

When working with Java, understanding and knowing how to use streams to optimize work performance is necessary. Today I will show you how to use some of the functions that Java streams provide, helping Your code become more visible and elegant.

First, let’s go through the popular functions that Java Stream provides.

filter removes elements that do not satisfy the predicate filter condition, or in other words, retains elements that satisfy the filter condition

import java.util.Arrays;
import java.util.stream.Collectors;

public class CafeincodeExample {

    public static void main(String[] args) {
        var domains = Arrays.asList("cafeincode", "medium", "google");
        var filtered = domains.stream()
                .filter(item -> item.startsWith("c"))
                .peek(item -> System.out.println("Result: " + item))
                .collect(Collectors.toList());
    }
}

Result: cafeincode
Enter fullscreen mode Exit fullscreen mode

map is responsible for mapping each element in the stream to another data type through the function you specify and creating a new stream, like in the example below, I use the available toUpperCase function

import java.util.Arrays;
import java.util.stream.Collectors;

public class CafeincodeExample {

    public static void main(String[] args) {
        var input = Arrays.asList("cafeincode", "medium", "google");
        var mapped = input.stream()
                .map(String::toUpperCase)
                .collect(Collectors.toList());
        System.out.println("Result: " + mapped);
    }
}

Result: [CAFEINCODE, MEDIUM, GOOGLE]
Enter fullscreen mode Exit fullscreen mode

flatMap is used to process the elements of a stream and help transform them into a new stream or a list of elements, i.e. it will combine child streams into a parent stream.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class CafeincodeExample {

    public static void main(String[] args) {
        var nestedNumbers = Arrays.asList(
                Arrays.asList(1, 2),
                Arrays.asList(3, 4),
                Arrays.asList(5, 6)
        );
        var flattenedNumbers = nestedNumbers.stream()
                .flatMap(List::stream)
                .collect(Collectors.toList());
        System.out.println("Result: " + flattenedNumbers);
    }
}

Result: [1, 2, 3, 4, 5, 6]
Enter fullscreen mode Exit fullscreen mode

distinct is used to remove duplicate elements from a stream and it will return a new stream containing only unique elements

import java.util.Arrays;
import java.util.stream.Collectors;

public class CafeincodeExample {

    public static void main(String[] args) {
        var numbers = Arrays.asList(1, 2, 2, 3, 3, 4, 5, 5);
        var distinctNumbers = numbers.stream()
                .distinct()
                .collect(Collectors.toList());
        System.out.println("Result: " + distinctNumbers);
    }
}

Result: [1, 2, 3, 4, 5]
Enter fullscreen mode Exit fullscreen mode

sorted is used to sort the elements of a stream in a certain order and will return a new stream containing the sorted elements

import java.util.Arrays;
import java.util.stream.Collectors;

public class CafeincodeExample {

    public static void main(String[] args) {
        var numbers = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6, 5);
        var sortedNumbers = numbers.stream()
                .sorted()
                .collect(Collectors.toList());
        System.out.println("Result: " + sortedNumbers);
    }
}

Result: [1, 1, 2, 3, 4, 5, 5, 6, 9]
Enter fullscreen mode Exit fullscreen mode

peek is often used to perform debugging or logging operations on elements during stream processing without changing the content of the stream.

import java.util.stream.IntStream;

public class CafeincodeExample {

    public static void main(String[] args) {
        IntStream.range(1, 6)
                .peek(element -> System.out.println("Processing element: " + element))
                .map(CafeincodeExample::mapping)
                .forEach(System.out::println);
    }

    private static Integer mapping(Integer input) {
        return input * input;
    }
}

Processing element: 1
1
Processing element: 2
4
Processing element: 3
9
Processing element: 4
16
Processing element: 5
25
Enter fullscreen mode Exit fullscreen mode

limit is used to limit the number of elements in a stream, it will return a new stream containing the number of elements limited to a certain value.

import java.util.stream.IntStream;

public class CafeincodeExample {

    public static void main(String[] args) {
        IntStream.range(1, 100)
                .limit(5)
                .forEach(System.out::println);
    }
}

1
2
3
4
5
Enter fullscreen mode Exit fullscreen mode

skip is used to skip a certain number of elements in a stream and return a new stream starting from the position that was skipped

import java.util.stream.IntStream;

public class CafeincodeExample {

    public static void main(String[] args) {
        IntStream.range(1, 11)
                .skip(5)
                .forEach(System.out::println);
    }
}

6
7
8
9
10
Enter fullscreen mode Exit fullscreen mode

In this example, IntStream.range(1, 11)create a Stream containing the numbers 1 to 10.

  • skip(5)used to skip the first 5 elements of the stream
  • forEach(System.out::println)used to print out the remaining elements of the stream, starting from the 6th element to the last element

toArray is used to convert a stream into an array, this method returns an array containing the elements of the stream in order.

import java.util.stream.IntStream;

public class CafeincodeExample {

    public static void main(String[] args) {
        int[] numbers = IntStream.range(1, 6)
                .toArray();

        for (int number : numbers) {
            System.out.println("Result: " + number);
        }
    }
}

Result: 1
Result: 2
Result: 3
Result: 4
Result: 5
Enter fullscreen mode Exit fullscreen mode

reduce is used to perform a transformation on the elements of the stream to calculate a final value

import java.util.Arrays;

public class CafeincodeExample {

    public static void main(String[] args) {
        Integer[] integers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        var result = Arrays.stream(integers).reduce(0, Integer::sum);
        System.out.println("Result: " + result);
    }
}

Result: 55
Enter fullscreen mode Exit fullscreen mode

collect is used to collect the elements of a stream into a specific data structure, such as a List, Set or Map

import java.util.stream.Collectors;
import java.util.stream.Stream;

public class CafeincodeExample {

    public static void main(String[] args) {
        Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
        var list = stream.collect(Collectors.toList());
        System.out.println("Result: " + list);
    }
}

Result: [1, 2, 3, 4, 5]
Enter fullscreen mode Exit fullscreen mode

count is used to count the number of elements in a stream, this method returns an integer value that is the number of elements in the stream

import java.util.stream.Stream;

public class CafeincodeExample {

    public static void main(String[] args) {

        var stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 11);
        var count = stream.count();
        System.out.println("Result: " + count);
    }
}

Result: 10
Enter fullscreen mode Exit fullscreen mode

anyMatch is used to check whether at least one element in the stream satisfies the condition

import java.util.stream.Stream;

public class CafeincodeExample {

    public static void main(String[] args) {
        var stream = Stream.of("apple", "banana", "orange", "grape", "kiwi");
không
        var anyMatch = stream.anyMatch(str -> str.startsWith("a"));

        if (anyMatch) {
            System.out.println("There are elements starting with the letter 'a' in Stream");
        } else {
            System.out.println("There are no elements starting with the letter 'a' in Stream");
        }
    }
}

There are elements starting with the letter 'a' in Stream
Enter fullscreen mode Exit fullscreen mode

allMatch is used to check whether all elements in the stream satisfy the given condition

import java.util.stream.Stream;

public class CafeincodeExample {

    public static void main(String[] args) {
        var stream = Stream.of(2, 4, 6, 8, 10);
        var allMatch = stream.allMatch(number -> number % 2 == 0);

        if (allMatch) {
            System.out.println("Result: All numbers in the Stream are divisible by 2");
        } else {
            System.out.println("Result: There is at least one number in the Stream that is not divisible by 2");
        }
    }
}

Result: All numbers in the Stream are divisible by 2
Enter fullscreen mode Exit fullscreen mode

noneMatch it still returns a boolean value, however, this function will check that all elements in the stream must not satisfy a condition.

import java.util.stream.Stream;

public class CafeincodeExample {

    public static void main(String[] args) {
        var stream = Stream.of(2, 4, 6, 8, 10);
        var noneMatch = stream.noneMatch(number -> number % 5 == 0);

        if (noneMatch) {
            System.out.println("Result: There are no numbers in the Stream that are divisible by 5");
        } else {
            System.out.println("Result: There is at least one number in the Stream that is divisible by 5");
        }
    }
}

Result: There is at least one number in the Stream that is divisible by 5
Enter fullscreen mode Exit fullscreen mode

findFirst returns the first element in the stream

import java.util.Optional;
import java.util.stream.Stream;

public class CafeincodeExample {

    public static void main(String[] args) {
        var stream = Stream.of("apple", "banana", "cherry", "avocado", "blueberry");
        Optional<String> firstElement = stream.findFirst();
        if (firstElement.isPresent()) {
            System.out.println("Result: First element: " + firstElement.get());
        } else {
            System.out.println("Result: Stream is empty.");
        }
    }
}

Result: First element: apple
Enter fullscreen mode Exit fullscreen mode

findAny returns any element in the stream

import java.util.Optional;
import java.util.stream.Stream;

public class CafeincodeExample {

    public static void main(String[] args) {
        var stream = Stream.of("apple", "banana", "cherry", "avocado", "blueberry");
        Optional<String> anyElement = stream.findAny();
        if (anyElement.isPresent()) {
            System.out.println("Result: Any element: " + anyElement.get());
        } else {
            System.out.println("Result: Stream is empty.");
        }
    }
}

Result: Any element: apple
Enter fullscreen mode Exit fullscreen mode

min returns the smallest word in the stream

import java.util.Optional;
import java.util.stream.Stream;

public class CafeincodeExample {

    public static void main(String[] args) {
        var stream = Stream.of(5, 2, 8, 1, 3);

        Optional<Integer> minElement = stream.min(Integer::compareTo);

        if (minElement.isPresent()) {
            System.out.println("Minimum element: " + minElement.get());
        } else {
            System.out.println("Stream is empty.");
        }
    }
}

Minimum element: 1
Enter fullscreen mode Exit fullscreen mode

max returns the largest element in the stream

import java.util.Optional;
import java.util.stream.Stream;

public class CafeincodeExample {

    public static void main(String[] args) {
        var stream = Stream.of(5, 2, 8, 1, 3);

        Optional<Integer> maxElement = stream.max(Integer::compareTo);

        if (maxElement.isPresent()) {
            System.out.println("Maximum element: " + maxElement.get());
        } else {
            System.out.println("Stream is empty.");
        }
    }
}

Maximum element: 8
Enter fullscreen mode Exit fullscreen mode

groupingBy is used to group elements in the stream according to a certain condition

import java.util.stream.Collectors;
import java.util.stream.Stream;

public class CafeincodeExample {

    public static void main(String[] args) {
        var stream = Stream.of("apple", "banana", "cherry", "avocado", "blueberry");
        var groupedByLength = stream.collect(Collectors.groupingBy(String::length));
        System.out.println("Result: " + groupedByLength);
    }
}

Result: {5=[apple], 6=[banana, cherry], 7=[avocado], 9=[blueberry]}
Enter fullscreen mode Exit fullscreen mode

partitioningBy is used to divide the stream elements into two groups based on a provided condition, the return result is a Map with two keys: true and false

Elements that satisfy the condition will be assigned to the key true and elements that do not satisfy the condition will be assigned to the key false

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class CafeincodeExample {
    public static void main(String[] args) {
        Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        Map<Boolean, List<Integer>> partitioned = stream.collect(Collectors.partitioningBy(i -> i % 2 == 0));
        System.out.println("Even numbers: " + partitioned.get(true));
        System.out.println("Odd numbers: " + partitioned.get(false));
    }
}

Even numbers: [2, 4, 6, 8, 10] 
Odd numbers: [1, 3, 5, 7, 9]
Enter fullscreen mode Exit fullscreen mode

joining is used to combine stream elements into one string, you can provide a separating string between elements or leave it as default

import java.util.Arrays;
import java.util.stream.Collectors;

public class CafeincodeExample {
    public static void main(String[] args) {
        var data = Arrays.asList("apple", "banana", "cherry");
        String result = data.stream().collect(Collectors.joining(", "));
        System.out.println("Result: " + result);
    }
}

Result: apple, banana, cherry
Enter fullscreen mode Exit fullscreen mode

Iterating is often used when it is necessary to create a sequence of values ​​​​generated according to a certain rule

import java.util.stream.Stream;

public class CafeincodeExample {
    public static void main(String[] args) {
        Stream<Integer> evenNumbers = Stream.iterate(2, n -> n + 2).limit(5);
        evenNumbers.forEach(System.out::println);
    }
}

2
4
6
8
10
Enter fullscreen mode Exit fullscreen mode

of is used to create a stream from the elements provided as arguments

import java.util.stream.Stream;

public class CafeincodeExample {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("apple", "banana", "cherry");
        stream.forEach(System.out::println);
    }
}

apple 
banana 
cherry
Enter fullscreen mode Exit fullscreen mode

concat is used to concatenate two streams together, creating a new stream containing all the elements of the two original streams

import java.util.stream.Stream;

public class CafeincodeExample {
    public static void main(String[] args) {
        Stream<String> stream1 = Stream.of("apple", "banana");
        Stream<String> stream2 = Stream.of("cherry", "grape");
        Stream<String> concatenatedStream = Stream.concat(stream1, stream2);
        concatenatedStream.forEach(System.out::println);
    }
}

apple
banana
cherry
grape
Enter fullscreen mode Exit fullscreen mode

unordered is used to specify that the stream will not follow the originally defined order.

This means that elements in the stream can appear in any order after performing transformations or collecting on the stream

import java.util.stream.Stream;

public class CafeincodeExample {
    public static void main(String[] args) {
        Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);

        Stream<Integer> unorderedStream = stream.unordered();


        unorderedStream.forEach(System.out::println);
    }
}

1
2
3
4
5
Enter fullscreen mode Exit fullscreen mode

range is used to create a stream containing consecutive integers, starting at a value A and ending at a value B-1

import java.util.stream.IntStream;

public class CafeincodeExample {
    public static void main(String[] args) {
        IntStream rangeStream = IntStream.range(1, 6);

        rangeStream.forEach(System.out::println);
    }
}

1
2
3
4
5
Enter fullscreen mode Exit fullscreen mode

rangeClosed is used to create a stream with integers in the range A to B

import java.util.stream.IntStream;

public class CafeincodeExample {
    public static void main(String[] args) {
        IntStream rangeClosedStream = IntStream.rangeClosed(1, 5);
        rangeClosedStream.forEach(System.out::println);
    }
}

1
2
3
4
5
Enter fullscreen mode Exit fullscreen mode

generate is used to generate a stream by generating elements based on a provided supplier, which independently generates elements each time it is called

import java.util.stream.Stream;

public class CafeincodeExample {
    public static void main(String[] args) {
        Stream<String> stream = Stream.generate(() -> "Cafeincode").limit(3);
        stream.forEach(System.out::println);
    }
}

Cafeincode
Cafeincode
Cafeincode
Enter fullscreen mode Exit fullscreen mode

takeWhile is used to retrieve elements from a stream until a condition is no longer satisfied.

import java.util.stream.Stream;

public class CafeincodeExample {
    public static void main(String[] args) {
        Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        Stream<Integer> takenStream = stream.takeWhile(n -> n <= 7);
        takenStream.forEach(System.out::println);
    }
}

1
2
3
4
5
6
7
Enter fullscreen mode Exit fullscreen mode

In contrast to takeWhile, dropWhile is used to remove elements from a stream until a condition is no longer satisfied, the remainder of the stream will be elements that do not satisfy the condition

import java.util.stream.Stream;

public class CafeincodeExample {
    public static void main(String[] args) {
        Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        Stream<Integer> droppedStream = stream.dropWhile(n -> n <= 6);
        droppedStream.forEach(System.out::println);
    }
}

7
8
9
10
Enter fullscreen mode Exit fullscreen mode

boxed is used to convert the elements of a stream from primitive types to boxed types, which is useful when you want to work with streams of objects instead of primitive types

import java.util.stream.IntStream;
import java.util.stream.Stream;

public class CafeincodeExample {
    public static void main(String[] args) {
        IntStream intStream = IntStream.of(1, 2, 3, 4, 5, 6, 7, 8);
        Stream<Integer> boxedStream = intStream.boxed();
        boxedStream.forEach(System.out::println);
    }
}

1
2
3
4
5
6
7
8
Enter fullscreen mode Exit fullscreen mode

parallel used to convert a stream into a stream that can be processed in parallel.

When a stream is processed in parallel, elements of the stream can be processed on multiple threads simultaneously, increasing application performance on systems with multiple processors.

However, in reality, using parallel in some cases does not really bring much significant performance, you can learn more about this part yourself.

import java.util.stream.Stream;

public class CafeincodeExample {
    public static void main(String[] args) {
        Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
        Stream<Integer> parallelStream = stream.parallel();
        parallelStream.forEach(System.out::println);
    }
}

3
5
4
2
1
Enter fullscreen mode Exit fullscreen mode

sequential used to convert a stream from parallel processing to sequential processing.

When a stream is processed sequentially, the stream’s elements are processed in order from start to finish on a single stream.

import java.util.stream.Stream;

public class CafeincodeExample {
    public static void main(String[] args) {
        Stream<Integer> parallelStream = Stream.of(1, 2, 3, 4, 5).parallel();
        Stream<Integer> sequentialStream = parallelStream.sequential();
        sequentialStream.forEach(System.out::println);
    }
}

1
2
3
4
5
Enter fullscreen mode Exit fullscreen mode

Best practice in using Java streams

Using streams properly makes your code more elegant, easier to see, and neater than the traditional coding style.

However, it must be said again and again, that too much of anything is not necessarily good. Overusing streams or confusing writing methods will also give you a real headache every time you debug.

So below are some best practices that I think should be applied to both make good use of it and avoid unnecessary abuse.

  • When using a stream where multiple methods are continuously applied, put each function on a different line, it will be extremely useful when debugging.
  • Use the methods I listed above map(), filter(), reduce(), collect(),... appropriately to perform operations on the stream
  • Check for null during map and filter operations
  • Avoid overusing parallel in the coding process, in many cases it does not achieve as much performance value as you think. If possible, just use the default sequential only.
  • Name variables when used appropriately. Do not use variable names with the default letters a, b, and c because it will be difficult to understand when reading.
  • Using Optional makes sense in cases where findFirst or findAny are used
  • In reality, there will be many methods you need to re-implement rather than using the default, for example, sorted
  • Use Peek to debug properly
  • In the case of converting List to Map , you need to be careful to pay attention to duplicate keys
  • Java streams use lazy evaluation (discussed in another article), which means elements are only calculated when necessary. You can use this to increase performance by avoiding unnecessary calculations.

Thanks, before you go:

👏 Please clap for the story and follow the author 👉

Please share your questions or insights in the comments section below.

Let’s connect on LinkedIn

Originally published at https://cafeincode.com on April 17, 2024.


Top comments (0)