Java 8 finally brings lambda syntax to Java.
Lambdas don't do anything that you couldn't have also done with anonymous nested classes, but lambdas do it...
Many lambda-friendly utilities have entered the core libraries along with them.
This session will focus on the difference between the "old" and "new" way of doing the same thing.
Java 8 adds the ability to declare a default implementation for an interface method.
This lets you add new methods to interfaces without breaking downstream implementations, as long as you can define their behavior in terms of existing methods.
public interface Doi {
/**
* @return a DOI name such as "10.1371/journal.pone.0000000"
*/
String getDoiName();
}
public interface Doi {
/**
* @return a DOI name such as "10.1371/journal.pone.0000000"
*/
String getDoiName();
default URI getAsUri() {
return URI.create("doi:" + getDoiName());
}
}
Interfaces with default
methods are similar to abstract classes. The differences are that
interfaces:
final
This looks spookily like multiple inheritance. Because interfaces inherit only multiple behaviors, not multiple states, it avoids the nastiest problems.
The compiler will throw an error if one class implements two interfaces, and each one provides a different
default
body for the same method signature.
Java is still a statically typed language. Every value must have a declared type.
A lambda expression's type is inferred from syntax.
The inferred type must be a functional interface.
An interface is functional if it has exactly one non-default
,
non-static
method.
(Object
methods (e.g., equals
, hashCode
, toString
) don't
count.)
@FunctionalInterface
annotation
You can use the
@FunctionalInterface
annotation to mark interfaces that meet the definition of functional.
It works like @Override
: it is never required (even if you actually use lambdas with that
interface), but the compiler will complain if you put it somewhere invalid.
You generally want to use it only on interfaces that you actually expect to get anonymous implementations. It can be misleading otherwise.
Don't put it on interfaces that you expect to add methods to one day – for example, a
*Service
class from our codebases.
Runnable
has been part of the language since 1.0, and meets the definition of functional. It didn't need the @FunctionalInterface
annotation to make it so.
package java.lang;
/**
* The <code>Runnable</code> interface should be implemented by any
* class whose instances are intended to be executed by a thread...
*/
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is
* used to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread...
* @see java.lang.Thread#run()
*/
public abstract void run();
}
Some of us have already implemented
HibernateCallback
,
a functional interface from Spring, many times.
package org.springframework.orm.hibernate3;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import java.sql.SQLException;
public interface HibernateCallback<T> {
T doInHibernate(Session session)
throws HibernateException, SQLException;
}
Java 8 provides some very flexible new ones.
@FunctionalInterface public interface Predicate<T> {
boolean test(T t);
}
@FunctionalInterface public interface Function<T, R> {
R apply(T t);
}
@FunctionalInterface public interface Consumer<T> {
void accept(T t);
}
@FunctionalInterface public interface Supplier<T> {
T get();
}
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
public static void main(String[] args) {
Consumer<String> printTrimmed = (String s) -> {
String trimmed = s.trim();
System.out.println(trimmed);
};
}
}
You can return things.
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
public static void main(String[] args) {
Predicate<Integer> isEven = (Integer n) -> {
int modulus = n % 2;
return (modulus == 0);
};
}
}
If it is a single return statement, you can leave off the braces and write only the returned expression.
@FunctionalInterface public interface Predicate<T> {
boolean test(T t);
public static void main(String[] args) {
Predicate<Integer> isEven = (Integer n) -> {
return (n % 2 == 0);
};
}
}
@FunctionalInterface public interface Predicate<T> {
boolean test(T t);
public static void main(String[] args) {
Predicate<Integer> isEven = (Integer n) -> (n % 2 == 0);
}
}
If the argument type can be inferred, you can leave it off.
@FunctionalInterface public interface Predicate<T> {
boolean test(T t);
public static void main(String[] args) {
Predicate<Integer> isEven = (Integer n) -> (n % 2 == 0);
}
}
@FunctionalInterface public interface Predicate<T> {
boolean test(T t);
public static void main(String[] args) {
Predicate<Integer> isEven = n -> (n % 2 == 0);
}
}
If it uses only a single method call, you can make it even terser with method reference syntax.
@FunctionalInterface public interface Function<T, R> {
R apply(T t);
public static void main(String[] args) {
Function<String, Integer> getLength = s -> s.length();
}
}
@FunctionalInterface public interface Function<T, R> {
R apply(T t);
public static void main(String[] args) {
Function<String, Integer> getLength = String::length;
}
}
You can even use a method reference on a particular instance.
@FunctionalInterface public interface Supplier<T> {
T get();
public static void main(String[] args) {
String theValue = "resonance cascade";
Supplier<Integer> getValueLength = () -> theValue.length();
}
}
@FunctionalInterface public interface Supplier<T> {
T get();
public static void main(String[] args) {
String theValue = "resonance cascade";
Supplier<Integer> getValueLength = theValue::length;
}
}
Lambdas can be inlined.
This is usually good style (but, as always, break things apart when they need to be clearer).
List<ArticleIdentity> getIdentities(List<Article> articles) {
Function<Article, ArticleIdentity> extractId =
article -> ArticleIdentity.create(article.getDoi());
return Lists.transform(articles, extractId);
}
List<ArticleIdentity> getIdentities(List<Article> articles) {
return Lists.transform(articles,
article -> ArticleIdentity.create(article.getDoi()));
}
Your IDE is your best friend while learning new syntax.
My process for learning all of this, when I needed to supply a lambda value to something, was:
new
..." and let IntelliJ auto-complete an old-fashioned anonymous class.
It shows you explicit types for all parameters and the return value.
Alt+Enter
and convert it to a lambda.Alt+Enter
again and convert the lambda to a method reference, if possible. List<String> getDois(List<Article> articles) {
return Lists.transform(articles,
new Function<Article, String>() {
@Override
public String apply(Article article) {
return article.getDoi();
}
});
}
List<String> getDois(List<Article> articles) {
return Lists.transform(articles, article -> article.getDoi());
}
List<String> getDois(List<Article> articles) {
return Lists.transform(articles, Article::getDoi);
}
A nice design pattern for surrounding a segment of code with guaranteed "before" and "after" steps.
public class NaiveService {
private final DataSource dataSource;
/**
* @param resourceName name of resource you want
* @return stream to resource
* (YOU MUST CLOSE THIS! TRY-FINALLY!
* SEVEN YEARS' BAD LUCK IF YOU DON'T!)
*/
public InputStream getResource(String resourceName) {
return dataSource.open(resourceName);
}
}
public class IncompetentClient {
private final NaiveService service;
public Model servePage(String pageName) {
Model model = new Model();
InputStream resource = service.getResource(pageName);
byte[] data = ByteStreams.toByteArray(resource);
model.addAttribute("data", data);
return model; // ...oops, forget something?
}
}
public class CynicalService {
private final DataSource dataSource;
public void getResource(String resourceName,
Consumer<InputStream> callback) {
try (InputStream resource = dataSource.open(resourceName)) {
callback.accept(resource);
}
}
}
public class IncompetentClient {
private final CynicalService service;
public Model servePage(String pageName) {
Model model = new Model();
service.getResource(pageName, resource -> {
byte[] data = ByteStreams.toByteArray(resource);
model.addAttribute("data", data);
});
return model;
}
}
Optional
classOptional
is a utility class that helps prevent null pointer exceptions.
Its purpose is to explicitly mark a value that, logically, may be either present or absent.
It works by convention:
we NEVER allow null
to be assigned to a variable of the Optional
type.
Optional
objects are immutable.
Optional
instancesMethod | Use it if... |
---|---|
Optional.of(x) |
x is definitely not null |
Optional.ofNullable(x) |
x may be null |
Optional.empty() |
The value is always logically absent |
Optional
instancesMethod | Use it if... |
---|---|
boolean isPresent() |
Checks whether it is present. |
T get() |
Gets value only if present. Throws an exception otherwise. |
T orElse(T other) |
Gets value, or a default. orElse(null) is common. |
Optional
has several lambda-licious utility methods that capture the most commons use patterns.
If you get comfortable with them, you'll almost never have to write if (optional.isPresent())
.
The simplest is ifPresent
, which takes the if
body as a Consumer
.
private final Optional<Issue> currentIssue;
public void addTo(Volume volume) {
if (currentIssue.isPresent()) {
volume.getIssues().add(currentIssue.get());
}
}
private final Optional<Issue> currentIssue;
public void addTo(Volume volume) {
currentIssue.ifPresent(issue -> {
volume.getIssues().add(issue);
});
}
orElse
provides a default value.
private final Issue defaultIssue;
private final Optional<Issue> currentIssue;
public void addTo(Volume volume) {
volume.getIssues().add(
currentIssue.isPresent() ? currentIssue.get() : defaultIssue);
}
private final Issue defaultIssue;
private final Optional<Issue> currentIssue;
public void addTo(Volume volume) {
volume.getIssues().add(currentIssue.orElse(defaultIssue));
}
What if getting the default value is expensive?
Use lazy evaluation with a Supplier
!
private final Optional<Issue> currentIssue;
public void addTo(Volume volume) {
Issue next = currentIssue.isPresent() ? currentIssue.get()
: fetchDefaultIssue();
volume.getIssues().add(next);
}
private final Optional<Issue> currentIssue;
public void addTo(Volume volume) {
Issue next = currentIssue.orElseGet(() -> fetchDefaultIssue());
volume.getIssues().add(next);
}
orElseThrow
takes an exception supplier. This is the best way to indicate that you
expect a value to be present. It is clearer than relying on get()
's exception.
String unpackName(Optional<String> name) {
if (name.isPresent()) {
return name.get();
} else {
throw new IllegalArgumentException("Name must be present");
}
}
String unpackName(Optional<String> name) {
return name.orElseThrow(() ->
new IllegalArgumentException("Name must be present"));
}
Use map
to apply an operation to an Optional
object's value, leaving it empty if it
is empty.
Optional<String> makeAttribution(Optional<String> name) {
return name.isPresent()
? Optional.of("By: " + name.get())
: Optional.empty();
}
Optional<String> makeAttribution(Optional<String> name) {
return name.map(nameValue -> "By: " + nameValue);
}
map
and orElse
chain nicely.
String makeAttribution(Optional<String> name) {
return name.isPresent()
? "By: " + name.get()
: "(anonymous)";
}
String makeAttribution(Optional<String> name) {
return name.map(nameValue -> "By: " + nameValue)
.orElse("(anonymous)");
}
Use filter
to apply a condition.
private final Optional<Issue> currentIssue;
Optional<Issue> getCurrentImageIssue() {
if (currentIssue.isPresent()) {
return (currentIssue.get().getImageUri() != null)
? currentIssue : Optional.empty();
} else {
return Optional.empty();
}
}
private final Optional<Issue> currentIssue;
Optional<Issue> getCurrentImageIssue() {
return currentIssue.filter(issue -> issue.getImageUri() != null);
}
Use flatMap
to capture conditional logic by mapping onto another Optional
.
private final Optional<Issue> currentIssue;
Optional<String> getCurrentImageUri() {
return currentIssue.isPresent()
? Optional.ofNullable(currentIssue.get().getImageUri())
: Optional.empty();
}
private final Optional<Issue> currentIssue;
Optional<String> getCurrentImageUri() {
return currentIssue.flatMap(image ->
Optional.ofNullable(currentIssue.get().getImageUri()));
}
private final Map<Category, Double> weights;
public double lookUpWeight(Category category) {
return weights.containsKey(category) ?
weights.get(category) : 0.0;
}
private final Map<Category, Double> weights;
public double lookUpWeight(Category category) {
return weights.getOrDefault(category, 0.0);
}
private final CategoryService categoryService;
private final Map<Category, Double> memo;
public double getWeight(Category category) {
if (memo.containsKey(category)) {
return memo.get(category);
}
double weight = categoryService.lookUpWeight(category);
memo.put(category, weight);
return weight;
}
private final CategoryService categoryService;
private final Map<Category, Double> memo;
public double getWeight(Category category) {
return memo.computeIfAbsent(category,
categoryService::lookUpWeight);
}
Comparator<ArticlePerson> personOrder =
(ArticlePerson o1, ArticlePerson o2) -> {
int cmp = o1.getSurnames().compareTo(o2.getSurnames());
if (cmp != 0) return cmp;
cmp = o1.getGivenNames().compareTo(o2.getGivenNames());
if (cmp != 0) return cmp;
return o1.getSuffix().compareTo(o2.getSuffix());
};
Comparator<ArticlePerson> personOrder =
Comparator.comparing(ArticlePerson::getSurnames)
.thenComparing(ArticlePerson::getGivenNames)
.thenComparing(ArticlePerson::getSuffix);
This is the best part.
Streams are a more sophisticated way to think about iteration.
You declare a chain of operations that will be applied to every element in the stream.
If you only do one piece of homework on this subject, check out the stream documentation.
Advantages of streaming over conventional iteration:
Some stream methods are intermediate operations that set up new streams.
Others are terminal operations that consume the stream to give you a result value.
Intermediate operations...
Collector
s
Stream.collect
is a terminal operation that gathers the stream elements, for example, into a
collection or map.
It needs to be passed a Collector
object, which provides reduction operations that will be
efficient even when parallelized.
We don't want to kill all our optimizations just so we can synchronize on a list.
void printAll(Collection<String> values) {
for (String value : values) {
System.out.println(value);
}
}
void printAll(Collection<String> values) {
values.stream().forEach(System.out::println);
}
forEach
is a terminal operation. In an intermediate operation, the side effect would be wrong.
Generally, use forEach
as little as possible. Most everything you want to do has its own terminal
operation.
List<ArticleView> createViews(Collection<Article> articles) {
List<ArticleOutputView> views = new ArrayList<>();
for (Article article : articles) {
views.add(new ArticleOutputView(article));
}
return views;
}
List<ArticleOutputView> createViews(Collection<Article> articles) {
return articles.stream()
.map(ArticleOutputView::new)
.collect(Collectors.toList());
}
List<Article> getResearchArticles(Collection<Article> articles) {
List<Article> researchArticles = new ArrayList<>();
for (Article article : articles) {
if (article.getTypes().contains("research-article")) {
researchArticles.add(article);
}
}
return researchArticles;
}
List<Article> getResearchArticles(Collection<Article> articles) {
return articles.stream()
.filter(article ->
article.getTypes().contains("research-article"))
.collect(Collectors.toList());
}
We can chain operations together.
List<ArticleView> getResearchViews(Collection<Article> articles) {
List<ArticleView> researchArticleViews = new ArrayList<>();
for (Article article : articles) {
if (article.getTypes().contains("research-article")) {
ArticleView view = new ArticleView(article);
researchArticleViews.add(view);
}
}
return researchArticleViews;
}
List<ArticleView> getResearchViews(Collection<Article> articles) {
return articles.stream()
.filter(article ->
article.getTypes().contains("research-article"))
.map(ArticleView::new)
.collect(Collectors.toList());
}
flatMap
can change the number of elements in a stream. It transforms each element into a new
mini-stream.
List<ArticleAuthor> getAllAuthors(Collection<Article> articles) {
List<ArticleAuthor> authors = new ArrayList<>();
for (Article article : articles) {
for (ArticleAuthor author : article.getAuthors()) {
authors.add(author);
}
}
return authors;
}
List<ArticleAuthor> getAllAuthors(Collection<Article> articles) {
return articles.stream()
.flatMap(article -> article.getAuthors().stream())
.collect(Collectors.toList());
}
private boolean isFamousEnough(ArticleAuthor author) {...}
List<ArticleAuthor> getFamousAuthors(Collection<Article> articles) {
Set<ArticleAuthor> famousAuthors = new HashSet<>();
for (Article article : articles) {
if (article.getTypes().contains("research-article")) {
for (ArticleAuthor author : article.getAuthors()) {
if (isFamousEnough(author)) {
famousAuthors.add(author);
}
}
}
}
List<ArticleAuthor> sorted = new ArrayList<>(famousAuthors);
sorted.sort(Comparator.comparing(ArticleAuthor::getSurnames));
return sorted;
}
private boolean isFamousEnough(ArticleAuthor author) {...}
List<ArticleAuthor> getFamousAuthors(Collection<Article> articles) {
return articles.stream()
.filter(article ->
article.getTypes().contains("research-article"))
.flatMap(article ->
article.getAuthors().stream())
.distinct() // instead of adding to a HashSet
.filter(this::isFamousEnough)
.sorted(Comparator.comparing(ArticleAuthor::getSurnames))
.collect(Collectors.toList());
}
int computeSum(Collection<Integer> numbers) {
int sum = 0;
for (Integer number : numbers) {
sum += number;
}
return sum;
}
int computeSum(Collection<Integer> numbers) {
IntStream intStream = numbers.stream().mapToInt(Integer::intValue);
return intStream.sum();
}
int computeSum(Collection<Integer> numbers) {
return numbers.stream().mapToInt(Integer::intValue).sum();
}
Define your own reductions!
int computeProduct(Collection<Integer> numbers) {
int product = 1;
for (Integer number : numbers) {
product *= number;
}
return product;
}
int computeProduct(Collection<Integer> numbers) {
return numbers.stream().mapToInt(Integer::intValue)
.reduce(1, (x, y) -> x * y);
}
Article findCorrection(Collection<Article> articles) {
for (Article article : articles) {
if (article.getTypes().contains("correction")) {
return article;
}
}
throw new IllegalArgumentException("Correction not found");
}
Article findCorrection(Collection<Article> articles) {
Optional<Article> correction = articles.stream()
.filter(article ->
article.getTypes().contains("correction"))
.findAny();
return correction.orElseThrow(() ->
new IllegalArgumentException("Correction not found"));
}
Stream
and Optional
chain together very naturally.
Article findCorrection(Collection<Article> articles) {
for (Article article : articles) {
if (article.getTypes().contains("correction")) {
return article;
}
}
throw new IllegalArgumentException("Correction not found");
}
Article findCorrection(Collection<Article> articles) {
return articles.stream()
.filter(article ->
article.getTypes().contains("correction"))
.findAny().orElseThrow(() ->
new IllegalArgumentException("Correction not found"));
}
Questions?