Например, в языках С/С++ можно написать вот так.
union value {
int i;
float f;
};
union value v;
v.i = 5; /* v.f - undefined behaivor */
При этом если мы установили значение одному полю, то считывание значения другого поля будет будет иметь неопределеное поведения.
Для упрощения работы с union типамы в С++17 был добавлен класс std::variant.
std::variant<int, float> v { 5 };
std::cout << "int value: " << std::get<int>(v) << std::endl;
Язык Java не поддерживает union типы. Как альтернативу, можно реализовать дата-класс с двумя полями определенных типов с сеттерами и геттерами. Но хотелось чтобы значение сохранялось в одном поле, а не в двух.
Как известно типу Object можно сохранить значение одного типа, а потом переприсвоить значения другого типа. И это можно использовать для реализации класса, наподобие к классу std::variant.
Поскольку в языке Java нельзя указать переменное число типов в дженерике, то для определенного количества типов нужна специализация класса(Union2, Union3 и тд.). Напишим основной класс Union и базовые его операции.
public abstract class Union {
private Union() {}
public abstract <T> void set(T value);
public abstract <T> T get(Class<T> clazz);
public abstract <T> boolean isActive(Class<T> clazz);
public abstract <T> Class<T> getActive();
}
Для создания объектов класса будем использовать фабричные методы. В зависимости от количества типов будет возращаться конкретная специализация класса.
public static <T1, T2> Union2<T1, T2> of(Class<T1> firstClass, Class<T2> secondClass) {
return new Union2<>(firstClass, secondClass);
}
public static <T1, T2, T3> Union3<T1, T2, T3> of(Class<T1> firstClass, Class<T2> secondClass, Class<T3> thirdClass) {
return new Union3<>(firstClass, secondClass, thirdClass);
}
Конкретная специализация union класса будет сохранять определенное количество типов и одно поле Object. В случае если мы указывает не корректный тип, то получим ошибку.
private static class Union2<T1, T2> extends Union {
private final Class<T1> firstClass;
private final Class<T2> secondClass;
private Object value;
private Union2(Class<T1> firstClass, Class<T2> secondClass) {
this.firstClass = firstClass;
this.secondClass = secondClass;
}
@Override
public <T> void set(T value) {
if (value.getClass() == firstClass || value.getClass() == secondClass) {
this.value = value;
} else {
throw new UnionException("Incorrect type: " + value.getClass().getName() +
"\n" + "Union two types: [" + firstClass.getName() + ", " +
secondClass.getName() + "]");
}
}
@Override
public <T> T get(Class<T> clazz) {
if (clazz == firstClass || clazz == secondClass) {
return (T) value;
} else {
throw new UnionException("Incorrect type: " + value.getClass().getName() +
"\n" + "Union two types: [" + firstClass.getName() + ", " +
secondClass.getName() + "]");
}
}
@Override
public <T> boolean isActive(Class<T> clazz) {
return value.getClass() == clazz;
}
@Override
public <T> Class<T> getActive() {
return (Class<T>) value.getClass();
}
}
private static class Union3<T1, T2, T3> extends Union {
private final Class<T1> firstClass;
private final Class<T2> secondClass;
private final Class<T3> thirdClass;
private Object value;
private Union3(Class<T1> firstClass, Class<T2> secondClass, Class<T3> thirdClass) {
this.firstClass = firstClass;
this.secondClass = secondClass;
this.thirdClass = thirdClass;
}
@Override
public <T> void set(T value) {
if (value.getClass() == firstClass || value.getClass() == secondClass ||
value.getClass() == thirdClass) {
this.value = value;
} else {
throw new UnionException("Incorrect type: " + value.getClass().getName() +
"\n" + "Union three types: [" + firstClass.getName() + ", " +
secondClass.getName() + ", " + thirdClass.getName() + "]");
}
}
@Override
public <T> T get(Class<T> clazz) {
if (clazz == firstClass || clazz == secondClass ||
value.getClass() == thirdClass) {
return (T) value;
} else {
throw new UnionException("Incorrect type: " + value.getClass().getName() +
"\n" + "Union three types: [" + firstClass.getName() + ", " +
secondClass.getName() + ", " + thirdClass.getName() + "]");
}
}
@Override
public <T> boolean isActive(Class<T> clazz) {
return value.getClass() == clazz;
}
@Override
public <T> Class<T> getActive() {
return (Class<T>) value.getClass();
}
}
А теперь посмотрим на примере как можно использовать этот класс. Как можно заметить не работает с конкретными специализации Union, что делает код проще.
Union triUnion = Union.of(Integer.class, String.class, Float.class);
triUnion.set(15f);
assertEquals(triUnion.getActive(), Float.class);
assertTrue(triUnion.isActive(Float.class));
triUnion.set("Dot");
assertEquals(triUnion.getActive(), String.class);
assertTrue(triUnion.isActive(String.class));
triUnion.set(10);
assertEquals(triUnion.getActive(), Integer.class);
assertTrue(triUnion.isActive(Integer.class));
Также для проверки текущего значения можно написать простой визитер.
Union biUnion = Union.of(Integer.class, String.class);
biUnion.set("Line");
Union triUnion = Union.of(Integer.class, String.class, Float.class);
triUnion.set(15f);
matches(biUnion,
Integer.class, i -> System.out.println("bi-union number: " + i),
String.class, s -> System.out.println("bi-union string: " + s)
);
matches(triUnion,
Integer.class, i -> System.out.println("tri-union int: " + i),
String.class, s -> System.out.println("tri-union string: " + s),
Float.class, f -> System.out.println("tri-union float: " + f)
);
public static <V, T1, T2> void matches(V value,
Class<T1> firstClazz, Consumer<T1> firstConsumer,
Class<T2> secondClazz, Consumer<T2> secondConsumer) {
Class<?> valueClass = value.getClass();
if (firstClazz == valueClass) {
firstConsumer.accept((T1) value);
} else if (secondClazz == valueClass) {
secondConsumer.accept((T2) value);
}
}
public static <T1, T2, T3> void matches(Union value,
Class<T1> firstClazz, Purchaser<T1> firstConsumer,
Class<T2> secondClazz, Purchaser<T2> secondConsumer,
Class<T3> thirdClazz, Purchaser<T3> thirdConsumer) {
Class<?> valueClass = value.getActive();
if (firstClazz == valueClass) {
firstConsumer.obtain(value.get(firstClazz));
} else if (secondClazz == valueClass) {
secondConsumer.obtain(value.get(secondClazz));
} else if (thirdClazz == valueClass) {
thirdConsumer.obtain(value.get(thirdClazz));
}
}
Подводя итоги, можно сказать что в языке Java можно реализовать на уровне библиотеки поддержку union типов. Но как недостаток, для каждого количества типов нужна своя специализиация union класса и дополнительно сохранять все типы.
Полный исходной код класса можно посмотреть на github: code
Комментарии (55)
mayorovp
27.08.2019 09:36Неужели вот такая конструкция "дешевле" просто второго поля в классе?
private final Class<T1> firstClass; private final Class<T2> secondClass; private Object value;
Сравните:
private T1 firstValue; private T2 secondValue;
koowaah Автор
27.08.2019 10:07-2В языке Scala 3.0 хотят завести union типы. Если не ошибаюсь в TypeScript уже есть union типы.
Концепция union типов стает популярной в языках.mayorovp
27.08.2019 10:08+1Станет-то станет, но в чём был смысл делать тремя полями и сложно там, где можно было сделать двумя полями и просто?
AnarchyMob
27.08.2019 11:13+1Если не ошибаюсь, в jvm языке Ceylon давно есть union типы...
koowaah Автор
27.08.2019 12:19Да. В Ceylon поддерживаются union типы.
void printType(String|Integer|Float val) { switch (val) case (is String) { print("String: ``val``"); } case (is Integer) { print("Integer: ``val``"); } case (is Float) { print("Float: ``val``"); } }
koowaah Автор
27.08.2019 11:19Было б классно если такую фичу завезли в Java.
CyberSoft
27.08.2019 18:14В скором времени завезут: Pattern-matching for switch, Sealed Types for Java Language.
koowaah Автор
27.08.2019 18:54Эти классные фичи. Очень бы упростили написание кода.
Ryppka
28.08.2019 08:42-2Ага, ага… Ява все больше напоминает холеную корову, на которую напялили несколько дорогих стампидных седел…
koowaah Автор
28.08.2019 10:24Pattern matching в Java хотят завезти. В добавок хотят добавить record & sealed type.
Я думаю Java правильно развивается. Это упростит много кода.
igormich88
27.08.2019 15:55+1Скажите что будет если я попробую запустить такой код?
Union biUnion = Union.of(Integer.class, CharSequence.class); biUnion.set("Line");
koowaah Автор
27.08.2019 16:08-1Он сравнивает с типами String и Integer. Но не сравнивает с реализуемыми интерфейсами.
mmMike
Я знаю зачем в "C" в использовались union. В основном от дикой нехватки памяти и/или работе с raw бинарными данными вида "код типа данных" + union структур разных типов.
Дает и экономию памяти и упрощение "парсинга" данных, когда бинарный блок нужно "разложить" на "поля" по быстрому.
К слову, "неопределенное поведение" с union в "C" не вполне описывает ширину проблемы. Тут могут быть и аппаратные прерывания по ошибке (сегментирование памяти, не верный формат float и прочее процессорнозависимое)
union переползло и в C++.
Но к чему тянуть этот стиль в Java — я не понял.
Фраза "нужно чтобы объект в определенный момент содержал значения одного типа или значения другого типа." не описывает зачем это нужно.
Да можно создать псевдо аналог. Но зачем?
Только потому, что есть привычка работы с union? Или можно сделать, потому что можно сделать..
Это не троллинг. Я действительно не понимаю в каком case может понадобится такой Java union class (что нельзя решить другими более характерными для Java методами).
koowaah Автор
Если два или более объектов большие, а Class занимают занимают менее памяти. В таком варианте можно использовать.
mmMike
Не понял…
Union у Вас это инкапсулятор содержащий фактически ссылки на объект (один из).
Какая связь с экономией памяти?
koowaah Автор
В джаве как минимум нужно хранить список допустимых типов, чтобы проверять корректность при считывании или записи.
mayorovp
Вы специально отвечаете не на тот комментарий?
Если в поле записано null — совершенно не важно насколько большой объект там мог бы быть записан, памяти требуется всего 4-8 байт. Один null никак не может быть больше двух
Class<>
koowaah Автор
В случае null это верно. Можно при передачи объекта null, кидать исключения.
Использование двух Class<> нужно чтобы сохранять типы для корректных полей. Без них не обойтись.
mayorovp
Да как не обойтись-то? Я же ниже уже писал:
Ну и где тут вообще нужно сравнивать типы?
koowaah Автор
Если имеется например, union с двух типов — Union2<String, Integer>. И хотим записать вещественное число. Нужно как то проверять типы. Для этого и нужны два поля Class<>.
mayorovp
Кому нужно их проверять? Почему этого не может сделать компилятор?
koowaah Автор
В случае двух полей разных типов сравнивать не нужно.
В случае union нужно как-то проверять типы.
mayorovp
А если внутри union два поля?
koowaah Автор
В определенный момент может сохранять только одно значение определенного типа в поле Object value. Я показал только для Union2, Union3. Можно специализацию сделать и для больше типов.
mayorovp
Вы сейчас ответили не на вопрос "Почему компилятор не может проверить типы сам, если внутри Union2 два поля?", а на какой-то другой.
koowaah Автор
А как вы это понимаете?
0xd34df00d
Так это ж не юнион.
изоморфно произведению
(Maybe T1, Maybe T2)
, а не суммеEither T1 T2
.mayorovp
Это если не накладывать дополнительного ограничения, что только одно из полей может быть непустым.
0xd34df00d
А как вы это ограничение наложите в джаве?
igormich88
Через сеттеры?
0xd34df00d
setFirst(null);
Теперь у вас в типе-сумме вообще ничего.
igormich88
Если такая ситуация является недопустимой, можно добавить проверку на null.
0xd34df00d
Так это рантайм-проверки (пусть и написанные вами в классе и поэтому почти невидимые клиентам).
mayorovp
Ну так это особенность именно что null. Не просто же так null называют ошибкой на миллион долларов.
Gorthauer87
Вообще union в Си это очень низкоуровневая версия Типа-Суммы, в которой сам программист должен решать какой тип там лежит. Обычно это решается через идентификатор. Такой паттерн называют tagged union.
А сама эта фича пришла из функциональных языков программирования и в совокупности с паттерн матчингом позволяет достигать очень существенных улучшений кодовой базы.
То есть тут вообще не стоит вопроса об экономии памяти, эта абстракция прежде всего для программиста, а не для оптимизации.
Хотя попытки эмулировать эту фичу через шаблоны смотрятся убого(
mayorovp
Уточнение: union в Си это очень низкоуровневая версия Типа-Объединения. И то до него толком не дотягивает.
Вот tagged union — да, уже тип-сумма.
И нет, union как языковая конструкция в Си нужна именно для экономии памяти, поскольку никаких фич она не даёт, и tagged union можно и без неё реализовать.
Gorthauer87
Можно в принципе и на голых массивах сделать, но это не очень удобно и к тому же, тогда нужно будет вручную считать максимальную длину объединения.
koowaah Автор
Можете привести пример, как это можно сделать.
koowaah Автор
По поводу union+pattern matching я с вами согласен.
А как бы вы реализовали, не используя дженерики?
Gorthauer87
Как фичу языка, введя новое ключевое слово или как поступили в Rust и Swift: переиспользовав существующее слово enum.
splix
На мой взгляд это полезно для структурирования кода.
Сейчас если метод может принимать MyDataInstance или MyDataRefid, где первое это весь объект целиком, а второе скажем id. В такой ситуации чаще всего метод объявляется принимающим Object, и внутри идет проверка типа инстанса. Это дублированием кода, тестированием, проблемы с добавлением новой поддерживаемой структуры, отсутсвие самодокументируемости кода и пр. Не знаю как у остальных, но я часто сталкиваюсь с таким кодом.
Был бы union можно было объявить метод myMethod(Union<MyDataInstance, MyDataRefid> data) и решить все проблемы компилятором. Я не согласен с реализацией идеи в статье, но в целом что-то такое наверное попробую.
igormich88
А что мешает вам делать вот так?
splix
Не уточнил что чаще случается что такой класс обьявлен как generic, т.е. это инстанс
ClassFoo<Long>
илиClassFoo<ClassBar>
. К сожалению в Java не получится сделать method overloading и метод объявляется какClassFoo<Object>
, как на прием так и на результат, и дальше программист кастит куда нужноДа, не забывайте что методы не только принимают, но и возвращают значения. Поэтому по факту получается:
Часто read не знает что он прочитает заранее, частая проблема в API и интеграции. Поэтому имеем
а потом опять куча ифов и кастингов после чтения.
Или в более реальном случае
Так как это в коде это может быть использовано много где, к тому же объекты могут быть зависимы а значит перемножаются. Конечно можно так писать, так и делают, но в итоге код становится нечитаемым и дорогим в поддержке (особенно добавлении нового подтипа). Может быть с Union это будет легче поддерживаемо
splix
Уточню еще что можно делать как угодно, все будет работать, проблема в том что это лишний код который приходится везде таскать и усложнять читаемость кода. В большинстве методов не важно же какой
MyData<?>
приходит, и вообще такой подтип незачем светить наружу. В идеале всегда нетипизированый MyData который ужеmmMike
Я может не совсем понял Вашу мысль. У меня был вопрос не про то как сделать, а про то, в каких случаях нужны такое "В такой ситуации чаще всего метод объявляется принимающим Object, и внутри идет проверка типа инстанса."
Т.е. совсем разнородные объекты, не объединенные даже общим интерфейсом.
Еще раз поясню. Не как реализовать. А в каких случаях (желательно пример) эта реализация может понадобится.
splix
Я думал я именно это ответил. Попробую пояснить — в моем примере используются разнородные объекты, но по факту это разные отражения одной сущности. В одном случае это указатель на данные, другой это полные данные. Long и MyDataInstance.
Это конкретный пример из жизни, не выдуманный.
mmMike
Простите за некоторое занудство.
Попробую переформулировать.
Для каких случаев может понадобится разнородные объекты в для хранения в одном union.
Коллекции списков объектов с одинаковыми свойствами (интерфейсами) — это понятно зачем. А вот коллекция объектов у которых только Object класс общий…
Как то выглядит концептуально не очень красиво (с моей точки зрения… не навязываю).
Поэтому мой вопрос был не про конкретную реализацию, а про саму необходимость программных реализаций union в Java.
Любую задачу можно решить разными путями.
Пример приведенный koowaah про void printType(String|Integer|Float val) не особо убеждает.
Ну чуть синтаксического сахара и проверки типов на этапе компиляции. Стоит ли оно того что бы городить программные реализации Union на Java.
Войдет в стандарт — ну хорошо. Не вошло еще. Так можно и без union делать.
koowaah Автор
Union начали добавлять во многие языки и может быть в Java в будущем тоже добавят. Поживем, увидим.
splix
Все можно сделать без union, оно так и сделано сейчас. Я нигде не утверждал что без него никак. Вообще все современные языки лишь дополнительное удобство, а технически все можно написать наверное и на Фортране.
Я не совсем понимаю чем мой ответ вас не устраивает, он же не выдуманный. Обычная ситуация когда внешний API в одном случае возвращает список с идентификаторами, в другом список с дынными. В одном случае лишь long во втором полноценный объект. Надо уметь работать и с тем и с тем.
igormich88
Я напишу своё понимание проблемы, из за удаления информации о дженериках в рантайме, нельзя написать два метода с одним именем один из которых принимает SomeContainer[Integer], а другой SomeContainer[String] и введение конструкции SomeContainer[Integer|String] помогло бы. Но это не совсем union в классическом понимании.
PS с телефона треугольные скобки неправильно отображаются.
mmMike
Это действительно не union. Это скорее проблема наследия Java 1.4 и способа появления дженериков при ее эволюции.
Да. Это раздражает сильно (недотипы с дженерик в языке).
Поскольку когда то активно писал на С/С++ (да и сейчас пишу под хобби и иногда сопровождаю/модифицирую легаси код на C++), то union воспринимаю, скорее в терминах C/C++.
mayorovp
При восприятии идеи типа-объединения или типа-суммы в терминах C++ надо смотреть на тип данных std::variant, а не на ключевое слово union...