Статьи

С осторожностью перегружайте методы API

Методы перегрузки являются сильной концепцией в дизайне API, особенно когда ваш API — это свободный API или DSL ( Domain Specific Language ).

Это касается jOOQ, где вы часто хотите использовать одно и то же имя метода для различных средств взаимодействия с библиотекой.







Пример: условия jOOQ

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
package org.jooq;
 
 
 
public interface Condition {
 
 
 
    // Various overloaded forms of the "AND" operation:
 
    Condition and(Condition other);
 
    Condition and(String sql);
 
    Condition and(String sql, Object... bindings);
 
 
 
    // [...]
 
}

Все эти методы связывают два условия друг с другом с помощью оператора «И». В идеале реализации зависят друг от друга, создавая единую точку отказа. Это делает вещи сухими :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package org.jooq.impl;
 
 
 
abstract class AbstractCondition implements Condition {
 
 
 
    // The single point of failure
 
    @Override
 
    public final Condition and(Condition other) {
 
        return new CombinedCondition(
 
            Operator.AND, Arrays.asList(this, other));
 
    }
 
 
 
    // "Convenience methods" delegating to the other one
 
    @Override
 
    public final Condition and(String sql) {
 
        return and(condition(sql));
 
    }
 
 
 
    @Override
 
    public final Condition and(String sql, Object... bindings) {
 
        return and(condition(sql, bindings));
 
    }
 
}

Беда с дженериками и перегрузками

При разработке с Eclipse мир Java 5 кажется более блестящим, чем он есть на самом деле. Varargs и дженерики были представлены как синтаксический сахар в Java 5. На самом деле они не существуют в JVM таким образом. Это означает, что компилятор должен правильно связывать вызовы методов, выводить типы при необходимости и создавать синтетические методы в некоторых случаях. Согласно JLS ( Спецификация языка Java ), существует много двусмысленности, когда varargs / generics используются в перегруженных методах.

Давайте рассмотрим дженерики:

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
// This is a convenience method:
 
public static <T> Field<T> myFunction(Field<T> field, T value) {
 
    return myFunction(field, val(value));
 
}
 
 
 
// It's equivalent to this one.
 
public static <T> Field<T> myFunction(Field<T> field, Field<T> value) {
 
    return MyFunction<T>(field, value);
 
}

Вышеуказанное работает очень хорошо в большинстве случаев. Вы можете использовать приведенный выше API следующим образом:

1
2
3
4
5
6
7
8
9
Field<Integer> field1  = //...
 
Field<String>  field2  = //...
 
 
 
Field<Integer> result1 = myFunction(field1, 1);
 
Field<String>  result2 = myFunction(field2, "abc");

Но проблема возникает, когда <T> связан с Object!

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
// While this works...
 
Field<Object>  field3  = //...
 
Field<Object>  result3 = myFunction(field3, new Object());
 
 
 
// ... this doesn't!
 
Field<Object>  field4  = //...
 
Field<Object>  result4 = myFunction(field4, field4);
 
Field<Object>  result4 = myFunction(field4, (Field) field4);
 
Field<Object>  result4 = myFunction(field4, (Field<Object>) field4);

Когда <T> привязан к объекту, внезапно применяются оба метода, и, согласно JLS, ни один из них не является более конкретным! Хотя компилятор Eclipse обычно немного мягче (и в этом случае интуитивно связывает второй метод), компилятор javac не знает, что делать с этим вызовом. И нет никакого способа обойти это. Вы не можете привести field4 к Field или Field <Object>, чтобы заставить компоновщик ссылаться на второй метод. Это довольно плохие новости для дизайнера API.

Для получения более подробной информации об этом особом случае рассмотрим следующий вопрос переполнения стека, о котором я сообщил как об ошибке в Oracle и Eclipse. Давайте посмотрим, какая реализация компилятора верна:
http://stackoverflow.com/questions/5361513/reference-is-ambiguous-with-generics

Беда со статическим импортом, varargs

Varargs — еще одна замечательная функция, представленная в Java 5. Будучи просто синтаксическим сахаром, вы можете сохранить довольно много строк кода при передаче массивов в методы:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
// Method declarations with or without varargs
 
public static String concat1(int[] values);
 
public static String concat2(int... values);
 
 
 
// The above methods are actually the same.
 
String s1 = concat1(new int[] { 1, 2, 3 });
 
String s2 = concat2(new int[] { 1, 2, 3 });
 
 
 
// Only, concat2 can also be called like this, conveniently
 
String s3 = concat2(1, 2, 3);

Это хорошо известно. Он работает так же с массивами примитивного типа, как и с Object []. Это также работает с T [], где T является универсальным типом!

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
// You can now have a generic type in your varargs parameter:
 
public static <T> T[] array(T... values);
 
 
 
// The above can be called "type-safely" (with auto-boxing):
 
Integer[] ints   = array(1, 2, 3);
 
String[] strings = array("1", "2", "3");
 
 
 
// Since Object could also be inferred for T, you can even do this:
 
Object[] applesAndOranges = array(1, "2", 3.0);

Последний пример уже намекает на проблему. Если у T нет верхней границы, безопасность типов полностью исчезает. Это иллюзия, потому что, в конце концов, параметр varargs всегда может быть выведен как «Объект…». И вот как это вызывает проблемы, когда вы перегружаете такой API.

1
2
3
4
5
6
7
// Overloaded for "convenience". Let's ignore the compiler warning
 
// caused when calling the second method
 
public static <T> Field<T> myFunction(T... params);
 
public static <T> Field<T> myFunction(Field<T>... params);

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

01
02
03
04
05
06
07
08
09
10
11
// The outer function can infer Integer for <T> from the inner
 
// functions, which can infer Integer for <T> from T...
 
Field<Integer> f1 = myFunction(myFunction(1), myFunction(2, 3));
 
 
 
// But beware, this will compile too!
 
Field<?> f2 = myFunction(myFunction(1), myFunction(2.0, 3.0));

Внутренние функции будут выводить Integer и Double для <T>. При несовместимых типах возвращаемых данных Field <Integer> и Field <Double> метод «предназначенный» с аргументом «Field <T>…» больше не применяется. Следовательно, метод один с «T…» связан компилятором как единственный применимый метод. Но вы не собираетесь угадывать (возможно) предполагаемое ограничение на <T>. Это возможные предполагаемые типы:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
// This one, you can always do:
 
Field<?> f2 = myFunction(myFunction(1), myFunction(2.0, 3.0));
 
 
 
// But these ones show what you're actually about to do
 
Field<? extends Field<?>>                       f3 = // ...
 
Field<? extends Field<? extends Number>>        f4 = // ...
 
Field<? extends Field<? extends Comparable<?>>> f5 = // ...
 
Field<? extends Field<? extends Serializable>>  f6 = // ...

Компилятор может вывести что-то вроде Field <? расширяет Number & Comparable <?> & Serializable> в качестве допустимой верхней границы для <T>. Однако нет точной точной границы для <T>. Следовательно, необходимо <? расширяет [верхняя граница]>.

Вывод

Будьте осторожны при объединении параметров varargs с обобщениями, особенно в перегруженных методах. Если пользователь правильно связывает параметр универсального типа с тем, что вы хотели, все работает нормально. Но если есть одна опечатка (например, путать Integer с двойным), то пользователь вашего API обречен. И они не могут легко найти свою ошибку, так как никто в здравом уме не может прочитать сообщения об ошибках компилятора как это:

1
2
3
4
5
6
7
Test.java:58: incompatible types
found   : Test.Field<Test.Field<
          ? extends java.lang.Number&java.lang.Comparable<
          ? extends java.lang.Number&java.lang.Comparable<?>>>>
required: Test.Field<java.lang.Integer>
        Field<Integer> f2 = myFunction(myFunction(1),
                                       myFunction(2.0, 3.0));

Ссылка: осторожно перегружайте методы API от нашего партнера по JCG Лукаса Эдера из блога JAVA, SQL и AND JOOQ .

Статьи по Теме :