Java Lambdas & Stream API
Lambda Expressions are the most remarkable feature added to the Java platform with Java 8. It's specified in JSR 335 and JEP 126.
The very need for this feature is to gain the capabilities supplied by functional programming.
The main idea behind this concept is to be able to parametrize the functions for subsequent executions of them.
Till Java 8, this can be simulated via the use of anonymous inner classes with some design patterns like command pattern and functors.
However the adoption of lambda expressions gave way to the direct use of this concept.
1. What is a Lambda Expression ?
A lambda expression is a piece of code that is giving an alternative way to the anonymous class to pass the function as parameter to other methods, constructors and etc..
In this approach, a function can be referenced with a variable and passed as reference to be executed in a subsequent flow of code execution.
2. Type of Lambda Expressions; Functional Interfaces
A lambda expression is identified by a special interface called Functional Interface that is an interface with a single method. This functional interface is the target type that's determined by the compiler and used as a reference to the lambda expression.
This binding of a lambda expression to a functional interface is determined from the context of the lambda expression. That means the binding of a lambda expression to a target type can take place in different contexts such as variable decleration, method arguments and etc. and from this binding the compiler finds the target type which is a functional interface and infers the types of parameters used in the lambda expression according to that functional interface.
A functional interface can be marked with an informative annotation @FunctionalInterface that can be used to inform other developers.
Let's do a simple example to understand it well:
Think that we want to lowercase or uppercase a text based on a condition.
It will be a dynamic evaluation so we can abstract the operation.
With using the lambda expressions we can do it as follows:
Here is the lambda expressions for case operations:
t -> t.toUpperCase();
t -> t.toLowerCase();
To be able to pass these expression to somewhere, we have to declare a functional interface as such:
interface CaseOperation {
public String operate(String text);
}
Then we can write our main code as such:
public void printWithCaseOperation(String text, CaseOperation operation){
System.out.println(operation.operate(text));
}
public void mainCode(){
if(upperCaseEnabled){
printWithCaseOperation("Hello Lambda!", t -> t.toUpperCase());
} else {
printWithCaseOperation("Hello Lambda!", t -> t.toLowerCase());
}
}
3. Some Internals
Then here, let's watch this video to listen the internals of lambda expressions. For whom needs some speed, i'll summerize it, you're welcome:
- We need lamdas simply since of; parallel friendly APIs and less code with the usage of closure -like functional programming capabilities.
- Lambdas is not a new function type in the VM level it's mostly about compiler level.
- Compiler does a great job for us by transforming our lambdas into the form of a related defined functional interface. In other words; compiler infers our lambdas as functional interfaces.
- When the compiler sees the lambda expression, it simply creates a static method from the resolved related functional interface and in the invocation time, the VM executes that method by calling invokedynamic which is an invocation mode introduced in Java SE 7.
4. More Lambda Syntax
4.1 Full Syntax
(int a, int b) -> { return a + b; }
4.2 Single Statements in Body
(int a, int b) -> a + b
4.3 Implicit target types
(a, b) -> a + b
4.4 Parantheses are optional in case of a single implicit target type
a -> a.size()
4.5 When using explicit target types, then parantheses are required
(String str) -> str.length()
4.6 Lambda expressions without parameters
() -> "abc"
4.7 The body can have multiple statements
(a, b) -> {
int result = a + b;
return result;
}
5. Built-in Functional Interfaces
Now that, we already know; ultimately, a functional interface is a method reference and also defines the target type of a lambda expression for the sake of compiler.
So we can structure our api around functional interfaces and use lambdas for more effective and clean code. However, as you see, a functional interface is just defines the target types of a lambda expression. Hence, the same functional interfaces could be used in most cases. For that aim, in Java 8; common built-in functional interfaces have already been created.
So instead of declaring our custom functional interfaces, we can use the built-in ones that will mostly meet our needs.
5.1 Functions
/**
* @param <T> the type of the input to the function
* @param <R> the type of the result of the function
*/
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
Function<Person, String> f = t -> t.getGivenName();
String name = f.apply(Person.createShortList().get(0));
System.out.println(name);
5.2 Suppliers
/**
* @param <T> the type of results supplied by this supplier
*/
@FunctionalInterface
public interface Supplier<T> {
T get();
}
public void supplierTest(){
display(() -> 4);
}
public void display(Supplier<Integer> arg){
System.out.println(arg.get());
}
5.3 Consumers
/**
* @param <T> the type of the input to the operation
*/
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
public void consumerTest(){
printLog("this log is via Consumer interface", (c) -> System.out.println(c));
}
public void printLog(String log, Consumer<String> logger){
logger.accept(log);
}
5.4 Predicates
/**
* @param <T> the type of the input to the predicate
*/
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
public static final Predicate<Person> YOUNG = p -> p.getAge() >= 18 && p.getAge() <= 25; public void predicateTest() { Person person = Person.createShortList().get(0); System.out.println(YOUNG.test(person)); }
5.4.1 BiPredicate
public boolean equals(String a, String b, BiPredicate<String, String> equalsPredicate){
return equalsPredicate.test(a, b);
}
public void testBiPredicate(){
boolean equals = equals("erol", "hira", (a, b) -> a.equals(b));
System.out.println(equals);
}
5.4.2 Primitives version of Predicate
public static final IntPredicate IS_YOUNG = age -> age >= 18 && age <= 25;
public void isYoung() {
System.out.println(IS_YOUNG.test(14));
}
5.5 UnaryOperator
/**
* @param <T> the type of the operand and result of the operator
*/
@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
static <T> UnaryOperator<T> identity() {
return t -> t;
}
}
public void testUnaryOperator(){
UnaryOperator<String> upperCase = t -> t.toUpperCase();
System.out.println(upperCase.apply("test"));
}
5.6 BinaryOperator
public static final BinaryOperator<Integer> SUM = (a, b) -> a + b;
public void sum() {
System.out.println(SUM.apply(10, 20));
}
6. Stream API
Streams are a new concept comes with Java 8 and enables easy processing of collections with the usage of lambda expressions. For the concept, there exists an API in the package java.util.stream.
6.1 Stream Terminology and Basics
- A stream is a sequence of elements.
- A stream can be traversed only once as in iterators.
- Various methods can be chained on elements of a stream in a single statement. That's called method chaining and is just like a builder pattern style.
- Chain of operations using streams is called a stream pipeline that consists of:
- a source which is a collection, file or stream.
- zero or more intermediate operations such as filter, map, peek..
- one terminal operation such as forEach, count, collect..
- Intermediate operations are lazy that means the operation on the source is performed only when the terminal operation is executed and only on the elements that are necessary.
6.3 Stream vs Collection
- A stream has an internal iteration over the elements.
- When iterating internally over the elements, for each element; several intermediate operations such as filtering and transformation can be executed with the usage of lambda expressions.
6.3 Stream Operations
-
Intermediate
An intermediate operation on a stream returns a new stream and so; this enables us to chain multiple intermediate operations in a single statement. -
Terminal
A terminal operation is the actual operation that acts on the elements remaining after all the intermediate operations.
6.4 Creating and Executing A Pipeline
A pipeline is formed of a collection, several intermediate operations and a terminal operation as mentioned above. The pipeline creation schema that's describing this flow is given below.
Double sum = salaries.parallelStream()
.filter(p -> p.getSalary() > 0)
.mapToDouble(p -> p.getSalary())
.sum();
Let's deep dive into the sample code above;
- A stream is created from a collection named salaries by calling the method parallelStream().
- The resulting stream here just references the collection with a special iterator called Spliterator.
- The methods stream() and parallelStream() are the two utilities added to Collection interface to create a stream from a collection instance.
- On a stream we can call several intermediate operations, each of which creates a new stream reference, which helps us filtering or transforming the data on which we work. In the example above; at first, the filter intermediate operation is called.
- Calling an intermediate operation on a stream does nothing on the data, it's just about the creation of pipeline.
- By calling the method filter, a new Stream instance is created having the intermediate operation filter to be called later on subsequent to the terminal operation.
- After that, mapToDouble intermediate operation is called on the stream. This creates another stream having the mapToDouble operation to be called after the first intermediate operation which is filter.
- Just note here that; parallel stream does not affect the calling sequence of the streams and so also the intermediate operations. Concurrency on a stream is about splitting the data into groups and processing each group concurrently in seperate threads but in the order of intermediate operations just as specified in the pipeline creation.
- After calling the terminal operation which is sum here, each element in the stream is forwarded to the intermediate operations in the pipeline and ultimately the terminal operation is executed on the resulting data set.
6.5 Intermediate Operations Provided by Stream API
Operation | Argument | Return Type | Function Descriptor |
filter | Predicate | Stream<T> | T -> boolean |
map | Function<T, R> | Stream<R> | T -> R |
limit | long | Stream<T> | |
distinct | Stream<T> | ||
sorted | Comparator<T> | Stream<T> | (T, T) -> int |
skip | long | Stream<T> |
6.6 Terminal Operations Provided by Stream API
Operation | Argument | Return Type | Description |
forEach | Consumer<T> | void | Performs an action for each element of the stream. |
collect | Collector | Collection | Performs a mutable reduction operation on the elements of the stream by creating a collection. |
count | long | Returns the count of elements in the stream. |
6.7 Samples
6.7.1 get first n values from filtered ones and collect them to a list
Stream<String> stream = Stream.of("abc", "defg", "hi", "jkl", "mnopr", "st");
List<String> list = stream //Stream<String>
.filter(s -> s.length() < 4) //Stream<String>
.limit(2) //Stream<String>
.collect(Collectors.toList()); //List<String>
6.7.2 print distinct values of a stream
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 3, 5, 6, 7, 6);
numbers.stream() //Stream<Integer>
.distinct() //Stream<Integer>
.forEach(t -> System.out.println(t));
6.7.3 skip first n values of filtered ones
Stream<String> stream = Stream.of("abc", "defg", "hi", "jkl", "mnopr", "st");
List<String> list = stream //Stream<String>
.filter(s -> s.length() < 4) //Stream<String>
.skip(2) //Stream<String>
.collect(Collectors.toList()); //List<String>
6.7.4 Collections max / min
Deque<String> stack = new ArrayDeque<>();
stack.push("1");
stack.push("2");
stack.push("3");
System.out.println("max in the stack: " +
Collections.max(stack, (s, t) -> Integer.compare(Integer.valueOf(s), Integer.valueOf(t))));
System.out.println("min with max method via using different Comparator: " +
Collections.max(stack, (s, t) -> Integer.compare(1/Integer.valueOf(s), 1/Integer.valueOf(t))));
6.7.5 Usages of peek to log
List<String> list = Arrays.asList(new String[]{"1", "2", "3", "4", "4"});
Consumer<String> logger = t -> System.out.print("log: " + t);
list.stream()
.filter(t -> !t.equals("1"))
.peek(logger)
.map(t -> Integer.parseInt(t) * 3)
.peek(rs -> System.out.println(" multiplied by 3: " + rs))
.count();
6.7.6 count, max, sum, average
List<String> list = Arrays.asList(new String[]{"1", "2", "3", "4", "4"});
//count
long count = list.stream()
.filter(t -> !t.equals("1"))
.count();
//max
String max = list.stream()
.filter(t -> !t.equals("1"))
.max(Comparator.comparing(t -> Integer.parseInt(t))).get();
//max2
int maxInt = list.stream()
.filter(t -> !t.equals("1"))
.mapToInt(t -> Integer.valueOf(t))
.max().getAsInt();
//sum
double sum = list.stream()
.mapToDouble(t -> Double.parseDouble(t))
.sum();
//average
double average = list.stream()
.mapToDouble(t -> Double.parseDouble(t))
.average().getAsDouble();
6.7.7 sort
List<String> list = Arrays.asList(new String[]{"1", "7", "0", "4", "4"});
List<String> sortedList = list.stream()
.sorted(Comparator.comparing(t -> Integer.parseInt(t)))
.collect(Collectors.toList());
List<Integer> numbers = new ArrayList<>(Arrays.asList(4, 2, 5, 8, 12, 3, 6, 9, 7));
numbers.stream()
.sorted((a, b) -> Integer.compare(a, b))
.sorted(Comparator.reverseOrder())
.forEach(p -> System.out.println(p));
6.7.8 concatenate
List<String> list = Arrays.asList(new String[]{"1", "7", "0", "4", "4"});
String str = list.stream()
.collect(Collectors.joining(", "));
6.7.9 group by
List<String> list = Arrays.asList(new String[]{"1", "7", "0", "4", "4"});
//group by hashCode to value
Map<Integer, List<String>> map = list.stream()
.collect(Collectors.groupingBy(t -> t.hashCode()));
map.forEach((k,v) -> System.out.println(k + ": " + v));
//group by hashCode to length
Map<Integer, Long> summingMap = list.stream()
.collect(Collectors.groupingBy(t -> t.hashCode(), Collectors.summingLong(t -> t.length())));
summingMap.forEach((k,v) -> System.out.println(k + ": " + v));
6.7.10 collect, groupingBy, averagingLong, summingInt, maxBy, comparingLong, joining
Map<Integer, List<String>> defaultGrouping = names.stream().collect(groupingBy(t -> t.length()));
Map<Integer, Set<String>> mappingSet = names.stream().collect(groupingBy(t -> t.length(), toSet()));
mappingSet = names.stream().collect(groupingBy(t -> t.length(), mapping(t -> t, toSet())));
//grouping by multiple fields
Map<Integer, Map<Integer, Set<String>>> multipleFieldsMap = names.stream().collect(groupingBy(t -> t.length(), groupingBy(t -> t.hashCode(), toSet())));
//average w.r.t lengths
Map<Integer, Double> averagesOfHashes = names.stream().collect(groupingBy(t -> t.length(), averagingLong(t -> t.hashCode())));
//sum w.r.t lengths
Map<Integer, Long> sumOfHashes = names.stream().collect(groupingBy(t -> t.length(), summingLong(t -> t.hashCode())));
//max or min hashCode from group
Map<Integer, Optional<String>> maxNames = names.stream().collect(groupingBy(t -> t.length(), maxBy(comparingLong(t -> t.hashCode()))));
Map<Integer, String> joinedMap = names.stream().collect(groupingBy(t -> t.length(), mapping(t -> t, joining(", ", "Joins To Lengths[", "]"))));
joinedMap.forEach((k,v) -> System.out.println(k + ": " + v));
6.7.11 Parallel streams
List<Salary> salaries = Arrays.asList(new Salary(12.2D, 2), new Salary(20.0D, 4), new Salary(30.5D, 2));
Double sum = salaries.parallelStream()
.filter(p -> p.getSalary() > 0)
.mapToDouble(p -> p.getSalary())
.sum();
System.out.println("sum of salaries: " + sum);
6.7.12 Reduction
/*
* Note that the integer value of 0 is passed into the reduce method. This is called the identity value.
* It represents the starting value for the reduce function and the default return value if there are no members in the reduction.
*/
int result = IntStream.rangeClosed(1, 5).parallel()
.reduce(0, (sum, element) -> sum + element);
System.out.println("sum of [1, 5]: " + result);
6.7.13 Split
List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
int chunkSize = 3;
AtomicInteger counter = new AtomicInteger();
Collection<List<Integer>> result = numbers.stream()
.collect(Collectors.groupingBy(it -> counter.getAndIncrement() / chunkSize))
.values();
result.forEach(t -> System.out.println(t));
6.8 Stream API Enhancements - Java 9
6.8.1 Stream.iterate
Stream.iterate in Java 8 creates an infinite stream.
Stream.iterate(initial value, next value)
Stream.iterate(0, n -> n + 1)
.limit(10)
.forEach(x -> System.out.println(x));
JDK 9 overloads iterate with three parameters that replicate the standard for loop syntax as a stream.
Stream.iterate(initial value, stopper predicate, next value)
Stream.iterate(1, n -> n < 20 , n -> n * 2)
.forEach(x -> System.out.println(x))
;
6.8.2 takeWhile
Stream.iterate("", s -> s + "t")
.takeWhile(s -> s.length() < 10)
.reduce((first, second) -> second) //find last
.ifPresent(s -> System.out.println(s));
;
6.8.3 dropWhile
dropWhile removes elements while the given predicate returns true.
System.out.print("when ordered:");
Stream.of(1,2,3,4,5,6,7,8,9,10)
.dropWhile(x -> x < 4)
.forEach(a -> System.out.print(" " + a));
System.out.print("when unordered:");
Stream.of(1,2,4,5,3,7,8,9,10)
.dropWhile(x -> x < 4)
.forEach(a -> System.out.print(" " + a));
6.8.4 Extracting null values - ofNullable
Extracting null values in Java 8:
Stream.of("1", "2", null, "4")
.flatMap(s -> s != null ? Stream.of(s) : Stream.empty())
.forEach(s -> System.out.print(s));
Extracting null values in Java 9 - ofNullable:
Stream.of("1", "2", null, "4")
.flatMap(s -> Stream.ofNullable(s))
.forEach(s -> System.out.print(s));
Yorumlar
Yorum Gönder