Лямбда-выражения в Java, как и зачем их сериализировать?



    Механизм лямбда-выражений, представленный в Java8, стал такой фичей, которая чётко разделила код до нее и после (кажется, такое же может случится с Java9 и модульной системой, но в плохом смысле). В Java8 стало больше функциональных трюков, разнообразная обработка больших массивов данных стала значительно проще и теперь занимает куда меньше места. Однако, касательно рационального использования лямбда-выражений существует много вопросов таких как: насколько рационально часто их использовать? существенна ли потеря производительности теряется при переходе от обычного цикла в `forEach()` с лямбда-выражением и так далее. Большинство курсов (даже курс Oracle) игнорируют эти вопросы. В этом посте будет описан как раз один из, наверное, наименее популярных вопросов, но не менее интересный чем остальные:
    Как работать сериализация лямбда-выражений в Java и как её можно использовать?



    Как сделать лямбда выражение сериализируемым?


    В первую очередь, важно понимать, что лямбда-функция в Java всё-таки объект с одним нереализированным методом (остальные могут быть объявлены как default методы в интерфейсе (с) uthark), а не функция. То есть, с ней доступны все те же манипуляции, как и с обычными объектами. Иными словами, лямбда-функция, которая реализует сериализируемый интерфейс, будет сериализируемой.

    public interface SDoubleUnaryOperator extends DoubleUnaryOperator, Serializable {
    
    }
    
    public static void main(String[] args){
      SDoubleUnaryOperator t = t->t*t; //Сериализируемая лямбда-функция
    }
    

    Однако, с новыми фичами в Java8 стал доступным более упрощённый вариант, без создания дополнительного интерфейса:

    public static void main(String[] args){
      DoubleUnaryOperator t = (DoubleUnaryOperator & Serializable)t->t*t; //Сериализируемая лямбда-функция
    }
    

    В обоих случаях, вместо создания обычной лямбда-функции, компилятор создаст лямбда-функцию в специальной, сериализируемой форме. Далее с лямбда-функцией можно работать как с любым другим серилизируемым объектом, если не забывать про…

    Как происходит сериализация лямбда-функций?


    Для разбора процесса сериализации лямбда-функций был создан такой класс:

    import java.io.*;
    
    public class Main {
        public static void main(String[] args) throws Exception {
            //saveLambda();
            loadLambda();
        }
    
    
        public static void saveLambda() throws IOException {
            Factory factory = new Factory(3);
            ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream(new File("lambdas")));
            output.writeObject(factory.createLambda(4,new SerializableData(3.3,4)));
            output.writeObject(factory.createLambda(5,new SerializableData(5.3,-4)));
            output.writeObject(factory.createLambda(3,new SerializableData(10.3,+80e-5)));
            output.close();
        }
    
    
        public static void loadLambda() throws IOException, ClassNotFoundException {
            ObjectInputStream input = new ObjectInputStream(new FileInputStream(new File("lambdas")));
            DoubleToString operator1 = (DoubleToString) input.readObject();
            DoubleToString operator2 = (DoubleToString) input.readObject();
            DoubleToString operator3 = (DoubleToString) input.readObject();
            System.out.println(operator1.get(5));
            System.out.println(operator2.get(5));
            System.out.println(operator3.get(5));
        }
    }
    

    Полный код можно посмотреть тут.

    При сериализации, лямбда-функция сериализирует все переменные, которые в ней используются, и внешний объект, в которым она реализуется, если в ней используются поля этого объекта. Соответственно, если хоть один из нужных компонентов не будет сериализируемым, то будет получена ошибка java.io.NotSerializableException. Сериализированная лямбда-функция не знает ничего о том, что она должна делать с данными. Она знает только идентификатор соответствущего специального класса для этой функции.

    Лично для меня это было небольшим разочарованием. По сути, во время сериализации лямбда-функции, просто сериализируется список параметров, которые используются в ней и её идентификатор. Тем не менее, после небольших раздумий и экспериментов, я обнаружил несколько полезных особенностей и один практический пример использования сериализации лямбда-функций.

    Особенности сериализации лямбда-функций

    • Отсутствие дубликатов в сериализации. Иными словами, если несколько лямбда-функций сериализируют один и тот же объект, он будет сериализирован только один раз;
    • Минимально необходимая сериализация. Сериализируются только те объекты, которые необходимы. Для других, например, если это внешний объект, сохраняется только название;
    • Скорость работы лямбда-функции подготовленной к сериализации практически не различается со скоростью обычной лямбда-функции.


    Практическое использование сериализации лямбда-функций


    Единственный полезный способ, который я нашёл, использования сериализации лямбда-функций заключается в получении лямбда-функций из класса, который подгружается во время выполнения программы. Дело в том, что стандартных методов, для получения лямбда-функций, которые были реализованы в этом классе я так и не нашёл. Так что сериализация может помочь передавать лямбда-функции между программами или встраивать их во внешние модули.

    Для начала реализуем некий внешний класс:

    public class RemoteLambdaClass{
      public static void main(String[] args) throws Exception{
          DoubleUnaryOperator t1 = (DoubleUnaryOperator & Serializable) t->t*t;
          DoubleUnaryOperator t2 = (DoubleUnaryOperator & Serializable) t->t*t*t;
          ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream(new File("lambdas")));
          output.writeObject(t1);
          output.writeObject(t2);
          output.close();
      }
    }
    

    После его запуска будет создан файл `lambdas`, в котором и будут находиться сериализированные лямбда-фукнции. Далее создадим простой класс для чтения (предполагается, что внешний класс и файл лежат в папке RemoteLambdaClass в корне проекта):

    public class Main {
        public static void main(String[] args) throws Exception {
            File f = new File("./RemoteLambdaClass/");
            //Добавления пути к файлу в стандартный лоадер
            URLClassLoader mainLoader = (URLClassLoader) Main.class.getClassLoader();
            Field classPathField = URLClassLoader.class.getDeclaredField("ucp");
            classPathField.setAccessible(true);
            URLClassPath urlClassPath = (URLClassPath) classPathField.get(mainLoader);
            urlClassPath.addURL(f.toPath().toUri().toURL());
    
    
            Class<?> clazz  = mainLoader.loadClass("RemoteLambdaClass");
            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File(f,"lambdas")));
            DoubleUnaryOperator o1 = (DoubleUnaryOperator) inputStream.readObject();
            DoubleUnaryOperator o2 = (DoubleUnaryOperator) inputStream.readObject();
            System.out.println(o1.applyAsDouble(2));
            System.out.println(o2.applyAsDouble(2));
        }
    }
    

    Результаты выполнения:

    4.0
    8.0
    

    Итог

    Сериализация лямбда-функций весьма представляет собой весьма интересный, но практически бесполезный механизм. Причины делать их сериализируемыми скрываются скорее в том, что бы избежать проблем с сериализируемыми интерфейсами. Тем не менее, думаю, такому механизму можно придумать еще несколько применений, которые я пропустил.
    Share post

    Similar posts

    Comments 12

      +1
      Да можно скрестить ужа с ежом и получить колючую проволоку. Но действительно нафиг это надо в Java?
        +3
        Это вы про сериализацю лямбда-выражений или про лямбда-выражения в Java вообще?
        +1
        То, что сам код лямбды не сериализуется (а только внутреннее имя и параметры), мне кажется совершенно нормальным. Ибо сериализация кода, весьма вероятно, может привести к куче проблем — как с безопасностью, так и с переносимостью. Кроме того, не знаю как в Java, но в Haxe можно сериализовать нечто в одной программе, а десериализовать в другой (понятно, что если сериализовались экземпляры классов, то во второй программе должны эти классы присутствовать). Поэтому в Haxe запрещена сериализация лямбд. Полагаю, что в Java будет такая же проблема — не получится выполнить десериализацию в другой программе, ибо лямбда там просто-напросто будет отсутствовать.
          0
          Возможо, если засунуть в другую программу класс из первой. Но в обычном случае получится NoSuchClassException.
          +2
          В первую очередь, важно понимать, что лямбда-функция в Java всё-таки объект с одним методом, а не функция.

          Строго говоря, объявленных методов может быть больше одного. Лямбда-функция в Java — это реализация функционального типа, а функциональный тип — любой интерфейс или абстрактный класс, у которых один метод объявлен как абстрактный (смотрим спеку (PDF), главы 9.8, 15.27).

          То есть, если у нас есть класс, у которого реализовано n методов, но один — абстрактный, то его можно использовать как функциональный интерфейс, если у нас есть интерфейс с m методами и m-1 метод реализует поведение по умолчанию (default methods) — то это тоже функциональный интерфейс.
            +1
            Касательно абстрактного класса, я не уверен, что это так. Например, такой код:
            public class Test {
                public static abstract class Test1{
                    public abstract double get();
                }
                public static void main(String[] args) {
                    Test1 t = ()->2;
                }
            }
            

            У меня не собирается с ошибкой, что нужен интерфейс.
            Но в остальном вы правы, сейчас исправлю.
          0
          Лямбда-выражения в Java, как и зачем их сериализировать?
          Надо до ката сразу написать "Tl;dr: не делаете так".
            +2
            > Сериализация лямбда-функций весьма представляет собой весьма интересный, но практически бесполезный механизм.

            Как ни странно, но сериализация лямбд вполне успешно и с большой пользой применяется на практике. Например, на этом основана распределенная обработка данных в Apache Spark (см. spark.apache.org/docs/latest/programming-guide.html#passing-functions-to-spark).
              0
              Это красиво, но потенциально небезопасно. Очень легко «захватить» с собой весь внешний объект со всем поддеревом, только лишь упомянув одно его поле в лямбде.
                0
                Да, есть такая проблема. В Scala ее пытаются решить с помощью макросов: github.com/heathermiller/spores
              0
              Я вот разбирался с Apache Ignite, который бесплатный клон GridGain
              И вижу там возможность выполнять лямбда код на удаленных нодах.
              типа такого вот:

              Ignite ignite = Ignition.ignite();
              // Print out hello message on all cluster nodes.
              ignite.compute().broadcast(() -> System.out.println(«Hello Node!»));

              Only users with full accounts can post comments. Log in, please.