Исследование обобщённого программирования: различия между версиями

Материал из Вики проекта PascalABC.NET
Перейти к навигацииПерейти к поиску
Строка 1: Строка 1:
== 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>.
Особенность 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>.
'''А вот это уже не работает'''. Причина — ограничение '''[*]''', которое даёт стирание типов. Класс <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 здесь есть ряд неприятных особенностей:

  1. интерфейс не может содержать реализации методов по умолчанию, поэтому во всех моделях EqualityComparable приходится дублировать реализацию метода neq (Scala traits допускают базовую реализацию).
  2. приходится описывать реализацию модели в конкретном классе, а потом создавать объект данного класса, чтобы передать его в вызов обобщенного алгоритма. В Scala для этой цели удобно использовать объект-наследник концепта (синглтон).
  3. любой концепт, который нам требуется, приходится передавать отдельным параметром. В 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;
	}
}

Однако