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

от автора


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

Как работать сериализация лямбда-выражений в Java и как её можно использовать?

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

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

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 
Итог

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

ссылка на оригинал статьи http://habrahabr.ru/post/264003/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *