Functional Programming

ISsues

  • Just because you are using Lambda expressions, does not mean you are doing functional programming.

    • Lambda expressions in Java are simply a less verbose way of creating (slightly constrained) Objects and as such the most likely outcome of adopting Lambda’s without a good understanding of core functional concepts is gnarly, twisted, hard to follow, obfuscated imperative Object Oriented code with a nice concise syntax.

  • Java is not a functional language, it is fundamentally an Object Oriented language that allows us to adopt some functional concepts in so far as we enforce their correct implementation through developer discipline

To use FP in java correctly

  • Make good use of Generic Types

    • Don’t ignore type parameters, declare them and enforce them everywhere.

    • Minimize casting and if instanceOfing

      • Where you do use them centralize each type of cast within a resuable method,

    • use proper type parameters on the inputs and outputs

    • write good tests!

  • Make illegal states unrepresentable in our code.

    • Avoid nulls,

      • Optionals

        • Use sparingly and not for fields (apart from data objects)

      • Dont allow null fields

      • USe inheritance

    • don’t throw Exceptions,

    • avoid locking and synchronization

  • Make our own data classes immutable and final where possible,

    • use immutable collections (proper ones).

    • Avoid instanceof checks where possible, and

    • where you use them make sure the types are not extensible.

  • use libraries that avoid runtime magic and reflection where pragmatically possible.

Immutability

  • Declare the class as final so it can’t be extended.

    • make the constructor private and construct instances in factory methods.

  • Make all fields private so that direct access is not allowed.

  • Don’t provide setter methods for variables

  • Make all mutable fields final so that it’s value can be assigned only once.

  • Initialize all the fields via a constructor performing deep copy.

  • Perform cloning of objects in the getter methods to return a copy rather than returning the actual object reference.

  • If the instance fields include references to mutable objects, don't allow those objects to be changed:

    -Don't provide methods that modify the mutable objects.

    • Don't share references to the mutable objects.

    • Never store references to external, mutable objects passed to the constructor;

      • if necessary, create copies, and store references to the copies.

      • Similarly, create copies of your internal mutable objects when necessary to avoid returning the originals in your methods.

streams

  • Streams

    • Give lazy evaluation (ie filter)

      • Also gives visual reminder that lazy evaluation is happening

      • Lazy evaluation will only happen when the terminal operation is hit ie forEach

    • Act like an iterator

    • Are monads

    • Stream operations are either intermediate (map, filter) or terminal (reduce, collect, foreach)

      • Intermediate operations return a stream so we can chain multiple intermediate operations without using semicolons

      • Terminal operations are either void or return a non-stream result

    • operation pipeline = a chain of stream operations

    • Most stream operations accept some kind of lambda expression parameter, a functional interface specifying the exact behavior of the operation.

      • Most of those operations must be both non-interfering and stateless.

      • A function is non-interfering when it does not modify the underlying data source of the stream, ie does not add or remove elements for the collection you are streaming from

      • A function is stateless when the execution of the operation is deterministic, e.g. in the above example no lambda expression depends on any mutable variables or states from the outer scope which might change during execution.

    • Reuse

      • Java 8 streams cannot be reuse

      • To overcome this limitation we have to to create a new stream chain for every terminal operation we want to execute

      • we could create a stream supplier to construct a new stream with all intermediate operations already set up:

        Supplier<Stream<String>> streamSupplier =
          () -> Stream.of("d2", "a2", "b1", "b3", "c")
                  .filter(s -> s.startsWith("a"));
        
        streamSupplier.get().anyMatch(s -> true);   // ok
        streamSupplier.get().noneMatch(s -> true);  // ok
  • CReating streams

    • Arrays.asList("a1", "a2", "a3").stream()...

    • Stream.of("a1", "a2", "a3")...

    • IntStream.range(1, 4)... similar to for loop

      • Also exists LongStream and DoubleStream

      • Form of primitive stream

      • Primitive streams use specialized lambda expressions

    • Convert from object to primitive stream Stream.of("a1", "a2", "a3").map(s -> s.substring(1)).mapToInt(Integer::parseInt)

      • As primitive stream have extra functions on the primitve type ie max, sum

    • Convert from primitve to object stream IntStream.range(1, 4).mapToObj(i -> "a" + i)

  • Processing order

    • An important characteristic of intermediate operations is laziness

      Stream.of("d2", "a2", "b1", "b3", "c")
      .filter(s -> {
        System.out.println("filter: " + s);
        return true;
      });
    • When executing this code snippet, nothing is printed to the console

      • As intermediate operations will only be executed when a terminal operation is present.

    • Each element is processed all the way through the operation pipeline until it hits the terminal op, then goes to the next element in the stream

    • This is why we do filter before maps

  • sorted

  • filter

    • lazy

  • reduce

    • terminal op

    • converts a stream into something concrete

      • takes a collection and reduces to a single value

      • Take a collection and reduce to another collection

    • Other Reduce

    • count

    • distinct

    • sum

  • map

  • forEach

    • terminal op

  • collect

    • Is a reduce operator

      • Terminal op

    • transform the elements of the stream into a different kind of result, e.g. a List, Set or Map

    • Can accept groupingBy

    • create aggregations on the elements of the stream, e.g. determining the average age of all persons

      Double averageAge = persons
      .stream()
      .collect(Collectors.averagingInt(p -> p.age));
      
      System.out.println(averageAge);
    • Collectors.joining(...) used join all elements in stream into String

    • In order to transform the stream elements into a map, we have to specify how both the keys and the values should be mapped. Keep in mind that the mapped keys must be unique, otherwise an IllegalStateException is thrown. You can optionally pass a merge function as an additional parameter to bypass the exception

      Map<Integer, String> map = persons
        .stream()
        .collect(Collectors.toMap(
            p -> p.age, // create key
            p -> p.name, // create value
            (name1, name2) -> name1 + ";" + name2)); // merge func
      
      System.out.println(map);
      // {18=Max, 23=Peter;Pamela, 12=David}
  • FlatMap

Checked exceptions

  • need to add try catch in the stream, not great

list.stream()
    .map(i -> i.toString())
    .map(s -> s.toUpperCase())
    .forEach(s -> System.out.println(s));

If they throw checked exceptions

list.stream()
    .map(i - > {
        try {
            return i.toString();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    })
    .map(s - > {
        try {
            return s.toUpperCase();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    })
    .forEach(s - > {
        try {
            System.out.println(s);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    });

Map and FlatMap

Collectors

Method references

lambdas/ Annoynmous functions

Functions

Functional interfaces

  • supplier

  • consumer

  • function

  • predicate

  • unary operator

  • binary operator

Moving from imperative to functional style

Optional

  • of

  • isPresent

  • using map

Comparators

currying and partial functions

Parallel an async

Last updated

Was this helpful?