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

Материал из Вики проекта PascalABC.NET
Перейти к навигацииПерейти к поиску
Строка 55: Строка 55:


<source lang="Java">
<source lang="Java">
public interface EqualityComparable<S, T> {
public interface EqualityComparable<T, S> {
public boolean eq(S x, T y);
public boolean eq(T x, S y);
public boolean neq(S x, T y);
public boolean neq(T x, S y);
}
}


Строка 71: Строка 71:
}
}


public class IntDoubleCmp implements EqualityComparable<Integer, Double> {
public class IntDoubleEqCmp implements EqualityComparable<Integer, Double> {
public boolean eq(Integer x, Double y) {
public boolean eq(Integer x, Double y) {
return x.doubleValue() == y;
return x.doubleValue() == y;
Строка 80: Строка 80:
}
}


public class IntCharCmp implements EqualityComparable<Integer, Character> {
public class IntCharEqCmp implements EqualityComparable<Integer, Character> {
public boolean eq(Integer x, Character y) {
public boolean eq(Integer x, Character y) {
return Character.getNumericValue(y) == x;
return Character.getNumericValue(y) == x;
Строка 92: Строка 92:
ArrayList<Integer> arr = new ArrayList<Integer>();
ArrayList<Integer> arr = new ArrayList<Integer>();
// ...
// ...
IntCharCmp cmpIC = new IntCharCmp();
IntCharEqCmp cmpIC = new IntCharEqCmp();
//Algos.contains(arr, new Double(3.14), cmpIC); // ERROR
//Algos.contains(arr, new Double(3.14), cmpIC); // ERROR
IntDoubleCmp cmpID = new IntDoubleCmp();
IntDoubleEqCmp cmpID = new IntDoubleEqCmp();
Algos.contains(arr, new Double(3.14), cmpID);
Algos.contains(arr, new Double(3.14), cmpID);
}
}
Строка 107: Строка 107:


<source lang="Java">
<source lang="Java">
public class EqualityComparableBasic<S, T> {
public class EqualityComparableBasic<T, S> {
public boolean eq(S x, T y) {
public boolean eq(T x, S y) {
return true;
return true;
}
}
public boolean neq(S x, T y) {
public boolean neq(T x, S y) {
return !eq(x, y);
return !eq(x, y);
}
}
Строка 127: Строка 127:
}
}


public class IntDoubleCmpB extends EqualityComparableBasic<Integer, Double> {
public class IntDoubleEqCmpB extends EqualityComparableBasic<Integer, Double> {
public boolean eq(Integer x, Double y) {
public boolean eq(Integer x, Double y) {
return x.doubleValue() == y;
return x.doubleValue() == y;
Строка 133: Строка 133:
}
}


public class IntCharCmpB extends EqualityComparableBasic<Integer, Character> {
public class IntCharEqCmpB extends EqualityComparableBasic<Integer, Character> {
public boolean eq(Integer x, Character y) {
public boolean eq(Integer x, Character y) {
return Character.getNumericValue(y) == x;
return Character.getNumericValue(y) == x;
Строка 140: Строка 140:
</source>
</source>


Однако
Очевидным недостатком такого подхода является то, что в классе мы вынуждены реализовывать некоторые заглушки всех методов. А потом в наследнике невозможно проверить, был ли переопределен метод для конкретных типов.
 
Вторая проблема — ''concept refinement''. Если обобщенный класс-концепт, улучшающий некоторый концепт, описать ещё можно (пример не имеет особого смысла, конечно):
<source lang="Java">
public class ComparableBasic<T, S> extends EqualityComparableBasic<T, S> {
public int cmp(T x, S y) {
return 0;
}
}
</source>
то как определять модель? У класса может быть только один предок. Поэтому, если мы определяем класс-модель, который наследует наш новый <code>ComparableBasic</code>, придётся переопределять в нём как методы <code>ComparableBasic</code>, так и <code>EqualityComparableBasic</code>.
 
В случае с интерфейсами этой проблемы нет. Мы можем определить класс <code>IntDoubleCmp</code> — наследник <code>IntDoubleEqCmp</code> (реализует <code>EqualityComparable</code>), который реализует <code>Comparable</code>.
<source lang="Java">
public interface Comparable<T, S> extends EqualityComparable<T, S>{
public int cmp(T x, S y);
}
 
public class IntDoubleCmp extends IntDoubleEqCmp implements Comparable<Integer, Double> {
public int cmp(Integer x, Double y) {
return new Double(x.doubleValue()).compareTo(y);
}
}
</source>

Версия от 13:57, 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<T, S> {
	public boolean eq(T x, S y);
	public boolean neq(T x, S 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 IntDoubleEqCmp 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 IntCharEqCmp 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>();
	// ...
	IntCharEqCmp cmpIC = new IntCharEqCmp();
	//Algos.contains(arr, new Double(3.14), cmpIC);	// ERROR
	IntDoubleEqCmp cmpID = new IntDoubleEqCmp();
	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<T, S> {
	public boolean eq(T x, S y) {
		return true;
	}
	public boolean neq(T x, S 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 IntDoubleEqCmpB extends EqualityComparableBasic<Integer, Double> {
	public boolean eq(Integer x, Double y) {
		return x.doubleValue() == y;
	}
}

public class IntCharEqCmpB extends EqualityComparableBasic<Integer, Character> {
	public boolean eq(Integer x, Character y) {
		return Character.getNumericValue(y) == x;
	}
}

Очевидным недостатком такого подхода является то, что в классе мы вынуждены реализовывать некоторые заглушки всех методов. А потом в наследнике невозможно проверить, был ли переопределен метод для конкретных типов.

Вторая проблема — concept refinement. Если обобщенный класс-концепт, улучшающий некоторый концепт, описать ещё можно (пример не имеет особого смысла, конечно):

public class ComparableBasic<T, S> extends EqualityComparableBasic<T, S> {
	public int cmp(T x, S y) {
		return 0;
	}
}

то как определять модель? У класса может быть только один предок. Поэтому, если мы определяем класс-модель, который наследует наш новый ComparableBasic, придётся переопределять в нём как методы ComparableBasic, так и EqualityComparableBasic.

В случае с интерфейсами этой проблемы нет. Мы можем определить класс IntDoubleCmp — наследник IntDoubleEqCmp (реализует EqualityComparable), который реализует Comparable.

public interface Comparable<T, S> extends EqualityComparable<T, S>{
	public int cmp(T x, S y);
}

public class IntDoubleCmp extends IntDoubleEqCmp implements Comparable<Integer, Double> {
	public int cmp(Integer x, Double y) {
		return new Double(x.doubleValue()).compareTo(y);
	}
}