При разработке кода иногда нужно чтобы объект в определенный момент содержал значения одного типа или значения другого типа. Языки программирования, которые поддерживают концепцию объединений, позволяют в определенный момент сохранять текущее значение в одной области памяти.

Например, в языках С/С++ можно написать вот так.

  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)


  1. mmMike
    27.08.2019 09:19

    Я знаю зачем в "C" в использовались union. В основном от дикой нехватки памяти и/или работе с raw бинарными данными вида "код типа данных" + union структур разных типов.
    Дает и экономию памяти и упрощение "парсинга" данных, когда бинарный блок нужно "разложить" на "поля" по быстрому.


    К слову, "неопределенное поведение" с union в "C" не вполне описывает ширину проблемы. Тут могут быть и аппаратные прерывания по ошибке (сегментирование памяти, не верный формат float и прочее процессорнозависимое)
    union переползло и в C++.


    Но к чему тянуть этот стиль в Java — я не понял.
    Фраза "нужно чтобы объект в определенный момент содержал значения одного типа или значения другого типа." не описывает зачем это нужно.


    Да можно создать псевдо аналог. Но зачем?
    Только потому, что есть привычка работы с union? Или можно сделать, потому что можно сделать..


    Это не троллинг. Я действительно не понимаю в каком case может понадобится такой Java union class (что нельзя решить другими более характерными для Java методами).


    1. koowaah Автор
      27.08.2019 10:11
      -2

      Если два или более объектов большие, а Class занимают занимают менее памяти. В таком варианте можно использовать.


      1. mmMike
        27.08.2019 10:37
        +1

        Не понял…
        Union у Вас это инкапсулятор содержащий фактически ссылки на объект (один из).
        Какая связь с экономией памяти?


        1. koowaah Автор
          27.08.2019 11:13

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


      1. mayorovp
        27.08.2019 10:46
        +1

        Вы специально отвечаете не на тот комментарий?


        Если в поле записано null — совершенно не важно насколько большой объект там мог бы быть записан, памяти требуется всего 4-8 байт. Один null никак не может быть больше двух Class<>


        1. koowaah Автор
          27.08.2019 11:08

          В случае null это верно. Можно при передачи объекта null, кидать исключения.
          Использование двух Class<> нужно чтобы сохранять типы для корректных полей. Без них не обойтись.


          1. mayorovp
            27.08.2019 12:29
            +1

            Да как не обойтись-то? Я же ниже уже писал:


                    private T1 firstValue;
                    private T2 secondValue;

            Ну и где тут вообще нужно сравнивать типы?


            1. koowaah Автор
              27.08.2019 12:34

              Если имеется например, union с двух типов — Union2<String, Integer>. И хотим записать вещественное число. Нужно как то проверять типы. Для этого и нужны два поля Class<>.


              1. mayorovp
                27.08.2019 12:36
                +1

                Кому нужно их проверять? Почему этого не может сделать компилятор?


                1. koowaah Автор
                  27.08.2019 12:53
                  -1

                  В случае двух полей разных типов сравнивать не нужно.
                  В случае union нужно как-то проверять типы.


                  1. mayorovp
                    27.08.2019 12:57

                    А если внутри union два поля?


                    1. koowaah Автор
                      27.08.2019 13:26
                      -1

                      В определенный момент может сохранять только одно значение определенного типа в поле Object value. Я показал только для Union2, Union3. Можно специализацию сделать и для больше типов.


                      1. mayorovp
                        27.08.2019 13:28

                        Вы сейчас ответили не на вопрос "Почему компилятор не может проверить типы сам, если внутри Union2 два поля?", а на какой-то другой.


                        1. koowaah Автор
                          27.08.2019 13:57

                          А как вы это понимаете?


            1. 0xd34df00d
              27.08.2019 17:29

              Так это ж не юнион.


              T1 first;
              T2 second;

              изоморфно произведению (Maybe T1, Maybe T2), а не сумме Either T1 T2.


              1. mayorovp
                27.08.2019 17:36

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


                1. 0xd34df00d
                  27.08.2019 17:39
                  -1

                  А как вы это ограничение наложите в джаве?


                  1. igormich88
                    27.08.2019 17:48
                    +1

                    Через сеттеры?

                    public void setFirst(T1 value) {
                    first = value;
                    second = null;
                    }
                    public void setSecond(T2 value) {
                    first = null;
                    second = value;
                    }
                    


                    1. 0xd34df00d
                      27.08.2019 17:53

                      setFirst(null);


                      Теперь у вас в типе-сумме вообще ничего.


                      1. igormich88
                        27.08.2019 18:07

                        Если такая ситуация является недопустимой, можно добавить проверку на null.

                        public void setFirst(@NonNull T1 value) {
                          if (value == null) {
                            throw new NullPointerException("value is marked @NonNull but is null");
                          }
                          first = value;
                          second = null;
                        }


                        1. 0xd34df00d
                          27.08.2019 18:10
                          -1

                          Так это рантайм-проверки (пусть и написанные вами в классе и поэтому почти невидимые клиентам).


                          1. mayorovp
                            27.08.2019 18:53
                            -1

                            Ну так это особенность именно что null. Не просто же так null называют ошибкой на миллион долларов.


    1. Gorthauer87
      27.08.2019 14:07

      Вообще union в Си это очень низкоуровневая версия Типа-Суммы, в которой сам программист должен решать какой тип там лежит. Обычно это решается через идентификатор. Такой паттерн называют tagged union.
      А сама эта фича пришла из функциональных языков программирования и в совокупности с паттерн матчингом позволяет достигать очень существенных улучшений кодовой базы.
      То есть тут вообще не стоит вопроса об экономии памяти, эта абстракция прежде всего для программиста, а не для оптимизации.
      Хотя попытки эмулировать эту фичу через шаблоны смотрятся убого(


      1. mayorovp
        27.08.2019 14:21

        Уточнение: union в Си это очень низкоуровневая версия Типа-Объединения. И то до него толком не дотягивает.


        Вот tagged union — да, уже тип-сумма.


        И нет, union как языковая конструкция в Си нужна именно для экономии памяти, поскольку никаких фич она не даёт, и tagged union можно и без неё реализовать.


        1. Gorthauer87
          27.08.2019 16:28

          Можно в принципе и на голых массивах сделать, но это не очень удобно и к тому же, тогда нужно будет вручную считать максимальную длину объединения.


          1. koowaah Автор
            27.08.2019 16:44

            Можете привести пример, как это можно сделать.


      1. koowaah Автор
        27.08.2019 14:23

        По поводу union+pattern matching я с вами согласен.
        А как бы вы реализовали, не используя дженерики?


        1. Gorthauer87
          27.08.2019 16:28
          +1

          Как фичу языка, введя новое ключевое слово или как поступили в Rust и Swift: переиспользовав существующее слово enum.


    1. splix
      27.08.2019 20:31

      На мой взгляд это полезно для структурирования кода.

      Сейчас если метод может принимать MyDataInstance или MyDataRefid, где первое это весь объект целиком, а второе скажем id. В такой ситуации чаще всего метод объявляется принимающим Object, и внутри идет проверка типа инстанса. Это дублированием кода, тестированием, проблемы с добавлением новой поддерживаемой структуры, отсутсвие самодокументируемости кода и пр. Не знаю как у остальных, но я часто сталкиваюсь с таким кодом.

      Был бы union можно было объявить метод myMethod(Union<MyDataInstance, MyDataRefid> data) и решить все проблемы компилятором. Я не согласен с реализацией идеи в статье, но в целом что-то такое наверное попробую.


      1. igormich88
        27.08.2019 20:42

        А что мешает вам делать вот так?

        myMethod(MyDataInstance data) {...}
        myMethod(MyDataRefid refid) {
          MyDataInstance data = getData(refid);
          myMethod(data);
        }


        1. splix
          27.08.2019 21:13

          Не уточнил что чаще случается что такой класс обьявлен как generic, т.е. это инстанс ClassFoo<Long> или ClassFoo<ClassBar>. К сожалению в Java не получится сделать method overloading и метод объявляется как ClassFoo<Object>, как на прием так и на результат, и дальше программист кастит куда нужно


          Да, не забывайте что методы не только принимают, но и возвращают значения. Поэтому по факту получается:


          myAction(MyDataInstance data) {...}
          myAction(MyDataRefid refid) {...}
          MyDataInstance readAsData() {...}
          MyDataRefid readAsRef() {...}

          Часто read не знает что он прочитает заранее, частая проблема в API и интеграции. Поэтому имеем


          Object read() {...}

          а потом опять куча ифов и кастингов после чтения.


          Или в более реальном случае


          <T> myAction(MyData<T> data, Class<T> type) {
            if (type.isAssignable(Long.class)) then {...} 
            else if (type.isAssignable(Instance.class)) then {...} 
            else throw new IllegalArgumetnException()
          }
          
          myActionAsData(MyData<Instance> data) {...}
          
          myActionAsRef(MyData<Ref> data) {...}
          
          <T> MyData<T> read(Class<T> type) {
            if (type.isAssignable(Long.class)) then {...} 
            else if (type.isAssignable(Instance.class)) then {...} 
            else throw new IllegalArgumentException()
          }
          
          MyData<Object> read() {...}

          Так как это в коде это может быть использовано много где, к тому же объекты могут быть зависимы а значит перемножаются. Конечно можно так писать, так и делают, но в итоге код становится нечитаемым и дорогим в поддержке (особенно добавлении нового подтипа). Может быть с Union это будет легче поддерживаемо


        1. splix
          27.08.2019 21:40

          Уточню еще что можно делать как угодно, все будет работать, проблема в том что это лишний код который приходится везде таскать и усложнять читаемость кода. В большинстве методов не важно же какой MyData<?> приходит, и вообще такой подтип незачем светить наружу. В идеале всегда нетипизированый MyData который уже


          class MyOtherDataAny extends Union<Long, MyOtherData> {};
          
          class MyData {
             MyOtherDataAny other;
          }


      1. mmMike
        28.08.2019 05:03

        Я может не совсем понял Вашу мысль. У меня был вопрос не про то как сделать, а про то, в каких случаях нужны такое "В такой ситуации чаще всего метод объявляется принимающим Object, и внутри идет проверка типа инстанса."


        Т.е. совсем разнородные объекты, не объединенные даже общим интерфейсом.


        Еще раз поясню. Не как реализовать. А в каких случаях (желательно пример) эта реализация может понадобится.


        1. splix
          28.08.2019 05:48

          Я думал я именно это ответил. Попробую пояснить — в моем примере используются разнородные объекты, но по факту это разные отражения одной сущности. В одном случае это указатель на данные, другой это полные данные. Long и MyDataInstance.
          Это конкретный пример из жизни, не выдуманный.


          1. mmMike
            28.08.2019 06:07

            Простите за некоторое занудство.
            Попробую переформулировать.
            Для каких случаев может понадобится разнородные объекты в для хранения в одном union.
            Коллекции списков объектов с одинаковыми свойствами (интерфейсами) — это понятно зачем. А вот коллекция объектов у которых только Object класс общий…
            Как то выглядит концептуально не очень красиво (с моей точки зрения… не навязываю).
            Поэтому мой вопрос был не про конкретную реализацию, а про саму необходимость программных реализаций union в Java.
            Любую задачу можно решить разными путями.


            Пример приведенный koowaah про void printType(String|Integer|Float val) не особо убеждает.


            Ну чуть синтаксического сахара и проверки типов на этапе компиляции. Стоит ли оно того что бы городить программные реализации Union на Java.
            Войдет в стандарт — ну хорошо. Не вошло еще. Так можно и без union делать.


            1. koowaah Автор
              28.08.2019 10:26

              Union начали добавлять во многие языки и может быть в Java в будущем тоже добавят. Поживем, увидим.


            1. splix
              28.08.2019 22:24

              Все можно сделать без union, оно так и сделано сейчас. Я нигде не утверждал что без него никак. Вообще все современные языки лишь дополнительное удобство, а технически все можно написать наверное и на Фортране.


              Я не совсем понимаю чем мой ответ вас не устраивает, он же не выдуманный. Обычная ситуация когда внешний API в одном случае возвращает список с идентификаторами, в другом список с дынными. В одном случае лишь long во втором полноценный объект. Надо уметь работать и с тем и с тем.


        1. igormich88
          28.08.2019 10:29

          Я напишу своё понимание проблемы, из за удаления информации о дженериках в рантайме, нельзя написать два метода с одним именем один из которых принимает SomeContainer[Integer], а другой SomeContainer[String] и введение конструкции SomeContainer[Integer|String] помогло бы. Но это не совсем union в классическом понимании.
          PS с телефона треугольные скобки неправильно отображаются.


          1. mmMike
            28.08.2019 10:55

            Это действительно не union. Это скорее проблема наследия Java 1.4 и способа появления дженериков при ее эволюции.
            Да. Это раздражает сильно (недотипы с дженерик в языке).


            Поскольку когда то активно писал на С/С++ (да и сейчас пишу под хобби и иногда сопровождаю/модифицирую легаси код на C++), то union воспринимаю, скорее в терминах C/C++.


            1. mayorovp
              28.08.2019 13:08

              При восприятии идеи типа-объединения или типа-суммы в терминах C++ надо смотреть на тип данных std::variant, а не на ключевое слово union...


  1. 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;


    1. koowaah Автор
      27.08.2019 10:07
      -2

      В языке Scala 3.0 хотят завести union типы. Если не ошибаюсь в TypeScript уже есть union типы.
      Концепция union типов стает популярной в языках.


      1. mayorovp
        27.08.2019 10:08
        +1

        Станет-то станет, но в чём был смысл делать тремя полями и сложно там, где можно было сделать двумя полями и просто?


      1. AnarchyMob
        27.08.2019 11:13
        +1

        Если не ошибаюсь, в jvm языке Ceylon давно есть union типы...


        1. 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``"); }
          }
          


  1. koowaah Автор
    27.08.2019 11:19

    Было б классно если такую фичу завезли в Java.


    1. CyberSoft
      27.08.2019 18:14

      В скором времени завезут: Pattern-matching for switch, Sealed Types for Java Language.


      1. koowaah Автор
        27.08.2019 18:54

        Эти классные фичи. Очень бы упростили написание кода.


        1. Ryppka
          28.08.2019 08:42
          -2

          Ага, ага… Ява все больше напоминает холеную корову, на которую напялили несколько дорогих стампидных седел…


          1. koowaah Автор
            28.08.2019 10:24

            Pattern matching в Java хотят завезти. В добавок хотят добавить record & sealed type.
            Я думаю Java правильно развивается. Это упростит много кода.


  1. igormich88
    27.08.2019 15:55
    +1

    Скажите что будет если я попробую запустить такой код?

    Union biUnion = Union.of(Integer.class, CharSequence.class);
    biUnion.set("Line");
    


    1. koowaah Автор
      27.08.2019 16:08
      -1

      Он сравнивает с типами String и Integer. Но не сравнивает с реализуемыми интерфейсами.


      1. igormich88
        27.08.2019 16:29

        А почему вы не используете?

        firstClass.isInstance(value)


        1. koowaah Автор
          27.08.2019 16:40

          Как вариант можно. Но что если класс реализует много интерфейсов?


  1. splix
    28.08.2019 05:48

    (удалил, не та ветка)