Разбор задачи Тернарный оператор на функциональных интерфейсах.
Метод ternaryOperator() должен вернуть экземпляр Function. Поскольку Function — функциональный интерфейс, есть следующие способы его инстанцировать:
- создать экземпляр анонимного или именованного класса, реализующего интерфейс Function;
- воспользоваться ссылкой на метод;
- написать лямбда-выражение.
Компактнее всего будет решение через лямбда-выражение, именно его и ожидает проверяющая система. В итоге решение записывается в одну строку:
return x -> condition.test(x) ? ifTrue.apply(x) : ifFalse.apply(x);
Надо помнить, что condition, ifTrue и ifFalse — это обычные объекты, у них есть методы. Если забыли, какие у них методы, то можно воспользоваться подсказкой IDE или сходить в JavaDoc/исходники интерфейсов Predicate и Function. Нельзя просто взять и написать:
return condition(x) ? ifTrue(x) : ifFalse(x); // это не скомпилируется
В задаче был дополнительный вопрос про сигнатуру метода ternaryOperator(). Почему он объявлен именно так, а не более простым способом, без всяких <? super T> и <? extends U>? Можно ведь было объявить его так:
public static <T, U> Function<T, U> ternaryOperator(
Predicate<T> condition,
Function<T, U> ifTrue,
Function<T, U> ifFalse)
Если метод ternaryOperator() объявить таким способом, то код из примера не скомпилируется. В Java типы Predicate<Object> и Predicate<String> несовместимы, поэтому нельзя передать Predicate<Object> в метод, ожидающий Predicate<String>.
Это касалось <? super T>. Если же вместо <? extends U> использовать <U>, то не скомпилируется следующий пример, т.к. несовместимыми являются типы Function<Object, String> и Function<Object, CharSequence>.
Predicate<Object> condition = Objects::isNull;
Function<Object, String> ifTrue = obj -> "null";
Function<Object, String> ifFalse = Object::toString;
Function<Object, CharSequence> objectToCharSequence =
ternaryOperator(condition, ifTrue, ifFalse);
Решение задачи Минимум и максимум.
У стрима есть методы min() и max(), но воспользоваться ими «в лоб» нельзя, т.к. оба являются терминальными операциями. Использовав одну из них, вторую уже вызвать нельзя — стрим бросит IllegalStateException. Некоторые обходили это ограничение, собирая элементы стрима в коллекцию, из которой можно было получить новый стрим столько раз, сколько нужно. Это решение проходит тесты, но его большой недостаток — необходимость хранить в памяти все элементы стрима, которых может быть очень много. Мы заранее не знаем, сколько их будет.
Оптимальным решением является нахождение минимума и максимума за один проход по стриму без использования промежуточного хранилища элементов. Сделать это несложно:
public static <T> void findMinMax(
Stream<? extends T> stream,
Comparator<? super T> order,
BiConsumer<? super T, ? super T> minMaxConsumer) {
MinMaxFinder<T> minMaxFinder = new MinMaxFinder<>(order);
stream.forEach(minMaxFinder);
minMaxConsumer.accept(minMaxFinder.min, minMaxFinder.max);
}
private static class MinMaxFinder<T> implements Consumer<T> {
private final Comparator<? super T> order;
T min;
T max;
private MinMaxFinder(Comparator<? super T> order) {
this.order = order;
}
@Override
public void accept(T t) {
if (min == null || order.compare(t, min) < 0) {
min = t;
}
if (max == null || order.compare(max, t) < 0) {
max = t;
}
}
}
Обратите внимание, что решение не использует приведение типа к (T). Благодаря этому отсутствуют предупреждения компилятора о небезопасном приведении типов. В других решениях, где Consumer реализован как лямбда-выражение или как анонимный класс, избежать предупреждений было бы гораздо сложнее.
P.S. В лекциях были рассмотрены только последовательные стримы, однако в Java бывают еще и параллельные стримы, обрабатывающие свои элементы одновременно в нескольких потоках. Это решение для параллельных стримов не подходит. Но это уже совсем другая история...
Решение задачи Частотный анализ текста.
import java.io.IOException;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
public class Main {
public static void main(String[] args) throws IOException {
// Для чтения входного потока используем Scanner.
// Поскольку словами мы считаем последовательности символов,
// состоящие из букв или цифр, то в качестве разделителя слов Scanner'у
// указываем регулярное выражение, означающее
// "один или более символ, не являющийся ни буквой, ни цифрой".
Scanner scanner = new Scanner(System.in, "UTF-8")
.useDelimiter("[^\\p{L}\\p{Digit}]+");
// Пройдем по всем словам входного потока и составим Map<String, Integer>,
// где ключом является слово, преобразованное в нижний регистр,
// а значением - частота этого слова.
Map<String, Integer> freqMap = new HashMap<>();
scanner.forEachRemaining(s -> freqMap.merge(s.toLowerCase(), 1, (a, b) -> a + b));
freqMap.entrySet().stream() // получим стрим пар (слово, частота)
.sorted(descendingFrequencyOrder()) // отсортируем
.limit(10) // возьмем первые 10
.map(Map.Entry::getKey) // из каждой пары возьмем слово
.forEach(System.out::println); // выведем в консоль
}
// Создание Comparator'а вынесено в отдельный метод, чтобы не загромождать метод main.
private static Comparator<Map.Entry<String, Integer>> descendingFrequencyOrder() {
// Нам нужен Comparator, который сначала упорядочивает пары частоте (по убыванию),
// а затем по слову (в алфавитном порядке). Так и напишем:
return Comparator.<Map.Entry<String, Integer>>comparingInt(Map.Entry::getValue)
.reversed()
.thenComparing(Map.Entry::getKey);
}
}
Решение задачи MailService.
// MailMessage и Salary имеют практически идентичный интерфейс
// за исключением типа поля content. Давайте абстрагируем это знание в интерфейс.
public static interface Sendable<T> {
String getFrom();
String getTo();
T getContent();
}
// Абстрагируем логику хранения всех элементов класса во внутренних полях класса,
// создав класс SimpleSendable. Не забудем указать реализуемый интерфейс.
public static class SimpleSendable<T> implements Sendable<T> {
private String from, to;
private T content;
public SimpleSendable(String from, String to, T content) {
this.from = from;
this.to = to;
this.content = content;
}
@Override
public String getFrom() {
return from;
}
@Override
public String getTo() {
return to;
}
@Override
public T getContent() {
return content;
}
}
// Теперь объявим MailMessage и Salary, отнаследовавшись от SimpleSendable
// с конкретным параметром типа.
public static class MailMessage extends SimpleSendable<String> {
public MailMessage(String from, String to, String content) {
super(from, to, content);
}
}
public static class Salary extends SimpleSendable<Integer> {
public Salary(String from, String to, Integer content) {
super(from, to, content);
}
}
// forEachOrdered и forEach ожидают в качестве аргумента класс,
// реализующий интерфейс Consumer.
// Судя по исходному коду, Consumer потребляет письма с содержимым,
// соответствующим параметру класса MailService.
public static class MailService<T> implements Consumer<Sendable<T>> {
// Если внимательно посмотреть на исходный код задания, можно заметить,
// что логика метода get у получаемого в getMailBox Map'а при отсутствующем элементе
// отличается от логики стандартных коллекций. Переназначим эту логику в анонимном
// наследнике HashMap.
private Map<String, List<T>> messagesMap = new HashMap<String, List<T>>(){
@Override
public List<T> get(Object key) {
if (this.containsKey(key)) {
// Возвращать изменяемый список во внешний мир – не очень хорошая идея
// по причине того, что его изменение может испортить внутреннее состояние
// словаря. Лучше оборачивать подобный вывод в
// Collections.unmodifiableList.
// Однако здесь мы не можем так поступить по причине того,
// что добавлять новые элементы в списки из accept будет неудобно.
// Нужно реализовать дополнительный метод getMutable, который возвращал
// бы изменяемый список, удобный для модификации.
// Но добавить новый метод мы можем только в именованный класс.
return super.get(key);
} else {
// Collections.emptyList() возвращает один и тот же экземпляр
// неизменяемого списка. Если бы мы возвращали здесь, допустим,
// new ArrayList<>(), то множество вызовов get по отсутвующему
// элементу создавало бы множество лишних объектов.
return Collections.emptyList();
}
}
};
@Override
public void accept(Sendable<T> sendable) {
List<T> ts = messagesMap.get(sendable.getTo());
if (ts.size() == 0) {
ts = new ArrayList<>();
}
ts.add(sendable.getContent());
messagesMap.put(sendable.getTo(), ts);
}
public Map<String, List<T>> getMailBox() {
return messagesMap;
}
}