Файл: Передача кода с параметризацией поведения Эта глава охватывает Приспособление к изменяющимся требованиям.docx
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 19.03.2024
Просмотров: 8
Скачиваний: 0
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
Лекция 2
Глава 2. Передача кода с параметризацией поведения
Эта глава охватывает
-
Приспособление к изменяющимся требованиям -
Параметризация поведения -
Анонимные классы -
Предварительный просмотр лямбда-выражений -
Реальные примеры: Comparator, Runnable и GUI
Хорошо известная проблема разработки программного обеспечения заключается в том, что независимо от того, что вы делаете, требования пользователей будут меняться. Например, представьте себе приложение, помогающее фермеру разобраться в своих запасах. Фермер может захотеть найти все зеленые яблоки в своем инвентаре. Но на следующий день он может сказать вам: «Вообще-то я тоже хочу найти все яблоки тяжелее 150 г». Через два дня фермер возвращается и добавляет: «Было бы здорово, если бы я мог найти все яблоки зеленого цвета и весом более 150 г». Как вы можете справиться с этими меняющимися требованиями?
В идеале вы хотели бы свести к минимуму свои инженерные усилия. Кроме того, аналогичные новые функции должны быть простыми в реализации и поддерживаться в долгосрочной перспективе.
Параметризация поведения — это шаблон разработки программного обеспечения, позволяющий справляться с частыми изменениями требований. В двух словах, это означает взять блок кода и сделать его доступным без выполнения. Этот блок кода может быть вызван позже другими частями
ваших программ, что означает, что вы можете отложить выполнение этого блока кода. Например, вы можете передать блок кода в качестве аргумента другому методу, который выполнит его позже. В результате поведение метода параметризуется на основе этого блока кода. Например, если вы обрабатываете коллекцию, вы можете написать метод, который
-
Может сделать «что-то» для каждого элемента списка -
Может сделать «что-то еще», когда закончите обработку списка -
Может сделать «еще что-то еще», если вы столкнулись с ошибкой
Это то, к чему относится параметризация поведения. Вот аналогия: ваш сосед по комнате знает, как доехать до супермаркета и вернуться домой. Вы можете попросить его купить список вещей, таких как хлеб, сыр и вино. Это эквивалентно вызову метода goAndBuy передает список продуктов в качестве аргумента. Но однажды ты в офисе, и тебе нужно, чтобы он сделал то, чего никогда раньше не делал, — забрал посылку с почты. Вам нужно передать ему список инструкций: пойти на почту, использовать этот номер ссылки, поговорить с менеджером и забрать посылку. Вы можете передать ему список инструкций по электронной почте, и когда он его получит, он сможет следовать инструкциям. Теперь вы сделали нечто более сложное, что эквивалентно методу goAndDo, который может выполнять различные новые действия в качестве аргументов.
Мы начнем эту главу с примера того, как вы можете развивать свой код, чтобы он был более гибким для меняющихся требований. Опираясь на эти знания, мы покажем, как использовать параметризацию поведения на нескольких примерах из реальной жизни.
Например, вы, возможно, уже использовали шаблон параметризации поведения, используя существующие классы и интерфейсы в Java API для сортировки списка, фильтрации имен файлов или указания потоку выполнить блок кода или даже выполнить обработку событий графического интерфейса. . Вы скоро поймете, что этот шаблон исторически многословен в Java. Лямбда-выражения в Java 8 и далее решают проблему многословия. В главе 3 мы покажем, как создавать лямбда-выражения, где их использовать и как можно сделать свой код более кратким, приняв их.
-
Как справиться с изменяющимися требованиями
Написание кода, способного справиться с меняющимися требованиями, — сложная задача. Давайте рассмотрим пример, который мы будем постепенно улучшать, демонстрируя некоторые рекомендации по повышению гибкости вашего кода. В контексте приложения для инвентаризации ферм вам необходимо реализовать функциональность для фильтрации зеленых яблок из списка. Звучит легко, правда?
-
Первая попытка: фильтрация зеленых яблок
Предположим, как и в главе 1, у вас есть перечисление Color для представления различных цветов яблока:
enum Color { RED, GREEN }
Первое решение может быть следующим:
public static List
List
for(Apple apple: inventory){
if( GREEN.equals(apple.getColor() ) {
result.add(apple);
}
}
return result;
}
Выделенная строка показывает условие, необходимое для выбора зеленых яблок. Вы можете предположить, что у вас есть перечисление Color с доступным набором цветов, например GREEN. Но теперь фермер передумал и хочет также фильтровать красные яблоки. Что ты можешь сделать?
Наивным решением было бы продублировать ваш метод, переименовать его в filterRedApples и изменить условие if, чтобы оно соответствовало красным яблокам. Однако этот подход плохо справляется с изменениями, если фермеру нужно несколько цветов. Вот хороший принцип: когда вы обнаружите, что пишете почти повторяющийся код, попробуйте вместо этого абстрагироваться.
-
Вторая попытка: параметризация цвета
Как избежать дублирования большей части кода в filterGreenApples для создания фильтра RedApples? Чтобы параметризовать цвет и быть более гибким к таким изменениям, вы можете добавить параметр в свой метод:
Color color) {
List
for (Apple apple: inventory) {
if ( apple.getColor().equals(color) ) {
result.add(apple);
}
}
return result;
}
Теперь вы можете осчастливить фермера и вызвать свой метод следующим образом:
List
List
…
Слишком легко, да? Немного усложним пример. Фермер возвращается к вам и говорит: «Было бы здорово различать легкие яблоки и тяжелые яблоки.
Тяжелые яблоки обычно имеют вес более 150 г».
Надев шляпу инженера-программиста, вы заранее понимаете, что фермер может захотеть изменить вес. Таким образом, вы создаете следующий метод, чтобы справиться с различными весами через дополнительный параметр:
public static List
int weight) {
List
For (Apple apple: inventory){
if ( apple.getWeight() > weight ) {
result.add(apple);
}
}
return result;
}
Это хорошее решение, но обратите внимание, что вам приходится дублировать большую часть реализации для обхода инвентаря и применения критериев фильтрации к каждому яблоку. Это несколько разочаровывает, потому что нарушает DRY (не повторяйтесь) принцип разработки программного обеспечения. Что делать, если вы хотите изменить обход фильтра для повышения производительности? Теперь вам нужно изменить реализацию всех ваших методов вместо только одного. Это дорого с точки зрения инженерных усилий.
Вы можете объединить цвет и вес в один метод, называемый фильтром. Но тогда вам все равно нужен способ различать, по какому атрибуту вы хотите фильтровать. Вы можете добавить флаг, чтобы различать запросы цвета и веса. (Но никогда не делайте этого! Вскоре мы объясним, почему.)
-
Третья попытка: фильтрация по всем возможным атрибутам
Уродливая попытка объединить все атрибуты может выглядеть следующим образом:
public static List
List
for (Apple apple: inventory) {
if ( (flag && apple.getColor().equals(color)) ||
(!flag && apple.getWeight() > weight) ){
result.add(apple);
}
}
return result;
}
Вы можете использовать это следующим образом (но это некрасиво):
List
List
...
Это решение очень плохое. Во-первых, клиентский код выглядит ужасно. Что означают истинное и ложное? Кроме того, это решение плохо справляется с изменяющимися требованиями.
Что, если фермер попросит вас отфильтровать по различным атрибутам яблока, например, по размеру, форме, происхождению и т. д.? Кроме того, что, если фермер потребует от вас более сложных запросов, сочетающих атрибуты, например, зеленые яблоки, которые к тому же тяжелые? У вас будет либо несколько повторяющихся методов фильтрации, либо один чрезвычайно сложный метод. До сих пор вы параметризовали метод filterApples такими значениями, как String, Integer, тип перечисления или логическое значение. Это может быть хорошо для определенных четко определенных проблем. Но в этом случае вам нужен лучший способ сообщить вашему методу filterApples критерии выбора яблок. В следующем разделе мы опишем, как использовать параметризацию поведения для достижения такой гибкости.
-
Параметризация поведения
В предыдущем разделе вы видели, что вам нужен лучший способ, чем добавление множества параметров, чтобы справиться с меняющимися требованиями. Давайте сделаем шаг назад и найдем лучший уровень абстракции. Одним из возможных решений является моделирование критериев выбора: вы работаете с яблоками и возвращаете логическое значение на основе некоторых атрибутов Apple. Например, он зеленый? Он тяжелее 150 г? Мы называем это предикатом (функция, возвращающая логическое значение). Поэтому давайте определим интерфейс для моделирования критериев выбора:
public interface ApplePredicate{
boolean test (Apple apple);
}
Теперь вы можете объявить несколько реализаций ApplePredicate для представления разных критериев выбора, как показано ниже (и показано на рис. 2.1):
public class AppleHeavyWeightPredicate implements ApplePredicate {
public boolean test(Apple apple) {
return apple.getWeight() > 150;
}
}
public class AppleGreenColorPredicate implements ApplePredicate {
public boolean test(Apple apple) {
return GREEN.equals(apple.getColor());
}
}
Отбирает только тяжелые яблоки
Выбирает только зеленые яблоки
Вы можете рассматривать эти критерии как различные варианты поведения метода фильтра. То, что вы только что сделали, связано с шаблоном разработки стратегии (см. http://en.wikipedia.org/wiki/Strategy_pattern), который позволяет определить семейство алгоритмов, инкапсулировать каждый алгоритм (называемый стратегией) и выбрать алгоритм. во время выполнения. В этом случае семейством алгоритмов является ApplePredicate, а различными стратегиями являются AppleHeavyWeightPredicate и AppleGreenColorPredicate.
Но как можно использовать различные реализации ApplePredicate?
Вам нужен метод filterApples, чтобы принимать объекты ApplePredicate для проверки условия на Apple. Вот что означает параметризация поведения: способность указать методу, что он должен принимать несколько вариантов поведения (или стратегий) в качестве параметров и использовать их внутренне для выполнения различных действий.
Рисунок 2.1 Различные стратегии выбора Apple
Чтобы добиться этого в работающем примере, вы добавляете параметр в метод filterApples для получения объекта ApplePredicate. Это дает большое преимущество при разработке программного обеспечения: теперь вы можете разделить логику перебора коллекции внутри метода Apple фильтра с поведением, которое вы хотите применить к каждому элементу коллекции (в данном случае к предикату).
-
Четвертая попытка: фильтрация по абстрактным критериям
Наш модифицированный метод фильтра, использующий ApplePredicate, выглядит следующим образом:
public static List
ApplePredicate p) {
List
for(Apple apple: inventory) {
if(p.test(apple)) {
result.add(apple);
}
}
return result;
}
ПЕРЕДАЧА КОДА/ПОВЕДЕНИЯ
Стоит сделать небольшую паузу для небольшого праздника. Этот код гораздо более гибкий, чем наша первая попытка, но в то же время его легко читать и использовать! Теперь вы можете создавать различные объекты ApplePredicate и передавать их в метод filterApples.
Рисунок 2.2 Параметризация поведения filterApples и передача различных стратегий фильтрации
Свободная гибкость! Например, если фермер просит вас найти все красные яблоки весом более 150 г, все, что вам нужно сделать, — это создать класс, реализующий ApplePredicate соответствующим образом. Ваш код теперь достаточно гибок для любого изменения требований, связанных с атрибутами Apple:
public class AppleRedAndHeavyPredicate implements ApplePredicate {
public boolean test(Apple apple){
return RED.equals(apple.getColor())
&& apple.getWeight() > 150;
}
}
List
filterApples(inventory, new AppleRedAndHeavyPredicate());
Вы достигли чего-то крутого; поведение метода filterApples зависит от кода, который вы передаете ему через объект ApplePredicate. Вы параметризовали поведение метода filterApples!
Обратите внимание, что в предыдущем примере единственным важным кодом является реализация метода тестирования, как показано на рис. 2.2; именно это определяет новое поведение метода filterApples. К сожалению, поскольку метод filterApples может принимать только объекты, вам придется поместить этот код в объект ApplePredicate.
Рисунок 2.3 Параметризация поведения filterApples и передача различных стратегий фильтрации
То, что вы делаете, похоже на передачу встроенного кода, потому что вы передаете логическое выражение через объект, реализующий тестовый метод. В разделе 2.3 (и более подробно в главе 3) вы увидите, что с помощью лямбда-выражений можно напрямую передать выражение RED.equals(apple.getColor()) && apple.getWeight() > 150 фильтруApples. без необходимости определять несколько классов ApplePredicate.
Это убирает ненужную многословность.
НЕСКОЛЬКО ПОВЕДЕНИЙ, ОДИН ПАРАМЕТР
Как мы объясняли ранее, параметризация поведения хороша тем, что позволяет разделить логику повторения коллекции для фильтрации и поведение, применяемое к каждому элементу этой коллекции. Как следствие, вы можете повторно использовать один и тот же метод и задавать ему разные варианты поведения для достижения разных целей, как показано на рис. 2.3.
Вот почему параметризация поведения является полезной концепцией, которую вы должны иметь в своем наборе инструментов для создания гибких API.
Чтобы убедиться, что вам нравится идея параметризации поведения, попробуйте пройти тест 2.1!
Тест 2.1: Напишите гибкий метод prettyPrintApple
Напишите метод prettyPrintApple, который принимает список яблок и может быть параметризован несколькими способами для создания вывода String из яблока (немного похоже на несколько настраиваемых методов toString). Например, вы можете сообщить методу prettyPrintApple, что он будет печатать только вес каждого яблока. Кроме того, вы можете указать вашему методу prettyPrintApple печатать каждое яблоко по отдельности и указать, тяжелое оно или легкое. Решение похоже на примеры фильтрации, которые мы рассматривали до сих пор. Чтобы помочь вам приступить к работе, мы приводим грубый скелет симпатичного метода PrintApple:
public static void prettyPrintApple(List
for(Apple apple: inventory) {
String output = ???.???(apple);
System.out.println(output);
}
}
Отвечать:
Во-первых, вам нужен способ представить поведение, которое принимает Apple и возвращает отформатированный результат String. Вы сделали что-то подобное, когда создавали интерфейс ApplePredicate:
(продолжение)
public interface AppleFormatter {
String accept(Apple a);
}
Теперь вы можете представить несколько режимов форматирования, реализовав интерфейс Apple Formatter:
public class AppleFancyFormatter implements AppleFormatter {
public String accept(Apple apple) {
String characteristic = apple.getWeight() > 150 ? "heavy" :
"light";
return "A " + characteristic +
" " + apple.getColor() +" apple";
}
}
public class AppleSimpleFormatter implements AppleFormatter {
public String accept(Apple apple) {
return "An apple of " + apple.getWeight() + "g";
}
}
Наконец, вам нужно указать методу prettyPrintApple принимать объекты AppleFormatter и использовать их внутри. Вы можете сделать это, добавив параметр в prettyPrintApple:
public static void prettyPrintApple(List
AppleFormatter formatter)
{
for(Apple apple: inventory) {
String output = formatter.accept(apple);
System.out.println(output);
}
}
Бинго! Теперь вы можете передать несколько поведений вашему методу prettyPrintApple. Вы делаете это, создавая экземпляры реализации AppleFormatter и передавая их в качестве аргументов для prettyPrintApple:
public static void prettyPrintApple(List
Это приведет к выводу в соответствии со строками
светло-зеленое яблоко
Тяжелое красное яблоко
…
Или попробуйте это:
prettyPrintApple(inventory, new AppleFancyFormatter());
Это приведет к выводу в соответствии с строками
Яблоко 80г.
Яблоко 155г.
…
Вы видели, что вы можете абстрагироваться от поведения и заставить свой код адаптироваться к изменениям требований, но этот процесс слишком многословен, потому что вам нужно объявить несколько классов, экземпляры которых вы создаете только один раз. Давайте посмотрим, как это улучшить.
-
Борьба с многословием
Мы все знаем, что функции или концепции, которые громоздки в использовании, будут избегать. На данный момент, когда вы хотите передать новое поведение вашему методу filterApples, вы вынуждены объявить несколько классов, реализующих интерфейс ApplePredicate и затем создайте экземпляры нескольких объектов ApplePredicate, которые вы выделяете только один раз, как показано в следующем листинге, который суммирует то, что вы видели до сих пор. Там много многословия, и это трудоемкий процесс!
public class AppleHeavyWeightPredicate implements ApplePredicate {
public boolean test(Apple apple) {
return apple.getWeight() > 150;
}
}
public class AppleGreenColorPredicate implements ApplePredicate {
public boolean test(Apple apple) {
return GREEN.equals(apple.getColor());
}
}
public class FilteringApples {
public static void main(String...args) {
List
new Apple(155, GREEN),
new Apple(120, RED));
List
filterApples(inventory, new AppleHeavyWeightPredicate());
List
filterApples(inventory, new AppleGreenColorPredicate());
}
public static List
ApplePredicate p) {
List
for (Apple apple : inventory) {
if (p.test(apple)){
result.add(apple);
}
}
return result;
}
}
Это ненужные накладные расходы. Можете ли вы сделать лучше? В Java есть механизмы, называемые анонимными классами, которые позволяют вам одновременно объявлять и создавать экземпляры класса. Они позволяют вам улучшить свой код еще на один шаг, сделав его немного более кратким. Но они не совсем удовлетворительны. Раздел 2.3.3 предвосхищает следующую главу с кратким обзором того, как лямбда-выражения могут сделать ваш код более читабельным.
-
Анонимные классы
Анонимные классы похожи на локальные классы (класс, определенный в блоке), с которыми вы уже знакомы в Java. Но у анонимных классов нет имени. Они позволяют вам объявлять и создавать экземпляр класса одновременно. Короче говоря, они позволяют создавать специальные реализации.
-
Пятая попытка: использование анонимного класса
В следующем коде показано, как переписать пример фильтрации, создав объект, реализующий ApplePredicate с помощью анонимного класса:
List
public boolean test(Apple apple){
return RED.equals(apple.getColor());
}
});
Анонимные классы часто используются в контексте приложений с графическим интерфейсом для создания объектов обработчиков событий. Мы не хотим навивать болезненные воспоминания о Swing, но следующий пример является распространенным шаблоном, который вы видите на практике (здесь используется JavaFX API, современная платформа пользовательского интерфейса для Java):
button.setOnAction(new EventHandler
public void handle(ActionEvent event) {
System.out.println("Whoooo a click!!");
}
});
Но анонимные классы все еще недостаточно хороши. Во-первых, они имеют тенденцию быть громоздкими, потому что занимают много места, как показано здесь в коде, выделенном полужирным шрифтом, с использованием тех же двух примеров, которые использовались ранее:
List
public boolean test(Apple a){
return RED.equals(a.getColor());
}
});
button.setOnAction(new EventHandler
public void handle(ActionEvent event) {
System.out.println("Whoooo a click!!");
}
Во-вторых, многие программисты считают их сложными в использовании. Например, в викторине 2.2 показана классическая Java-головоломка, которая застает большинство программистов врасплох! Попробуйте свои силы в этом.
Викторина 2.2: Головоломка анонимного класса
Что будет на выходе при выполнении этого кода: 4, 5, 6 или 42?
public class MeaningOfThis {
public final int value = 4;
public void doIt() {
int value = 6;
Runnable r = new Runnable() {
public final int value = 5;
public void run(){
int value = 10;
System.out.println(this.value);
}
};
r.run();
}
public static void main(String...args) {
MeaningOfThis m = new MeaningOfThis();
m.doIt();
}
}
Ответ:
Ответ — 5, потому что это относится к охватывающему Runnable, а не охватывающему классу MeaningOfThis.
Многословие вообще плохо; это не поощряет использование языковой функции, потому что для написания и поддержки подробного кода требуется много времени, и его неприятно читать!
Хороший код должен быть понятным с первого взгляда. Несмотря на то, что анонимные классы несколько решают проблему многословия, связанную с объявлением нескольких конкретных классов для интерфейса, они все равно неудовлетворительны. В контексте передачи простого фрагмента кода (например, логического выражения, представляющего критерий выбора) вам все равно придется создать объект и явно реализовать метод для определения нового поведения (например, метод test для Predicate или дескриптор метода для EventHandler).
В идеале мы хотели бы побудить программистов использовать шаблон параметризации поведения, потому что, как вы только что видели, он делает ваш код более адаптивным к изменениям требований. В главе 3 вы увидите, что разработчики языка Java 8 решили эту проблему, введя лямбда-выражения — более лаконичный способ передачи кода. Достаточно sus pense; вот краткий предварительный обзор того, как лямбда-выражения могут помочь вам в вашем стремлении к чистому коду.
-
Шестая попытка: использование лямбда-выражения
Предыдущий код можно переписать на Java 8 следующим образом, используя лямбда-выражение:
List
filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor()));
Вы должны признать, что этот код выглядит намного чище, чем наши предыдущие попытки! Это здорово, потому что он начинает выглядеть намного ближе к постановке проблемы. Теперь мы решили проблему многословия. Рисунок 2.4 суммирует наше путешествие.
Рисунок 2.4 Параметризация поведения и параметризация значений
-
Седьмая попытка: абстрагирование над типом List
Есть еще один шаг, который вы можете сделать на пути к абстракции. На данный момент метод filterApples работает только для Apple. Но вы также можете абстрагироваться от типа списка, чтобы выйти за рамки проблемной области, о которой вы думаете, как показано ниже:
public interface Predicate
boolean test(T t);
}
public static
List
for(T e: list) {
if(p.test(e)) {
result.add(e);
}
}
return result;
}
Теперь вы можете использовать фильтр метода со списком бананов, апельсинов, целых чисел или строк! Вот пример с использованием лямбда-выражений:
List
filter(inventory, (Apple apple) -> RED.equals(apple.getColor()));
List
filter(numbers, (Integer i) -> i % 2 == 0);
Разве это не круто? Вам удалось найти золотую середину между гибкостью и лаконичностью, что было невозможно до Java 8!
-
Реальные примеры
Теперь вы увидели, что параметризация поведения — это полезный паттерн, позволяющий легко адаптироваться к изменяющимся требованиям. Этот шаблон позволяет инкапсулировать поведение (фрагмент кода) и параметризовать поведение методов, передавая и используя эти созданные вами поведения (например, различные предикаты для Apple). Ранее мы упоминали, что этот подход похож на шаблон разработки стратегии. Возможно, вы уже использовали этот образец на практике. Многие методы в API Java можно параметризовать с различным поведением. Эти методы часто используются вместе с анонимными классами. Мы покажем четыре примера, которые должны закрепить для вас идею передачи кода: сортировка с помощью Comparator, выполнение блока кода с помощью Runnable, возврат результата задачи с помощью Callable и обработка событий графического интерфейса.
-
Сортировка компаратором
Сортировка коллекции — повторяющаяся задача программирования. Например, предположим, что ваш фермер хочет, чтобы вы сортировали запасы яблок по их весу. Или, возможно, он передумал и хочет, чтобы вы рассортировали яблоки по цвету. Звучит знакомо? Да, вам нужен способ представления и использования различных способов сортировки, чтобы легко адаптироваться к изменяющимся требованиям.
В Java 8 список поставляется с методом сортировки (вы также можете использовать Collections.sort). Поведение сортировки можно параметризовать с помощью объекта java.util.Comparator со следующим интерфейсом:
/ java.util.Comparator
public interface Comparator
int compare(T o1, T o2);
}
Таким образом, вы можете создать различные варианты поведения для метода сортировки, создав специальную реализацию Comparator. Например, вы можете использовать его для сортировки инвентаря по возрастанию веса с помощью анонимного класса:
inventory.sort(new Comparator
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
});;
Если фермер изменит свое мнение о том, как сортировать яблоки, вы можете создать специальный компаратор, соответствующий новому требованию, и передать его методу сортировки. Внутренние детали того, как сортировать, абстрагируются. С лямбда-выражением это будет выглядеть как:
inventory.sort(
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
Опять же, пока не беспокойтесь об этом новом синтаксисе; в следующей главе подробно рассказывается, как писать и использовать лямбда-выражения.
-
Выполнение блока кода с помощью Runnable
Потоки Java позволяют выполнять блок кода одновременно с остальной частью программы. Но как вы можете указать потоку, какой блок кода он должен выполнять? Каждый из нескольких потоков может выполнять различный код. Что вам нужно, так это способ представить фрагмент кода, который будет выполняться позже. До Java 8 в конструктор Thread можно было передавать только объекты, поэтому типичным неуклюжим шаблоном использования была передача анонимного класса, содержащего метод run, возвращающий void (без результата). Такие анонимные классы реализуют интерфейс Runnable.
В Java вы можете использовать интерфейс Runnable для представления блока кода, который должен быть выполнен; обратите внимание, что код возвращает void (без результата):
// java.lang.Runnable
public interface Runnable {
void run();
}
Вы можете использовать этот интерфейс для создания потоков с выбранным вами поведением следующим образом:
Thread t = new Thread(new Runnable() {
public void run() {
System.out.println("Hello world");
}
});
Но начиная с Java 8 вы можете использовать лямбда-выражение, поэтому вызов Thread будет выглядеть так:
Thread t = new Thread(() -> System.out.println("Hello world"));
-
Возврат результата с помощью Callable
Возможно, вы знакомы с абстракцией ExecutorService, представленной в Java 5. Интерфейс ExecutorService разделяет способы отправки и выполнения задач. Что полезно по сравнению с использованием потоков и Runnable, так это то, что с помощью ExecutorService вы можете отправить задачу в пул потоков и сохранить ее результат в Future. Не волнуйтесь, если это вам незнакомо, мы вернемся к этой теме в следующих главах, когда будем более подробно обсуждать параллелизм. На данный момент все, что вам нужно знать, это то, что интерфейс Callable используется для моделирования задачи, которая возвращает результат. Вы можете увидеть его как обновленный Runnable:
// java.util.concurrent.Callable
public interface Callable
V call();
}
Вы можете использовать его следующим образом, отправив задачу в службу-исполнитель. Здесь вы возвращаете имя потока, который отвечает за выполнение задачи:
ExecutorService executorService = Executors.newCachedThreadPool();
Future
@Override
public String call() throws Exception {
return Thread.currentThread().getName();
}
});
Используя лямбда-выражение, этот код упрощается до следующего:
Future
() -> Thread.currentThread().getName());
-
Обработка событий графического интерфейса
Типичным шаблоном в программировании графического интерфейса является выполнение действия в ответ на определенное событие, такое как щелчок или наведение курсора на текст. Например, если пользователь нажимает кнопку «Отправить», вы можете отобразить всплывающее окно или, возможно, записать действие в файл. Опять вам нужен способ справиться с изменениями; вы должны быть в состоянии выполнить любой ответ. В JavaFX вы можете использовать EventHandler для представления ответа на событие, передав его в setOnAction:
button.setOnAction(new EventHandler
public void handle(ActionEvent event) {
label.setText("Sent!!");
}
});
Здесь поведение метода setOnAction параметризуется объектами EventHandler. С лямбда-выражением это будет выглядеть так:
button.setOnAction((ActionEvent event) -> label.setText("Sent!!"));
Резюме
-
Параметризация поведения — это способность метода принимать несколько различных поведений в качестве параметров и использовать их внутренне для достижения различных поведений. -
Параметризация поведения позволяет сделать ваш код более адаптивным к изменяющимся требованиям и сэкономить на инженерных усилиях в будущем. -
Передача кода — это способ задать новое поведение в качестве аргументов метода. Но до Java 8 он был многословным. До Java 8 анонимные классы немного помогали избавиться от многословия, связанного с объявлением нескольких конкретных классов для интерфейса, которые нужны только один раз. -
Java API содержит множество методов, которые можно параметризовать с различными режимами работы, включая сортировку, потоки и обработку графического пользовательского интерфейса.