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..d3b1e66 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -## Lambda Tutorial +## Lambda Tutorial [![Build Status](https://api.travis-ci.org/AdoptOpenJDK/lambda-tutorial.png?branch=solutions-postjava8)](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,6 @@ To follow the exercises: 1. Internal vs External Iteration (the forEach method) 2. Filtering and Collecting 3. Mapping + 4. Method References + 5. Default methods on interfaces -[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. diff --git a/pom.xml b/pom.xml index e13fb1f..688d5cc 100644 --- a/pom.xml +++ b/pom.xml @@ -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..0298e1f 100644 --- a/src/main/java/org/adoptopenjdk/lambda/setupcheck/ConfigureYourLambdaBuildOfJdk.java +++ b/src/main/java/org/adoptopenjdk/lambda/setupcheck/ConfigureYourLambdaBuildOfJdk.java @@ -39,15 +39,13 @@ 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."); 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..c44a409 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,7 @@ public class Shapes { * @see Shape#setColor(Color) */ public static void colorAll(List shapes, Color newColor) { - // [your code here] + shapes.forEach(s -> s.setColor(newColor)); } /** @@ -68,7 +68,7 @@ public static void colorAll(List shapes, Color newColor) { * @see Shape#toString() */ public static void makeStringOfAllColors(List shapes, StringBuilder stringBuilder) { - // [your code here] + shapes.forEach(s -> stringBuilder.append(s)); } /** @@ -93,6 +93,7 @@ public static void makeStringOfAllColors(List shapes, StringBuilder strin * @see Shape#toString() */ public static void changeColorAndMakeStringOfOldColors(List shapes, Color newColor, StringBuilder stringBuilder) { - // [your code here] + shapes.forEach(s -> { 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..c4fb95e 100644 --- a/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise2/ElectoralDistrict.java +++ b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise2/ElectoralDistrict.java @@ -25,6 +25,9 @@ import java.util.Collection; import java.util.Collections; import java.util.Set; +import java.util.stream.Collectors; +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.toSet; /** * Some (inaccurate) London electrical districts @@ -54,9 +57,9 @@ public enum ElectoralDistrict { * @return filtered set of registered voters in a district */ public static Set votersIn(ElectoralDistrict district, Collection voters) { - // [your code here] - - return Collections.emptySet(); + return voters.stream() + .filter(v -> v.getElectorId().startsWith(district.prefix)) + .collect(collectingAndThen(toSet(), Collections::unmodifiableSet)); } /** @@ -66,9 +69,9 @@ public static Set votersIn(ElectoralDistrict district, Collecti * @return filtered set of unspoiled ballots */ public static Set unspoiledBallots(Set votes) { - // [your code here] - - return Collections.emptySet(); + return votes.stream() + .filter(v -> !v.isSpoiled()) + .collect(Collectors.toSet()); } 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..cd669b3 100644 --- a/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise2/VotingRules.java +++ b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise2/VotingRules.java @@ -22,9 +22,6 @@ * #L% */ -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -41,8 +38,8 @@ public class VotingRules { * @return a list of eligible voters */ public static List eligibleVoters(List potentialVoters, int legalAgeOfVoting) { - // [your code here] - - return Collections.emptyList(); + return potentialVoters.stream() + .filter(p -> p.getAge() >= legalAgeOfVoting) + .collect(Collectors.toList()); } } 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..9c6fcc8 100644 --- a/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise3/Books.java +++ b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise3/Books.java @@ -25,6 +25,7 @@ import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.Collectors; @@ -32,17 +33,15 @@ * Domain object representing a collection of books */ public class Books { - + /** * Apply a mapping of Books to titles (Strings) - * + * * @param books - books to transform * @return list of book titles */ public static List titlesOf(List books) { - // [your code here] - - return Collections.emptyList(); + return books.stream().map(b -> b.getTitle()).collect(Collectors.toList()); } /** @@ -52,9 +51,7 @@ public static List titlesOf(List books) { * @return list of author full names */ public static List namesOfAuthorsOf(List books) { - // [your code here] - - return Collections.emptyList(); + return books.stream().map(b -> b.getAuthor()).map(Author::fullName).collect(Collectors.toList()); } /** @@ -64,8 +61,6 @@ public static List namesOfAuthorsOf(List books) { * @return set of publishers */ public static Set publishersRepresentedBy(List books) { - // [your code here] - - return Collections.emptySet(); + return books.stream().map(b -> b.getPublisher()).collect(Collectors.toSet()); } } 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..923184c --- /dev/null +++ b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise4/Document.java @@ -0,0 +1,76 @@ +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 java.util.stream.Collectors; + +import static java.lang.String.format; +import static java.util.stream.Collectors.collectingAndThen; + +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() { + return getPages().stream() + .map(this::appendFooter) + .collect(collectingAndThen(Collectors.toList(), 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..0baff6b --- /dev/null +++ b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise4/Documents.java @@ -0,0 +1,74 @@ +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.Arrays; +import java.util.List; + +import static java.lang.String.format; +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.toList; + +public class Documents { + + /** + * Return the titles from a list of documents. + */ + public static List titlesOf(Document... documents) { + return Arrays.stream(documents) + .map(Document::getTitle) + .collect(toList()); + } + + public static Integer characterCount(Page page) { + return page.getContent().length(); + } + + public static List pageCharacterCounts(Document document) { + return document.getPages().stream() + .map(Documents::characterCount) + .collect(toList()); + } + + public static String print(Document document, PagePrinter pagePrinter) { + StringBuilder output = new StringBuilder(); + + output.append(pagePrinter.printTitlePage(document)); + document.getPages().stream() + .map(pagePrinter::printPage) + .forEach(output::append); + + return output.toString(); + } + + public static Document translate(Document document, Translator translator) { + return document.getPages().stream() + .map(Page::getContent) + .map(translator::translate) + .map(Page::new) + .collect(collectingAndThen(toList(), + pages -> new Document(translator.translate(document.getTitle()), pages))); + } +} 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..dabc498 --- /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 - 2014 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/main/java/org/adoptopenjdk/lambda/tutorial/exercise5/musicplayer/Album.java b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise5/musicplayer/Album.java new file mode 100644 index 0000000..33626f2 --- /dev/null +++ b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise5/musicplayer/Album.java @@ -0,0 +1,26 @@ +package org.adoptopenjdk.lambda.tutorial.exercise5.musicplayer; + +/* + * #%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 class Album { +} diff --git a/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise5/musicplayer/MusicLibrary.java b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise5/musicplayer/MusicLibrary.java new file mode 100644 index 0000000..5be9ff2 --- /dev/null +++ b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise5/musicplayer/MusicLibrary.java @@ -0,0 +1,64 @@ +package org.adoptopenjdk.lambda.tutorial.exercise5.musicplayer; + +/* + * #%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.Collection; +import java.util.List; + +import static java.util.stream.Collectors.toList; + +public interface MusicLibrary { + + /** + * @return every song in the collection, in no specified order. + */ + Collection allSongs(); + + /** + * Will sort a given collection of Songs by artist. + */ + static class SongByArtistSorter { + public static List sort(Collection songs) { + return songs.stream().sorted((song1, song2) -> song1.getArtist().compareTo(song2.getArtist())) + .collect(toList()); + } + } + + /** + * Provides a rating for this song, between 1-100, inclusive. + * + * Default implementation takes a rating by normalising the play count for the given song with the play count for + * all songs in this MusicLibrary. + */ + default Rating ratingOf(Song song) { + int totalPlayCount = allSongs().stream().mapToInt(this::timesPlayed).sum(); + float score = (timesPlayed(song) / totalPlayCount) * 100.0f; + return new Rating(Math.round(score)); + } + + default List sortedByArtist() { + return SongByArtistSorter.sort(allSongs()); + } + + int timesPlayed(Song song); +} diff --git a/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise5/musicplayer/Rating.java b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise5/musicplayer/Rating.java new file mode 100644 index 0000000..4cf1729 --- /dev/null +++ b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise5/musicplayer/Rating.java @@ -0,0 +1,61 @@ +package org.adoptopenjdk.lambda.tutorial.exercise5.musicplayer; + +/* + * #%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% + */ + +/** +* Lambda Tutorial -- Adopt Open JDK +* +* @author Graham Allan grundlefleck at gmail dot com +*/ +public final class Rating { + public final int score; + + public Rating(int score) { + if (score < 0 || score > 100) { + throw new IllegalArgumentException("Rating must be between 0 and 100, inclusive"); + } + this.score = score; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Rating rating = (Rating) o; + + if (score != rating.score) return false; + + return true; + } + + @Override + public int hashCode() { + return score; + } + + @Override + public String toString() { + return "Rating{score=" + score + '}'; + } +} diff --git a/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise5/musicplayer/Song.java b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise5/musicplayer/Song.java new file mode 100644 index 0000000..2685141 --- /dev/null +++ b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise5/musicplayer/Song.java @@ -0,0 +1,42 @@ +package org.adoptopenjdk.lambda.tutorial.exercise5.musicplayer; + +/* + * #%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 class Song { + private final String title; + private final String artist; + + public Song(String title, String artist) { + + this.title = title; + this.artist = artist; + } + + public String getTitle() { + return title; + } + + public String getArtist() { + return artist; + } +} diff --git a/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise5/musicplayer/StarRating.java b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise5/musicplayer/StarRating.java new file mode 100644 index 0000000..08fff5f --- /dev/null +++ b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise5/musicplayer/StarRating.java @@ -0,0 +1,39 @@ +package org.adoptopenjdk.lambda.tutorial.exercise5.musicplayer; + +/* + * #%L + * lambda-tutorial + * %% + * Copyright (C) 2013 - 2014 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 enum StarRating { + + FIVE_STARS(5), + FOUR_STARS(4), + THREE_STARS(3), + TWO_STARS(2), + ONE_STARS(1), + ZERO_STARS(0); + + public final int numberOfStars; + + private StarRating(int stars) { + this.numberOfStars = stars; + } +} diff --git a/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise5/musicplayer/UserRatedMusicLibrary.java b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise5/musicplayer/UserRatedMusicLibrary.java new file mode 100644 index 0000000..c725b17 --- /dev/null +++ b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise5/musicplayer/UserRatedMusicLibrary.java @@ -0,0 +1,38 @@ +package org.adoptopenjdk.lambda.tutorial.exercise5.musicplayer; + +/* + * #%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 UserRatedMusicLibrary extends MusicLibrary { + + StarRating userRatingOf(Song song); + + static class StarRatingConverter { + public Rating convert(StarRating starRating) { + return new Rating(starRating.numberOfStars * 20); + } + } + + default Rating ratingOf(Song song) { + return new StarRatingConverter().convert(userRatingOf(song)); + } +} diff --git a/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise5/thirdpartyplugin/CloudScrobblingMusicLibrary.java b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise5/thirdpartyplugin/CloudScrobblingMusicLibrary.java new file mode 100644 index 0000000..8e2b47c --- /dev/null +++ b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise5/thirdpartyplugin/CloudScrobblingMusicLibrary.java @@ -0,0 +1,90 @@ +package org.adoptopenjdk.lambda.tutorial.exercise5.thirdpartyplugin; + +/* + * #%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.exercise5.musicplayer.MusicLibrary; +import org.adoptopenjdk.lambda.tutorial.exercise5.musicplayer.Rating; +import org.adoptopenjdk.lambda.tutorial.exercise5.musicplayer.Song; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Random; + +public class CloudScrobblingMusicLibrary implements MusicLibrary { + private final CloudScrobblingService cloudScrobblingService; + + public CloudScrobblingMusicLibrary() { + this.cloudScrobblingService = new CloudScrobblingService(); + } + + @Override + public Collection allSongs() { + return cloudScrobblingService.retrieveAllSongs(); + } + + @Override + public int timesPlayed(Song song) { + return cloudScrobblingService.retrieveTimesPlayedFromCloud(song); + } + + public static final class CloudScrobblingService { + + public int retrieveTimesPlayedFromCloud(Song song) { + // Simulate real cloud service by returning a random number + return (int) (1 + Math.round(Math.random() % 500)); + } + + public int retrieveScrobbledRatingOf(Song song) { + // Simulate real cloud service by returning a stable but meaningless number + switch (song.getTitle()) { + case "Candy": + return 78; + case "A Change Is Gonna Come": + return 65; + case "Desolation Row": + return 34; + case "Bad Moon Rising": + return 28; + case "Eleanor Rigby": + return 42; + default: + return 0; + } + } + + public Collection retrieveAllSongs() { + return Arrays.asList( + new Song("A Change Is Gonna Come", "Sam Cooke"), + new Song("Bad Moon Rising", "Creedence Clearwater Revival"), + new Song("Candy", "Paulo Nutini"), + new Song("Desolation Row", "Bob Dylan"), + new Song("Eleanor Rigby", "The Beatles")); + } + } + + @Override + public Rating ratingOf(Song song) { + return new Rating(cloudScrobblingService.retrieveScrobbledRatingOf(song)); + } +} diff --git a/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise5/thirdpartyplugin/LocalFilesystemMusicLibrary.java b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise5/thirdpartyplugin/LocalFilesystemMusicLibrary.java new file mode 100644 index 0000000..5183b80 --- /dev/null +++ b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise5/thirdpartyplugin/LocalFilesystemMusicLibrary.java @@ -0,0 +1,53 @@ +package org.adoptopenjdk.lambda.tutorial.exercise5.thirdpartyplugin; + +/* + * #%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.exercise5.musicplayer.MusicLibrary; +import org.adoptopenjdk.lambda.tutorial.exercise5.musicplayer.Song; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class LocalFilesystemMusicLibrary implements MusicLibrary { + + + private final Set allSongs; + + public LocalFilesystemMusicLibrary(Song... allSongs) { + this.allSongs = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(allSongs))); + } + + @Override + public Collection allSongs() { + return allSongs; + } + + @Override + public int timesPlayed(Song song) { + // Could read a local database file to find the number of times played + return 4; + } +} diff --git a/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise5/thirdpartyplugin/UserRatedLocalFilesystemMusicLibrary.java b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise5/thirdpartyplugin/UserRatedLocalFilesystemMusicLibrary.java new file mode 100644 index 0000000..c04c713 --- /dev/null +++ b/src/main/java/org/adoptopenjdk/lambda/tutorial/exercise5/thirdpartyplugin/UserRatedLocalFilesystemMusicLibrary.java @@ -0,0 +1,73 @@ +package org.adoptopenjdk.lambda.tutorial.exercise5.thirdpartyplugin; + +/* + * #%L + * lambda-tutorial + * %% + * Copyright (C) 2013 - 2014 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.exercise5.musicplayer.Song; +import org.adoptopenjdk.lambda.tutorial.exercise5.musicplayer.StarRating; +import org.adoptopenjdk.lambda.tutorial.exercise5.musicplayer.UserRatedMusicLibrary; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class UserRatedLocalFilesystemMusicLibrary implements UserRatedMusicLibrary { + + private final Set allSongs = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + new Song("A Change Is Gonna Come", "Sam Cooke"), + new Song("Bad Moon Rising", "Creedence Clearwater Revival"), + new Song("Candy", "Paulo Nutini"), + new Song("Desolation Row", "Bob Dylan"), + new Song("Eleanor Rigby", "The Beatles")))); + + @Override + public StarRating userRatingOf(Song song) { + // Simulate real user ratings + switch (song.getTitle()) { + case "Candy": + return StarRating.FIVE_STARS; + case "A Change Is Gonna Come": + return StarRating.FOUR_STARS; + case "Desolation Row": + return StarRating.THREE_STARS; + case "Bad Moon Rising": + return StarRating.TWO_STARS; + case "Eleanor Rigby": + return StarRating.ONE_STARS; + default: + return StarRating.ZERO_STARS; + } + } + + @Override + public Collection allSongs() { + return allSongs; + } + + @Override + public int timesPlayed(Song song) { + // Could read a local database file to find the number of times played + return 5; + } +} 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..4803a9e 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,6 +38,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collector; import java.util.stream.Collectors; @@ -154,7 +155,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 +243,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 { 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..1bb2ffc --- /dev/null +++ b/src/test/java/org/adoptopenjdk/lambda/tutorial/Exercise_4_Test.java @@ -0,0 +1,295 @@ +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 java.util.function.Consumer; + +import static java.lang.String.format; +import static org.adoptopenjdk.lambda.tutorial.util.CodeUsesMethodReferencesMatcher.usesMethodReferences; +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: + *

    + *
  1. Static method belonging to a particular class
  2. + *
  3. Instance method bound to a particular object instance
  4. + *
  5. Instance method bound to a particular class
  6. + *
  7. Constructor belonging to a particular class
  8. + *
+ *

+ * 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")); + assertThat(Documents.class, usesMethodReferences("getTitle")); + + } + + /** + * 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)); + assertThat(Documents.class, usesMethodReferences("characterCount")); + } + + /** + * 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"))); + assertThat(Documents.class, allOf(usesMethodReferences("printPage"), usesMethodReferences("append"))); + } + + + /** + * 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"))); + assertThat(Document.class, allOf(usesMethodReferences("appendFooter"), usesMethodReferences("copyWithPages"))); + } + + + /** + * 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"))); + assertThat(Documents.class, usesMethodReferences("new")); + } + + private static Matcher pageEndingWith(String ending) { + return FeatureMatchers.from(endsWith(ending), "page containing", "contents", Page::getContent); + } + + private static Matcher pageContaining(String content) { + return FeatureMatchers.from(isString(content), "page containing", "contents", 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..540edf4 --- /dev/null +++ b/src/test/java/org/adoptopenjdk/lambda/tutorial/Exercise_5_Test.java @@ -0,0 +1,262 @@ +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.exercise5.musicplayer.StarRating; +import org.adoptopenjdk.lambda.tutorial.exercise5.musicplayer.UserRatedMusicLibrary; +import org.adoptopenjdk.lambda.tutorial.exercise5.thirdpartyplugin.CloudScrobblingMusicLibrary; +import org.adoptopenjdk.lambda.tutorial.exercise5.musicplayer.MusicLibrary; +import org.adoptopenjdk.lambda.tutorial.exercise5.musicplayer.Rating; +import org.adoptopenjdk.lambda.tutorial.exercise5.musicplayer.Song; +import org.adoptopenjdk.lambda.tutorial.exercise5.thirdpartyplugin.LocalFilesystemMusicLibrary; +import org.adoptopenjdk.lambda.tutorial.exercise5.thirdpartyplugin.UserRatedLocalFilesystemMusicLibrary; +import org.adoptopenjdk.lambda.tutorial.util.FeatureMatchers; +import org.adoptopenjdk.lambda.tutorial.util.HasConcreteMethod; +import org.hamcrest.Matcher; +import org.junit.Test; + +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + +/** + * 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 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 { + + /** + * Add a default method to {@link MusicLibrary} that returns every song in the library, sorted by artist. You should + * NOT need to add a method to any implementation of MusicLibrary. + *
+ * There is a helper class within MusicLibrary that can help perform sorting. + *
+ * Uncomment the line below that causes a compiler error until the default method is included. + * + * + * @see MusicLibrary#allSongs() + * @see MusicLibrary.SongByArtistSorter#sort(Collection) + * + */ + @Test + public void useDefaultMethodToReturnPlaylistOrderedByArtist() { + MusicLibrary library = new LocalFilesystemMusicLibrary( + new Song("A Change Is Gonna Come", "Sam Cooke"), + new Song("Bad Moon Rising", "Creedence Clearwater Revival"), + new Song("Candy", "Paulo Nutini"), + new Song("Desolation Row", "Bob Dylan"), + new Song("Eleanor Rigby", "The Beatles") + ); + +// UNCOMMENT THE LINES BELOW +// Until the sortedByArtist method is added to MusicLibrary, there will be a compiler error. + assertThat(library.sortedByArtist(), containsSongsBy("Bob Dylan", "Creedence Clearwater Revival", + "Paulo Nutini", "Sam Cooke", "The Beatles")); + assertThat(MusicLibrary.class, HasConcreteMethod.called("sortedByArtist")); + assertThat(LocalFilesystemMusicLibrary.class, not(HasConcreteMethod.called("sortedByArtist"))); + } + + /** + * Override the default method {@link MusicLibrary#ratingOf(Song)} in {@link CloudScrobblingMusicLibrary} to return + * a {@link Rating} based on the rating given by a cloud scrobbling service. + *
+ * There is a helper method within {@link CloudScrobblingMusicLibrary.CloudScrobblingService} that can be called + * to retrieve the rating from the cloud. + * + * @see - Definition of Scrobbling - http://www.last.fm/help/faq?category=99 + * @see MusicLibrary#ratingOf(Song) + * @see CloudScrobblingMusicLibrary.CloudScrobblingService#retrieveScrobbledRatingOf(Song) + */ + @Test + public void overridesDefaultMethodInClassToProvideCustomSongRatingAlgorithm() { + MusicLibrary library = new CloudScrobblingMusicLibrary(); + + assertThat(library.ratingOf(new Song("Candy", "Paulo Nutini")), is(new Rating(78))); + assertThat(CloudScrobblingMusicLibrary.class, HasConcreteMethod.called("ratingOf")); + } + + /** + * Override the default method {@link MusicLibrary#ratingOf(Song)} in {@link UserRatedMusicLibrary} to return + * a {@link Rating} based on the {@link StarRating} entered by the user. + *
+ * The method {@link UserRatedMusicLibrary#userRatingOf(Song)} provides a user-entered rating that can be converted + * to a Rating type with the {@link UserRatedMusicLibrary.StarRatingConverter#convert(StarRating)} method. + * + */ + @Test + public void overrideDefaultMethodInInterfaceToProvideUserEnteredSongRatings() { + MusicLibrary library = new UserRatedLocalFilesystemMusicLibrary(); + + assertThat(library.ratingOf(new Song("Desolation Row", "Bob Dylan")), is(new Rating(60))); + assertThat(UserRatedMusicLibrary.class, HasConcreteMethod.called("ratingOf")); + } + + private Matcher songBy(String artist) { + return FeatureMatchers.from(equalTo(artist), "a song by", "artist", Song::getArtist); + } + + private Matcher> containsSongsBy(String... artists) { + List> songMatchers = Stream.of(artists).map(this::songBy).collect(Collectors.toList()); + return contains(songMatchers); + } + + +} diff --git a/src/test/java/org/adoptopenjdk/lambda/tutorial/util/CodeUsesMethodReferencesMatcher.java b/src/test/java/org/adoptopenjdk/lambda/tutorial/util/CodeUsesMethodReferencesMatcher.java new file mode 100644 index 0000000..70a9ba2 --- /dev/null +++ b/src/test/java/org/adoptopenjdk/lambda/tutorial/util/CodeUsesMethodReferencesMatcher.java @@ -0,0 +1,169 @@ +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.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Opcodes; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Optional; + +import static java.util.stream.Collectors.toList; + +public final class CodeUsesMethodReferencesMatcher extends TypeSafeDiagnosingMatcher> { + + private final String methodName; + + private CodeUsesMethodReferencesMatcher(String methodName) { + this.methodName = methodName; + } + + public static CodeUsesMethodReferencesMatcher usesMethodReferences(String methodName) { + return new CodeUsesMethodReferencesMatcher(methodName); + } + + @Override + public void describeTo(Description description) { + description.appendText("a source file using a method reference to invoke ").appendValue(methodName); + } + + + @Override + protected boolean matchesSafely(Class clazz, Description mismatchDescription) { + try { + Optional sourceFileContent = getSourceContent(clazz); + return sourceFileContent.map(c -> usesMethodReference(c, mismatchDescription)).orElseGet(() -> { + mismatchDescription.appendText("could not read source file to discover if you used method references."); + return false; + }); + } catch (IOException e) { + mismatchDescription.appendText("could not read source file to discover if you used method references."); + mismatchDescription.appendValue(e); + return false; + } + } + + private boolean usesMethodReference(String sourceCode, Description mismatchDescription) { + if (sourceCode.contains("::"+methodName)) { + return true; + } else { + mismatchDescription.appendText("source code did not use a method reference to invoke " + methodName + ". "); + context(sourceCode, methodName, mismatchDescription); + return false; + } + } + + private void context(String sourceCode, String methodName, Description mismatchDescription) { + if (!sourceCode.contains(methodName)) { + mismatchDescription.appendText("You did not appear to invoke the method at all."); + } else { + String[] lines = sourceCode.split("\\n"); + mismatchDescription.appendText("Actual invocations: "); + mismatchDescription.appendValueList("[", ",", "]", + Arrays.stream(lines).filter(l -> l.contains(methodName)).map(String::trim).collect(toList())); + } + } + + private Optional getSourceContent(Class clazz) throws IOException { + String sourceFileName = getSourceFileName(clazz); + Optional sourceFile = findPathTo(sourceFileName); + + return sourceFile.map(this::toContent); + } + + private Optional findPathTo(String sourceFileName) throws IOException { + File cwd = new File("."); + File rootOfProject = findRootOfProject(cwd); + return findSourceFile(rootOfProject, sourceFileName); + } + + private String toContent(File file) { + try { + byte[] encoded = Files.readAllBytes(Paths.get(file.toURI())); + return StandardCharsets.UTF_8.decode(ByteBuffer.wrap(encoded)).toString(); + } catch (IOException e) { + throw new RuntimeException("Could not read Java source file.", e); + } + } + + private Optional findSourceFile(File rootOfProject, String sourceFileName) throws IOException { + Path startingDir = Paths.get(rootOfProject.toURI()); + return Files.find(startingDir, 15, (path, attrs) -> path.endsWith(sourceFileName)) + .map(p -> new File(p.toUri())) + .findFirst(); + } + + private File findRootOfProject(File cwd) { + File[] pomFiles = cwd.listFiles((file, name) -> { return name.equals("pom.xml"); }); + if (pomFiles != null && pomFiles.length == 1) { + return cwd; + } else if (cwd.getParentFile() == null) { + throw new RuntimeException("Couldn't find directory containing pom.xml. Last looked in: " + cwd.getAbsolutePath()); + } else { + return findRootOfProject(cwd.getParentFile()); + } + } + + private String getSourceFileName(Class clazz) throws IOException { + String resourceName = clazz.getName().replace(".", "/").concat(".class"); + ClassReader reader = new ClassReader(clazz.getClassLoader().getResourceAsStream(resourceName)); + SourceFileNameVisitor sourceFileNameVisitor = new SourceFileNameVisitor(); + reader.accept(sourceFileNameVisitor, 0); + + return sourceFileNameVisitor.getSourceFile(); + } + + + private static final class SourceFileNameVisitor extends ClassVisitor { + + private String sourceFile = null; + private boolean visitedYet = false; + + public SourceFileNameVisitor() { + super(Opcodes.ASM5); + } + + @Override + public void visitSource(String source, String debug) { + this.visitedYet = true; + this.sourceFile = source; + super.visitSource(source, debug); + } + + public String getSourceFile() { + if (!visitedYet) throw new IllegalStateException("Must visit a class before asking for source file"); + return this.sourceFile; + } + } + +} diff --git a/src/test/java/org/adoptopenjdk/lambda/tutorial/util/HasConcreteMethod.java b/src/test/java/org/adoptopenjdk/lambda/tutorial/util/HasConcreteMethod.java new file mode 100644 index 0000000..e5da61c --- /dev/null +++ b/src/test/java/org/adoptopenjdk/lambda/tutorial/util/HasConcreteMethod.java @@ -0,0 +1,99 @@ +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.Matcher; +import org.hamcrest.TypeSafeDiagnosingMatcher; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import java.io.IOException; + +public class HasConcreteMethod extends TypeSafeDiagnosingMatcher> { + private final String methodName; + + public HasConcreteMethod(String defaultMethodName) { + this.methodName = defaultMethodName; + } + + public static Matcher> called(String defaultMethodName) { + return new HasConcreteMethod(defaultMethodName); + } + + @Override + protected boolean matchesSafely(Class item, Description mismatchDescription) { + if (!hasConcreteMethod(methodName, item)) { + mismatchDescription.appendText("did not have default method named ").appendText(methodName); + return false; + } + + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("a type with a default method called " + methodName); + } + + private boolean hasConcreteMethod(String defaultMethodName, Class clazz) { + try { + + String resourceName = clazz.getName().replace(".", "/").concat(".class"); + ClassReader reader = new ClassReader(clazz.getClassLoader().getResourceAsStream(resourceName)); + HasDefaultMethodVisitor sourceFileNameVisitor = new HasDefaultMethodVisitor(defaultMethodName); + reader.accept(sourceFileNameVisitor, 0); + + return sourceFileNameVisitor.defaultMethodExists(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + + private static final class HasDefaultMethodVisitor extends ClassVisitor { + + private final String defaultMethodName; + private boolean visitedYet = false; + private boolean hasDefaultMethod = false; + + public HasDefaultMethodVisitor(String defaultMethodName) { + super(Opcodes.ASM5); + this.defaultMethodName = defaultMethodName; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + visitedYet = true; + hasDefaultMethod |= name.equals(defaultMethodName) && ((access & Opcodes.ACC_ABSTRACT) == 0); + return super.visitMethod(access, name, desc, signature, exceptions); + } + + public boolean defaultMethodExists() { + if (!visitedYet) throw new IllegalStateException("Must visit a class before asking for result"); + return this.hasDefaultMethod; + } + } +} 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