Статьи

Шаблон трансформатора

Шаблон 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();
}

Этот интерфейс сам по себе мало что делает, но я рассматриваю его как спецификацию для:

Подводя итог, эта пара ( 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() :

  • на две строки:
    1. "no change" (без операции)
    2. "Some Change"
  • используя три типа вызовов:
    1. прямой (базовый уровень)
    2. transform()
    3. 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, являются их собственными.