- 1. Requirements
- 2. Scope
- 3. Interfaces in Java 8
- 4. Optional
- 5. Lambda expressions
- 6. Stream API
- 6.1. What they are
- 6.2. How to build a Stream
- 6.3. How to turn streams into arrays and collections
- 6.4. Example - forEach
- 6.5. Common operations on streams
- 6.6. Collectors - create different outputs
- 6.7. Lazy evaluation
- 6.8. Specialized streams
- 6.9. Types of operations on Stream
- 6.10. Parallel streams
- 6.11. Infinite Streams (unbounded)
- 7. Date and Time API (java.time)
- 8. Other things
- 9. Other resources
Wymagania: IDE (Intellij Idea, Eclipse, etc.) z JDK 8 i git, żeby można było komitowac (opcjonalnie). Jeśli Intellij to wystarczy Community Edition.
- Changes in interfaces (default and static methods)
- Optional
- Lambda Expressions
- Streams
- Date Time API
- Other things (Map, Base64, JavaFx, G1, etc.)
Java 8 heads towards functional programming, where things are immutable, and therefore new classes are mostly immutable. You write code with similar mindset.
- single inheritance of implementations
- multiple inheritance of types (interfaces)
- interfaces cannot provide implementations
Provide real implementations and can be overridden. So interfaces became more like abstract classes.
If a class implements two interfaces with the same name of default method, then it must be overridden or it won't compile.
Cannot be overridden.
It has: abstract methods, default methods, and static methods.
@FunctionalInterface
public interface Function<T, R> {
R apply(T t); // abstract method
default <V> Function<V, R> compose(Function<V, T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<R, V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
@FunctionalInterface tells the compiler to check if the interface has only single abstract method. If not then it won't compile it.
Interfaces with SAM, but without the annotation are also functional merely won't have compile type check - when you add another abstract method to such interface it will compile, but classes that implement them won't.
Functional interface, like any other interface, can have default and static methods. Also, when two interfaces provide the same default method, then implementing class must override the method or it won't compile.
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t); //SAM
default Predicate<T> and(Predicate<? super T> other) {
//...
}
default Predicate<T> negate() {
//...
}
default Predicate<T> or(Predicate<? super T> other) {
//...
}
static <T> Predicate<T> isEqual(Object targetRef) {
//...
}
}
-
Example
Predicate<Player> isGoodPlayer = p -> p.getPoints() >= 20; if (isGoodPlayer.test(player)) { // ... } // for (Player p : players) { if (isGoodPlayer.test(p)) return player; } players.stream().filter(isGoodPlayer).map(Player::getName).collect(toList());
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
default <V> Function<V, R> compose(Function<V, T> before) {
//...
}
default <V> Function<T, V> andThen(Function<R, V> after) {
//...
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
BiFunction<T,T,R> takes two T and returns R
Function<Player,Stat> takeStats = p -> new Stat(p);
@FunctionalInterface
public interface Supplier<T> {
T get();
}
Supplier<Player> recruiter = Player::new;
Supplier<Player> recruiter = () -> Player("Michael Jordan", 23);
team.add(recruiter.get());
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after) {
//...
}
}
Consumer<Player> incScore = p -> p.setScore(p.getScore()+2);
for (Player player : player) {
incScore.accept(player);
}
-
XUnaryOperator (X: double/int/long): same type out
-
XBinaryOperator (X: double/int/long): two X in, same type out
-
XPredicate: double/long/int in, boolean out
-
XConsumer: double/long/int in, nothing out
- set/list.forEach(Consumer)
- set/list.removeIf(Predicate)
- replaceAll(Function)
- compute(Key, BiFunction)
- computeIfAbsent(key,Function)
- computeIfPresent(Key,BiFunction)
- forEach(BiConsumer)
- getOrDefault
- merge(key, value, BiFunction)
- putIfAbsent(key, value)
- remove(key, value)
- replace(key, value)
- replace(key, oldValue, newValue)
- replaceAll(BiFunction)
Result of computation that may not exists and what to do about it. For example finding max/min element in empty collection. Or sum of empty array/list/set of numbers.
- Optional.ofNullable(T) How to use Optional to handle potentially nullable values.
- Optional.map(Function) How to convert value conditionally using Optional.
// long and less readable
if (Optional.ofNullable(x).isPresent()) {...}
// old school, but short and clear:
if (x != null) {
...
}
They have benefits of Inner/Anonymous Classes, namely:
- they have access to members of class instantiating them
- don't require create a new class (can be defined inline)
- can be assigned to fields
- can be passed as parameters.
To provide concise and expressive syntax.
button.addActionListener(
new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
handleButton(e);
}
}
);
button.addActionListener(e -> handleButton(e));
They require an interface with Single Abstract Method (SAM).
A functional interface is an interface that contains exactly one abstract method. Additionally it may contain default methods and/or static methods. Because a functional interface contains exactly one abstract method, you can omit the name of that method when you implement it using a Lambda Expression.
Consider the following interface:
interface Tester<T> {
boolean test(T t);
}
Sample class for processing:
public class Player {
public int points;
}
Both can be used like this:
public void printPlayer(List<Player> players, Tester<Data> tester) {
for (Player p: players) {
if (tester.test(p)) {
System.out.println(p);
}
}
}
In Java 7 the Tester could be implemented like this:
printPlayer(al, new Tester<Player>() {
public boolean test(Player player) {
return player.points > 20;
}
});
In Java 8 the call to above method can be simplified as follows:
printPlayer(al, (Player p) -> { return p.points > 20; } );
Notice that method name can be omitted here, because the interface has only one abstract method, therefore the compiler can figure out the name.
It's the same as above:
printPlayer(players, (Player player) -> player.points > 20);
Curly brackets and the return keyword also are not needed, because both the method and the expression player.points > 1 also return a boolean. So, the compiler is able to figure out that the value of this expression should be returned from the method.
This can be shortened even more to:
printPlayer(players, p -> p.points > 20);
The declaration of p is gone! The compiler can figure out all this, because the interface has only one abstract method and that method also has only one parameter. So you don't have to write all those things in your code.
JDK 8 ships with a bunch of general interfaces that can be used in many situations. The interfaces are defined in java.util.function package.
Lambda Expressions don't introduce a new scope. Variables in LE have the same scope as their outside object. So, "this" refers to the outside object.
Anonymous Java classes can access variables from method parameters or method local only if they are declared final. In Lambda Expressions it is the same case, but they also can access variables that are "effectively" final. Variables that never change once assigned.
Method Reference | Example | Equivalent lambda |
---|---|---|
AClass::staticMethod | Math::min | (a,b) -> Math.min(a,b) |
var::instanceMethod | list::add | (t) -> list.add(t) |
AClass::instanceMethod | List::add | (l,e) -> l.add(e) |
AClass::new | ArrayList::new | () -> new ArrayList() |
List<Integer> result = new ArrayList<>();
source.forEach(by2::add);
// -> on each source integer: by2.add(s)
AClass::instancMethod version produces lambda that takes one more argument that the method expects. The first argument is the object on which the method is called; the rest are the parameters to the method:
List<String> result = process(names, String::toUpperCase);
// -> on each string: s.toUpperCase()
This way or another must match SAM.
AClass::new
It is inferred from the context and depends on to what it is assigned to. When not assigned it has no type and won't compile!
Function<Double, Double> round = Math::rint;
Fun<D,D> a = new Function<D,D> { D apply(D x) { return Math::rint;}}
round.compose(Math::sqrt).apply(64d, 2d); // ok
// Math::rint is not assigned, so has no type:
Math::rint.compose(Math::sqrt); // compile error!
Predicate<Player> isStrong = p -> p.getStrength() >= 100;
Predicate<Player> hasStamina = p -> p.getStamina() > 0;
allMatches(players, isStrong.and(hasStamina));
public static Predicate<Player> buildIsSP(int minStrength) {
return p -> p.getStrength() >= minStrength; // returns Lambda
}
public static Function<Integer, Predicate<Player>> createIsStrongPredicate
// returns Function that will produce the Predicate:
= minStrength -> (p -> p.getStrength() >= minStrength);
Map.merge
- loop that finds matching element
- implement map (transformation of elements)
- implement reduce (using BinaryOperator)
Wrappers around data sources (arrays, lists, etc.) that use lambda expressions.
They are pipelines of operations on data sources (collections). They don't have own data (don't copy it from source collection).
Streams are pipelines of operations that consume the data so they cannot be reused!
You can build a Stream from:
- individual elements: Stream.of(x, y, …);
- array: Stream.of(numsArray);
- a collection: list.stream()
- a function: Stream.generate(Supplier), Stream.iterate(val, F)
- a StreamBuilder: someBuilder.builder()
- a String: "aoeu".chars(), Stream.of(String.split(…))
- other Streams: distinct, filter, limit, map, sorted, substream
-
see specialized streams
-
1-item stream instead of an array
int[] nums = {1, 2, 3}; Stream.of(nums); // Stream with 1 element, which is an array Integer[] numbers = {1, 2, 3}; Stream.of(numbers); // 3-element stream of Integers
Back into array:
Player[] array = players.stream().toArray(Player[]::new);
Back into a collection:
// import java.util.stream.collectors.Collectors;
List<Player> processed = players.stream().collect(Collectors.toList());
Traditional for loop process sequentially:
for (Flight flight : flights) {
flight.setCarrier("XX");
}
Java 8 forEach can be made concurrent automatically by using parallelStream:
flights.parallelStream().forEach(f -> f.setCarrier("XX"));
With forEach you cannot:
- break out of loop using "break" nor "return"
- modify local variables outside loop (remember Effective Final?)
reduce(BiF, 0, [1, 2, 3]) -> BiF(0, 1) = 1 -> BiF(1, 2) = 3 -> BiF(3, 3) = 6
int[] nums = {1, 2, 3};
boolean allEven = Arrays.stream(nums).allMatch(isEven);
int[] nums = {1, 2, 3};
int size = Arrays.stream(nums).count();
static <T,R> Stream<R> flatMap(Function<T,Stream<R>> streaming);
-
Find even and odd numbers in an array (use Predicates)
Integer[] numbers = {1, 2, 3, 4, 5}; // Predicate isEven = n -> n % 2 == 0; // Predicate isOdd = Predicate.negate(isEven); // Stream.of(numbers).filter(isEven).collect(toList()); List<Integer> even = ...; List<Integer> odd = ...;
-
Count distinct chars in names of players using flatMap
toCollection(TreeSet::new) -> new Supplier { Collection get() { new TreeSet(); }}
-
partitioningBy(Predicate) -> Map<Boolean,T>
-
groupingBy(Function<T,R> keyFunction) -> Map<Pos,List>
// group students by name: Map<String, List<Student>> roster = students.stream() .collect(groupingBy(Student::getName));
-
Composing collectors
Grouping collector can use another (downstream) collector that will have possibility to post-process values. The result will the same keys, but values from the "post-processor".
Map<String, Integer> roster = students.stream() .collect( // group by name: groupingBy(Student::getName, // count occurrences Collectors.counting()));
-
Rewrite finding even/odd numbers, but with help of Collectors
-
Find best scorers on each position
It means that operations on a stream are deferred (postponed) until the system notices that they are really needed. Only then they are evaluated.
List<Integer> numbers = asList(null, 2, 3, 4, 5);
Integer result = numbers.stream()
.filter(Objects::nonNull)
.map(n -> n + 1)
.filter(n -> n > 2)
.filter(4 -> n % 2 == 0)
.anyMatch()
.orElse(null);
IntStream, LongStream, DoubleStream - specialized streams with restrictions on operations (e.g. map must produce int/long/double), but with some common methods for those types - sum, avg, min, max, etc.
int[] nums = { 1, 2, 3 };
IntStream intStream = Arrays.stream(nums);
Integer[] numbers = { 1, 2, 3 };
Stream<Integer> stream = Arrays.stream(numbers); // or Stream.of(numbers)
Intermediate (non-terminal) operations produce Streams, are added to processing pipeline, but are not executed immediately - only after Short-Circuit operation is found.
When executed, they operate on one element at a time. So pipeline of intermediate operations is executed on the first element of the stream, then on the second, and so on.
Terminal operations consume the Stream and no more operations can executed on it. Examples: forEach(Consumer), findFirst(), allMatch(Predicate).
They cause evaluation of intermediate operations that appear earlier in the pipeline. Both intermediate and terminal operations can be short-circuit:
- Intermediate: limit, substream
- Terminal: forEach, findFirst
Marking a stream as parallel automatically allows it to be processed concurrently, without implementing Runnables nor using ExecutorService. It doesn't mean that the operations will be run parallel, but may be.
By default it uses the common ForkJoinPool.
Use for slow operations.
Consumer<Player> slowConsumer = p -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
} finally {
System.out.println(p);
}
};
List<Players> players;
players.stream().forEach(slowConsumer);
players.stream().parallel().forEach(slowConsumer);
They are not really infinite streams, but streams that don't have fixed size, where values are calculated when are needed.
Good with stateful suppliers.
"seed" - the first value of the stream The operator takes the first element and returns second. Repeatedly.
-
Write a stream that will produce Fibonacci numbers
Return nth Fibonacci number.
They are immutable, thread-safe and cacheable.
Before Java 8 there was java.util.Date and java.util.Calendar.
Date now = new Date();
Calendar cal = Calendar.getInstance(); // now
cal.set(2017, 1, 18); // 18th of February
Date date = cal.getTime();
It is a point on a time line with nanosecond precision.
Instant is immutable as many other classes in java.time.
Instant.MIN; // bilion years ago
Instant 0 // beginning of the Epoch
Instant.now(); // it's now!
Instant.MAX; // in bilion years
Represents objects without time - e.g. event from the past.
LocalDate today = LocalDate.now();
LocalDate battle = LocalDate.of(1410, Month.July, 15);
Represents a time of day and comes with a bunch of methods to manipulate time.
LocalTime now = LocalTime.now();
LocalTime workshopStarts = LocalTime.of(9, 30); // 9:30 AM
LocalTime night = LocalTime.of(23, 30).plusHours(2); // 1:30 AM
DateTime with zone information - date and time expressed for selected time zone.
ZoneDateTime.of(y, m, d, h, m, s, ZoneId);
It's the duration between two Instant objects.
Instant beginning = Instant.EPOCH;
Instant end = Instant.now();
Duration length = Duration.between(beginning, end);
length.toMillis();
Amount of time between two LocalDates. Similar to Duration, but with date granularity whereas Duration has nanosecond.
public static int daysAgo(LocalDate pastDate) {
return Period.between(LocalDate.now(), pastDate).getDays();
}
Gotcha: period.get(ChronoUnit) returns parts of period split according to used ChonoUnit.
They allow to add/substract an amount of time to Instant/LocalDate.
It has a bunch of static methods that create immutable instances of TemporalAdjuster:
- finding the first or last day of the month
- finding the first day of next month
- finding the first or last day of the year
- finding the first day of next year
- finding the first or last day-of-week within a month, such as "first Wednesday in June"
- finding the next or previous day-of-week, such as "next Thursday"
Usage:
Temporal t = adjuster.adjustInto(t);
Temporal t = t.with(adjuster);
java.time.format.DateTimeFormatter is for formatting java.time dates. It ships a bunch of predefined formatters.
-
Format date using new DateFormatter and given locale
package java8.time; import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.Locale; import static java.time.format.FormatStyle.LONG; public class DateUtil { public static String reformatDate(Instant time, Locale locale) { DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(LONG, LONG) .withLocale(locale) .withZone(ZoneId.systemDefault()); return formatter.format(time); } }
Date date = new Date();
Instant instant = date.toInstant();
LocalDate localDate = date.toLacalDate();
date = Date.from(instant);
date = Date.from(localDate);
Time time = Time.from(localTime);
LocalTime localTime = time.toLocalTime();
For testing - SystemClock and FixedClock.
For example for date: 2017-03-31 (Friday) Last year date: 2016-04-01 (Friday)
public static LocalDate lastYearSameDay(LocalDate currentDate) {
return currentDate
.minusYears(1)
.with(TemporalAdjusters.nextOrSame(currentDate.getDayOfWeek()));
}
@Test
public void lastYearLogicalDate() {
//Current date: 2017-03-31 (Friday)
//Last year date: 2016-04-01 (Friday)
LocalDate date = LocalDate.of(2017, Month.MARCH, 31);
LocalDate expected = LocalDate.of(2016, Month.APRIL, 1);
assertEquals(expected, DateUtil.lastYearSameDay(date));
}
@Repeatable(Filterables.class)
@interface Filterable {
int value();
}
// wrapping annotation
@interface Filterables {
Filterable[] value();
}
- declarations
- with parameters
- with return values
- with generic parameters
- 4clojure: a lot of functional exercises taking you from zero to hero