Semana 12 — Lambdas y streams
Objetivos
- Escribir lambdas y referencias a métodos (method references) y explicar qué interfaz funcional (functional interface) implementan.
- Usar las interfaces funcionales de
java.util.functiony componer comparadores. - Procesar colecciones con streams: fuente, operaciones intermedias y una operación terminal.
- Agrupar y resumir datos con
Collectorsy manejar resultados ausentes conOptional.
Apuntes de la semana: notes/week-12.md · Código: code/week-12
Sesión A — Lambdas, interfaces funcionales, referencias a métodos
Lecturas:
- Eck, sec. 4.5 Lambda Expressions, subsecciones 4.5.1 a 4.5.4.
- Eck, subsecs. 5.8.3 Anonymous Inner Classes y 5.8.4 Local Classes and Lambda Expressions.
- Eck, subsec. 10.6.1 Generic Functional Interfaces.
- Eck, subsec. 10.4.3 Using a Comparator, ahora completa.
Lecturas complementarias:
- dev.java, Lambda Expressions: Writing Your First Lambda Expression, Using Lambdas Expressions in Your Application, Writing Lambda Expressions as Method References, Combining Lambda Expressions y Writing and Combining Comparators.
- API de Java 25: paquete
java.util.function. - JLS 25, §9.8 Functional Interfaces y §15.27 Lambda Expressions.
Temas:
- Del comparador como clase (semana 11) a la clase anónima y a la lambda: cuatro implementaciones (clase, clase anónima, lambda,
comparingInt), el mismo criterio de orden. La lambda es un objeto que implementa una interfaz con un solo método abstracto. - Ahora sí: la flecha de
assertThrows(..., () -> ...)que usamos como receta desde la semana 3 es una lambda que implementaExecutable. - Interfaces funcionales de la API:
Predicate<T>,Function<T, R>,Consumer<T>,Supplier<T>,UnaryOperator<T>,BinaryOperator<T>.@FunctionalInterface. - Referencias a métodos:
String::length,Song::title,System.out::println,ArrayList::new. - Variables capturadas: solo las effectively final (nunca se reasignan). Error común: intentar incrementar un contador local dentro de una lambda.
- Comparadores compuestos:
Comparator.comparing(Song::artist).thenComparing(Song::seconds).reversed(); métodos de colecciones que reciben lambdas:removeIf,forEach,Map.computeIfAbsent,Map.merge.
Sesión B — Streams
Lecturas:
- Eck, sec. 10.6 Introduction the Stream API, subsecciones 10.6.2 a 10.6.4.
Lecturas complementarias:
- dev.java, The Stream API: Processing Data in Memory Using the Stream API, Adding Intermediate Operations on a Stream, Adding a Terminal Operation on a Stream, Using a Collector as a Terminal Operation y Using Optionals.
- API de Java 25:
Stream,Collectors,Optional.
Temas:
- Un stream no es una colección: es un pipeline que se recorre una sola vez y es lazy (nada se ejecuta hasta la operación terminal).
- Fuentes:
list.stream(),Stream.of,IntStream.range. Intermedias:filter,map,sorted,distinct,limit. Terminales:count,sum,min,max,anyMatch,toList,collect,reduce. Collectors.groupingBy,counting,summingLong,joining,toMap. Ejemplo propio: duración total por artista de una lista de canciones, y los cinco artistas con más canciones.Optional<T>: lo que devuelvenfindFirstymax.orElse,orElseThrow,map. Error común: llamar aget()sin verificar, que es elNullPointerExceptionde siempre con otro nombre.- Error común: modificar una variable o una colección externa desde dentro de
mapoforEach(efectos secundarios); y reescribir con streams un bucle simple que era más claro como bucle. - Streams de primitivos (
IntStream,mapToInt) para evitar boxing en sumas y promedios.
Laboratorio — Reportes con streams
Código inicial en code/week-12/lab/: el record Sale, las ventas de ejemplo de una cafetería (SampleSales) y un esqueleto de prueba (SalesReportsTest). Los comparadores de la semana 11 y su prueba, ComparatorsTest, están copiados en code/week-12/.
- Reescribir con lambdas los comparadores de la semana 11 y comprobar que las pruebas siguen pasando.
- Escribir cada reporte primero con bucles y después con streams, con la misma prueba para ambas versiones: total vendido, ventas por día, producto más vendido, clientes con más de N compras.
- Agrupar con
groupingByy ordenar el resultado; devolverOptionaldonde el resultado puede no existir y probar ese caso. - Discutir en parejas cuál versión de cada reporte es más legible.
Entregas
- P3 entrega.
- P4 publicado: Reportes (lambdas y streams; refactoring con principios SOLID). Entrega en semana 15. Los reportes se escriben con lo de esta semana; el refactoring con SOLID se ve en la semana 13. Ver
projects/README.md.
Preguntas de repaso
- ¿Qué interfaz funcional implementa
s -> s.length() > 3? ¿YString::length? - ¿Por qué no compila incrementar una variable local
int countdentro de una lambda? ¿Cómo se cuenta con un stream? - ¿Qué imprime un stream con
filterymapque tiene unIO.printlndentro delmappero no tiene operación terminal? ¿Por qué? - Escriban con
groupingByun mapa de categoría a cantidad de productos. - ¿Qué ventaja tiene
maxdevolviendoOptional<T>frente a devolvernull?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (11)Pruebas (3)Laboratorio (3)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
import java.util.Comparator;
/** Ordena por artista y, entre canciones del mismo artista, por título. */
public class ByArtistThenTitle implements Comparator<Song> {
@Override
public int compare(Song a, Song b) {
int byArtist = a.artist().compareTo(b.artist());
if (byArtist != 0) {
return byArtist;
}
return a.title().compareTo(b.title());
}
}import java.util.Comparator;
/** Ordena canciones de la más corta a la más larga. */
public class ByDuration implements Comparator<Song> {
@Override
public int compare(Song a, Song b) {
return Integer.compare(a.seconds(), b.seconds());
}
}import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/** Métodos de las colecciones que reciben lambdas: removeIf, forEach, merge, computeIfAbsent. */
public class CollectionLambdasDemo {
public static void main(String[] args) {
List<Song> songs = Songs.sample();
songs.removeIf(s -> s.seconds() < 200); // sin iterador ni ConcurrentModificationException
songs.forEach(s -> IO.println("queda: " + s.title()));
// Contar palabras (semana 11) con merge: si no está, pone 1; si está, suma.
Map<String, Integer> counts = new TreeMap<>();
for (String w : "la garúa cae en lima en lima la garúa no moja".split(" ")) {
counts.merge(w, 1, Integer::sum);
}
IO.println(counts);
// Índice de artista a títulos con computeIfAbsent: crea la lista la primera vez.
Map<String, List<String>> byArtist = new TreeMap<>();
for (Song s : Songs.sample()) {
byArtist.computeIfAbsent(s.artist(), k -> new ArrayList<>()).add(s.title());
}
byArtist.forEach((artist, titles) -> IO.println(artist + " -> " + titles));
}
}import java.util.ArrayList;
import java.util.List;
import java.util.function.BinaryOperator;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
/** Las interfaces funcionales de java.util.function y las referencias a métodos. */
public class FunctionsDemo {
/** Una interfaz funcional propia: un solo método abstracto. */
@FunctionalInterface
interface Discount {
int apply(int cents);
}
public static void main(String[] args) {
Predicate<Song> isLong = s -> s.seconds() > 200;
Function<Song, String> label = s -> s.artist() + " - " + s.title();
Function<String, Integer> length = String::length; // método de instancia, objeto por parámetro
Consumer<String> show = IO::println; // método estático
Supplier<List<Song>> newList = ArrayList::new; // constructor
UnaryOperator<String> shout = String::toUpperCase;
BinaryOperator<Integer> add = Integer::sum;
Song garua = Songs.sample().get(0);
show.accept("isLong(Garúa) = " + isLong.test(garua));
show.accept("negate = " + isLong.negate().test(garua));
show.accept("label = " + label.apply(garua));
show.accept("andThen = " + label.andThen(length).apply(garua));
show.accept("shout = " + shout.apply("garúa"));
show.accept("add(185, 210) = " + add.apply(185, 210));
List<Song> longOnes = newList.get();
for (Song s : Songs.sample()) {
if (isLong.test(s)) {
longOnes.add(s);
}
}
show.accept("largas = " + longOnes.size());
Discount tenPercent = cents -> cents - cents / 10;
Discount minusFive = cents -> Math.max(0, cents - 500);
show.accept("descuentos = " + tenPercent.apply(2000) + " " + minusFive.apply(2000));
}
}import java.util.List;
import java.util.stream.Stream;
/** Los streams son lazy: nada corre hasta la terminal, y limit corta antes. */
public class LazyDemo {
public static void main(String[] args) {
List<Song> songs = Songs.sample();
Stream<String> pipeline = songs.stream()
.filter(s -> {
IO.println(" filter " + s.title());
return s.seconds() < 215;
})
.map(s -> {
IO.println(" map " + s.title());
return s.title().toUpperCase();
});
IO.println("pipeline armado, sin operación terminal todavía");
List<String> firstTwo = pipeline.limit(2).toList();
IO.println("resultado: " + firstTwo);
}
}import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import java.util.stream.Collectors;
/** Reportes sobre una lista de canciones, escritos con streams. */
public final class LibraryReports {
private LibraryReports() {
}
/** Duración total (en segundos) de cada artista, por orden alfabético de artista. */
public static Map<String, Integer> totalDurationByArtist(List<Song> songs) {
return songs.stream()
.collect(Collectors.groupingBy(Song::artist, TreeMap::new,
Collectors.summingInt(Song::seconds)));
}
/** Cuántas canciones tiene cada artista, por orden alfabético de artista. */
public static Map<String, Long> songsPerArtist(List<Song> songs) {
return songs.stream()
.collect(Collectors.groupingBy(Song::artist, TreeMap::new, Collectors.counting()));
}
/** Los n artistas con más canciones; si empatan, por orden alfabético. */
public static List<String> topArtists(List<Song> songs, int n) {
return songsPerArtist(songs).entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed()
.thenComparing(Map.Entry.comparingByKey()))
.limit(n)
.map(Map.Entry::getKey)
.toList();
}
/** La canción más larga, si la lista no está vacía. */
public static Optional<Song> longest(List<Song> songs) {
return songs.stream().max(Comparator.comparingInt(Song::seconds));
}
/** Los títulos de un artista, ordenados y separados por comas. */
public static String titlesBy(List<Song> songs, String artist) {
return songs.stream()
.filter(s -> s.artist().equals(artist))
.map(Song::title)
.sorted()
.collect(Collectors.joining(", "));
}
}import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
/** Los reportes de LibraryReports sobre la biblioteca de ejemplo. */
public class ReportDemo {
public static void main(String[] args) {
List<Song> library = Songs.library();
IO.println("canciones: " + library.size());
IO.println("duración total: " + LibraryReports.totalDurationByArtist(library));
IO.println("por artista: " + LibraryReports.songsPerArtist(library));
IO.println("top 3 artistas: " + LibraryReports.topArtists(library, 3));
IO.println("la más larga: " + LibraryReports.longest(library).map(Song::title).orElse("-"));
IO.println("de Duna: " + LibraryReports.titlesBy(library, "Duna"));
IO.println("lista vacía: " + LibraryReports.longest(List.of()));
// toMap: una entrada por elemento; aquí, de título a duración (los títulos no se repiten).
Map<String, Integer> secondsByTitle = Songs.sample().stream()
.collect(Collectors.toMap(Song::title, Song::seconds, Integer::sum, TreeMap::new));
IO.println("toMap: " + secondsByTitle);
}
}/**
* Una canción: título, artista y duración en segundos.
* El orden natural es por título; si empatan, por artista y luego por duración.
* Así compareTo da 0 solo cuando equals da true (es consistente con equals).
*/
public record Song(String title, String artist, int seconds) implements Comparable<Song> {
@Override
public int compareTo(Song other) {
int byTitle = title.compareTo(other.title);
if (byTitle != 0) {
return byTitle;
}
int byArtist = artist.compareTo(other.artist);
if (byArtist != 0) {
return byArtist;
}
return Integer.compare(seconds, other.seconds);
}
}import java.util.ArrayList;
import java.util.List;
/** La lista de canciones de ejemplo (inventadas) que usan las demostraciones. */
public final class Songs {
private Songs() {
}
/** Devuelve una lista nueva (modificable) con seis canciones. */
public static List<Song> sample() {
List<Song> songs = new ArrayList<>();
songs.add(new Song("Garúa", "Costa Verde", 220));
songs.add(new Song("Puente de piedra", "Los Andinos", 245));
songs.add(new Song("Malecón", "Costa Verde", 190));
songs.add(new Song("Arena", "Duna", 185));
songs.add(new Song("Cumbre", "Los Andinos", 190));
songs.add(new Song("Brisa", "Duna", 210));
return songs;
}
/** Una biblioteca más grande (doce canciones, cinco artistas) para los reportes. */
public static List<Song> library() {
List<Song> songs = sample();
songs.add(new Song("Neblina", "Costa Verde", 200));
songs.add(new Song("Olas", "Costa Verde", 175));
songs.add(new Song("Quebrada", "Los Andinos", 260));
songs.add(new Song("Oasis", "Duna", 230));
songs.add(new Song("Totora", "Lago Azul", 205));
songs.add(new Song("Selva", "Río Verde", 240));
return songs;
}
}import java.util.List;
import java.util.Optional;
import java.util.stream.IntStream;
import java.util.stream.Stream;
/** Fuente, operaciones intermedias y una terminal; Optional; streams de primitivos. */
public class StreamDemo {
public static void main(String[] args) {
List<Song> songs = Songs.sample();
List<String> longTitles = songs.stream() // fuente
.filter(s -> s.seconds() > 200) // intermedia
.map(Song::title) // intermedia
.sorted() // intermedia
.toList(); // terminal
IO.println("largas: " + longTitles);
long dunas = songs.stream().filter(s -> s.artist().equals("Duna")).count();
int total = songs.stream().mapToInt(Song::seconds).sum();
double average = songs.stream().mapToInt(Song::seconds).average().orElse(0);
IO.println("Duna: " + dunas + ", total: " + total + " s, promedio: " + average + " s");
boolean anyShort = songs.stream().anyMatch(s -> s.seconds() < 180);
List<String> artists = songs.stream().map(Song::artist).distinct().sorted().toList();
IO.println("¿alguna de menos de 3 min? " + anyShort + "; artistas: " + artists);
Optional<Song> longest = songs.stream().max((a, b) -> Integer.compare(a.seconds(), b.seconds()));
IO.println("la más larga: " + longest.map(Song::title).orElse("(ninguna)"));
Optional<Song> none = songs.stream().filter(s -> s.seconds() > 600).findFirst();
IO.println("¿más de 10 min? " + none.isPresent() + " -> " + none.map(Song::title).orElse("(ninguna)"));
int sumOfSquares = IntStream.rangeClosed(1, 4).map(n -> n * n).sum();
Integer reduced = Stream.of(185, 210, 190).reduce(0, Integer::sum);
IO.println("cuadrados de 1 a 4, sumados = " + sumOfSquares + "; reduce = " + reduced);
// Un stream se recorre una sola vez.
Stream<Song> once = songs.stream();
IO.println("primera vez: " + once.count());
try {
IO.println("segunda vez: " + once.count());
} catch (IllegalStateException e) {
IO.println(e);
}
}
}import java.util.Comparator;
import java.util.List;
/** El mismo comparador escrito de cuatro maneras: clase, clase anónima, lambda y referencia a método. */
public class ThreeWaysDemo {
static String titles(List<Song> songs) {
StringBuilder sb = new StringBuilder();
for (Song s : songs) {
sb.append(s.title()).append(s.seconds() == 190 ? "*" : "").append(" ");
}
return sb.toString().strip();
}
public static void main(String[] args) {
// 1. Una clase con nombre, en su propio archivo (semana 11).
Comparator<Song> byClass = new ByDuration();
// 2. Una clase anónima: se declara y se instancia en el mismo lugar.
Comparator<Song> byAnonymous = new Comparator<Song>() {
@Override
public int compare(Song a, Song b) {
return Integer.compare(a.seconds(), b.seconds());
}
};
// 3. Una lambda: solo los parámetros y el cuerpo del único método abstracto.
Comparator<Song> byLambda = (a, b) -> Integer.compare(a.seconds(), b.seconds());
// 4. Un static factory method que recibe la función que extrae la clave.
Comparator<Song> byKey = Comparator.comparingInt(Song::seconds);
List<Comparator<Song>> all = List.of(byClass, byAnonymous, byLambda, byKey);
String[] names = {"clase", "anónima", "lambda", "comparingInt"};
for (int i = 0; i < all.size(); i++) {
List<Song> songs = Songs.sample();
songs.sort(all.get(i));
IO.println(String.format("%-13s %s", names[i] + ":", titles(songs)));
}
// Comparadores compuestos: por artista, luego de la más larga a la más corta.
List<Song> songs = Songs.sample();
songs.sort(Comparator.comparing(Song::artist)
.thenComparing(Song::seconds, Comparator.reverseOrder()));
IO.println("artista, luego duración descendente:");
songs.forEach(s -> IO.println(" " + s.artist() + " - " + s.title() + " (" + s.seconds() + ")"));
songs.sort(Comparator.comparing(Song::title).reversed());
IO.println("título, al revés: " + titles(songs));
}
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeSet;
import org.junit.jupiter.api.Test;
class ComparatorsTest {
private static List<String> titles(List<Song> songs) {
List<String> result = new ArrayList<>();
for (Song s : songs) {
result.add(s.title());
}
return result;
}
@Test
void byDurationSortsShortestFirstAndKeepsTiesInOrder() {
List<Song> songs = Songs.sample();
songs.sort(new ByDuration());
// Malecón y Cumbre duran 190: el orden es estable, quedan como estaban.
assertEquals(List.of("Arena", "Malecón", "Cumbre", "Brisa", "Garúa", "Puente de piedra"),
titles(songs));
}
@Test
void byArtistThenTitleBreaksTiesByTitle() {
List<Song> songs = Songs.sample();
songs.sort(new ByArtistThenTitle());
assertEquals(List.of("Garúa", "Malecón", "Arena", "Brisa", "Cumbre", "Puente de piedra"),
titles(songs));
}
@Test
void treeSetWithByDurationDropsSongsWithTheSameDuration() {
TreeSet<Song> set = new TreeSet<>(new ByDuration());
set.addAll(Songs.sample());
assertEquals(5, set.size());
}
}import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
class LibraryReportsTest {
private final List<Song> library = Songs.library();
@Test
void totalDurationByArtistAddsEverySong() {
Map<String, Integer> totals = LibraryReports.totalDurationByArtist(Songs.sample());
assertEquals(Map.of("Costa Verde", 410, "Duna", 395, "Los Andinos", 435), totals);
}
@Test
void topArtistsBreaksTiesAlphabetically() {
// Costa Verde tiene 4; Duna y Los Andinos, 3 cada uno.
assertEquals(List.of("Costa Verde", "Duna", "Los Andinos"), LibraryReports.topArtists(library, 3));
assertEquals(List.of("Costa Verde"), LibraryReports.topArtists(library, 1));
}
@Test
void longestOfEmptyListIsEmpty() {
assertTrue(LibraryReports.longest(List.of()).isEmpty());
assertEquals("Quebrada", LibraryReports.longest(library).orElseThrow().title());
}
@Test
void titlesByUnknownArtistIsEmptyText() {
assertEquals("Arena, Brisa, Oasis", LibraryReports.titlesBy(library, "Duna"));
assertEquals("", LibraryReports.titlesBy(library, "Nadie"));
}
@Test
void theArrowOfAssertThrowsIsALambda() {
// La receta de siempre, con la lambda guardada en una variable de tipo Executable.
Executable getFromEmpty = () -> LibraryReports.longest(List.of()).orElseThrow();
assertThrows(NoSuchElementException.class, getFromEmpty);
}
}import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Comparator;
import java.util.List;
import org.junit.jupiter.api.Test;
// Las cuatro formas de escribir "por duración" ordenan exactamente igual.
class ThreeWaysTest {
private static List<Song> sortedWith(Comparator<Song> order) {
List<Song> songs = Songs.sample();
songs.sort(order);
return songs;
}
@Test
void lambdaAndComparingIntAgreeWithByDuration() {
List<Song> expected = sortedWith(new ByDuration());
assertEquals(expected, sortedWith((a, b) -> Integer.compare(a.seconds(), b.seconds())));
assertEquals(expected, sortedWith(Comparator.comparingInt(Song::seconds)));
}
@Test
void reversedTurnsTheOrderAround() {
List<Song> songs = sortedWith(Comparator.comparingInt(Song::seconds).reversed());
assertEquals("Puente de piedra", songs.get(0).title());
assertEquals("Arena", songs.get(songs.size() - 1).title());
}
}Laboratorio
Archivos de partida del laboratorio de la semana.
package lab;
import java.time.LocalDate;
/**
* Una venta de la cafetería: qué día, quién compró, qué, cuántas unidades y a qué
* precio unitario (en céntimos).
*/
public record Sale(LocalDate date, String customer, String item, int quantity, int unitPrice) {
public Sale {
if (quantity <= 0 || unitPrice <= 0) {
throw new IllegalArgumentException("cantidad y precio deben ser positivos");
}
}
/** El importe de la venta en céntimos. */
public int total() {
return quantity * unitPrice;
}
}package lab;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
// Esqueleto del laboratorio. Para cada reporte, una prueba que se corre contra la
// versión con bucles y contra la versión con streams (con los mismos valores esperados).
class SalesReportsTest {
@Test
void sampleDataHasTwelveSales() {
assertEquals(12, SampleSales.sales().size());
}
// TODO: total vendido, ventas por día, producto más vendido,
// clientes con más de N compras (y el caso en que no hay ninguno).
}package lab;
import java.time.LocalDate;
import java.util.List;
/** Ventas de ejemplo de una cafetería durante tres días (datos inventados). */
public final class SampleSales {
private SampleSales() {
}
private static final LocalDate MON = LocalDate.of(2026, 11, 2);
private static final LocalDate TUE = LocalDate.of(2026, 11, 3);
private static final LocalDate WED = LocalDate.of(2026, 11, 4);
public static List<Sale> sales() {
return List.of(
new Sale(MON, "alice", "café americano", 2, 800),
new Sale(MON, "bob", "empanada", 1, 650),
new Sale(MON, "carol", "café americano", 1, 800),
new Sale(MON, "alice", "alfajor", 3, 350),
new Sale(TUE, "dave", "jugo de naranja", 1, 1000),
new Sale(TUE, "alice", "café americano", 1, 800),
new Sale(TUE, "bob", "café con leche", 2, 900),
new Sale(TUE, "erin", "empanada", 2, 650),
new Sale(WED, "carol", "alfajor", 4, 350),
new Sale(WED, "alice", "café con leche", 1, 900),
new Sale(WED, "bob", "café americano", 1, 800),
new Sale(WED, "dave", "empanada", 1, 650));
}
}Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 12: lambdas, interfaces funcionales, referencias a métodos y streams.
# make compila todo a build/ (incluido el código inicial del laboratorio, en lab/)
# make test corre las demostraciones y las pruebas JUnit
# make clean borra build/
JAVAC = javac
JFLAGS = -Xlint:all -Werror
SRC = $(shell find . -name '*.java' -not -path './build/*')
DEMOS = ThreeWaysDemo FunctionsDemo CollectionLambdasDemo StreamDemo LazyDemo ReportDemo
all: build/.stamp
build/.stamp: $(SRC)
mkdir -p build
$(JAVAC) $(JFLAGS) -cp $(JUNIT_JAR) -d build $(SRC)
touch $@
test: all
@for d in $(DEMOS); do echo "== $$d"; java -cp build $$d || exit 1; done
java -jar $(JUNIT_JAR) execute --class-path build --scan-class-path \
--disable-banner --disable-ansi-colors --details=tree
clean:
rm -rf build
.PHONY: all test clean