Аннотировать конструктор Java как реализующий @FunctionalInterface

Я хотел бы использовать этот функциональный интерфейс из Spring:

@FunctionalInterface
public interface RowMapper<T> {
    T mapRow(ResultSet rs, int rowNum) throws SQLException;
}

Вот способ использовать его, явно объявив константу RowMapper, которая просто вызовет конструктор:

namedParameterJdbcTemplate.queryForObject(sql, parameters, ValueObject.ROW_MAPPER);

public class ValueObject {
    public static final RowMapper<ValueObject> ROW_MAPPER = (resultSet, rowNum) -> new ValueObject(resultSet);

    public final long field;

    public ValueObject(ResultSet resultSet) throws SQLException {
        field = resultSet.getLong("FIELD");
    }
}

Видите ли: я не использую аргумент rowNum.

Я хотел бы иметь более лаконичный и выразительный код. Я хотел бы использовать конструктор напрямую, не объявляя RowMapper:

namedParameterJdbcTemplate.queryForObject(sql, parameters, ValueObject::new);

public class ValueObject {
    public final long field;

    public ValueObject(ResultSet resultSet, int unusedRowNumFromRowMapperInterface) throws SQLException {
        field = resultSet.getLong("FIELD");
    }
}

Намного чище, но IDE и Sonar теперь жалуются на неиспользуемый параметр.

Я мог бы добавить @SuppressWarnings({unused, java:S1172}) к этому параметру. Но это загрязнило бы чистое решение: я не хочу, чтобы другие разработчики проекта слепо копировали/вставляли это вуду-заклинание для каждого создаваемого ими объекта ValueObject. И я не хочу, чтобы они создавали константу + шаблон конструктора.

Есть ли способ сообщить компилятору, что конструктор фактически реализует RowMapper @FunctionalInterface, чтобы он знал, что требуется второй аргумент, даже если он не используется?

Или другой менее прямой способ избавиться от предупреждения?

Я попытался создать эту аннотацию, чтобы аннотировать неиспользуемый параметр осмысленным именем, инкапсулируя детали реализации удаления предупреждений, но это тоже не работает:

@Target(ElementType.PARAMETER)
@Inherited
@SuppressWarnings({"unused", "java:S1172"})
public @interface ThisParameterIsFromRowMapperInterface {
}

person Sebien    schedule 15.01.2021    source источник


Ответы (2)


Аннотировать конструктор Java как реализующий @FunctionalInterface

Это все равно, что просить «Добавить угол к зеленому цвету». это не имеет никакого смысла.

Смысл FunctionalInterface заключается в том, чтобы пометить интерфейс как определяющий «функцию», в том смысле, что вы можете написать лямбда-синтаксис ((a, b) -> result) или конструкцию ссылки на метод (foo::bar) в месте, где значение этот интерфейс необходим, и тогда javac автоматически исправит все для вас, чтобы он работал.

Вы не аннотируете метод (или конструктор), который соответствует шаблону и может использоваться в качестве ссылки на метод.

Намного чище, но IDE и Sonar теперь жалуются на неиспользуемый параметр.

Вы слышали притчу о докторе?

Пациент спросил врача: Доктор, мне больно, когда я нажимаю сюда!

Врач сказал: Хорошо. Ну перестань так делать!

Проблема в вашей IDE/Sonar, а не в вашем коде. Выключите эту функцию «проверки» / «линтера», это глупо, и она отключена по умолчанию, поэтому кто-то включил ее, думая (ошибочно), что это полезная проверка.

Существует правильная версия этой проверки: тогда и только тогда, когда инструмент линтера имеет полное представление обо всей иерархии методов (то есть не только сам метод, но и все методы, которые его переопределяют, все методы, которые этот метод переопределяет, и все внешние по отношению к этой кодовой базе методы, которые могут переопределить его в будущем), и этот параметр игнорируется для всех этих методов, тогда можно предупредить об этом.

Учитывая, что у линтеров нет хрустального шара, он начинается с обеспечения того, чтобы метод был либо эффективно final, либо эффективно (пакетно) приватным, чтобы уменьшить набор неизвестных внешних до 0. Проверка не может применяться к любому нефинальному общедоступному методу. что-нибудь: возможно, этот параметр существует для кода, который переопределит этот метод. (Подумайте об этом: когда у вас есть метод abstract, все параметры «игнорируются», так как кода вообще нет!)

Если бы этот более интеллектуальный подход был включен, здесь не появилось бы никакого предупреждения: лямбда-выражения переопределяют что-то по определению.

Есть ли способ сообщить компилятору, что конструктор фактически реализует RowMapper @FunctionalInterface, чтобы он знал, что требуется второй аргумент, даже если он не используется?

Нет. Вы можете создать второй конструктор, который принимает второй параметр (типа int), только для того, чтобы он полностью игнорировал этот параметр, но это будет фактически вызывать это предупреждение линтера, если это интеллектуальный линтинг, как конструкторы по определению не может быть переопределено что-то и не может быть переопределено, таким образом квалифицируя проверку «игнорируемого параметра» как полезную, разве это не иронично?

Я настоятельно рекомендую не создавать конструктор, который фактически нуждается в следующем javadoc:

/**
 * This constructor completely ignores the second parameter.
 * It is intended to be used in the form of `MyType::new`,
 * when you need a `RowMapper`.
 * <strong>NB: Any other use is neccessarily a bug</strong>.
 */

Потому что, ну, читай. Выделение предупреждений, выделенных жирным шрифтом, для объяснения того, что предполагаемое использование вещи вызывает удивление, — это плохо: вам не нужны сюрпризы в вашей кодовой базе, и вам не нужны методы, которые могут быть неправильно поняты без прочтения документации.

Я попытался создать эту аннотацию, чтобы аннотировать неиспользуемый параметр значимым именем, инкапсулируя детали реализации удаления предупреждений, но это тоже не работает

Это не работает; аннотации не «мета», как это. Вы можете аннотировать определение аннотации, но это не означает, что «аннотирование вещи этой аннотацией подразумевает, что вещь аннотирована всеми этими аннотациями». Это может означать это, но только если аннотация (и связанные с ней инструменты!) определены для такой работы, потому что они не встроены в java. @SuppressWarnings так не работает.

Итак, что мне делать в этой ситуации?

Я предлагаю вам попробовать что-то вроде этого:

public class WhateverYouHaveThere {
    public static RowMapper asRowMapper() {
        return (rs, idx) -> new WhateverYouHaveThere(rs);
    }
}

Этот метод не нуждается ни в javadoc, ни в комментариях: название метода + модификатор static на 100% покрывает его назначение, и реализация не вызывает ни малейшего удивления.

И затем, конечно же, исправьте тупоголовую настройку в вашем линтере :) — тот факт, что он будет предупреждать об этом методе, является достаточным доказательством того, что он просто мешает и не дает вам никакой значимой информации о подозрительном коде.

Затем он позволяет вам написать:

public static final RowMapper<ValueObject> ROW_MAPPER = ValueObject.asRowMapper();

NB: вы также можете сделать это статическим полем:

public class ValueObject {
    public static final RowMapper<ValueObject> ROW_MAPPER =
      (rs, idx) -> new ValueObject(rs);
}

Но я бы не стал. Скорее стильная вещь. Здесь также применимо множество аргументов в пользу геттеров, а не публичных конечных полей. В основном: предоставьте себе больше гибкости в будущем, чтобы добавить ведение журнала, изменить реализации и т. д.

person rzwitserloot    schedule 15.01.2021
comment
Спасибо за такой подробный ответ: я полностью согласен! Да, предупреждения — это только подсказки. Но они также невероятно полезны в других случаях. В этом случае, если я собираюсь порекомендовать решение для всего проекта, я бы не хотел содержать какую-либо пограничную хрупкую конструкцию. Здесь предупреждение указывает на странный/хрупкий дизайн, в котором каждый разработчик время от времени удалял бы этот параметр только для того, чтобы обнаружить, что он необходим, или назвать его по-другому, не оставляя ни малейшего представления о том, почему он здесь. Я не знал, что предпочитаю статические методы вместо статических полей: я постараюсь применять это чаще. - person Sebien; 20.01.2021
comment
Вы заставили меня задуматься о другом варианте asRowMapper. Я добавил подробный ответ на вопрос. Ваш asRowMapper() похож на статическое поле, которое я использовал: этот шаблон должен быть в каждом классе: я переместил его в одно центральное место. - person Sebien; 20.01.2021
comment
Но они также невероятно полезны в других случаях. Не в этом. Полезна «интеллектуальная» проверка неиспользуемых параметров (которая срабатывает только для сигнатур двустороннего частного/внутрипроектного под-линтинга). Одеяло просто не то. Это даже не "граница", это бесполезная проверка, выключите ее. Здесь предупреждение указывает на странный/хрупкий дизайн, в котором каждый разработчик время от времени удаляет этот параметр Это присуще дизайну лямбда-выражений. Сообщество в целом приняло это; вы используете код от сообщества; вы должны жить с этим. Линтер помочь не может. - person rzwitserloot; 21.01.2021

Спасибо rzwitserloot за подробный ответ. Он заставил меня задуматься о другой вариации asRowMapper:

@FunctionalInterface
public interface SimpleRowMapper<T> {
    T mapRow(ResultSet rs) throws SQLException;
}

public class RowMapperUtil {
    public static <T> RowMapper<T> asRowMapper(SimpleRowMapper<T> mapper) {
        return (resultSet, rowNum) -> mapper.mapRow(resultSet);
    }
}

namedParameterJdbcTemplate.queryForObject(sql, parameters, asRowMapper(ValueObject::new));

Конструктор не привязан к одному использованию: его можно повторно использовать для других целей (будучи другим @FunctionalInterface или нет), он чист, и asRowMapper не нужно копировать/вставлять/адаптировать для каждого ValueObject (и если мы добавим ведение журнала или другие относится к методу, мы можем сделать это в одном месте).

Единственным недостатком является то, что это класс Util: эти классы могут легко разрастаться множеством несвязанных методов, каждый из которых страдает от запаха кода Feature Envy.

Чтобы решить эту проблему, всегда полезно инкапсулировать наши зависимости в интерфейсы/классы, которыми мы владеем, чтобы мы могли адаптировать их к использованию в нашем проекте. Это означало бы инкапсуляцию NamedParameterJdbcTemplate в своего рода прокси-класс, которым мы владеем, и в который мы можем добавить наш собственный метод, принимающий параметр SimpleRowMapper:

public <T> T queryForObject(String sql, SqlParameterSource paramSource, SimpleRowMapper<T> rowMapper) throws DataAccessException { ... }

Таким образом, мы можем оставить конструктор только с одним параметром и по-прежнему использовать его без asRowMapper():

namedParameterJdbcTemplate.queryForObject(sql, parameters, ValueObject::new);

И/или попросите Spring включить этот новый метод в свой собственный NamedParameterJdbcTemplate, если они считают, что вариант использования достаточно важен для поддержки.

person Sebien    schedule 20.01.2021