Это первый день моего участия в первом испытании обновлений 2022. Подробную информацию о мероприятии см.:Вызов первого обновления 2022 г.
Немного в связи с предыдущей статьей:
Дети, научитесь учить лямбда-выражения!
Сосредоточьтесь на потоке, все может быть потоком
введение
Долгое время Java рассматривалась как объектно-ориентированный язык программирования, и идея «все является объектом» глубоко укоренилась в сердцах людей. Но с выходом Java 8, похоже, все изменилось. Внедрение лямбда-выражений и потоков вдохнуло новую жизнь в Java, позволив людям мыслить в рамках функционального программирования. Эта статья в основном знакомит с применением идей функционального программирования в Java.
Императив или декларативный?
Сначала взгляните на фрагмент кода: Рассчитайте максимальное значение цены товара. Обычно мы делаем это так:
int max = 0;
for(int price : prices) {
if (max < price) {
max = price;
}
}
Это типичный "императив«(императивное) письмо. Он использует инструкции или синтаксис компьютера, чтобы шаг за шагом сообщать компьютеру, что делать. Для той же функции рассмотрим другой способ записи:
int max = prices.stream().reduce(0, Math::max);
Этот краткий код "декларативный«(декларативное) письмо. Он больше похож на указание компьютеру, что делать, а не на то, чтобы обращать внимание на то, что происходит внутри компьютера.
Читатели, знакомые с принципами проектирования программного обеспечения, обнаружат, что именно здесь используются «Голливудские принципы» «Говори, не спрашивай!».
Реализация шаблонов проектирования с функциональным мышлением
В классической книге GoF «Шаблоны проектирования» подробно представлены 23 распространенных шаблона проектирования. Внимательные читатели могут обнаружить, что под названием книги есть небольшая строчка:Основы повторно используемого объектно-ориентированного программного обеспечения. То есть он реализуется с объектно-ориентированным мышлением. После всех этих лет споры о шаблонах проектирования продолжаются, но они уже не актуальны, сегодня давайте вернемся к шаблонам проектирования с точки зрения функционального программирования.
командный режим
Командный режим обычно инкапсулирует команды и предоставляет пользователям внешние интерфейсы для вызова. Давайте сначала рассмотрим пример: подметальный робот может выполнять такие команды, как движение прямо, поворот налево и поворот направо. Сначала определите интерфейс инструкции и класс реализации:
// 命令接口
public interface Command {
void execute();
}
// 前进命令实现
public class forward implements Command {
public void execute() {
System.out.println("go forward");
}
}
// 右转命令实现
public class Right implements Command {
@Override
public void execute() {
System.out.println("go right");
}
}
Следующее реализует робота:
public class Robot {
public static void move(Command... commands) {
for (Command command : commands) {
command.execute();
}
}
public static void main(String[] args) {
Robot.move(new Forward(), new Right());
}
}
Хоть функция и реализована, но есть одна проблема:Создано слишком много классов!
Бизнес-логика должна была быть в центре внимания, но она утонула во множестве реализаций классов.
Давайте посмотрим, как реализовано функциональное программирование?
Поскольку execute() в интерфейсе Command является методом без входных параметров и без возвращаемого результата, он естественно напоминает нам о встроенном функциональном интерфейсе Java Runnable, который также имеет метод run() с той же сигнатурой. Хотя интерфейс Runnable изначально использовался в многопоточности, здесь мы используем его в функциональном программировании.
Во-первых, мы заменяем входной параметр метода move() класса Robot на интерфейс Runnable:
public static void flexibleMove(Runnable... commands) {
Stream.of(commands).forEach(Runnable::run);
}
Таким образом, нам нужно только реализовать команду в методе класса.
public static void forward() {
System.out.println("go forward");
}
public static void right() {
System.out.println("go right");
}
Вызов становится:
Robot.flexibleMove(Robot::forward, Robot::right);
Эта реализация уменьшает количество классов реализации команд,Больше внимания уделяйте бизнес-логике.
режим стратегии
Шаблон стратегии может реализовать разделение интерфейса и реализации путем внедрения различных стратегий реализации. Давайте сначала рассмотрим шаблон стратегии, реализованный с помощью объектно-ориентированного мышления: задайте разные стратегии форматирования для текста, чтобы получить разные результаты.
Вот реализация кода:
Определите класс текстового редактора, конструктор реализует стратегию форматирования по умолчанию, и вы также можете установить другие стратегии форматирования с помощью методов,
public class Editor {
private Formatter formatter;
private String text;
public Editor(String text) {
this.text = text;
this.formatter = new DefaultFormatter();
}
public void setFormatter(Formatter formatter) {
this.formatter = formatter;
}
public String output() {
return formatter.format(text);
}
}
Реализация различных стратегий:
// 默认实现策略,也即原文本输出
public class DefaultFormatter implements Formatter {
@Override
public String format(String text) {
return text;
}
}
// 转成大写输出策略
public class UppercaseFormatter implements Formatter {
@Override
public String format(String text) {
return text.toUpperCase();
}
}
Звонок клиента:
String text = "Hello, World";
Editor editor = new Editor(text);
String defaultText = editor.output();
editor.setFormatter(new UppercaseFormatter());
String upperText = editor.output();
Это стандартная реализация шаблона стратегии, давайте посмотрим, как реализовать ее функционально?
Из метода формата форматтераИ входные параметры, и возвращаемые результаты имеют тип String., естественно подумайте о встроенном функциональном интерфейсе JavaUnaryOperator.
Вот реализация кода:
Текстовый класс:
public class EditorPlus {
private String text;
private UnaryOperator<String> formatter;
public EditorPlus(String text) {
this.text = text;
this.formatter = s -> s;
}
public void setFormatter(UnaryOperator<String> formatter) {
this.formatter = formatter;
}
public String output() {
return formatter.apply(text);
}
}
Звонок клиента:
EditorPlus editor = new EditorPlus("Hello, World");
String defaultText = editor.output();
editor.setFormatter(String::toUpperCase);
String upperText = editor.output();
Тип средства форматирования становится UnaryOperator, поэтому реализация по умолчанию может быть записана в виде лямбда-выражения: s->s, а другие стратегии форматирования также могут быть реализованы с помощью лямбда-выражений, поэтому нет необходимости писать так много классов реализации стратегии.
орнамент
Режим украшения может добавлять новые функции, не изменяя поведение исходного компонента, а сами функции могут накладываться друг на друга.
Давайте сначала рассмотрим требование, реализованное с использованием объектно-ориентированного мышления: добавление в камеру функций фильтра, а фильтров может быть несколько.
Сначала определите интерфейс устройства, который содержит только метод получения цвета:
public interface Equipment {
Color getColor();
}
Снова определите класс камеры и реализуйте интерфейс устройства:
public class Camera implements Equipment {
private Color color;
public Camera(Color color) {
this.color = color;
}
@Override
public Color getColor() {
return this.color;
}
}
Мы определяем еще один абстрактный класс для фильтра, который также реализует интерфейс устройства:
public abstract class FilterDecorator implements Equipment {
protected Equipment equipment;
public FilterDecorator(Equipment equipment) {
this.equipment = equipment;
}
public abstract Color getColor();
}
Затем определите два фильтра: осветлить и затемнить, наследуя абстрактный класс фильтра.
public class BrighterFilter extends FilterDecorator {
public BrighterFilter(Equipment equipment) {
super(equipment);
}
@Override
public Color getColor() {
return equipment.getColor().brighter();
}
}
public class DarkerFilter extends FilterDecorator {
public DarkerFilter(Equipment equipment) {
super(equipment);
}
@Override
public Color getColor() {
return equipment.getColor().darker();
}
}
Вызов клиента:
Equipment decoratedCamera = new DarkerFilter(
new BrighterFilter(
new Camera(new Color(155, 120, 30))));
decoratedCamera.getColor()
Этот код вполне удовлетворительный, давайте посмотрим, как реализовать его функционально:
Функция фильтра здесь заключается в преобразовании одного цвета в другой цвет, поэтому естественно думать о функциональном интерфейсе, который поставляется с Java.Function. Он имеет метод compose, который позволяет компоновать методы.
См. реализацию кода ниже:
public class Camera {
private Color color;
private Stream<Function<Color, Color>> filterStream;
public Camera(Color color, Function<Color, Color>... filters) {
this.color = color;
this.filterStream = Stream.of(filters);
}
public Color getColor() {
Function<Color, Color> composedFilter = filterStream.reduce(
(filter, next) -> filter.compose(next))
.orElse(color -> color);
return composedFilter.apply(this.color);
}
}
Вход камеры изменен с исходного интерфейса оборудования на список функций. И внутренне используйте метод reduce() в сочетании с методом compose() для достижения накопления функций. Код сильно упрощен.
Наконец, посмотрите на вызов клиента:
Camera camera = new Camera(new Color(155, 120, 30), Color::brighter,Color::darker);
camera.getColor();
Еще больше упростите свой код с помощью ссылок на методы.
Из функциональной реализации трех вышеперечисленных режимов видно, что,Использование функционального подхода позволяет значительно сократить количество классов, код становится более компактным, а семантика более понятной.
Разработка согласованных интерфейсов
Свободный интерфейс является внутреннимDSLРаспространенный дизайнерский прием. Обычно он использует шаблон проектирования Builder, чтобы позволить методу возвращать этот объект, чтобы метод можно было вызывать как цепочку для формирования согласованного интерфейса.
Давайте посмотрим на пример согласованного интерфейса:
Разработайте функцию отправки почты, включая адрес отправки, адрес назначения, получателя, тему, тему и т. д.
// 定义一个邮件发送builder
public class MailBuilder {
public MailBuilder from(final String address) {return this; }
public MailBuilder to(final String address) {return this; }
public MailBuilder subject(final String line) {return this; }
public MailBuilder body(final String message) {return this; }
public void send() { System.out.println("sending..."); }
}
Вызов клиента:
new MailBuilder()
.from("ding.yi@zte.com.cn")
.to("wxcop@zte.com.cn")
.subject("article")
.body("fp in java")
.send();
Именно так мы обычно используем когерентные интерфейсы. Но у него также есть две небольшие проблемы:
- Новый метод снижает читаемость согласованного интерфейса.
- Это по-прежнему «учебный» метод написания: сначала создайте тело письма, а затем отправьте письмо. Лучшей семантикой является способ mailer.send(mail) .
Можно ли сделать оба? Это можно сделать с помощью функциональных интерфейсов.
Во встроенном функциональном интерфейсе Java есть интерфейс Consumer, который можно использовать для получения параметров для потребления, что полностью соответствует нашему замыслу.
Код реализован следующим образом:
public class Mailer {
private Mailer() {}
public Mailer from(final String address) { return this; }
public Mailer to(final String address) { return this; }
public Mailer subject(final String line) { return this; }
public Mailer body(final String message) { return this; }
public static void send(final Consumer<Mailer> block) {
final Mailer mailer = new Mailer();
block.accept(mailer);
System.out.println("sending...");
}
}
Как видно из приведенной выше реализации, конструктор становится приватным, метод send() становится статическим и получает блок типа Consumer.
Таким образом, вызов клиента становится:
Mailer.send(mail ->
mail.from("ding.yi@zte.com.cn")
.to("wxcop@zte.com.cn")
.subject("article")
.body("fp in java"));
Такой код сохраняет связный интерфейс и более выразителен.
использовать ресурсы
Мы часто работаем с ресурсами в нашей повседневной разработке, такими как соединения с базой данных, операции с файлами, операции блокировки и т. д. У этих операций над ресурсами есть общая черта: сначала открыть ресурс, затем выполнить над ним операцию и, наконец, закрыть ресурс. Код обычно пишется так:
resource.open();
try {
doSomethingWith(resource);
} finally {
resource.close();
}
Это кусок шаблонного кода, он кажется простым, но в процессе его использования всегда найдутся нерадивые программисты, которые забудут добавить оператор finally для освобождения ресурсов, что приведет к утечкам памяти.
Есть ли способ автоматически закрыть ресурс после выполнения операции?
Возможно использование функционального интерфейса, можно передать интерфейс Consumer в шаблонный код, чтобы клиенту нужно было обращать внимание только на операцию обработки ресурса, а операция закрытия выполнялась автоматически.
Код реализован следующим образом:
public static void handle(Consumer<Resource> consumer) {
Resource resource = new Resource();
try {
consumer.accept(resource);
} finally {
resource.close();
}
}
Таким образом, когда клиент использует его, это можно записать как:
handle(resource -> doSomethinWith(resource));
Таким образом, вам больше не нужно беспокоиться об использовании ресурсов и забывании их освобождения!
ленивая загрузка
ленивая загрузкаЭто важная особенность функционального программирования. При правильном использовании она может значительно повысить производительность программного обеспечения. Давайте рассмотрим пример: выполните операцию «И» над длительной операцией. Сначала посмотрите на эффект отказа от отложенной загрузки:
public class Evaluation {
public static boolean evaluate(final int value) {
System.out.println("evaluating ..." + value);
try {
TimeUnit.SECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
return value > 100;
}
public static void eagerEvaluator(final boolean input1, final boolean input2) {
System.out.println("eagerEvaluator called...");
System.out.println("accept?: " + (input1 && input2));
}
public static void main(String[] args) throws InterruptedException {
eagerEvaluator(evaluate(1), evaluate(2));
}
}
Операция Evaluator() требует много времени. Например, она занимает 20 с. Метод энтузиастEvaluator() передает два логических результата и выполняет внутреннюю операцию "И". Поскольку оба метода Evaluator() должны выполняться, Общее время работы требует не менее 40 с.
Мы знаем, что операция && в Java является операцией короткого замыкания, то есть, если результат первого условия ложный, последующие условия не будут оцениваться, а ложное будет возвращено напрямую.
Можем ли мы использовать эту функцию? Если параметр, переданный в метод Assessment(), является функциональным интерфейсом, он не будет выполняться немедленно, а будет выполняться при фактическом вызове, таким образом достигается эффект использования операций короткого замыкания.
Вот реализация кода:
public static void lazyEvaluator(final Supplier<Boolean> input1, final Supplier<Boolean> input2) {
System.out.println("lazyEvaluator called...");
System.out.println("accept?: " + (input1.get() && input2.get()));
}
В метод lazyEvaluator() передается функциональный интерфейс поставщика, так что при вызове его можно записать так:
lazyEvaluator(() -> evaluate(1), () -> evaluate(2));
Лямбда-выражение, переданное таким образом, выполняется только при его вызове.Поскольку первое условие возвращает false, выполняется операция короткого замыкания, и результат возвращается напрямую. Таким образом, время работы сокращается вдвое, а производительность значительно повышается.
резюме
Хотя Java вводит элементы функционального программирования, возможно, Java все же не может стать языком функционального программирования, но это не мешает нам использовать мышление функционального программирования для решения проблем. В конце концов, проблемы в мире не противоположны, возможно, сочетание объектно-ориентированного и функционального программирования может дать неожиданные результаты.