Стирание типа обработки в конструкторах с дженериками
Я пытаюсь создать класс, который может содержать только один из двух объектов, и я хочу сделать это с помощью дженериков. Вот идея:
public class Union<A, B> {
private final A a;
private final B b;
public Union(A a) {
this.a = a;
b = null;
}
public Union(B b) {
a = null;
this.b = b;
}
// isA, isB, getA, getB...
}
Конечно, это не сработает, потому что из-за стирания типа конструкторы имеют одну и ту же подпись типа. Я понимаю, что одно решение состоит в том, чтобы иметь один конструктор, беря оба, но я хочу, чтобы одно из значений было пустым, поэтому кажется более элегантным иметь конструкторы с одним параметром.
// Ugly solution
public Union(A a, B b) {
if (!(a == null ^ b == null)) {
throw new IllegalArgumentException("One must exist, one must be null!");
}
this.a = a;
this.b = b;
}
Есть ли элегантное решение для этого?
Изменить 1: Я использую Java 6.
Изменить 2: Причина, по которой я хочу это сделать, заключается в том, что у меня есть метод, который может возвращать один из двух типов. Я сделал конкретную версию без каких-либо дженериков, но задавался вопросом, могу ли я сделать ее обобщенной. Да, я понимаю, что наличие метода с двумя разными типами возвращаемых данных является реальной проблемой, но мне все еще было любопытно, был ли хороший способ сделать это.
Я думаю, durron597 answer лучше всего, потому что он указывает, что Union<Foo, Bar>
и Union<Bar, Foo>
должны действовать одинаково, но они этого не делают (что является основной причиной почему я решил прекратить это делать). Это гораздо более уродливая проблема, чем "уродливый" конструктор.
Для чего я думаю, что лучший вариант, вероятно, делает этот абстрактный (потому что интерфейсы не могут диктовать видимость) и создавать isA
и getA
материал protected
, а затем в классе реализации лучше назвать методы, чтобы избежать проблемы <A, B>
!= <B, A>
. Я добавлю свой собственный ответ более подробно.
Заключительное редактирование:. Для чего я решил, что использование статических методов в качестве псевдоконструкторов (public static Union<A, B> fromA(A a)
и public static Union<A, B> fromB(B b)
) - лучший подход (вместе с тем, что частный конструктор является частным). Union<A, B>
и Union<B, A>
никогда не сравнивались бы реалистично друг с другом, когда он использовался только как возвращаемое значение.
Другое редактирование, 6 месяцев вне. Я действительно не могу поверить, насколько наивным был я, когда я спрашивал об этом, статические методы factory настолько очевидны, что это абсолютно правильный выбор и, очевидно,.
Все, что в стороне, я нашел Functional Java очень интригующим. Я еще не использовал его, но я нашел этот Either
, когда googling 'java disjunct union', это именно то, что я искал, Недостатком является то, что функциональная Java предназначена только для Java 7 и 8, но, к счастью, проект теперь я работаю над используемой Java 8.
Ответы
Ответ 1
На самом деле это не имеет никакого смысла. Когда это когда-нибудь будет иметь смысл? Например (предполагая, что на данный момент ваш исходный код работал):
Union<String, Integer> union = new Union("Hello");
// stuff
if (union.isA()) { ...
Но если вы это сделали, вместо:
Union<Integer, String> union = new Union("Hello");
// stuff
if (union.isA()) { ...
Это будет другое поведение, хотя классы и данные будут одинаковыми. Ваша концепция isA
и isB
в основном "левая и правая" - это более важно, какой из них остается справа от того, какой из них представляет собой String vs, который является целым. Другими словами, Union<String, Integer>
сильно отличается от Union<Integer, String>
, что, вероятно, не является тем, что вы хотите.
Подумайте, что произойдет, если бы мы, например, имели, скажем:
List<Union<?, ?>> myList;
for(Union<?, ?> element : myList) {
if(element.isA()) {
// What does this even mean?
Факт того, что есть A
, не имеет значения, если вы не заботитесь о том, является ли это левым или правым, и в этом случае вы должны это назвать.
Если это обсуждение не касается левого и правого, то единственное, что имеет значение, это использование ваших конкретных типов при создании класса. Было бы разумнее просто иметь интерфейс,
public interface Union<A, B> {
boolean isA();
boolean isB();
A getA();
B getB();
}
Вы можете даже сделать метод "is" в абстрактном классе:
public abstract class AbstractUnion<A, B> {
public boolean isA() { return getB() == null; }
public boolean isB() { return getA() == null; }
}
И тогда, когда вы на самом деле создаете экземпляр класса, вы все равно будете использовать определенные типы...
public UnionImpl extends AbstractUnion<String, Integer> {
private String strValue;
private int intValue
public UnionImpl(String str) {
this.strValue = str;
this.intValue = null;
}
// etc.
}
Затем, когда вы действительно выбрали типы своей реализации, вы действительно узнаете, что вы получаете.
Кроме того, если после прочтения всего вышеизложенного вы все же хотите сделать это так, как вы описываете в своем первоначальном вопросе, правильный способ сделать это с помощью статических factory методов с частным конструктором, как описано @JoseAntoniaDuraOlmos ответьте здесь. Тем не менее, я надеюсь, что вы еще раз подумаете о том, что вам действительно нужно, чтобы ваш класс работал в реальном использовании.
Ответ 2
Я бы использовал частный конструктор и 2 статических создателя
public class Union<A, B> {
private final A a;
private final B b;
// private constructor to force use or creation methods
private Union(A a, B b) {
if ((a == null) && (b == null)) { // ensure both cannot be null
throw new IllegalArgumentException();
}
this.a = a;
this.b = b;
}
public static <A, B> Union<A, B> unionFromA(A a) {
Union<A,B> u = new Union(a, null);
return u;
}
public static <A, B> Union<A, B> unionFromB(B b) {
Union<A,B> u = new Union(null, b);
return u;
}
...
}
Ответ 3
Если вы должны использовать конструктор, то, скорее всего, нет элегантного решения.
С помощью методов factory вы можете иметь элегантное решение, которое сохраняет окончательные для a и b.
Методы factory будут использовать "уродливый" конструктор, но это нормально, поскольку он является частью реализации. Открытый интерфейс сохраняет все ваши требования, сохраняя переход от конструкторов к factory методам.
Это делается с намерением сделать Union<A,B>
взаимозаменяемым с Union<B,A>
в соответствии с ответом durron597.
Это не вполне возможно, как я покажу на примере, но мы можем приблизиться.
public class Union<A, B> {
private final A a;
private final B b;
private Union(A a, B b) {
assert a == null ^ b == null;
this.a = a;
this.b = b;
}
public static <A, B> Union<A, B> valueOfA(A a) {
if (a == null) {
throw new IllegalArgumentException();
}
Union<A, B> res = new Union<>(a, null);
return res;
}
public static <A, B> Union<A, B> valueOfB(B b) {
if (b == null) {
throw new IllegalArgumentException();
}
Union<A, B> res = new Union<>(null, b);
return res;
}
public boolean isClass(Class<?> clazz) {
return a != null ? clazz.isInstance(a) : clazz.isInstance(b);
}
// The casts are always type safe.
@SuppressWarnings("unchecked")
public <C> C get(Class<C> clazz) {
if (a != null && clazz.isInstance(a)) {
return (C)a;
}
if (b != null && clazz.isInstance(b)) {
return (C)b;
}
throw new IllegalStateException("This Union does not contain an object of class " + clazz);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Union)) {
return false;
}
Union union = (Union) o;
Object parm = union.a != null ? union.a : union.b;
return a != null ? a.equals(parm) : b.equals(parm);
}
@Override
public int hashCode() {
int hash = 3;
hash = 71 * hash + Objects.hashCode(this.a);
hash = 71 * hash + Objects.hashCode(this.b);
return hash;
}
}
Вот примеры того, как использовать и как не использовать его.
useUnionAsParm2
показывает ограничение этого решения. Компилятор не может обнаружить неправильный параметр, используемый для метода, который предназначен для принятия любого Союза, содержащего String. Мы должны прибегать к проверке типа времени выполнения.
public class Test {
public static void main(String[] args) {
Union<String, Integer> alfa = Union.valueOfA("Hello");
Union<Integer, String> beta = Union.valueOfB("Hello");
Union<HashMap, String> gamma = Union.valueOfB("Hello");
Union<HashMap, Integer> delta = Union.valueOfB( 13 );
// Union<A,B> compared do Union<B,A>.
// Prints true because both unions contain equal objects
System.out.println(alfa.equals(beta));
// Prints false since "Hello" is not an Union.
System.out.println(alfa.equals("Hello"));
// Union<A,B> compared do Union<C,A>.
// Prints true because both unions contain equal objects
System.out.println(alfa.equals(gamma));
// Union<A,B> compared to Union<C,D>
// Could print true if a type of one union inherited or implement a
//type of the other union. In this case contained objects are not equal, so false.
System.out.println(alfa.equals(delta));
useUnionAsParm(alfa);
// Next two lines produce compiler error
//useUnionAsParm(beta);
//useUnionAsParm(gamma);
useUnionAsParm2(alfa);
useUnionAsParm2(beta);
useUnionAsParm2(gamma);
// Will throw IllegalStateException
// Would be nice if it was possible to declare useUnionAsParm2 in a way
//that caused the compiler to generate an error for this line.
useUnionAsParm2(delta);
}
/**
* Prints a string contained in an Union.
*
* This is an example of how not to do it.
*
* @param parm Union containing a String
*/
public static void useUnionAsParm(Union<String, Integer> parm) {
System.out.println(parm.get(String.class));
}
/**
* Prints a string contained in an Union. Correct example.
*
* @param parm Union containing a String
*/
public static void useUnionAsParm2(Union<? extends Object, ? extends Object> parm) {
System.out.println( parm.get(String.class) );
}
}
Ответ 4
"Союз" - это неправильное слово здесь. Мы не говорим о объединении двух типов, которые будут включать все объекты в обоих типах, возможно с перекрытием.
Эта структура данных больше похожа на кортеж, с дополнительным индексом, указывающим на один значительный элемент. Лучшее слово для него, вероятно, "вариант". Фактически, java.util.Optional
является частным случаем.
Итак, я мог бы создать его таким образом
interface Opt2<T0,T1>
int ordinal(); // 0 or 1
Object value();
default boolean is0(){ return ordinal()==0; }
default T0 get0(){ if(is0()) return (T0)value(); else throw ... }
static <T0,T1> Opt2<T0,T1> of0(T0 value){ ... }
Ответ 5
Как durron597 answer указывает, Union<Foo, Bar>
и Union<Bar, Foo>
должны, по идее, вести себя одинаково, но вести себя по-разному. Не имеет значения, что есть A
и которое B
, имеет значение, какой тип.
Вот что я думаю, может быть лучше,
// I include this because if it not off in its own package away from where its
// used these protected methods can still be called. Also I specifically use an
// abstract class so I can make the methods protected so no one can call them.
package com.company.utils;
public abstract class Union<A, B> {
private A a;
private B b;
protected Union(A a, B b) {
assert a == null ^ b == null: "Exactly one param must be null";
this.a = a;
this.b = b;
}
// final methods to prevent over riding in the child and calling them there
protected final boolean isA() { return a != null; }
protected final boolean isB() { return b != null; }
protected final A getA() {
if (!isA()) { throw new IllegalStateException(); }
return a;
}
protected final B getB() {
if (!isB()) { throw new IllegalStateException(); }
return b;
}
}
И реализация. Если это используется (кроме com.company.utils
), можно найти только методы с однозначными именами.
package com.company.domain;
import com.company.utils.Union;
public class FooOrBar extends Union<Foo, Bar> {
public FooOrBar(Foo foo) { super(foo, null); }
public FooOrBar(Bar bar) { super(null, bar); }
public boolean isFoo() { return isA(); }
public boolean isBar() { return isB(); }
public Foo getFoo() { return getA(); }
public Bar getBar() { return getB(); }
}
Другая идея может быть Map<Class<?>, ?>
или что-то еще, или, по крайней мере, для хранения значений. Я не знаю. Весь этот код воняет. Он был создан из плохо разработанного метода, который нуждался в нескольких типах возврата.