diff --git a/.gitignore b/.gitignore
index fc2a2f9..7fe7beb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
*iml
*ipr
*iws
+*eml
# Eclipse
.classpath
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..f331bf5
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,5 @@
+language: java
+jdk:
+ - openjdk8
+script: "mvn clean test"
+
diff --git a/README.md b/README.md
index d59ff77..f0ec9af 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,12 @@
-## Lambda Tutorial
+## Lambda Tutorial [](https://travis-ci.org/AdoptOpenJDK/lambda-tutorial)
-A set of exercises to teach use of Java 8 lambda syntax, and the new Streams API.
+A set of exercises to teach use of Java 8 lambda syntax, and the Streams API.
To follow the exercises:
- fork and clone the repository
- - ensure you have a correctly configured, [lambda-enabled Java build](#getting-lambda-jdk)
- - Maven can help generate configuration for your favourite IDE, though you will likely have to set the JDK manually
+ - ensure you have a correctly configured, JDK8+ build
+ - Maven can help generate configuration for your favourite IDE
- ensure your cloned project, particularly the class `ConfigureYourLambdaBuildOfJdk` compiles and runs correctly
- navigate to the first exercise, `Exercise_1_Test` (tests are in `src/test/java`, in the package `org.adoptopenjdk.lambda.tutorial`)
- read background information in the JavaDoc, and follow instructions, making the test pass
@@ -18,19 +18,5 @@ To follow the exercises:
1. Internal vs External Iteration (the forEach method)
2. Filtering and Collecting
3. Mapping
-
-[More to come]
-
-
-### Getting Lambda JDK
-Early access builds of JDK 8 are available [here](https://jdk8.java.net/lambda/).
-
-
-#### Lamba JDK Build Compatibility
-The current tutorial is known to work with the following JDK build:
-
-|JDK Build Number|Released On |
-|:---------------|:---------- |
-|b88 |May 09, 2013|
-
-lambda-tutorial will try to track against the newest version available. If you find that you are working with a newer version of the Lambda JDK and the tutorial does not compile or run, please file an issue.
+ 4. Method References
+ 5. Default methods on interfaces
diff --git a/pom.xml b/pom.xml
index e13fb1f..046741e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -64,7 +64,7 @@
UTF-8
- 1.8
+ 1.7
@@ -87,6 +87,12 @@
1.3
test
+
+ org.ow2.asm
+ asm-debug-all
+ 5.0_ALPHA
+ test
+
@@ -103,8 +109,13 @@
org.hamcrest
hamcrest-library
+
+ org.ow2.asm
+ asm-debug-all
+
-
+
+
diff --git a/src/main/java/org/adoptopenjdk/lambda/setupcheck/ConfigureYourLambdaBuildOfJdk.java b/src/main/java/org/adoptopenjdk/lambda/setupcheck/ConfigureYourLambdaBuildOfJdk.java
index 8c87c4a..ea3b5cb 100644
--- a/src/main/java/org/adoptopenjdk/lambda/setupcheck/ConfigureYourLambdaBuildOfJdk.java
+++ b/src/main/java/org/adoptopenjdk/lambda/setupcheck/ConfigureYourLambdaBuildOfJdk.java
@@ -39,18 +39,18 @@ public class ConfigureYourLambdaBuildOfJdk {
public static void main(String... args) {
List messages = asList(
"If this source file does not compile, you have not configured your development setup correctly.",
- "It uses both a new JDK 8 syntax (method references with '::') and a new JDK 8 library method (Iterable#forEach)",
+ "It uses both a JDK 8+ syntax (method references with '::') and a JDK 8+ library method (Iterable#forEach)",
"You should also be able to execute this main method, and see this message printed to the console.",
"",
"To configure your development environment, you need:",
- " - a lambda build of JDK 8, available at: http://jdk8.java.net/lambda/",
- " - a lambda-aware IDE.",
- " IntelliJ and NetBeans support lambdas in early access versions, available at: http://openjdk.java.net/projects/lambda/ \n" +
- " Eclipse support is more sketchy, the method described here just about works: http://tuhrig.de/?p=921",
- " Maven will compile your code and run your tests, just, add JDK 8 javac and java executables to your system path and use 'mvn test'",
+ " - an install of JDK 8 or higher",
+ " - an IDE that supports JDK8+. All mainstream IDEs (Eclipse/IntelliJ IDEA/NetBeans) support JDK8+ ",
+ " - and/or Maven, to compile your code and run your tests, using 'mvn test'",
"",
"Until this source file compiles, you will be unable to make progress in the tutorial.");
- messages.forEach(System.out::println);
+ // This is not expected to compile with JDK 7.
+ // (for pre-JDK8 solutions examples only)
+ // messages.forEach(System.out::println);
}
}
diff --git a/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise1/Shapes.java b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise1/Shapes.java
index ba02533..929ba0f 100644
--- a/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise1/Shapes.java
+++ b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise1/Shapes.java
@@ -48,7 +48,9 @@ public class Shapes {
* @see Shape#setColor(Color)
*/
public static void colorAll(List shapes, Color newColor) {
- // [your code here]
+ for (Shape s: shapes) {
+ s.setColor(newColor);
+ }
}
/**
@@ -68,7 +70,9 @@ public static void colorAll(List shapes, Color newColor) {
* @see Shape#toString()
*/
public static void makeStringOfAllColors(List shapes, StringBuilder stringBuilder) {
- // [your code here]
+ for (Shape s: shapes) {
+ stringBuilder.append(s);
+ }
}
/**
@@ -93,6 +97,9 @@ public static void makeStringOfAllColors(List shapes, StringBuilder strin
* @see Shape#toString()
*/
public static void changeColorAndMakeStringOfOldColors(List shapes, Color newColor, StringBuilder stringBuilder) {
- // [your code here]
+ for (Shape s: shapes) {
+ stringBuilder.append(s.toString());
+ s.setColor(newColor);
+ }
}
}
diff --git a/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise2/ElectoralDistrict.java b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise2/ElectoralDistrict.java
index 1c59a59..769b08c 100644
--- a/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise2/ElectoralDistrict.java
+++ b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise2/ElectoralDistrict.java
@@ -24,6 +24,7 @@
import java.util.Collection;
import java.util.Collections;
+import java.util.HashSet;
import java.util.Set;
/**
@@ -54,9 +55,14 @@ public enum ElectoralDistrict {
* @return filtered set of registered voters in a district
*/
public static Set votersIn(ElectoralDistrict district, Collection voters) {
- // [your code here]
+ Set votersInDistrict = new HashSet<>();
+ for (RegisteredVoter v: voters) {
+ if (v.getElectorId().startsWith(district.prefix)) {
+ votersInDistrict.add(v);
+ }
+ }
- return Collections.emptySet();
+ return Collections.unmodifiableSet(votersInDistrict);
}
/**
@@ -66,9 +72,14 @@ public static Set votersIn(ElectoralDistrict district, Collecti
* @return filtered set of unspoiled ballots
*/
public static Set unspoiledBallots(Set votes) {
- // [your code here]
+ Set unspoiledBallots = new HashSet<>();
+ for (Ballot v: votes) {
+ if (!v.isSpoiled()) {
+ unspoiledBallots.add(v);
+ }
+ }
- return Collections.emptySet();
+ return unspoiledBallots;
}
public String getPrefix() {
diff --git a/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise2/VotingRules.java b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise2/VotingRules.java
index b353887..2b8a2ae 100644
--- a/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise2/VotingRules.java
+++ b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise2/VotingRules.java
@@ -23,10 +23,7 @@
*/
import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
import java.util.List;
-import java.util.stream.Collectors;
/**
* Class representing voting rules in an election
@@ -41,8 +38,12 @@ public class VotingRules {
* @return a list of eligible voters
*/
public static List eligibleVoters(List potentialVoters, int legalAgeOfVoting) {
- // [your code here]
-
- return Collections.emptyList();
+ List eligible = new ArrayList<>();
+ for (Person p: potentialVoters) {
+ if (p.getAge() >= legalAgeOfVoting) {
+ eligible.add(p);
+ }
+ }
+ return eligible;
}
}
diff --git a/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise3/Books.java b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise3/Books.java
index 2e2d661..4a83ba6 100644
--- a/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise3/Books.java
+++ b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise3/Books.java
@@ -1,5 +1,10 @@
package org.adoptopenjdk.lambda.tutorial.exercise3;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
/*
* #%L
* lambda-tutorial
@@ -22,17 +27,11 @@
* #L%
*/
-import java.util.Collections;
-import java.util.List;
-import java.util.Set;
-
-import java.util.stream.Collectors;
-
/**
* Domain object representing a collection of books
*/
public class Books {
-
+
/**
* Apply a mapping of Books to titles (Strings)
*
@@ -40,9 +39,12 @@ public class Books {
* @return list of book titles
*/
public static List titlesOf(List books) {
- // [your code here]
-
- return Collections.emptyList();
+ List titles = new ArrayList<>();
+ for (Book b: books) {
+ titles.add(b.getTitle());
+ }
+
+ return titles;
}
/**
@@ -52,9 +54,12 @@ public static List titlesOf(List books) {
* @return list of author full names
*/
public static List namesOfAuthorsOf(List books) {
- // [your code here]
-
- return Collections.emptyList();
+ List fullNames = new ArrayList<>();
+ for (Book b: books) {
+ fullNames.add(b.getAuthor().fullName());
+ }
+
+ return fullNames;
}
/**
@@ -64,8 +69,11 @@ public static List namesOfAuthorsOf(List books) {
* @return set of publishers
*/
public static Set publishersRepresentedBy(List books) {
- // [your code here]
+ Set publishers = new HashSet<>();
+ for (Book b: books) {
+ publishers.add(b.getPublisher());
+ }
- return Collections.emptySet();
+ return publishers;
}
}
diff --git a/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise4/Document.java b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise4/Document.java
new file mode 100644
index 0000000..82c8986
--- /dev/null
+++ b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise4/Document.java
@@ -0,0 +1,77 @@
+package org.adoptopenjdk.lambda.tutorial.exercise4;
+
+/*
+ * #%L
+ * lambda-tutorial
+ * %%
+ * Copyright (C) 2013 Adopt OpenJDK
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static java.lang.String.format;
+
+public final class Document {
+ private final String title;
+ private final List pages;
+
+ public Document(String title, List pages) {
+ this.title = title;
+ this.pages = Collections.unmodifiableList(new ArrayList<>(pages));
+ }
+
+ public List getPages() {
+ return this.pages;
+ }
+
+ public String getTitle() {
+ return this.title;
+ }
+
+ private Page appendFooter(Page original) {
+ String footer = "Document: " + getTitle();
+ return new Page(format("%s%n%s", original.getContent(), footer));
+ }
+
+ private Document copyWithPages(List newPages) {
+ return new Document(title, newPages);
+ }
+
+ public Document copyWithFooter() {
+ // No equivalent in pre-Java 8
+ List pagesWithFooter = new ArrayList<>();
+ for (Page page: getPages()) {
+ pagesWithFooter.add(appendFooter(page)); // this::appendFooter
+ }
+ return copyWithPages(pagesWithFooter); // this::copyWithPages
+ }
+
+ public static final class Page {
+ private final String content;
+
+ public Page(String content) {
+ this.content = content;
+ }
+
+ public String getContent() {
+ return this.content;
+ }
+ }
+}
diff --git a/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise4/Documents.java b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise4/Documents.java
new file mode 100644
index 0000000..b4fa38a
--- /dev/null
+++ b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise4/Documents.java
@@ -0,0 +1,86 @@
+package org.adoptopenjdk.lambda.tutorial.exercise4;
+
+/*
+ * #%L
+ * lambda-tutorial
+ * %%
+ * Copyright (C) 2013 Adopt OpenJDK
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+
+import org.adoptopenjdk.lambda.tutorial.exercise4.Document.Page;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static java.lang.String.format;
+
+public class Documents {
+
+ /**
+ * Return the titles from a list of documents.
+ */
+ public static List titlesOf(Document... documents) {
+ // No equivalent in pre-Java 8
+ List titles = new ArrayList<>();
+ for (Document doc: documents) {
+ titles.add(doc.getTitle()); // Document::getTitle
+ }
+ return titles;
+ }
+
+ public static Integer characterCount(Page page) {
+ return page.getContent().length();
+ }
+
+ public static List pageCharacterCounts(Document document) {
+ // No equivalent in pre-Java 8
+ List characterCounts = new ArrayList<>();
+ for (Page page: document.getPages()) {
+ characterCounts.add(Documents.characterCount(page)); // Documents::characterCount
+ }
+ return characterCounts;
+ }
+
+ public static String print(Document document, PagePrinter pagePrinter) {
+ // No equivalent in pre-Java 8
+ StringBuilder output = new StringBuilder();
+
+ output.append(pagePrinter.printTitlePage(document));
+
+ for (Page page: document.getPages()) {
+ String content = pagePrinter.printPage(page); // PagePrinter::printPage
+ output.append(content); // StringBuilder::append
+ }
+
+ return output.toString();
+ }
+
+ public static Document translate(Document document, Translator translator) {
+ // No equivalent in pre-Java 8
+
+ List translatedPages = new ArrayList<>();
+ for (Page page: document.getPages()) {
+ String content = page.getContent();
+ String translatedContent = translator.translate(content);
+ Page translatedPage = new Page(translatedContent); // Page::new
+ translatedPages.add(translatedPage);
+ }
+
+ return new Document(translator.translate(document.getTitle()), translatedPages);
+ }
+}
diff --git a/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise4/PagePrinter.java b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise4/PagePrinter.java
new file mode 100644
index 0000000..503692a
--- /dev/null
+++ b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise4/PagePrinter.java
@@ -0,0 +1,48 @@
+package org.adoptopenjdk.lambda.tutorial.exercise4;
+
+/*
+ * #%L
+ * lambda-tutorial
+ * %%
+ * Copyright (C) 2013 Adopt OpenJDK
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+
+import org.adoptopenjdk.lambda.tutorial.exercise4.Document.Page;
+
+import static java.lang.String.format;
+
+public final class PagePrinter {
+
+ private final String pageBreak;
+
+ public PagePrinter(String pageBreak) {
+ this.pageBreak = pageBreak;
+ }
+
+ public String printTitlePage(Document document) {
+ return format(
+ "%s%n" +
+ "%s%n", document.getTitle(), pageBreak);
+ }
+
+ public String printPage(Page page) {
+ return format(
+ "%s%n" +
+ "%s%n", page.getContent(), pageBreak);
+ }
+}
diff --git a/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise4/Translator.java b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise4/Translator.java
new file mode 100644
index 0000000..7d3a700
--- /dev/null
+++ b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise4/Translator.java
@@ -0,0 +1,41 @@
+package org.adoptopenjdk.lambda.tutorial.exercise4;
+
+/*
+ * #%L
+ * lambda-tutorial
+ * %%
+ * Copyright (C) 2013 Adopt OpenJDK
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+
+public interface Translator {
+
+ String translate(String input);
+
+ enum Languages implements Translator {
+ REVERSISH {
+ @Override
+ public String translate(String input) {
+ return new StringBuilder(input).reverse().toString();
+ }
+ }
+
+ // TODO: implement other, real languages.
+ }
+
+}
+
diff --git a/src/test/java/org/adoptopenjdk/lambda/tutorial/Exercise_1_Test.java b/src/test/java/org/adoptopenjdk/lambda/tutorial/Exercise_1_Test.java
index 4a4a0b9..00ea7f4 100644
--- a/src/test/java/org/adoptopenjdk/lambda/tutorial/Exercise_1_Test.java
+++ b/src/test/java/org/adoptopenjdk/lambda/tutorial/Exercise_1_Test.java
@@ -165,6 +165,10 @@ public void changeColorOfAllShapes_AND_buildStringShowingAllTheOldColors() {
// ----- Test helpers -----
private static Matcher hasColor(Color color) {
- return FeatureMatchers.from(Matchers.is(color), "has color", "color", Shape::getColor);
+ return FeatureMatchers.from(Matchers.is(color), "has color", "color", new FeatureMatchers.Extractor() {
+ @Override public Color get(Shape shape) {
+ return shape.getColor();
+ }
+ });
}
}
diff --git a/src/test/java/org/adoptopenjdk/lambda/tutorial/Exercise_2_Test.java b/src/test/java/org/adoptopenjdk/lambda/tutorial/Exercise_2_Test.java
index ec344aa..eee3c02 100644
--- a/src/test/java/org/adoptopenjdk/lambda/tutorial/Exercise_2_Test.java
+++ b/src/test/java/org/adoptopenjdk/lambda/tutorial/Exercise_2_Test.java
@@ -38,10 +38,6 @@
import java.util.HashSet;
import java.util.List;
import java.util.Set;
-import java.util.function.Predicate;
-import java.util.stream.Collector;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
import static java.util.Arrays.asList;
import static java.util.Arrays.binarySearch;
@@ -154,7 +150,7 @@
public class Exercise_2_Test {
/**
- * Use Stream.filter() to produce a list containing only those
+ * Use Stream.filter() to produce a list containing only those
* Persons eligible to vote.
*
* @see Person#getAge()
@@ -242,13 +238,10 @@ public void getAllPersonsEligibleToVote() {
* Ensure that the Set returned cannot be modified by callers by wrapping the result
* in Collections.unmodifiableSet().
*
- *
- * The Streams API does not provide a way to wrap the final result, as one of its operations. So just wrap the
- * result in an unmodifiableSet yourself. Sometimes it's just as important to know what an API _doesn't_ do.
- *
* @throws ClassNotFoundException If the lambdas binary build no longer contains the class
* @see Stream#collect(java.util.stream.Collector)
- * @see java.util.stream.Collectors#toSet()
+ * @see Collectors#collectingAndThen(Collector, Function)
+ * @see Collectors#toSet()
* @see Collections#unmodifiableSet(java.util.Set)
*/
@Test public void setOfVotersInDistrictInUnmodifiableSet() throws ClassNotFoundException {
@@ -270,15 +263,27 @@ public void getAllPersonsEligibleToVote() {
// Test helpers
private static Matcher aPersonNamed(String name) {
- return FeatureMatchers.from(is(name), "a person", "name", person -> person.getName());
+ return FeatureMatchers.from(is(name), "a person", "name", new FeatureMatchers.Extractor() {
+ @Override public String get(Person person) {
+ return person.getName();
+ }
+ });
}
private static Matcher aVoterWithId(String name) {
- return FeatureMatchers.from(is(name), "a voter", "electorId", voter -> voter.getElectorId());
+ return FeatureMatchers.from(is(name), "a voter", "electorId", new FeatureMatchers.Extractor() {
+ @Override public String get(RegisteredVoter registeredVoter) {
+ return registeredVoter.getElectorId();
+ }
+ });
}
private static Matcher spoiled() {
- return FeatureMatchers.from(equalTo(Boolean.TRUE), "a spoiled ballot", "isSpoiled", ballot -> ballot.isSpoiled());
+ return FeatureMatchers.from(equalTo(Boolean.TRUE), "a spoiled ballot", "isSpoiled", new FeatureMatchers.Extractor() {
+ @Override public Boolean get(Ballot ballot) {
+ return ballot.isSpoiled();
+ }
+ });
}
}
\ No newline at end of file
diff --git a/src/test/java/org/adoptopenjdk/lambda/tutorial/Exercise_3_Test.java b/src/test/java/org/adoptopenjdk/lambda/tutorial/Exercise_3_Test.java
index df14c3a..8ebaff3 100644
--- a/src/test/java/org/adoptopenjdk/lambda/tutorial/Exercise_3_Test.java
+++ b/src/test/java/org/adoptopenjdk/lambda/tutorial/Exercise_3_Test.java
@@ -33,10 +33,6 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
-import java.util.function.Function;
-import java.util.stream.Collector;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
@@ -185,7 +181,11 @@ public void getPublishersRepresentedByBooks() {
// Test helpers
private static Matcher publisherNamed(String name) {
- return FeatureMatchers.from(equalTo(name), "is named", "name", publisher -> publisher.getName());
+ return FeatureMatchers.from(equalTo(name), "is named", "name", new FeatureMatchers.Extractor() {
+ @Override public String get(Publisher publisher) {
+ return publisher.getName();
+ }
+ });
}
}
diff --git a/src/test/java/org/adoptopenjdk/lambda/tutorial/Exercise_4_Test.java b/src/test/java/org/adoptopenjdk/lambda/tutorial/Exercise_4_Test.java
new file mode 100644
index 0000000..9f2abe5
--- /dev/null
+++ b/src/test/java/org/adoptopenjdk/lambda/tutorial/Exercise_4_Test.java
@@ -0,0 +1,297 @@
+package org.adoptopenjdk.lambda.tutorial;
+
+/*
+ * #%L
+ * lambda-tutorial
+ * %%
+ * Copyright (C) 2013 Adopt OpenJDK
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+
+import org.adoptopenjdk.lambda.tutorial.exercise4.Document;
+import org.adoptopenjdk.lambda.tutorial.exercise4.Document.Page;
+import org.adoptopenjdk.lambda.tutorial.exercise4.Documents;
+import org.adoptopenjdk.lambda.tutorial.exercise4.PagePrinter;
+import org.adoptopenjdk.lambda.tutorial.exercise4.Translator;
+import org.adoptopenjdk.lambda.tutorial.exercise4.Translator.Languages;
+import org.adoptopenjdk.lambda.tutorial.util.FeatureMatchers;
+import org.hamcrest.Matcher;
+import org.junit.Test;
+
+import java.util.Arrays;
+
+import static java.lang.String.format;
+import static org.adoptopenjdk.lambda.tutorial.util.StringWithComparisonMatcher.isString;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.endsWith;
+import static org.hamcrest.Matchers.everyItem;
+
+/**
+ * Exercise 4 - Method References
+ *
+ * Method references are another syntactic addition to JDK 8. They are intended to be used in lambda expressions,
+ * preventing unnecessary boilerplate. Consider the following lambda from an earlier test:
+ * books.stream().map(b -> b.getTitle()). Here the lambda does nothing more than invoke a method on each
+ * element of the stream. This can be written alternatively as: books.stream().map(Book::getTitle). Both
+ * forms are equivalent.
+ *
+ *
+ * The parameter list and return type when using a method reference must match the signature of the method. A mismatch
+ * between the two will result in a compile error, just as invoking a method with parameters or return type with an
+ * incorrect type will cause a compile error.
+ * For example, consider the following code:
+ *
+ * public class Printers {
+ * public static void print(String s) {...}
+ * }
+ *
+ * Note that the printNumber method takes a single String parameter, and has a void return
+ * type. Although declared using different type names, this is the same signature as {@link Consumer}, the functional
+ * interface that's passed to {@link Iterable#forEach(Consumer)}. Since they match, we can use a reference to the
+ * print method when invoking forEach on a list of Strings, like so:
+ *
+ * Arrays.asList("a", "b", "c").forEach(Printers::print)
+ *
+ * If we changed the signature of print to public static void print(String s, String t)
+ * the use of the method reference would no longer compile, with an error message pointing to the mismatch in argument
+ * lists.
+ *
+ *
+ * There are four different kinds of methods that can be used with the method reference syntax:
+ *
+ * - Static method belonging to a particular class
+ * - Instance method bound to a particular object instance
+ * - Instance method bound to a particular class
+ * - Constructor belonging to a particular class
+ *
+ *
+ * We'll discuss each in turn:
+ *
+ * Static method belonging to a particular class
+ *
+ * In the previous example, the Printers::print method reference is to a static method ('print') belonging
+ * to the Printers class. Here the argument list of the lambda must match the argument list of the method, the first
+ * argument to the lambda is the first argument passed into the method.
+ *
+ *
+ *
+ * Instance method bound to a particular object instance
+ *
+ * It's possible to use a method invoked an a specific instance of a class. Consider the following code:
+ *
+ * public class Document {
+ * // field, constructor, etc
+ *
+ * public String getPageContent(int pageNumber) {
+ * return this.pages.get(pageNumber).getContent();
+ * }
+ * }
+ *
+ * public static void printPages(Document doc, int[] pageNumbers) {
+ * Arrays.stream(pageNumbers).map(doc::getPageContent).forEach(Printers::print);
+ * }
+ *
+ *
+ * In this case, when the map operation runs, the method getPageContent will
+ * be invoked on the doc instance. Regardless of the current page number at that point
+ * of the stream, it will always be transformed by calling the equivalent of doc.getPageContent(i).
+ *
+ *
+ * Instance method belonging to a particular class
+ *
+ * When iterating over a stream of objects, you can invoke a method on each object by using an instance method
+ * reference. As in this code:
+ *
+ * public static void printDocuments(List<Page> pages) {
+ * pages.stream().map(Page::getContent).forEach(Printers::print);
+ * }
+ *
+ * In this case the method getContent will still be invoked on an instance of Page, however,
+ * it will be invoked on each Page instance that is mapped over.
+ *
+ *
+ * Constructor belonging to a particular class
+ *
+ * By now, we know how to use method references for static methods and instance methods, that leaves an odd case:
+ * constructors.
+ *
+ * While we don't invoke a constructor like a static method, it is useful to think of it that way. Currently we write:
+ * Page p = new Page("content"); but imagine we changed the syntax of the Java language to allow this:
+ * Page p = Page.new("content");. We can consider the Page.new method to have the exact
+ * semantics of a constructor, that is, use a reference to the class object to invoke the constructor method and return
+ * the newly created instance as the result of new.
+ *
+ * With that in mind, consider the following code:
+ *
+ * public static Stream<Page> createPagesFrom(Stream<String> contents) {
+ * return contents.map(Page::new).
+ * }
+ *
+ * The method will return a Stream of newly constructed Page objects. new is
+ * still a special keyword in Java, but can now be used in the method reference construct. Note that just like other
+ * method references, the method signature of the constructor must match the types fed by the map
+ * operation.
+ *
+ *
+ */
+@SuppressWarnings("unchecked")
+public class Exercise_4_Test {
+
+ /**
+ * The Documents class has a method which transforms a list of Document into a list of
+ * their titles. The implementation has already been filled out, but it uses a lambda, as in:
+ * .map(document -> document.getTitle())
+ *
+ * Instead of using a lambda, use a method reference instead.
+ *
+ * @see Documents#titlesOf(Document...)
+ * @see Document#getTitle()
+ *
+ */
+ @Test
+ public void getListOfDocumentTitlesUsingReferenceOfInstanceMethodBelongingToAClass() {
+ Document expenses = new Document("My Expenses",
+ Arrays.asList(new Page("LJC Open Conference ticket: £25"), new Page("Beer stipend: £100")));
+ Document toDoList = new Document("My ToDo List",
+ Arrays.asList(new Page("Build a todo app"), new Page("Pick up dry cleaning")));
+ Document certificates = new Document("My Certificates",
+ Arrays.asList(new Page("Oracle Certified Professional"), new Page("Swimming 10m")));
+
+ assertThat(Documents.titlesOf(expenses, toDoList, certificates),
+ contains("My Expenses", "My ToDo List", "My Certificates"));
+
+ }
+
+ /**
+ * The Documents class has a method which calculates a list of the character counts of Pages in a
+ * Document. The method characterCount can be applied to each Page to calculate the number of
+ * characters in that page. Currently it is invoked using a lambda.
+ *
+ * Change to use a method reference which uses the static characterCount method.
+ *
+ * @see Documents#pageCharacterCounts(Document)
+ * @see Documents#characterCount(Page)
+ */
+ @Test
+ public void getListOfPageCharacterCountsFromDocumentUsingReferenceOfStaticMethodBelongingToAClass() {
+ Document diary = new Document("My Diary", Arrays.asList(
+ new Page("Today I went shopping"),
+ new Page("Today I did maths"),
+ new Page("Today I wrote in my diary")));
+
+ assertThat(Documents.pageCharacterCounts(diary), contains(21, 17, 25));
+ }
+
+ /**
+ * The Documents class has a method which takes a PagePrinter and renders a
+ * Document to text. The method uses two lambda expressions where method references can be used. In
+ * this case the method references are to methods belonging to a particular instance object.
+ *
+ * Change {@link Documents#print(Document, PagePrinter)} to use method references to invoke instance methods of
+ * particular objects.
+ *
+ * @see Documents#print(Document, PagePrinter)
+ * @see StringBuilder#append
+ * @see PagePrinter#printPage(Page)
+ *
+ */
+ @Test
+ public void printContentsOfDocumentUsingReferenceOfInstanceMethodBeloningToAnObject() {
+ Document diary = new Document("My Diary", Arrays.asList(
+ new Page("Today I went shopping"),
+ new Page("Today I did maths"),
+ new Page("Today I wrote in my diary")));
+
+ assertThat(Documents.print(diary, new PagePrinter("----")),
+ isString(format("My Diary%n" +
+ "----%n" +
+ "Today I went shopping%n" +
+ "----%n" +
+ "Today I did maths%n" +
+ "----%n" +
+ "Today I wrote in my diary%n" +
+ "----%n")));
+ }
+
+
+ /**
+ * The Document class has a method which can create a new Document where all the pages have had a
+ * footer appended to it of the format "Document: {title}". The method uses two lambda expressions where method
+ * references can be used. In this case the method references are to methods belonging to this object
+ * instance. That is, the methods to be invoked should be invoked on this.
+ *
+ * Change {@link Document#copyWithFooter()} to use method references to invoke instance methods on this
+ * instance.
+ */
+ @Test
+ public void transformPagesToHaveFooterUsingReferenceOfInstanceMethodBelonginToThisObject() {
+ Document diary = new Document("My Diary", Arrays.asList(
+ new Page("Today I went shopping"),
+ new Page("Today I did maths"),
+ new Page("Today I wrote in my diary")));
+
+ Document diaryWithFooters = diary.copyWithFooter();
+
+ assertThat(diaryWithFooters.getPages(), everyItem(pageEndingWith("Document: My Diary")));
+ }
+
+
+ /**
+ * The Documents class has a method which can translate a document into another language. The method
+ * uses a lambda expression to construct each translated Page, where it could use a method reference
+ * to Page's constructor.
+ *
+ * Change {@link Documents#translate} to use a method reference to construct each translated Page.
+ *
+ * @see Documents#translate(Document, Translator)
+ * @see Translator.Languages
+ * @see Page
+ */
+ @Test
+ public void createNewDocumentWithTranslatedPagesUsingReferenceOfConstructorMethod() {
+ Document diary = new Document("My Diary", Arrays.asList(
+ new Page("Today I went shopping"),
+ new Page("Today I did maths"),
+ new Page("Today I wrote in my diary")));
+
+ Document translated = Documents.translate(diary, Languages.REVERSISH);
+
+ assertThat(translated.getPages(),
+ contains(pageContaining("gnippohs tnew I yadoT"),
+ pageContaining("shtam did I yadoT"),
+ pageContaining("yraid ym ni etorw I yadoT")));
+ }
+
+ private static Matcher pageEndingWith(String ending) {
+ return FeatureMatchers.from(endsWith(ending), "page containing", "contents", new FeatureMatchers.Extractor() {
+ @Override public String get(Page page) {
+ return page.getContent();
+ }
+ });
+ }
+
+ private static Matcher pageContaining(String content) {
+ return FeatureMatchers.from(isString(content), "page containing", "contents", new FeatureMatchers.Extractor() {
+ @Override
+ public String get(Page page) {
+ return page.getContent();
+ }
+ });
+ }
+}
diff --git a/src/test/java/org/adoptopenjdk/lambda/tutorial/Exercise_5_Test.java b/src/test/java/org/adoptopenjdk/lambda/tutorial/Exercise_5_Test.java
new file mode 100644
index 0000000..bedf2c3
--- /dev/null
+++ b/src/test/java/org/adoptopenjdk/lambda/tutorial/Exercise_5_Test.java
@@ -0,0 +1,166 @@
+package org.adoptopenjdk.lambda.tutorial;
+
+/*
+ * #%L
+ * lambda-tutorial
+ * %%
+ * Copyright (C) 2013 Adopt OpenJDK
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+
+
+import java.util.function.Consumer;
+
+/**
+ * Exercise 5 - Default Methods
+ *
+ * The introduction of Default Methods is intended to allow for libraries to evolve more easily - with the
+ * JDK Collections library as their first main user. They permit adding concrete methods, with an implementation,
+ * to Java interfaces. Prior to JDK 8, every method on an interface had to be abstract, with implementations provided
+ * by classes. With JDK 8, it's possible to declare the following interface:
+ *
+ *
+ *
+ * // (stripped down version of the real Iterable.java)
+ * public interface Iterable {
+ *
+ * Iterator iterator(); // As in prior versions
+ *
+ * // example of a new JDK 8 default method
+ * default void forEach(Consumer super T> action) {
+ * Objects.requireNonNull(action);
+ * for (T t : this) {
+ * action.accept(t);
+ * }
+ * }
+ * }
+ *
+ *
+ * In this case the method forEach is a default method with a concrete implementation. With this
+ * declaration forEach() can be invoked on any implementation of Iterable. This is very
+ * similar to adding a concrete method to an abstract class -- if no implementation overrides it, the code in the method
+ * on the interface is executed. Crucially, the new method can be added without causing compiler errors in the client
+ * code. Consider if a normal forEach method was added to Iterable: every class that
+ * implemented the Iterable interface would now fail to compile. Using default methods, the interface can
+ * be evolved without breaking client code.
+ *
+ * Allowing evolution of a library is the primary use case of default methods.
+ *
+ *
+ *
Rules of Method Lookup
+ *
+ * Inheritance of method implementations is not new to Java. When a subclass method is invoked, the runtime finds the
+ * nearest implementation available. This could be declared on the subclass itself, or on any of it's superclasses, or
+ * even all the way up in java.lang.Object. This last case is what occurs if your class does not override
+ * toString. However, if anywhere in the inheritance hierarchy from your class to Object, toString is
+ * implemented, that would be executed instead of Object's toString. This behaviour is still in
+ * place in JDK 8, but has been augmented to cope with default methods.
+ *
+ *
+ * The main difference between superclass methods and default methods in JDK 8 is that there is only single
+ * inheritance of classes, while there is multiple inheritance of interfaces. As such, there needs to be
+ * some extra rules around method lookup.
+ *
+ * Method lookup has all the following characteristics:
+ *
+ *
For a default method to be invoked, there must be no declaration anywhere in the class hierarchy.
+ *
+ * This can
+ * be thought of like Miranda rights, as in, "You have
+ * the right to a method implementation. If your class hierarchy cannot afford an implementation, one will be
+ * provided for you". A method implementation anywhere in the class hierarchy will take priority over a default
+ * method. This also applies to default methods which match the signature from java.lang.Object; the default method
+ * can never be invoked. As such, declaring a default method which matches a method from Object is a compiler error.
+ *
+ * The closest default method wins.
+ *
+ * As with superclasses, when searching for a concrete method the nearest
+ * superclass wins. For example, given "class A", "class B extends A", and "class C
+ * extends B", if a method is invoked on C, the Java runtime will first look for the implementation in C,
+ * then B, then A, then finally java.lang.Object, invoking the first method it finds. This is also the case with
+ * super-interfaces. Indeed, given "interface X", "interface Y extends X" and "
+ * interface Z extends Y", and declaring "class C extends B, implements Z", the lookup for a
+ * default method would traverse the hierarchy in the following order: C -> B -> A -> Object -> Z -> Y -> X.
+ *
+ * Ambiguous inheritance of a default method must be resolved in the implementing class, at compile time.
+ *
+ * If a class inherits the same default method from more than one source, a compiler error will be emitted. This happens
+ * when unrelated types declare the same method signature, and a class becomes a subtype of more than one of them.
+ * Consider the following example:
+ *
+ * interface A {
+ * default void speak() { System.out.println("A says hi!"; }
+ * }
+ * interface B {
+ * default void speak() { System.out.println("Regards from B!"; }
+ * }
+ * class C implements A, B { } // compiler error: inherits unrelated default methods from A and B
+ *
+ * This principle applies regardless of how deep the interface inheritance hierarchy is, and also applies to
+ * sub-interfaces as well as classes, i.e. "interface Y extends A, B" results in the same compile error.
+ * This error can be resolved by removing ambiguity in the subtype, by overriding the default method. This can be any
+ * compliant implementation, including directly invoking a specific inherited default method. A new syntax in JDK 8 is
+ * available to allow choosing an inherited default method, like so:
+ *
+ * class C implements A, B {
+ * public void speak() { A.super.speak(); } // prints "A says hi!"
+ * }
+ *
+ * In this case, when instanceOfC.speak(); is executed, the default speak from
+ * interface A is invoked. This syntax is also available within default method bodies, allowing an interface
+ * to choose an implementation from one of its super-interfaces. Note that this syntax is not entirely unfamiliar: it
+ * can be considered just like invoking super.someMethod();, except that with the single inheritance of
+ * classes, the name of the super class is can be nothing other than the single superclass, so it can remain implicit.
+ *
+ * It should be noted that ambiguities are never resolved by the order in which interface implementations are
+ * declared (such as with Scala's traits). They must always be resolved explicitly in the subtype.
+ *
+ * Do default methods mean Java supports multiple inheritance?
+ *
+ * In a way. Java has always supported multiple inheritance of interfaces, previously this did not include any of the
+ * implementation, just the contract. Inheritance can be subdivided again, into inheritance of state, and
+ * inheritance of behaviour. Default methods introduce the latter, multiple inheritance of behaviour.
+ *
What about the diamond problem?
+ * There are two aspects to the diamond problem: a) disambiguating implementations and b) handling state. We have seen
+ * how ambiguities are resolved in JDK 8, requiring that users specify the implementation at compile time. Default
+ * methods do not introduce a problem in that aspect of the diamond problem. The second problem, state, is where trickier
+ * issues of the diamond problem live. It can be too easy to accidentally introduce bugs into an implementing class
+ * because it must adhere to the contract defined in the interface and/or superclass, which can include maintaining
+ * state. Any methods which manipulate that state must also be invoked in the implementing class, and accidentally
+ * "losing" that invocation is an easy way to introduce a subtle bug. Also, there is the problem between the ordering of
+ * conflicting methods, that cannot be resolved by disambiguating. Default methods in Java avoid this issue, due to an
+ * existing virtue of interfaces, that they do not contain state. Because an interface cannot be constructed
+ * with fields, there is no state available to encounter this issue. Java still has single inheritance of state,
+ * through superclasses. If the contract of an interface requires state (like, e.g. Iterator) it will be
+ * provided through the single inheritance chain.
+ *
+ *
+ * Note that there are still ways to "simulate" state in interfaces with default methods, since there is no restriction
+ * on accessing static fields defined in other classes. However, that should be avoided for all the reasons that both
+ * multiple inheritance of state, and global mutable state should be avoided.
+ *
+ *
+ * @see Iterable#forEach(Consumer)
+ */
+@SuppressWarnings("unchecked")
+public class Exercise_5_Test {
+
+ /*
+ Exercise on default methods in interfaces does not apply pre-java8.
+ */
+
+}
diff --git a/src/test/java/org/adoptopenjdk/lambda/tutorial/util/FeatureMatchers.java b/src/test/java/org/adoptopenjdk/lambda/tutorial/util/FeatureMatchers.java
index 7010d57..005c066 100644
--- a/src/test/java/org/adoptopenjdk/lambda/tutorial/util/FeatureMatchers.java
+++ b/src/test/java/org/adoptopenjdk/lambda/tutorial/util/FeatureMatchers.java
@@ -25,7 +25,6 @@
import org.hamcrest.FeatureMatcher;
import org.hamcrest.Matcher;
-import java.util.function.Function;
/**
* Helper utility for matching on features
@@ -46,14 +45,18 @@ private FeatureMatchers() {}
* @return A Matcher
*/
public static Matcher from(Matcher featureMatcher,
- String description,
- String name,
- Function extractor) {
+ String description,
+ String name,
+ final Extractor extractor) {
return new FeatureMatcher(featureMatcher, description, name) {
@Override protected FEATURE featureValueOf(FROM t) {
- return extractor.apply(t);
+ return extractor.get(t);
}
};
}
+ public static interface Extractor {
+ TO get(FROM from);
+ }
+
}
diff --git a/src/test/java/org/adoptopenjdk/lambda/tutorial/util/StringWithComparisonMatcher.java b/src/test/java/org/adoptopenjdk/lambda/tutorial/util/StringWithComparisonMatcher.java
new file mode 100644
index 0000000..dc661d0
--- /dev/null
+++ b/src/test/java/org/adoptopenjdk/lambda/tutorial/util/StringWithComparisonMatcher.java
@@ -0,0 +1,55 @@
+package org.adoptopenjdk.lambda.tutorial.util;
+
+/*
+ * #%L
+ * lambda-tutorial
+ * %%
+ * Copyright (C) 2013 Adopt OpenJDK
+ * %%
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this program. If not, see
+ * .
+ * #L%
+ */
+
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeDiagnosingMatcher;
+import org.junit.ComparisonFailure;
+
+public class StringWithComparisonMatcher extends TypeSafeDiagnosingMatcher {
+ private final String expected;
+
+ private StringWithComparisonMatcher(String expected) {
+ this.expected = expected;
+ }
+
+ public static StringWithComparisonMatcher isString(String expected) {
+ return new StringWithComparisonMatcher(expected);
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText(expected);
+ }
+
+ @Override
+ protected boolean matchesSafely(String actual, Description mismatchDescription) {
+ if (!expected.equals(actual)) {
+ String compactedMismatch = new ComparisonFailure("did not match:", expected, actual).getMessage();
+ mismatchDescription.appendText(compactedMismatch);
+ return false;
+ }
+ return true;
+ }
+
+}
\ No newline at end of file