Исследование обобщённого программирования: различия между версиями
Juliet (обсуждение | вклад) |
Juliet (обсуждение | вклад) |
||
Строка 1: | Строка 1: | ||
== Java Generics == | == Java Generics == | ||
Особенность Java Generics — '' | Особенность Java Generics — ''стирание типов''. Обобщенный класс <code>A<T></code> транслируется в обычный класс <code>A</code>, где везде вместо типа <code>T</code> используется <code>Object</code>. Если на параметр типа есть ограничение, например, <code>A<T extends Comparable<T> ></code>, то в результирующем коде вместо <code>T</code> стоит <code>Comparable</code>. | ||
Из-за | Из-за стирания типов возникает неприятное для нас ограничение: ''параметр шаблона не может реализовывать разные специализации одного интерфейса'' '''[*]'''. Что это и чем оно нам грозит? | ||
Концепты хороши тем, что позволяют нам устанавливать связи между разными типами. Например, можно иметь концепт | Концепты хороши тем, что позволяют нам устанавливать связи между разными типами. Например, можно иметь концепт | ||
Строка 12: | Строка 12: | ||
U S::convert() | U S::convert() | ||
Можем ли мы реализовать нечто подобное на Java? У нас ведь есть обобщённые интерфейсы, может это выход? | Можем ли мы естественным образом реализовать нечто подобное на Java? У нас ведь есть обобщённые интерфейсы, может это выход? | ||
<source lang="Java"> | <source lang="Java"> | ||
Строка 48: | Строка 48: | ||
</source> | </source> | ||
'''А вот это уже не работает'''. Причина — ограничение '''[*]''', которое даёт | '''А вот это уже не работает'''. Причина — ограничение '''[*]''', которое даёт стирание типов. Класс <code>A</code> может реализовывать только какой-нибудь один <code>Convertible</code>. | ||
=== Использование объектов-моделей === | |||
Придётся использовать наши любимые объекты-концепты в обобщенном коде и объекты-модели — в конкретном. | |||
<source lang="Java"> | |||
public interface EqualityComparable<S, T> { | |||
public boolean eq(S x, T y); | |||
public boolean neq(S x, T y); | |||
} | |||
public static <T, S> boolean contains(Collection<T> elems, S x, EqualityComparable<T, S> cmp) { | |||
boolean found = false; | |||
for (T t : elems) { | |||
if (cmp.eq(t, x)) { | |||
found = true; | |||
break; | |||
} | |||
} | |||
return found; | |||
} | |||
public class IntDoubleCmp implements EqualityComparable<Integer, Double> { | |||
public boolean eq(Integer x, Double y) { | |||
return x.doubleValue() == y; | |||
} | |||
public boolean neq(Integer x, Double y) { | |||
return !eq(x, y); | |||
} | |||
} | |||
public class IntCharCmp implements EqualityComparable<Integer, Character> { | |||
public boolean eq(Integer x, Character y) { | |||
return Character.getNumericValue(y) == x; | |||
} | |||
public boolean neq(Integer x, Character y) { | |||
return !eq(x, y); | |||
} | |||
} | |||
private static void testEqCmp() { | |||
ArrayList<Integer> arr = new ArrayList<Integer>(); | |||
// ... | |||
IntCharCmp cmpIC = new IntCharCmp(); | |||
//Algos.contains(arr, new Double(3.14), cmpIC); // ERROR | |||
IntDoubleCmp cmpID = new IntDoubleCmp(); | |||
Algos.contains(arr, new Double(3.14), cmpID); | |||
} | |||
</source> | |||
Таким образом в обобщённый алгоритм нужно явно передавать объекты, которые реализуют нужный концепт. Похожим образом мы поступали и в Scala, но в отличие от Scala здесь есть ряд неприятных особенностей: | |||
# интерфейс не может содержать реализации методов по умолчанию, поэтому во всех моделях <code>EqualityComparable</code> приходится дублировать реализацию метода <code>neq</code> (Scala traits допускают базовую реализацию). | |||
# приходится описывать реализацию модели в конкретном классе, а потом создавать объект данного класса, чтобы передать его в вызов обобщенного алгоритма. В Scala для этой цели удобно использовать объект-наследник концепта (синглтон). | |||
# любой концепт, который нам требуется, приходится передавать отдельным параметром. В Scala ситуацию спасают implisits, а здесь нам может потребоваться довольно большое число объектов-моделей, что весьма громоздко. | |||
Недостаток (1) можно обойти, если для концепта использовать обобщённый класс вместо обобщённого интерфейса. Тогда можно сделать в классе реализацию по умолчанию, а в наследниках переопределять только <code>eq</code>: | |||
<source lang="Java"> | |||
public class EqualityComparableBasic<S, T> { | |||
public boolean eq(S x, T y) { | |||
return true; | |||
} | |||
public boolean neq(S x, T y) { | |||
return !eq(x, y); | |||
} | |||
} | |||
public static <T, S> boolean contains(Collection<T> elems, S x, EqualityComparableBasic<T, S> cmp) { | |||
boolean found = false; | |||
for (T t : elems) { | |||
if (cmp.eq(t, x)) { | |||
found = true; | |||
break; | |||
} | |||
} | |||
return found; | |||
} | |||
public class IntDoubleCmpB extends EqualityComparableBasic<Integer, Double> { | |||
public boolean eq(Integer x, Double y) { | |||
return x.doubleValue() == y; | |||
} | |||
} | |||
public class IntCharCmpB extends EqualityComparableBasic<Integer, Character> { | |||
public boolean eq(Integer x, Character y) { | |||
return Character.getNumericValue(y) == x; | |||
} | |||
} | |||
</source> | |||
Однако |
Версия от 13:10, 14 июня 2013
Java Generics
Особенность Java Generics — стирание типов. Обобщенный класс A<T>
транслируется в обычный класс A
, где везде вместо типа T
используется Object
. Если на параметр типа есть ограничение, например, A<T extends Comparable<T> >
, то в результирующем коде вместо T
стоит Comparable
.
Из-за стирания типов возникает неприятное для нас ограничение: параметр шаблона не может реализовывать разные специализации одного интерфейса [*]. Что это и чем оно нам грозит?
Концепты хороши тем, что позволяют нам устанавливать связи между разными типами. Например, можно иметь концепт
Convertible<S, U>
который содержит функцию
U convert(S x)
или
U S::convert()
Можем ли мы естественным образом реализовать нечто подобное на Java? У нас ведь есть обобщённые интерфейсы, может это выход?
public interface Convertible<S> {
public S convert();
}
public class A1 {}
public class A implements Convertible<A1> {
public A1 convert() {
return new A1();
}
}
Здорово! Но мы хотим, чтобы класс A
мог быть преобразован и в какой-нибудь A2
.
public interface Convertible<S> {
public S convert();
}
public class A1 {}
public class A2 {}
public class A implements Convertible<A1>, Convertible<A2> {
public A1 convert() {
return new A1();
}
public A2 convert() {
return new A2();
}
}
А вот это уже не работает. Причина — ограничение [*], которое даёт стирание типов. Класс A
может реализовывать только какой-нибудь один Convertible
.
Использование объектов-моделей
Придётся использовать наши любимые объекты-концепты в обобщенном коде и объекты-модели — в конкретном.
public interface EqualityComparable<S, T> {
public boolean eq(S x, T y);
public boolean neq(S x, T y);
}
public static <T, S> boolean contains(Collection<T> elems, S x, EqualityComparable<T, S> cmp) {
boolean found = false;
for (T t : elems) {
if (cmp.eq(t, x)) {
found = true;
break;
}
}
return found;
}
public class IntDoubleCmp implements EqualityComparable<Integer, Double> {
public boolean eq(Integer x, Double y) {
return x.doubleValue() == y;
}
public boolean neq(Integer x, Double y) {
return !eq(x, y);
}
}
public class IntCharCmp implements EqualityComparable<Integer, Character> {
public boolean eq(Integer x, Character y) {
return Character.getNumericValue(y) == x;
}
public boolean neq(Integer x, Character y) {
return !eq(x, y);
}
}
private static void testEqCmp() {
ArrayList<Integer> arr = new ArrayList<Integer>();
// ...
IntCharCmp cmpIC = new IntCharCmp();
//Algos.contains(arr, new Double(3.14), cmpIC); // ERROR
IntDoubleCmp cmpID = new IntDoubleCmp();
Algos.contains(arr, new Double(3.14), cmpID);
}
Таким образом в обобщённый алгоритм нужно явно передавать объекты, которые реализуют нужный концепт. Похожим образом мы поступали и в Scala, но в отличие от Scala здесь есть ряд неприятных особенностей:
- интерфейс не может содержать реализации методов по умолчанию, поэтому во всех моделях
EqualityComparable
приходится дублировать реализацию методаneq
(Scala traits допускают базовую реализацию). - приходится описывать реализацию модели в конкретном классе, а потом создавать объект данного класса, чтобы передать его в вызов обобщенного алгоритма. В Scala для этой цели удобно использовать объект-наследник концепта (синглтон).
- любой концепт, который нам требуется, приходится передавать отдельным параметром. В Scala ситуацию спасают implisits, а здесь нам может потребоваться довольно большое число объектов-моделей, что весьма громоздко.
Недостаток (1) можно обойти, если для концепта использовать обобщённый класс вместо обобщённого интерфейса. Тогда можно сделать в классе реализацию по умолчанию, а в наследниках переопределять только eq
:
public class EqualityComparableBasic<S, T> {
public boolean eq(S x, T y) {
return true;
}
public boolean neq(S x, T y) {
return !eq(x, y);
}
}
public static <T, S> boolean contains(Collection<T> elems, S x, EqualityComparableBasic<T, S> cmp) {
boolean found = false;
for (T t : elems) {
if (cmp.eq(t, x)) {
found = true;
break;
}
}
return found;
}
public class IntDoubleCmpB extends EqualityComparableBasic<Integer, Double> {
public boolean eq(Integer x, Double y) {
return x.doubleValue() == y;
}
}
public class IntCharCmpB extends EqualityComparableBasic<Integer, Character> {
public boolean eq(Integer x, Character y) {
return Character.getNumericValue(y) == x;
}
}
Однако