Шаблон Transformer — это шаблон проектирования для Java (и, возможно, других ОО-языков с только дисперсией сайта использования и инвариантными типами параметров), который помогает объектам в иерархии подтипов свободно преобразовывать себя в объекты любого типа.
контекст
Я следил за потоками OpenJDK ( 18-21 сентября, 12-13 ноября, 13-30 ноября , 3-4 декабря ), связанными с проблемой JDK-8203703 Джима Ласки, и мне пришла в голову идея. Позвольте мне повторить соответствующие части обсуждения.
Предложение String.transform
Предложение согласно JDK-8203703 сводится к следующему дополнению:
1
2
3
4
5
6
7
|
public final class String implements /*...*/ CharSequence { // ... public <R> R transform(Function<? super String, ? extends R> f) { return f.apply( this ); } // ... } |
Как видите, этот метод просто вызывает данную Function
для себя, и все. Тем не менее, это очень полезно для объединения служебных методов, подобных тем, которые используются в StringUtils от Apache Commons :
1
2
3
4
|
String result = string .toLowerCase() .transform(StringUtils::stripAccents) .transform(StringUtils::capitalize); |
Обычно мы должны написать:
1
|
String result = StringUtils.capitalize(StringUtils.stripAccents(string.toLowerCase())); |
Учитывая CharSequence.transform
В какой-то момент Алан Бейтман поднял вопрос о потенциальном определении transform
в CharSequence
как:
1
|
<R> R transform(Function<? super CharSequence, ? extends R> f) |
Это даст преимущество в том, что вы сможете применять CharSequence
основе CharSequence
(например, StringUtils.isNumeric ) к любому CharSequence
, например:
1
2
3
|
boolean isNumeric = charSequence .transform(s -> StringUtils.defaultIfBlank( '0' )) .transform(StringUtils::isNumeric); |
Однако, как отметил Реми Форакс, проблема с этой подписью заключается в том, что:
- если бы он был унаследован
String
: большинство служебных методов принимаютString
в качестве параметра — такие методы не будут работать (например, StringUtils :: capitalize ), - если он был переопределен
String
: полезное переопределение сделать невозможно, потому что:-
Function<? super String, R>
Function<? super String, R>
— это супертипFunction<? super CharSequence, R>
Function<? super CharSequence, R>
(что на самом деле хорошо), - Java не поддерживает контравариантные типы параметров (что является настоящим препятствием здесь).
-
В результате тема CharSequence.transform
была удалена.
проблема
Подводя итог, проблема состоит в том, чтобы иметь возможность преобразовать :
-
CharSequence
, используяFunction
которая принимаетCharSequence
илиObject
(? super CharSequence
), -
String
, используяFunction
которая принимаетString
или любой из ее супертипов (? super String
).
Когда я посмотрел на эти нижние границы здесь, я понял, что я уже видел такого рода проблемы (см. Шаблон Filterer ).
Таким образом, эта проблема сводится к следующему: как ковариантно указать контравариантную оценку для Function
.
Решение
Java не поддерживает контравариантные типы параметров , а ее синтаксис не позволяет ковариантно ( ? extends
) определять контравариантную ( ? super
) границу в одном объявлении. Однако это можно сделать в двух отдельных объявлениях с помощью промежуточного вспомогательного типа.
Предполагая, что мы хотим решить это для универсальной Function<? super T, ? extends R>
Function<? super T, ? extends R>
Function<? super T, ? extends R>
, нам нужно:
- переместите вышеуказанный параметр
Function
в интерфейс помощника, параметризованный с помощьюT
, - используйте этот вспомогательный интерфейс с верхней границей (
? extends T
) в качестве возвращаемого типа.
Интерфейс трансформатора
Я определил такой вспомогательный интерфейс (который я назвал Transformer
) следующим образом:
1
2
3
4
|
@FunctionalInterface interface Transformer<T> { <R> R by(Function<? super T, ? extends R> f); } |
Трансформируемый интерфейс
Определив Transformer
, мы можем определить следующий базовый интерфейс, называемый Transformable
:
1
2
3
|
interface Transformable { Transformer<?> transformed(); } |
Этот интерфейс сам по себе мало что делает, но я рассматриваю его как спецификацию для:
- разработчики подтипов : он напоминает им переопределить
transformed
метод с правильной верхней границей и реализовать его, - пользователи подтипов : это напоминает им, что они могут вызывать
transformed().by(f)
Подводя итог, эта пара ( Transformer
& Transformable
) позволяет нам заменить:
-
obj.transform(function)
- with:
obj.transformed().by(function)
Пример реализации
Прежде чем мы вернемся к String
, давайте посмотрим, насколько легко реализовать оба этих интерфейса:
01
02
03
04
05
06
07
08
09
10
11
|
class Sample implements Transformable { @Override public Transformer<Sample> transformed() { return this ::transform; // method reference } private <R> R transform(Function<? super Sample, ? extends R> f) { return f.apply( this ); } } |
Как видите, все, что нужно, это ссылка на метод для transform
.
Метод transform
был закрытым, чтобы в подтипах не возникало конфликтов, когда они определяют свое собственное (соответственно ограниченное снизу ) transform
.
Решение в контексте
Реализация в контексте
Как это может применяться к CharSequence
и String
? Во-первых, мы сделаем CharSequence
расширяемым Transformable
:
1
2
3
4
5
6
|
public interface CharSequence extends Transformable { // ... @Override Transformer<? extends CharSequence> transformed(); // ... } |
Затем мы реализовали transformed
в String
, возвращая ссылку на метод public transform
( добавлен в JDK 12 ):
1
2
3
4
5
6
7
8
|
public final class String implements /*...*/ CharSequence { // ... @Override public Transformer<String> transformed() { return this ::transform; } // ... } |
Обратите внимание, что мы внесли ковариантное изменение в возвращаемый тип transformed
: Transformer<? extends CharSequence>
Transformer<? extends CharSequence>
→ Transformer<String>
.
Риск совместимости
Я CharSequence.transformed
риск совместимости добавления CharSequence.transformed
как минимальный. Это может нарушить обратную совместимость только для тех подклассов CharSequence
, у которых уже есть transformed
метод без аргументов (который кажется маловероятным).
Использование в контексте
Использование для String
не изменится, потому что нет смысла вызывать transformed().by()
вместо transform()
.
Однако для использования универсального CharSequence
необходимо прибегнуть к transformed().by()
CharSequence
transformed().by()
поскольку он может иметь много реализаций, поэтому методы transform
должны быть private
:
1
2
3
|
boolean isNumeric = charSequence .transformed().by(s -> StringUtils.defaultIfBlank( '0' )) .transformed().by(StringUtils::isNumeric); |
Производительность
Если вы не знакомы с тем, как работает JVM (что чаще всего означает HotSpot ) и его JIT-компилятор , вы можете задаться вопросом, не повлияет ли это очевидное создание дополнительного объекта ( Transformer
in transformed
) на производительность.
К счастью, благодаря escape-анализу * и скалярной замене этот объект никогда не размещается в куче. Так что ответ: нет, не будет.
* Эта запись в Википедии содержит ложное утверждение: « Таким образом, компилятор может безопасно разместить оба объекта в стеке. «Как объясняет Алексей Шипилёв , Java не выделяет целые объекты в стеке.
эталонный тест
Если вам нужны доказательства, вот небольшой эталонный тест (с использованием превосходного жгута проводов JMH Алексея Шипилёва). Поскольку я не мог (легко) добавить необходимые методы в String
, я создал простую оболочку для String
и реализовал эталонный тест поверх него.
Тест тестирует работу toLowerCase()
:
- на две строки:
-
"no change"
(без операции) -
"Some Change"
-
- используя три типа вызовов:
- прямой (базовый уровень)
-
transform()
-
transformed().by()
Вы можете найти полный исходный код для этого теста в этой главе GitHub .
Вот результаты (запустить на Oracle JDK 8, заняло 50 минут):
1
2
3
4
5
6
7
8
9
|
Benchmark (string) Mode Cnt Score Error Units TransformerBenchmark.baseline no change avgt 25 22,215 ± 0,054 ns /op TransformerBenchmark.transform no change avgt 25 22,540 ± 0,039 ns /op TransformerBenchmark.transformed no change avgt 25 22,565 ± 0,059 ns /op TransformerBenchmark.baseline Some Change avgt 25 63,122 ± 0,541 ns /op TransformerBenchmark.transform Some Change avgt 25 63,405 ± 0,196 ns /op TransformerBenchmark.transformed Some Change avgt 25 62,930 ± 0,209 ns /op |
Как видите, для обеих строк нет разницы в производительности между тремя типами вызовов.
Резюме
Я понимаю, что Transformable
, вероятно, слишком «экстравагантен», чтобы действительно превращать его в JDK. На самом деле, даже один Transformer
, возвращаемый CharSequence
и String
, не стоит того. Это потому, что унарные операции над CharSequence
s не кажутся такими распространенными (например, StringUtils содержит всего несколько).
Тем не менее, я нашел общую идею Transformer
и Transformable
довольно заманчивым. Надеюсь, вам понравилось чтение, и вы найдете его полезным в определенных контекстах.
Опубликовано на Java Code Geeks с разрешения Томаша Линковски, партнера нашей программы JCG. Смотреть оригинальную статью здесь: Transformer Pattern Мнения, высказанные участниками Java Code Geeks, являются их собственными. |