Search

Dark theme | Light theme
Showing posts with label Java. Show all posts
Showing posts with label Java. Show all posts

July 22, 2023

Java Joy: Using mapMulti Method Of The Stream API

Since Java 16 we can use the method mapMulti(BiConsumer) of the Stream API. This method allows us to map each element of the stream to multiple elements. We can also do that with the flatMap(Function) method, but if we want to map a limited set of elements, mapMulti is more convenient. Internally a shared stream is used and we don’t have the cost of creating a new stream for each element. Another use case is if the logic to map an element to multiple elements is complex and is hard to implement by returning a stream. Then mapMulti allows us to write that logic in a BiConsumer instead of a Function.

In the following code we use the mapMulti method in several examples:

package mrhaki.stream;

import javax.print.attribute.HashPrintServiceAttributeSet;
import java.util.Arrays;
import java.util.IntSummaryStatistics;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class MapMulti {

    public static void main(String[] args) {
        // We want to return a stream of string values 
        // and the uppercase variant
        // if the original element has the letter o.
        assert Stream.of("Java", "Groovy", "Clojure")
                     .mapMulti((language, downstream) -> {
                         if (language.contains("o")) {
                             downstream.accept(language);
                             downstream.accept(language.toUpperCase());
                         }
                     })
                     .toList()
                     .equals(List.of("Groovy", "GROOVY", "Clojure", "CLOJURE"));

        // Same logic implemented with flatMap.
        assert Stream.of("Java", "Groovy", "Clojure")
                     .filter(language -> language.contains("o"))
                     .flatMap(language -> Stream.of(language, language.toUpperCase()))
                     .toList()
                     .equals(List.of("Groovy", "GROOVY", "Clojure", "CLOJURE"));


        // Helper record to store a name and set of language names.
        record Developer(String name, List<String> languages) {}

        // Number of sample developers that work with different languages.
        var hubert = new Developer("mrhaki", List.of("Java", "Groovy", "Clojure"));
        var java = new Developer("java", List.of("Java"));
        var clojure = new Developer("clojure", List.of("Clojure"));
        var groovy = new Developer("groovy", List.of("Groovy"));

        record Pair(String name, String language) {}

        // Let's find all developers that have Java in their
        // set of languages and return a new Pair
        // object with the name of the developer and a language.
        assert Stream.of(hubert, java, clojure, groovy)
                     // We can explicitly state the class that will be 
                     // in the downstream of the compiler cannot
                     // deduct it using a <...> syntax.
                     .<Pair>mapMulti((developer, downstream) -> {
                         var languages = developer.languages();
                         if (languages.contains("Java")) {
                             for (String language : developer.languages()) {
                                 downstream.accept(new Pair(developer.name(), language));
                             }
                         }
                     })
                     .toList()
                     .equals(List.of(new Pair("mrhaki", "Java"),
                                     new Pair("mrhaki", "Groovy"),
                                     new Pair("mrhaki", "Clojure"),
                                     new Pair("java", "Java")));

        // Same logic using filter and flatMap.
        assert Stream.of(hubert, java, clojure, groovy)
                     .filter(developer -> developer.languages().contains("Java"))
                     .flatMap(developer -> developer.languages()
                                                    .stream()
                                                    .map(language -> new Pair(developer.name(), language)))
                     .toList()
                     .equals(List.of(new Pair("mrhaki", "Java"),
                                     new Pair("mrhaki", "Groovy"),
                                     new Pair("mrhaki", "Clojure"),
                                     new Pair("java", "Java")));
        
        
        // We want to expand each number to itself and its square root value
        // and we muse mapMultiToInt here.
        var summaryStatistics = Stream.of(1, 2, 3)
                                      .mapMultiToInt((number, downstream) -> {
                                          downstream.accept(number);
                                          downstream.accept(number * number);
                                      })
                                      .summaryStatistics();

        assert summaryStatistics.getCount() == 6;
        assert summaryStatistics.getSum() == 20;
        assert summaryStatistics.getMin() == 1;
        assert summaryStatistics.getMax() == 9;
    }
}

Written with Java 20.

March 24, 2021

Java Joy: Run Action When Optional Value Present Or Not

If we have an Optional instance we can consume the value if it is present using the ifPresent method. Since Java 9 the method ifPresentOrElse has been added to the Optional class. The first argument is of type Consumer and is invoked when there is an optional value. The second argument is of type Runnable and is executed when the the optional is empty. The method in the Consumer and Runnable implementations does not return a type but returns void. Therefore we should use ifPresentOrElse when we need a conditional side effect for an Optional instance.

In the following example we have a method handleName that will update a list if an optional value is present or increases a counter when the optional value is empty:

package mrhaki.optional;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;

public class IfPresentOrElse {

    // Store names.
    private static List<String> names = new ArrayList<>();
    
    // Counter for counting empty or null names.
    private static AtomicInteger emptyCounter = new AtomicInteger();

    public static void main(String[] args) {
        // Optional name property will have a value.
        handleName(new User("mrhaki"));

        assert "MRHAKI".equals(names.get(0));
        assert 0 == emptyCounter.get();
        
        // Reset properties
        names.clear();
        emptyCounter.set(0);
        
        // Optional name property is empty.
        handleName(new User());

        assert names.isEmpty();
        assert 1 == emptyCounter.get();
    }

    private static void handleName(User user) {
        user.getName().ifPresentOrElse(
                // If present add name in uppercase to names.
                s -> names.add(s.toUpperCase(Locale.ROOT)),
                // If not present increment the counter.
                emptyCounter::incrementAndGet);
    }

    // Simple class with an optional property name.
    private static class User {
        private final String name;

        private User() {
            this(null);
        }

        private User(final String name) {
            this.name = name;
        }

        Optional<String> getName() {
            return Optional.ofNullable(name);
        }
    }
}

Written with Java 16.

March 19, 2021

Java Joy: Getting Multiple Results From One Stream With Teeing Collector

If we want to get two types of information from a Stream of objects we can consume the Stream twice and collect the results. But that is not very efficient, especially when the stream has a lot of objects. Since Java 12 we can use the teeing method of the java.util.stream.Collectors class to get multiple results while consuming the stream of objects only once. The teeing method takes two collectors as argument each returning a separate result for the stream items. As third argument we must pass a function that will merge the results of the two collectors into a new object.

In the following code we have two example use cases that use the teeing method to get multiple results while consuming a Stream of objects only one time:

package mrhaki.stream;

import java.util.List;
import java.util.Locale;
import java.util.function.BiFunction;
import java.util.function.Predicate;
import java.util.stream.Collector;
import java.util.stream.Collectors;

public class Teeing {
    public static void main(String[] args) {
        // Simple list of language names.
        List<String> languageNames = List.of("Java", "Clojure", "Kotlin", "Groovy", "Scala");

        // Predicate for String value that has the letter j or J.
        Predicate<String> containsJ = s -> s.toLowerCase(Locale.ROOT).contains("j");

        // Collector to apply two filters for one stream process and combine the result.
        Collector<String, ?, List<List<String>>> splitOnLetterJ =
                Collectors.teeing(
                        // Filter on language name with a j or J.
                        Collectors.filtering(containsJ, Collectors.toList()),
                        // Filter on language name withtout a j or J.
                        Collectors.filtering(containsJ.negate(), Collectors.toList()),
                        // Combine both results into a  new List with two items.
                        (withJ, withoutJ) -> List.of(withJ, withoutJ));

        List<List<String>> split = languageNames.stream().collect(splitOnLetterJ);

        assert List.of("Java", "Clojure").equals(split.get(0));
        assert List.of("Kotlin", "Groovy", "Scala").equals(split.get(1));


        // List of language records with a name and 
        // boolean to indicate the language is dynamic or not. 
        List<Language> languages =
                List.of(new Language("Clojure", true),
                        new Language("Java", false),
                        new Language("Groovy", true),
                        new Language("Scala", false),
                        new Language("Kotlin", false));

        // Filter for dynamic languages and transform to list.
        Collector<Language, ?, List<Language>> filteringDynamicLanguages =
                Collectors.filtering(Language::dynamic, Collectors.toUnmodifiableList());
        
        // Make a list with the language names in upper case.
        Collector<Language, ?, List<String>> upperCaseLanguageNames =
                Collectors.mapping(language -> language.name.toUpperCase(Locale.ROOT),
                                   Collectors.toUnmodifiableList());

        // Function to merge both list into a list with first item the result
        // of filteringDynamicLanguages and the second item the result
        // of upperCaseLanguageNames.
        final BiFunction<List<Language>, List<String>, List<List<?>>> mergeLists =
                (dynamicLanguages, upperCaseLanguages) -> List.of(dynamicLanguages, upperCaseLanguages);

        List<List<?>> result = languages
                .stream()
                .collect(
                        Collectors.teeing(
                                filteringDynamicLanguages,
                                upperCaseLanguageNames,
                                mergeLists));

        assert List.of(new Language("Clojure", true), new Language("Groovy", true)).equals(result.get(0));
        assert List.of("CLOJURE", "JAVA", "GROOVY", "SCALA", "KOTLIN").equals(result.get(1));
    }

    // Record to store language name and if the language is dynamic.
    record Language(String name, boolean dynamic) {}
}

Written with Java 16.

March 11, 2021

Java Joy: Formatting A String Value With Formatted Method

Java 15 introduced the multi-line string value referred to as a text block. With this introduction also the formatted method was added to the String class. The method can be invoked on a String value directly and function exactly as the static String.format method. The nice thing is that now we directly can use a method on the value instead of having to use a static method where the value is passed as argument.

In the following example we use the formatted method for a normal String value and a text block:

package mrhaki.string;

public class Formatted {
    public static void main(String[] args) {
        String text = "Hi, %s, thank you for reading %d blogs".formatted("mrhaki", 2);
        assert "Hi, mrhaki, thank you for reading 2 blogs".equals(text);

        String email = """
        Hello %s,

        thank you for reading %d blogs.
        """.formatted("Hubert", 2);

        assert """
                Hello Hubert,

                thank you for reading 2 blogs.
                """.equals(email);
    }
}

Written with Java 15.

March 3, 2021

Java Joy: Apply Function To String With Transform

In Java 12 the transform method was add to the String class. This method accepts a Function as argument. The function must have a single parameter of type String and can return any other type. The nice thing is that it works on a String instance, so we can directly use the transform method when we have a String value. We don't have to pass the String object to another method to tranform it, but we can define the tranformation function close to the String value.

In the following example we take a String value and apply some functions with the transform method:

package mrhaki.lang;

import java.util.stream.Collectors;

public class StringTransform {
    public static void main(String[] args) {
        String alphabet = "abcdefghijklmnopqrstuvwxyz";
        
        // Find all letters that have an even
        // int representation and join the results
        // back into a string.
        String letters = alphabet
                .transform(s -> s.chars()
                                 .filter(n -> n % 2 == 0)
                                 .mapToObj(n -> String.valueOf((char) n))
                                 .collect(Collectors.joining()));

        assert "bdfhjlnprtvxz".equals(letters);

        // Transform the string to a User object.
        User user = "mrhaki,Hubert Klein Ikkink"
                .transform(name -> new User(name.split(",")[0], name.split(",")[1]));

        assert "mrhaki".equals(user.alias);
        assert "Hubert Klein Ikkink".equals(user.fullName);
    }

    /**
     * Simple class to store alias and full name.
     */
    private static class User {
        private final String alias, fullName;

        private User(final String alias, final String fullName) {
            this.alias = alias;
            this.fullName = fullName;
        }
    }
}

Written with Java 15.

March 1, 2021

Java Joy: Format Numbers In Compact Form

Since Java 12 we can format numbers in a compact style with the CompactNumberFormat class in the java.text package. A number like 23000 is formatted as 23K for the English locale. Instead of the short representation of K for 1000 we can also use a longer style where K is transformed as thousand for the English locale. We can use the same class to parse a String value that is in the compact style into a number.

In the following example we use several options of the CompactNumberFormat class:

package mrhaki.text;

import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Locale;

public class CompactNumberFormat {

    public static void main(String[] args) throws ParseException {
        // When we would use NumberFormat.getCompactNumberInstance() the default system Locale
        // is used with the short style.
        // Create formatter for UK Locale and short style. 
        NumberFormat nf = NumberFormat.getCompactNumberInstance(Locale.UK, NumberFormat.Style.SHORT);
        assert "23K".equals(nf.format(23_000));
        assert "23K".equals(nf.format(23_491));
        assert "24K".equals(nf.format(23_791));
        assert "4M".equals(nf.format(4_250_392));

        nf.setMinimumFractionDigits(1);
        assert "4.3M".equals(nf.format(4_250_392));

        nf.setMaximumFractionDigits(3);
        assert "4.25M".equals(nf.format(4_250_392));

        // We can also parse the String value back to a number.
        assert Long.valueOf(23491).equals(nf.parse("23.491K"));

        // Instead of a short style we can also use a long style.
        nf = NumberFormat.getCompactNumberInstance(Locale.UK, NumberFormat.Style.LONG);
        assert "23 thousand".equals(nf.format(23_000));
        assert "4 million".equals(nf.format(4_250_392));

        // We can specify the minimum fraction digits to be used in the formatted number.
        nf.setMinimumFractionDigits(1);
        assert "4.3 million".equals(nf.format(4_250_392));

        // And the maximum fraction digits to change the ouput.
        nf.setMaximumFractionDigits(3);
        assert "4.25 million".equals(nf.format(4_250_392));
        
        // If we would use another locale we can see the output will be changed
        // if applicable.
        Locale dutch = new Locale("nl", "NL");
        nf = NumberFormat.getCompactNumberInstance(dutch, NumberFormat.Style.SHORT);
        assert "23K".equals(nf.format(23_230));

        nf = NumberFormat.getCompactNumberInstance(dutch, NumberFormat.Style.LONG);
        assert "23 duizend".equals(nf.format(23_230));
    }
}

Written with Java 15.

February 25, 2021

Java Joy: Merge Maps Using Stream API

In Java we can merge a key/value pair into a Map with the merge method. The first parameter is the key, the second the value and the third parameter of the merge method is a remapping function that is applied when the key is already present in the Map instance. The remapping function has the value of the key in the original Map and the new value. We can define in the function what the resulting value should be. If we return null the key is ignored.

If we want to merge multiple Map instances we can use the Stream API. We want to convert the Map instances to a stream of Map.Entry instances which we then turn into a new Map instance with the toMap method from the class Collectors. The toMap method also takes a remapping function when there is a duplicate key. The function defines what the new value is based on the two values of the duplicate key that was encountered. We can choose to simply ignore one of the values and return the other value. But we can also do some computations in this function, for example creating a new value using both values.

In the following example we use the Stream API to merge multiple Map instances into a new Map using a remapping function for duplicate keys:

package com.mrhaki.sample;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class MapMerge {
    public static void main(String[] args) {
        Map<Character, Integer> first = Map.of('a', 2, 'b', 3, 'c', 4);
        Map<Character, Integer> second = Map.of('a', 10, 'c', 11);
        Map<Character, Integer> third = Map.of('a', 3, 'd', 100);

        // First we turn multiple maps into a stream of entries and
        // in the collect method we create a new map and define
        // a function to multiply the entry value when there is a 
        // duplicate entry key.
        Map<Character, Integer> result =
                Stream.of(first, second, third)
                      .flatMap(m -> m.entrySet().stream())
                      .collect(
                              Collectors.toMap(
                                      Map.Entry::getKey,
                                      Map.Entry::getValue,
                                      (value1, value2) -> value1 * value2));

        // The values for duplicate keys are multiplied in the resulting map.
        assert Map.of('a', 60, 'b', 3, 'c', 44, 'd', 100).equals(result);


        // In this sample the value is a Java class Characteristic.
        // The function to apply when a key is duplicate will create
        // a new Characteristic instance contains all values.
        // The resulting map will contain all concatenated characteristic values
        // for each key.
        var langauges =
                Stream.of(Map.of("Java", new Characteristic("jvm")),
                          Map.of("Clojure", new Characteristic("dynamic", "functional")),
                          Map.of("Groovy", new Characteristic("jvm", "dynamic")),
                          Map.of("Clojure", new Characteristic("jvm")),
                          Map.of("Groovy", new Characteristic("dynamic")),
                          Map.of("Java", new Characteristic("static")))
                      .flatMap(m -> m.entrySet().stream())
                      .collect(
                              Collectors.toMap(
                                      Map.Entry::getKey,
                                      Map.Entry::getValue,
                                      (c1, c2) -> c1.addCharateristics(c2.getValues())));

        assert new Characteristic("static", "jvm").equals(langauges.get("Java"));
        assert new Characteristic("dynamic", "functional", "jvm").equals(langauges.get("Clojure"));
        assert new Characteristic("dynamic", "jvm").equals(langauges.get("Groovy"));
    }

    /**
     * Supporting class to store language characteristics.
     */
    static class Characteristic {
        // Store unique characteristic value.
        private Set<String> values = new HashSet<>();

        Characteristic(String characteristic) {
            values.add(characteristic);
        }

        Characteristic(String... characteristics) {
            values.addAll(Arrays.asList(characteristics));
        }

        Characteristic addCharateristics(Set<String> characteristics) {
            values.addAll(characteristics);
            return this;
        }

        Set<String> getValues() {
            return values;
        }

        @Override
        public boolean equals(final Object o) {
            if (this == o) { return true; }
            if (o == null || getClass() != o.getClass()) { return false; }
            final Characteristic that = (Characteristic) o;
            return Objects.equals(values, that.values);
        }

        @Override
        public int hashCode() {
            return Objects.hash(values);
        }
    }
}

Written with Java 15.

February 19, 2021

Java Joy: Composing Functions

In Java we can write single argument functions that implement the java.util.function.Function interface. We can combine multiple functions into a new function using the andThen and compose methods from the Function interface. We need to give another function as argument to these methods. When we use the andThen method the output of the original function will be input of the function passed as argument. With the compose method our function will get as input the output of the function that is passed as argument. It is important to know the difference, because it can change the result of the function we are composing. The andThen and compose methods are also available on the IntUnaryOperator, LongUnaryOperator and DoubleUnaryOperator interface.

In the following example we use both andThen and compose to chain together some functions. We can see the result can be different when using andThen and compose with the same functions.

package com.mrhaki.sample;

import java.util.Map;
import java.util.function.Function;
import java.util.function.IntUnaryOperator;
import java.util.function.UnaryOperator;

public class Compose {
    public static void main(String[] args) {
        // Two simple functions that take a int argument
        // and do some calculations.
        IntUnaryOperator f = x -> 11 + (x - 90);
        IntUnaryOperator g = x -> x * 2;

        // Using andThen will first execute f and use
        // the result as input for g:
        // (11 + (100 - 90)) * 2
        assert 42 == f.andThen(g).applyAsInt(100);

        // Using compose will first execute g and use
        // the result as input for f:
        // 11 + ((100 * 2) - 90)
        assert 121 == f.compose(g).applyAsInt(100);


        // Map with some user data.
        var user =
                Map.of("name", "Hubert",
                       "alias", "MrHaki");

        // Function to duplicate a String.
        UnaryOperator<String> duplicate = s -> String.format("%1$s,%1$s", s);

        // Function to turn String into lowercase.
        UnaryOperator<String> lowerCase = String::toLowerCase;

        // Function with Map parameter to create a new function with
        // a String parameter that will get the
        // value for a given key from the map that is passed.
        Function<Map<String, String>, UnaryOperator<String>> getFromMap =
                map -> key -> map.get(key);

        // Chain using andThen.
        Function<String, String> andThenUserKey =
                getFromMap.apply(user)
                          .andThen(lowerCase)
                          .andThen(duplicate);

        assert "mrhaki,mrhaki".equals(andThenUserKey.apply("alias"));

        // Chain using compose.
        Function<String, String> composeUserKey =
                duplicate.compose(lowerCase)
                         .compose(getFromMap.apply(user));

        assert "mrhaki,mrhaki".equals(composeUserKey.apply("alias"));
    }
}

Written with Java 15.

January 27, 2021

Java Joy: Transform Stream Of Strings To List Of Key Value Pairs Or Map

Suppose we have a Stream of String objects where two sequential values belong together as a pair. We want to transform the stream into a List where each pair is transformed into a Map object with a key and value. We can write a custom Collector that stores the first String value of a pair. When the next element in the Stream is processed by the Collector a Map object is created with the stored first value and the new value. The new Map is added to the result List.

In the next example we write the ListMapCollector to transform a Stream of paired String values into a List of Map objects:

package mrhaki.streams;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        List<Map<String, String>> pairs =
                Stream.of("language", "Java", "username", "mrhaki")
                      .collect(new ListMapCollector());

        // Result is list of maps: [{language=Java},{username=mrhaki}]
        assert pairs.size() == 2;
        assert pairs.get(0).get("language").equals("Java");
        assert pairs.get(1).get("username").equals("mrhaki");
    }

    private static class ListMapCollector
            implements Collector<String, List<Map<String, String>>, List<Map<String, String>>> {

        private String key;

        /**
         * @return An empty list to add our Map objects to.
         */
        @Override
        public Supplier<List<Map<String, String>>> supplier() {
            return () -> new ArrayList<>();
        }

        /**
         * @return Accumulator to add Map with key and value to the result list.
         */
        @Override
        public BiConsumer<List<Map<String, String>>, String> accumulator() {
            return (list, value) -> {
                if (key != null) {
                    list.add(Map.of(key, value));
                    key = null;
                } else {
                    key = value;
                }
            };
        }

        /**
         * @return Combine two result lists into a single list with all Map objects.
         */
        @Override
        public BinaryOperator<List<Map<String, String>>> combiner() {
            return (list1, list2) -> {
                list1.addAll(list2);
                return list1;
            };
        }

        /**
         * @return Use identity function to return result.
         */
        @Override
        public Function<List<Map<String, String>>, List<Map<String, String>>> finisher() {
            return Function.identity();
        }

        /**
         * @return Collector characteristic to indicate finisher method is identity function.
         */
        @Override
        public Set<Characteristics> characteristics() {
            return Set.of(Characteristics.IDENTITY_FINISH);
        }
    }
}

Another solution could be to turn the Stream with values into a single Map. Each pair of values is a key/value pair in the resulting Map:

package mrhaki.streams;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        Map<String, String> map = 
                Stream.of("language", "Java", "username", "mrhaki")
                      .collect(new MapCollector());
        
        // Result is map: {language=Java,username=mrhaki}
        assert map.size() == 2;
        assert map.get("language").equals("Java");
        assert map.get("username").equals("mrhaki");
    }

    private static class MapCollector
            implements Collector<String, Map<String, String>, Map<String, String>> {

        private String key;

        /**
         * @return An empty map to add keys with values to.
         */
        @Override
        public Supplier<Map<String, String>> supplier() {
            return () -> new HashMap<>();
        }

        /**
         * @return Accumulator to add key and value to the result map.
         */
        @Override
        public BiConsumer<Map<String, String>, String> accumulator() {
            return (map, value) -> {
                if (key != null) {
                    map.put(key, value);
                    key = null;
                } else {
                    key = value;
                }
            };
        }

        /**
         * @return Combine two result maps into a single map.
         */
        @Override
        public BinaryOperator<Map<String, String>> combiner() {
            return (map1, map2) -> {
                map1.putAll(map2);
                return map1;
            };
        }

        /**
         * @return Use identity function to return result.
         */
        @Override
        public Function<Map<String, String>, Map<String, String>> finisher() {
            return Function.identity();
        }

        /**
         * @return Collector characteristic to indicate finisher method is identity function.
         */
        @Override
        public Set<Characteristics> characteristics() {
            return Set.of(Characteristics.IDENTITY_FINISH);
        }
    }
}

Written with Java 15.0.1.

January 25, 2021

Java Joy: Partition Stream By Predicate

The Java Stream API has many useful methods. If we want to partition a stream of objects by a given predicate we can use the partitioningBy() method from the java.util.stream.Collectors package. We must use this method in the collect() method of the stream. The result is a Map with the keys true and false. The objects from the stream that are true for the predicate will end up in the true value list and if the result of the predicate is false the value will end up in the list of values for the false key. The partitionBy method accepts a collector as second parameter. This collector will be applied to the values before they are put in the true or false keys in the result.

In the following example we use the partitioningBy method with different streams:

package mrhaki.stream;

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

public class Main {
    public static void main(String[] args) {
        // Let's create an infinitive stream of integers.
        var range = Stream.iterate(0, i -> i + 1);

        // We can partition them by odd and even numbers
        // using a predicate i % 2 == 0, where i is integer from the stream.
        var oddEvenNumbers = 
            range.limit(10)
                  .collect(Collectors.partitioningBy(i -> i % 2 == 0));

        // Even numbers are assigned to the "true" key,
        // odd numbers to the "false" key.
        var odds = oddEvenNumbers.get(false);
        var evens = oddEvenNumbers.get(true);

        assert odds.size() == 5;
        assert odds.equals(List.of(1, 3, 5, 7, 9));
        assert evens.size() == 5;
        assert evens.equals(List.of(0, 2, 4, 6, 8));

        // We use a second argument to sum all odd and even numbers.
        var sumEvenOdd = 
            Stream.iterate(0, i -> i + 1)
                  .limit(100)
                  .collect(
                      Collectors.partitioningBy(i -> i % 2 == 0, 
                                                Collectors.reducing(0, (result, i) -> result += i)));

        assert sumEvenOdd.get(true) == 2450;
        assert sumEvenOdd.get(false) == 2500;
        

        // In the next exmample we start with an immutable map.
        var map = Map.of("language", "Java", "username", "mrhaki", "age", 47);

        // This time we partition on the type of the value where values
        // of type String are assigned to "true" and other types to "false".
        var partitionByStringValue = 
            map.entrySet()
               .stream()
               .collect(Collectors.partitioningBy(entry -> entry.getValue() instanceof String));

        var stringValueEntries = partitionByStringValue.get(true);
        var nonStringValueEntries = partitionByStringValue.get(false);

        assert stringValueEntries.size() == 2;

        var keys = stringValueEntries.stream().map(Map.Entry::getKey).collect(Collectors.toUnmodifiableList());
        var values = stringValueEntries.stream().map(Map.Entry::getValue).collect(Collectors.toUnmodifiableList());
        assert keys.containsAll(List.of("language", "username"));
        assert values.containsAll(List.of("Java", "mrhaki"));

        assert nonStringValueEntries.size() == 1;
        assert nonStringValueEntries.get(0).getKey().equals("age");
        assert nonStringValueEntries.get(0).getValue().equals(47);       
    }
}

Written with Java 15.0.1.

January 20, 2021

Java Joy: Turn Stream Into An Array

The Java Stream API has many useful methods. If we want to transform a Stream to a Java array we can use the toArray method. Without an argument the result is an object array (Object[]), but we can also use an argument to return an array of another type. The easiest way is to use the contructor of the array type we want as method reference. Then the result is an array of the given type with the elements of the stream.

This is very useful if we have a Java Stream and want to use the elements to invoke a method with a variable arguments parameter. In Java we can pass an array object as variable arguments argument to a method. So if we transform the Stream to an array we can invoke the method with that value.

In the following example we see how we transform a Stream with String values to a String[] object. We use this String[] object to invoke a method with a varargs parameter of type String....

package mrhaki.stream;

import java.util.stream.Stream;

public class Sample {

    public static void main(String[] args) {
        // With the .toArray method we can convert a Stream
        // to an array. 
        // We can use the constructur of the array type
        // we want to convert to as method reference to get 
        // the correct array result.
        final var result = 
            Stream.of("Java", "Clojure", "Groovy", "Kotlin")
                  .filter(language -> language.contains("o"))
                  .toArray(String[]::new);

        assert result[0].equals("Clojure");
        assert result[1].equals("Groovy");
        assert result[2].equals("Kotlin");

        // This is useful with varargs parameters as well,
        // as arrays can be used for a varargs parameter.
        assert "Clojure".equals(first(result));
        assert "Clojure".equals(first("Clojure", "Groovy", "Kotlin"));
    }

    private static String first(String... values) {
        return values[0];
    }
}

Written with Java 15.0.1

December 30, 2020

Java Joy: Optional orElse orElseGet That Is The Question

The Optional class has the orElse and orElseGet methods to return a value when the Optional object is empty. This is useful to return a default value for example. But there is a small difference between the two methods. The orElseGet method needs a Supplier argument that returns a value of the type of the Optional value. The Supplier is only invoked when the Optional value is empty. The statement passed as argument to the orElse method is always executed, even when the Optional value is not empty. Preferrably we should use orElseGet as it will only invoke statements if needed.

In the following example code we see when our method getDefaultGreeting is invoked by using orElse and orElseGet with an empty and non-empty Optional object:

package mrhaki.optional;

import java.util.Optional;

public class Sample {

    /** 
      * Keep track of total number of times method getDefaultGreeting is invoked.
      */
    private static int totalDefaultGreetingInvoked = 0;

    public static void main(String[] args) {
        // Define an empty optional.
        var name = Optional.ofNullable(null);

        // orElse returns value from argument when optional is empty.
        assert ("Hello " + name.orElse(getDefaultGreeting())).equals("Hello world");
        assert totalDefaultGreetingInvoked == 1;

        // orElseGet needs a Supplier that is executed when the optional is empty.
        assert ("Hello " + name.orElseGet(Sample::getDefaultGreeting)).equals("Hello world");
        assert totalDefaultGreetingInvoked == 2;


        // This time the optional is not empty.
        name = Optional.ofNullable("mrhaki");

        // orElse will always get the value, even when the optional is not empty.
        assert ("Hello " + name.orElse(getDefaultGreeting())).equals("Hello mrhaki");
        assert totalDefaultGreetingInvoked == 3;

        // orElseGet will not call the Supplier when the optional is not empty.
        assert ("Hello " + name.orElseGet(Sample::getDefaultGreeting)).equals("Hello mrhaki");
        assert totalDefaultGreetingInvoked == 3;
    }

    private static String getDefaultGreeting() {
        // Increase counter, so we can check the method is invoked.
        totalDefaultGreetingInvoked++;
        return "world";
    }
}

Written with Java 15.

October 16, 2020

Automatic Switching Of Java Versions With SDKMAN!

SDKMAN! is a very useful tool to manage versions of so-called software development kits. There are a lot of SDKs supported by SDKMAN!: Java, Groovy, Kotlin, Scala, Gradle, Maven, Leiningen, Micronaut, Grails, Vert.x, JBake, AsciidoctorJ and more. When we look at Java we can use a simple install java <version> command from the command-line to install a version of Java on our computer. SDKMAN! will take care of downloading the Java version and setting all the correct system variables to use that Java version. With the use command we can switch between version in the current shell we are working in. But we can even automatically switch to a specific installed Java version when we enter a directory. This is very useful when we have to work on multiple projects on our computer and each project requires a specific Java version to be used.

To support automatic switching of a Java version we must first run the env init command in the directory of our project. This creates a new file .sdkmanrc in the directory. The file contains the Java version that was active when we invoked the env init command. It is a text file so we can change the Java version in the file, or regenerate the file by running the env init command again, but with a different active Java version.

In the following shell output we have a Java 15 version set as default. We first change it to Java 11 and then run env init to set Java 11 as default version for our projec directory:

project-dir $ java -version
openjdk version "15" 2020-09-15
OpenJDK Runtime Environment AdoptOpenJDK (build 15+36)
Eclipse OpenJ9 VM AdoptOpenJDK (build openj9-0.22.0, JRE 15 Mac OS X amd64-64-Bit Compressed References 20200922_45 (JIT enabled, AOT enabled)
OpenJ9   - 1830b1927
OMR      - 73d5e7623
JCL      - 7e7613c015 based on jdk-15+36)
project-dir $ sdk use java 11.0.8.hs-adpt

Using java version 11.0.8.hs-adpt in this shell.
project-dir $ java -version
openjdk version "11.0.8" 2020-07-14
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.8+10)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.8+10, mixed mode)
project-dir $ sdk env init
.sdkmanrc created.
project-dir $ cat .sdkmanrc
# Enable auto-env through the sdkman_auto_env config
# Add key=value pairs of SDKs to use below
java=11.0.8.hs-adpt
project-dir $

When a directory contains the .sdkmanrc file we can use the SKDMAN! env command and SDKMAN! will set the Java version based on the contents of the .sdkmanrc file. This is already useful, because we simply go to our project directory, invoke the env command and the Java version is set to the version we expect for our poject.

In the next example we start with Java 15 as default version, next we run the env command in the directory with the .sdkmanrc file:

~ $ java -version
openjdk version "15" 2020-09-15
OpenJDK Runtime Environment AdoptOpenJDK (build 15+36)
Eclipse OpenJ9 VM AdoptOpenJDK (build openj9-0.22.0, JRE 15 Mac OS X amd64-64-Bit Compressed References 20200922_45 (JIT enabled, AOT enabled)
OpenJ9   - 1830b1927
OMR      - 73d5e7623
JCL      - 7e7613c015 based on jdk-15+36
~ $ cd project-dir
project-dir $ sdk env

Using java version 11.0.8.hs-adpt in this shell.
project-dir $ java -version
openjdk version "11.0.8" 2020-07-14
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.8+10)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.8+10, mixed mode)
project-dir $

By changing a setting in the SDKMAN! configuration we can even let SDKMAN! run the env command when we change to our project directory in our shell. The default location of the configuration file is $HOME/.sdkman/etc/config and we must add the following line sdkman_auto_env=true. With this option we can simply change to our project directory and the correct Java version is set to active.

In our final example we have enabled the setting sdkman_auto_env, so when we change to the project directory the correct Java version is activated automatically:

~ $ java -version
openjdk version "15" 2020-09-15
OpenJDK Runtime Environment AdoptOpenJDK (build 15+36)
Eclipse OpenJ9 VM AdoptOpenJDK (build openj9-0.22.0, JRE 15 Mac OS X amd64-64-Bit Compressed References 20200922_45 (JIT enabled, AOT enabled)
OpenJ9   - 1830b1927
OMR      - 73d5e7623
JCL      - 7e7613c015 based on jdk-15+36
~ $ cd project-dir

Using java version 11.0.8.hs-adpt in this shell.
project-dir $ java -version
openjdk version "11.0.8" 2020-07-14
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.8+10)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.8+10, mixed mode)
project-dir $

Wrtten with SDKMAN 5.9.1+575.

June 11, 2020

Java Joy: Reapply Function With Stream iterate

In Java we can use the iterate method of the Stream class to create an unbounded stream based on function invocations. We pass to the iterate method an initial value and a function that can be applied to the value. The first element in the unbounded stream is the initial value, the next element is the result of the function invocation with as argument the value from the previous element and this continues for each new element. Suppose we have a function expressed as lambda expression i -> i + 2. When we use this lambda expression with the iterate method and a initial value of 1 we get a stream of 1, 1 -> 1 + 2, 3 -> 3 + 2, ....

As we get an unbounded stream we must for example use limit to get the values we want from the stream. But we can also use an extra argument for the iterate method that is a Predicate definition. The iterate method will provide elements as long as the result of the Predicate is true. This way we the result of the iterate method is a bounded stream.

In the following Java example we use the iterate method with different arguments and lambda expressions:

package mrhaki.stream;

import java.math.BigInteger;
import java.util.List;
import java.util.function.UnaryOperator;
import java.util.stream.Stream;

import static java.util.stream.Collectors.toUnmodifiableList;

public class Iterate {

    public static void main(String[] args) {
        // Create unbounded stream with odd numbers.
        var odds = Stream.iterate(1, i -> i + 2);

        // We use limit(5) to get the first 10 odd numbers from the unbounded stream.
        assert odds.limit(5).collect(toUnmodifiableList()).equals(List.of(1, 3, 5, 7, 9));

        // Create stream with even numbers, but here we use a predicate as
        // second argument to determine that we stop after value 10.
        var evens = Stream.iterate(0, i -> i <= 10, i -> i + 2);
        assert evens.collect(toUnmodifiableList()).equals(List.of(0, 2, 4, 6, 8, 10));


        // Define infinite stream with growing string.
        // The first element is ar, next argh, then arghgh etc.
        var pirate = Stream.iterate("ar", s -> s + "gh");

        // We get the 5-th element for a crumpy pirate.
        var crumpyPirate = pirate.skip(4).findFirst().get();
        assert crumpyPirate.equals("arghghghgh");


        // Function that returns the given amount
        // plus interest of 1.25%.
        UnaryOperator<Double> cumulativeInterest = amount -> amount + (amount * 0.0125);

        // Lazy sequence where each entry is the
        // cumulative amount with interest based
        // on the previous entry.
        // We start our savings at 500.
        var savings = Stream.iterate(500d, cumulativeInterest);

        // First element is start value, so we skip first five elements
        // to get value after 5 years.
        assert savings.skip(5).findFirst().get() == 532.0410768127441;

        
        // Define infinite unbounded stream
        // where each element is the doubled value of the previous element.
        var wheatChessboard = Stream.iterate(BigInteger.valueOf(1), value -> value.add(value));

        // Sum of all values for all chessboard squares is an impressive number.
        var square64 = wheatChessboard.limit(64).reduce(BigInteger::add).get();
        assert square64.equals(new BigInteger("18446744073709551615"));
    }
}

Written with Java 14.

June 10, 2020

Java Joy: Infinite Stream Of Values Or Method Invocations

In Java we can use the generate method of the Stream class to create an infinite stream of values. The values are coming from a Supplier instance we pass as argument to the generate method. The Supplier instance usually will be a lambda expression. To give back a fixed value we simply implement a Supplier that returns the value. We can also have different values when we use a method that returns a different value on each invocation, for example the randomUUID method of the UUID class. When we use such a method we can create the Supplier as method reference: UUID::randomUUID.

The generate method returns an unbounded stream. We must use methods like limit and takeWhile to get a bounded stream again. We must use findFirst or findAny to terminate the unbounded stream and get a value.

In the following example we use the generate method with a Supplier that returns a repeating fixed String value and some different values from invoking the now method of LocalTime:

package mrhaki.stream;

import java.time.LocalTime;
import java.util.List;
import java.util.stream.Stream;

import static java.util.stream.Collectors.toUnmodifiableList;

public class Generate {
    
    public static void main(String[] args) {
        // Create a infinite stream where each item is the string value "Java".
        // We let the supplier function return a fixed value "Java".
        assert Stream.generate(() -> "Java")
                     .limit(4)
                     .collect(toUnmodifiableList()).equals(List.of("Java", "Java", "Java", "Java"));

        // Create an infinite stream of function invocations of LocalTime.now().
        // The supplier function returns a different value for each invocation.
        var currentTimes = Stream.generate(LocalTime::now)
                                 .limit(2)
                                 .collect(toUnmodifiableList());
        assert currentTimes.get(0).isBefore(currentTimes.get(1));

        // Create a list of 100 time values where each value should be later than the next.
        var timeSeries = Stream.generate(LocalTime::now).limit(100).collect(toUnmodifiableList());
        assert latestTime(timeSeries).equals(timeSeries.get(timeSeries.size() - 1));
    }

    /**
     * Get the latest time from a serie of time values.
     *
     * @param times List with time values.
     * @return Latest time value from the collection.
     */
    private static LocalTime latestTime(List<LocalTime> times) {
        return times.stream().reduce((acc, time) -> {
            if (acc.isAfter(time)) { return acc; } else { return time; }
        }).get();
    }
}

Written with Java 14.

April 11, 2020

Java Joy: Using Functions To Replace Values In Strings

Since Java 9 we can use a function as argument for the Matcher.replaceAll method. The function is invoked with a single argument of type MatchResult and must return a String value. The MatchResult object contains a found match we can get using the group method. If there are capturing groups in the regular expression used for replacing a value we can use group method with the capturing group index as argument.

In the following example we use the replaceAll method and we use a regular expression without and with capturing groups:

package mrhaki.pattern;

import java.util.function.Function;
import java.util.regex.MatchResult;
import java.util.regex.Pattern;

public class Replace {
    public static void main(String[] args) {
        // Define a pattern to find text between brackets.
        Pattern admonition = Pattern.compile("\\[\\w+\\]");
        
        // Sample text.
        String text = "[note] Pay attention. [info] Read more.";
        
        // Function to turn a found result for regular expression to upper case.
        Function<MatchResult, String> bigAdmonition = match -> match.group().toUpperCase();
        
        assert admonition.matcher(text).replaceAll(bigAdmonition).equals("[NOTE] Pay attention. [INFO] Read more.");

        
        // Pattern for capturing numbers from string like: run20=390ms.
        Pattern boundaries = Pattern.compile("run(\\d+)=(\\d+)ms");

        // Function to calculate seconds from milliseconds.
        Function<MatchResult, String> runResult = match -> {
            double time = Double.parseDouble(match.group(2));
            return "Execution " + match.group(1) + " took " + (time / 1000) + " seconds.";
        };

        assert boundaries.matcher("run20=390ms").replaceAll(runResult).equals("Execution 20 took 0.39 seconds.");
    }
}

Written with Java 14.

April 9, 2020

Java Joy: Using Named Capturing Groups In Regular Expressions

In Java we can define capturing groups in regular expression. We can refer to these groups (if found) by the index from the group as defined in the regular expression. Instead of relying on the index of the group we can give a capturing group a name and use that name to reference the group. The format of the group name is ?<name> as first element of the group definition. The name of the group can be used with the group method of the Matcher class. Also we can use the name when we want to reference the capturing group for example with the replaceAll method of a Matcher object. The format is ${name} to reference the group by name. Finally we can use a named capturing group also as backreference in a regular expression using the syntax \k<name>.

In the following example we define a regular expression with named groups and use them with several methods:

package mrhaki.pattern;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class NamedPatterns {
    public static void main(String[] args) {
        // Define pattern and use names for the capturing groups.
        // The first group has the name project, second org unit number and finally a project number.
        // The format is ?<name>.
        // To make sure the separator is - or / (and not a combination)
        // we use group with name sep and use the backreference \k<sep> to match.
        Pattern issuePattern = Pattern.compile("(?<project>[A-Z]{3})(?<sep>[-/])(?<org>\\w{3})\\k<sep>(?<num>\\d+)$");

        // Create Matcher with a string value.
        Matcher issueMatcher = issuePattern.matcher("PRJ-CLD-42");

        assert issueMatcher.matches();
        
        // We can use capturing group names to get group.
        assert issueMatcher.group("project").equals("PRJ");
        assert issueMatcher.group("org").equals("CLD");
        assert issueMatcher.group("num").equals("42");
        
        // Using separator / also matches.
        assert issuePattern.matcher("EUR/ACC/91").matches();
        
        // But we cannot mix - and /.
        assert !issuePattern.matcher("EUR-ACC/91").matches();

        // Backreferences to the capturing groups can be used by
        // their names, using the syntax ${name}.
        assert issueMatcher.replaceAll("${project} ${num} in ${org}.").equals("PRJ 42 in CLD.");
    }
}

Written with Java 14.

February 28, 2020

Java Joy: Turn A Pattern Into A Predicate

This week at work a colleague showed a nice feature of the Pattern class in Java: we can easily turn a Pattern into a Predicate with the asPredicate and asMatchPredicate methods. The asPredicate method return a predicate for testing if the pattern can be found given string. And the asMatchPredicate return a predicate for testing if the pattern matches a given string.

In the following example code we use both methods to create predicates:

package mrhaki;

import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class PatternPredicate {

    public static void main(String[] args) {
        // Sample pattern.
        var pattern = Pattern.compile("J");

        // asPredicate returns a predicate to test if 
        // the pattern can be found in the given string.
        assert pattern.asPredicate().test("Java");
        assert !pattern.asPredicate().test("Groovy");

        // asMatchPredicate returns a predicate to test if
        // the pattern completely matches the given string.
        assert pattern.asMatchPredicate().test("J");
        assert !pattern.asMatchPredicate().test("Java");


        // Using the asPredicate and asMatchPredicate is very
        // useful with the streams API.
        var languagesWithJ = Stream.of("Java", "Groovy", "Clojure", "JRuby", "JPython")
                                   .filter(pattern.asPredicate())
                                   .collect(Collectors.toList());

        assert languagesWithJ.size() == 3;
        assert languagesWithJ.equals(List.of("Java", "JRuby", "JPython"));

        var startsWith_J_EndsWith_y = Pattern.compile("^J\\w+y$").asMatchPredicate();
        var languagesWithJy = Stream.of("Java", "Groovy", "Clojure", "JRuby", "JPython")
                                    .filter(startsWith_J_EndsWith_y)
                                    .collect(Collectors.toList());

        assert languagesWithJy.size() == 1;
        assert languagesWithJy.equals(List.of("JRuby"));
    }
}

Written with Java 13.

September 10, 2019

Java Joy: Transform Elements In Stream Using a Collector

Using the Stream API and the map method we can transform elements in a stream to another object. Instead of using the map method we can also write a custom Collector and transform the elements when we use the collect method as terminal operation of the stream.

First we have an example where we transform String value using the map method:

package mrhaki;

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

public class CollectorString {
    public static void main(String[] args) {
        final var items = List.of("JFall", "JavaZone", "CodeOne");

        final List<String> upper =
                items.stream()
                     .map(String::toUpperCase)
                     .collect(Collectors.toUnmodifiableList());
        
        assert upper.equals(List.of("JFALL", "JAVAZONE", "CODEONE"));
    }
}

In our next example we don't use the map method, but we write a custom Collector using the Collector.of method. As first argument we must provide the data structure we want to add elements too, the so-called supplier, which is an ArrayList. The second argument is an accumulator where we add each element from the stream to the list and transform the value. The third argument is the combiner and here we combine multiple List instances to one List instance. The last argument is a finisher and we make an immutable List to be returned.

package mrhaki;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collector;

public class CollectorString1 {
    public static void main(String[] args) {
        final var items = List.of("JFall", "JavaZone", "CodeOne");

        final List<String> upper =
                items.stream()
                     // Use collector to transform values
                     // in the items List.
                     .collect(upperCollect());

        assert upper.equals(List.of("JFALL", "JAVAZONE", "CODEONE"));
    }

    private static Collector<String, ?, List<String>> upperCollect() {
        return Collector.of(
                // First we specify that we want to add
                // each element from the stream to an ArrayList.
                () -> new ArrayList<String>(),

                // Next we add each String value to the list
                // and turn it into an uppercase value.
                (list, value) -> list.add(value.toUpperCase()),

                // Next we get two lists we need to combine,
                // so we add the values of the second list
                // to the first list.
                (first, second) -> { first.addAll(second); return first; },

                // Finally (and optionally) we turn the 
                // ArrayList into an unmodfiable List.
                list -> Collections.unmodifiableList(list));
    }
}

Written with Java 12.

September 9, 2019

Java Joy: Combining Predicates

In Java we can use a Predicate to test if something is true or false. This is especially useful when we use the filter method of the Java Stream API. We can use lambda expressions to define our Predicate or implement the Predicate interface. If we want to combine different Predicate objects we can use the or, and and negate methods of the Predicate interfaces. These are default methods of the interface and will return a new Predicate.

Let's start with an example where we have a list of String values. We want to filter all values that start with Gr or with M. In our first implementation we use a lambda expression as Predicate and implements both tests in this expression:

package mrhaki;

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

public class PredicateComposition1 {
    public static void main(String[] args) {
        final var items = List.of("Groovy", "Gradle", "Grails", "Micronaut", "Java", "Kotlin");

        final List<String> gr8Stuff =
                items.stream()
                     // Use lambda expression with both tests as Predicate.
                     .filter(s -> s.startsWith("Gr") || s.startsWith("M"))
                     .collect(Collectors.toUnmodifiableList());

        assert gr8Stuff.size() == 4 : "gr8Stuff contains 4 items";
        assert gr8Stuff.contains("Groovy");
        assert gr8Stuff.contains("Gradle");
        assert gr8Stuff.contains("Grails");
        assert gr8Stuff.contains("Micronaut");
    }
}

We will rewrite the previous example and introduce the startsWith method that returns a new Predicate. Then in our filter method we use the or method of the Predicate object to combine the two Predicate objects:

package mrhaki;

import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class PredicateComposition2 {
    public static void main(String[] args) {
        final var items = List.of("Groovy", "Gradle", "Grails", "Micronaut", "Java", "Kotlin");

        final List<String> gr8Stuff =
                items.stream()
                     // Use the Predicate.or method to combine two Predicate objects.
                     .filter(startsWith("Gr").or(startsWith("M")))
                     .collect(Collectors.toUnmodifiableList());

        assert gr8Stuff.size() == 4 : "gr8Stuff contains 4 items";
        assert gr8Stuff.contains("Groovy");
        assert gr8Stuff.contains("Gradle");
        assert gr8Stuff.contains("Grails");
        assert gr8Stuff.contains("Micronaut");
    }

    // Create a predicate to check if String value starts with a given value.
    private static Predicate<String> startsWith(final String begin) {
        return s -> s.startsWith(begin);
    }
}

In the following example we use the negate and and method to find all values that do not start with Gr and with a length less than 8 characters:

package mrhaki;

import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class PredicateComposition3 {
    public static void main(String[] args) {
        final var items = List.of("Groovy", "Gradle", "Grails", "Micronaut", "Java", "Kotlin");

        final List<String> otherStuff =
                items.stream()
                     // Find all values that do not start with "Gr"
                     // and have less than 8 characters.
                     .filter(startsWith("Gr").negate().and(smallerThan(8)))
                     .collect(Collectors.toUnmodifiableList());

        assert otherStuff.size() == 2 : "otherStuff contains 2 items";
        assert otherStuff.contains("Java");
        assert otherStuff.contains("Kotlin");
    }

    // Create a predicate to check if String value starts with a given value.
    private static Predicate<String> startsWith(final String begin) {
        return s -> s.startsWith(begin);
    }

    // Create a predicate to check if String value has 
    // less characters than the given size.
    private static Predicate<String> smallerThan(final int size) {
        return s -> size >= s.length();
    }
}

In our previous example we can replace the negate method call on our predicate with the static Predicate.not method. The predicate is than an argument and is just another way to express the same predicate:

package mrhaki;

import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class PredicateComposition4 {
    public static void main(String[] args) {
        final var items = List.of("Groovy", "Gradle", "Grails", "Micronaut", "Java", "Kotlin");

        final List<String> otherStuff =
                items.stream()
                     // Find all values that do not start with "Gr",
                     // using Predicate.not instead of negate, 
                     // and have less than 8 characters.
                     .filter(Predicate.not(startsWith("Gr")).and(smallerThan(8)))
                     .collect(Collectors.toUnmodifiableList());

        assert otherStuff.size() == 2 : "otherStuff contains 2 items";
        assert otherStuff.contains("Java");
        assert otherStuff.contains("Kotlin");
    }

    // Create a predicate to check if String value starts with a given value.
    private static Predicate<String> startsWith(final String begin) {
        return s -> s.startsWith(begin);
    }

    // Create a predicate to check if String value has 
    // less characters than the given size.
    private static Predicate<String> smallerThan(final int size) {
        return s -> size >= s.length();
    }
}

Written with Java 12.