6. Generics. Collections. Streams



Разбор задачи Тернарный оператор на функциональных интерфейсах.

Метод ternaryOperator() должен вернуть экземпляр Function. Поскольку Function — функциональный интерфейс, есть следующие способы его инстанцировать:
  1. создать экземпляр анонимного или именованного класса, реализующего интерфейс Function;
  2. воспользоваться ссылкой на метод;
  3. написать лямбда-выражение.
Компактнее всего будет решение через лямбда-выражение, именно его и ожидает проверяющая система. В итоге решение записывается в одну строку:
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;
    }
}