Effective Java (Third Edition) learning notes - Chapter 7 Lambda and Stream Rule42~Rule48

Posted by Xu Wei Jie on Sun, 16 Jan 2022 13:20:26 +0100

catalogue

Rule42 Lambda takes precedence over anonymous classes

Rule43 method reference takes precedence over Lambda

Rule44 insists on using standard function interfaces

Rule45 use Stream with caution

Rule46 gives priority to functions without side effects in the Stream

Rule47 Stream should preferentially use Collection as the return type

Rule48 use Stream parallelism with caution

Rule42 Lambda takes precedence over anonymous classes

Anonymous classes do not understand, please look first Four nested classes in Java

// Sorting with function objects (Pages 193-4)
public class SortFourWays {
    public static void main(String[] args) {
        List<String> words = Arrays.asList(args);
//        List<String> words = Arrays.asList("51", "12345", "2345", "711");

        // Anonymous class instance as a function object - obsolete! (Page 193)
        // 1: Anonymous inner class method
        // idea prompt: anonymous new comparator < string > () can be replaced with lambda
        Collections.sort(words, new Comparator<String>() {
            public int compare(String s1, String s2) {
                return Integer.compare(s1.length(), s2.length());
            }
        });
        System.out.println(words);
        Collections.shuffle(words);

        // Lambda expression as function object (replaces anonymous class) (Page 194)
        // 2: Lambda parameter method
        // idea tip: can be replaced with comparator comparingInt
        Collections.sort(words,
                (s1, s2) -> Integer.compare(s1.length(), s2.length()));
        System.out.println(words);
        Collections.shuffle(words);

        // Comparator construction method (with method reference) in place of lambda (Page 194)
        // 3: Lambda uses comparator Comparingint mode
        Collections.sort(words, comparingInt(String::length));
        System.out.println(words);
        Collections.shuffle(words);

        // Default method List.sort in conjunction with comparator construction method (Page 194)
        // 4: Use the sort method of the List object directly
        words.sort(comparingInt(String::length));
        System.out.println(words);
    }
}

As in this code example, five lines of code from the original anonymous inner class are finally directly reduced to one line. At the same time, the amount of code in the current line is also continuously reduced without affecting understanding. It should be noted that even if it is implemented in the Lambda mode of 2, there is no need to specify the parameter type, because the compiler can deduce the current type according to the context by using the type derivation process (java10 introduces the var keyword, but there are certain restrictions), and generic paradigm The function of Lambda is reflected. Only in rare cases do you need to specify the parameter type of Lambda.

Lambda can greatly reduce the amount of code, but if a function is not self described or the amount of code exceeds a few lines, it can be completed in another way, because lambda has no name and document. In a few lines, a large amount of lambda code may have some negative impact on readability.

At the same time, the book points out that you should not serialize a Lambda (or anonymous inner instance) as far as possible. If you don't understand this, you can refer to it java nested classes The introduction in that article.

// Enum with function object fields & constant-specific behavior (Page 195)
public enum Operation {
    PLUS  ("+", (x, y) -> x + y),
    MINUS ("-", (x, y) -> x - y),
    TIMES ("*", (x, y) -> x * y),
    DIVIDE("/", (x, y) -> x / y);

    private final String symbol;
    private final DoubleBinaryOperator op;

    Operation(String symbol, DoubleBinaryOperator op) {
        this.symbol = symbol;
        this.op = op;
    }

    @Override public String toString() { return symbol; }

    public double apply(double x, double y) {
        return op.applyAsDouble(x, y);
    }

    // Main method from Item 34 (Page 163)
    public static void main(String[] args) {
        double x = Double.parseDouble("1");
        double y = Double.parseDouble("2");
        for (Operation op : Operation.values())
            System.out.printf("%f %s %f = %f%n",
                    x, op, y, op.apply(x, y));
    }
}

Rule34 replacing int constants with enumerations The code example in can be transformed into the above. Note that Java util. Function. Double binary operator, in Java 8, Java util. Many Function interfaces are provided under the Function package. After careful observation, you will find that these interfaces have only one method to be implemented, and the other methods provided are default methods with default implementation. This echoes the Java 8 concept that "interfaces with a single abstract method are special and deserve special treatment".

Rule43 method reference takes precedence over Lambda

First, what is a method reference?

Take the example in Rule 42, words sort(comparingInt(String::length));, String:: length is the method reference. When I look at this writing at the beginning, it will be regarded as a Lambda expression. After reading this part, I know that although this writing method is generally used with Lambda expressions, it has a special title, that is, method reference (although it is often called Lambda writing method).

Method reference and Lambda expression
Method reference typeexampleLambda equation
static stateInteger::parseIntstr -> Integer.parseInt(str)
LimitedInstant.now()::isAfter

Instant then = Instant.now();

t -> then.isAfter(t)

unlimitedString::toUpperCasestr -> str.toUpperCase()
class constructor TreeMap<K, V>::new() -> new TreeMap<K, V>
array constructor int[]::newlen -> new int[len]

In contrast, method references are often more concise than Lambda expressions. But sometimes there are exceptions, such as customizing a class GoshThisClassNameIsHumongous

  • service.execute(GoshThisClassNameIsHumongous::action);
  • service.execute(() -> action());

In contrast, the Lambda expression will be more concise, so we only need to follow a principle in our daily development. If the method reference will be more concise, we will use the method reference, otherwise we will use the Lambda expression.

Rule44 insists on using standard function interfaces

  • Function interface: the main purpose of this interface is to provide it for functional programming, that is, an interface containing only one abstract method. It can be marked with the annotation @ functional interface.
  • Standard function interface: Java util. Function the defined function interface provided below. If the standard function interface can meet the requirements, we should not define another set ourselves.

@What does the functional interface do

  1. Tell you that this interface is designed for Lambda.
  2. This interface only allows one abstract method, otherwise it cannot be compiled.
  3. Do not provide overloaded methods, which may cause ambiguity on the client.

At present, Java 8 is Java util. Under function, there are dozens of interfaces, but we just need to remember the six basic interfaces, and other interfaces can be analogized through these six basic interfaces.

Standard function interface - Basic 6 types
InterfaceFunction signatureexampleremarks

Unary operator

UnaryOperator<T>

T apply(T t)String::toUpperCase

Unary represents an input parameter, and Operator represents a function whose return result is consistent with the parameter

※ it inherits from function < T, R > interface and unifies generic types

Binary operator

BinaryOperator<T>

T apply(T t1, T t2)BigInteger::add

Binary represents two input parameters, and Operator represents a function whose return result is consistent with the parameters

※ it inherits from bifunction < T, u, R > interface and unifies generic types

Assert

Predicate<T>

boolean test(T t)Collection::isEmptyPredicate represents a function that has an input parameter and returns a Boolean value

function

Function<T, R>

R apply(T t)Arrays::asListFunction represents a function whose return type is inconsistent with the input parameter type

Provider

Supplier<T>

T get()Instant::nowSupplier represents a function that has no parameters and returns a value

consumer

Consumer<T>

void accept(T t)System.out::printlnConsumer represents a function with no return value and one parameter

Rule45 use Stream with caution

For a basic introduction to Stream flow, please see this article< Java 8 stream related sharing>

Although the writing method of stream can greatly simplify the amount of code, it will have negative effects if it is not used well.

// Overuse of streams - don't do this! (page 205)
public class StreamAnagrams {
    public static void main(String[] args) throws IOException {
        Path dictionary = Paths.get(args[0]);
        int minGroupSize = Integer.parseInt(args[1]);

        try (Stream<String> words = Files.lines(dictionary)) {
            words.collect(
                    groupingBy(word -> word.chars().sorted()
                            .collect(StringBuilder::new,
                                    (sb, c) -> sb.append((char) c),
                                    StringBuilder::append).toString()))
                    .values().stream()
                    .filter(group -> group.size() >= minGroupSize)
                    .map(group -> group.size() + ": " + group)
                    .forEach(System.out::println);
        }
    }
}

For example, this code is difficult to quickly understand what its intention is. Proper extraction methods will improve the readability of the code.

// Tasteful use of streams enhances clarity and conciseness (Page 205)
public class HybridAnagrams {
    public static void main(String[] args) throws IOException {
        Path dictionary = Paths.get(args[0]);
        int minGroupSize = Integer.parseInt(args[1]);

        try (Stream<String> words = Files.lines(dictionary)) {
            words.collect(groupingBy(word -> alphabetize(word)))
                    .values().stream()
                    .filter(group -> group.size() >= minGroupSize)
                    .forEach(g -> System.out.println(g.size() + ": " + g));
        }
    }

    private static String alphabetize(String s) {
        char[] a = s.toCharArray();
        Arrays.sort(a);
        return new String(a);
    }
}

Some work can be done with Lambda expressions, but Lambda expressions are not omnipotent. For example, the following cases:

  • In the code block, any local variable within the range can be read and modified; Lambda can only read the final variable and cannot change the value
  • In the code block, you can return, break, continue, or throw an exception; Not even Lambda

On the contrary, the following situations are suitable for Lambda:

  • Sequence of unified transformation elements
  • Filter the sequence of elements
  • Merge element order with a single operation, such as adding, connecting, or calculating its minimum value
  • The sequence of elements is stored in a collection and grouped according to some common attributes
  • Search for a sequence of elements that meet certain conditions

Rule46 gives priority to functions without side effects in the Stream

Stream side effects are also described in another stream. The ideal stream focuses on turning a series of operations into pure functions as much as possible, without relying on any variable state or updating any state. The result of the function only depends on the input function, and the stateless and side-effect free method shall be selected as far as possible.

// Frequency table examples showing improper and proper use of stream (Page 210-11)
public class Freq {
    public static void main(String[] args) throws FileNotFoundException {
        File file = new File(args[0]);

        // Uses the streams API but not the paradigm--Don't do this!
        Map<String, Long> freq1 = new HashMap<>();
        try (Stream<String> words = new Scanner(file).tokens()) {
            words.forEach(word -> {
                freq1.merge(word.toLowerCase(), 1L, Long::sum);
            });
        }

        // Proper use of streams to initialize a frequency table (
        Map<String, Long> freq2;
        try (Stream<String> words = new Scanner(file).tokens()) {
            freq2 = words
                    .collect(groupingBy(String::toLowerCase, counting()));
        }
    }
}

In these two implementations, freq1 only looks like a Stream operation, but in fact, it is an iterator operation in essence, while freq2 is a pure Stream operation. The forEach operation should only be used to report the results of the Stream operation, not to perform calculations.

Rule47 Stream should preferentially use Collection as the return type

Three categories, Collection collection, array and iterator. Generally, we only focus on loops, and iterators are recommended; If the basic type is returned and performance is required, array is recommended; In other cases, Collection return is generally recommended.

※ unfortunately, I don't quite understand what the core wants to express about the four pages of the book.

Rule48 use Stream parallelism with caution

Java5 introduces JUC, Java7 introduces fork join (a high-performance framework for processing parallel decomposition), and Java8 introduces Stream (parallel processing can be implemented only, and the internal implementation is the default ForkJoinPool). It is easier and easier to write concurrent programming, but we still need to pay attention to the security and activity failure of concurrent programming.

※ activity failure: if A modifies the shared variable and thread B does not perceive the change of the shared variable, it is called activity failure. The common countermeasure is that two threads have A happens before relationship with this contribution variable. The most common is volatile or locking. (about concurrent programming, it will be improved and sorted out later)

// Parallel stream-based program to generate the first 20 Mersenne primes - HANGS!!! (Page 222)
public class ParallelMersennePrimes {
    public static void main(String[] args) {
        primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
                .parallel()
                .filter(mersenne -> mersenne.isProbablePrime(50))
                .limit(20)
                .forEach(System.out::println);
    }

    static Stream<BigInteger> primes() {
        return Stream.iterate(TWO, BigInteger::nextProbablePrime);
    }
}

This code uses parallel() for parallelism, but because the source is stream Iterate, or use the limit() stateful intermediate operation, the final effect will be completely opposite to the expectation, and even cause activity failure, resulting in no progress. Where is parallel() suitable? Data sources can be segmented into accurate and easy sub ranges of any size. For example: ArrayList, HashMap, ConcurrentHashMap, HashSet, array, int range, long range, etc.

These data structures also have another feature, excellent reference locality. References to serialized elements are saved in memory together. The objects accessed by those references may not be next to each other in memory. This discontinuity will reduce the locality of references. When executing in parallel, the thread needs to wait to get the object from memory to the processor's cache, so it is easy to wait. The data structures with the best locality of reference are basic type arrays, because they are adjacent and stored in memory.

If a large number of operation logic are placed in the terminal method and executed in an inherent sequence, the efficiency in parallel will also be reduced. The most ideal terminal operation for parallel operation is subtraction. All pipeline results are combined with a reduce method. Alternatively, the short-circuit method can also be used in parallel.

The operation performed by the collection of Stream is not the best parallel method, because the cost of merging the collection itself is relatively high.

If you want to use parallel operations, make sure you need to do so, and it's best to test performance in a real environment.

This technology is for novices to learn and use. If there is anything wrong, you are welcome to point out the correction. xuweijsnj

Topics: Java