Wednesday, December 4, 2013

Lambda, will it serialize?

So I have been ponder an enhancement required on the Tyrus project that would allow a user to broadcast to a subset of client connected to a URL across a cluster of machines. There are various way of doing this; but since I was playing with JDK 8 this problem definitely looked like a nail.

To this end I created a simple unit test class that would take my filter, serialise it to disk, read it back and in then execute it. It had a instance field "VALUE" that we could use to reference directly or indirectly to find out what would cause the serialisation to fail.

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.NotSerializableException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.Serializable;

import java.util.function.Predicate;

import org.junit.Test;

public class SerializablePredicateFilterTest {

  public String VALUE = "Bob";

  public interface SerializablePredicate<T> extends Predicate<T>, Serializable {

  }


  public <T> void filter(SerializablePredicate<T> sp, T value) throws IOException, ClassNotFoundException {


    sp.getClass().isLocalClass();

    File tempFile = File.createTempFile("labmda", "set");


    try (ObjectOutput oo = new ObjectOutputStream(new FileOutputStream(tempFile))) {
      oo.writeObject(sp);
    }


    try (ObjectInput oi = new ObjectInputStream(new FileInputStream(tempFile))) {
      SerializablePredicate<T> p = (SerializablePredicate<T>) oi.readObject();

      System.out.println(p.test(value));
    }


  }

}


So just to calibrate lets make sure that an anonymous inner class will fail, because it will always contain a reference to enclosing object....

  @Test(expected = NotSerializableException.class)
  public void testAnonymousDirect() throws IOException, ClassNotFoundException {

    String value = VALUE;


    filter(new SerializablePredicate<String>() {

      @Override
      public boolean test(String t) {
        return value.length() > t.length();
      }
    }, "Bob");

  }


The same is true for local classes, what you don't use local classes?

  @Test(expected = NotSerializableException.class)
  public void testLocalClass() throws IOException, ClassNotFoundException {

    class LocalPredicate implements SerializablePredicate<String> {
      @Override
      public boolean test(String t) {
        // TODO Implement this method
        return false;
      }
    }

    filter(new LocalPredicate(), "Bobby");

  }


So a standalone class will of course work, in this case a nested class for convenience.

  public static class LengthPredicate implements SerializablePredicate<String> {

    private String value;


    public LengthPredicate(String value) {
      super();
      this.value = value;
    }

    public void setValue(String value) {
      this.value = value;
    }

    public String getValue() {
      return value;
    }

    @Override
    public boolean test(String t) {
      // TODO Implement this method
      return false;
    }
  }


  @Test
  public void testStaticInnerClass() throws IOException, ClassNotFoundException {

    filter(new LengthPredicate(VALUE), "Bobby");

  }


So lets get down with JDK 8, it turns out that my first try also fails but it does confirm that the serialisation is quite happy to take a Lambda in general.

  @Test(expected = NotSerializableException.class)
  public void testLambdaDirect() throws IOException, ClassNotFoundException {


    filter((String s) -> VALUE.length() > s.length(), "Bobby");

  }


A slight modification to copy the value into a effectively final attributes, and voila the lambda is now serialised and retrieved properly.

  @Test
  public void testLambdaInDirect() throws IOException, ClassNotFoundException {

    String value = VALUE;

    filter((String s) -> value.length() > s.length(), "Bobby");

  }


And of course if the value is a simple method parameter it also works fine.


  @Test
  public void testLambdaParameter() throws IOException, ClassNotFoundException {

    invokeWithParameter(VALUE);

  }

  private void invokeWithParameter(String value) throws java.lang.ClassNotFoundException, java.io.IOException {
    filter((String s) -> value.length() > s.length(), "Bobby");
  }


So the answer is yes, you can get it to serialise if you are a bit careful.

1 comment:

lucidfox said...

Actually, an anonymous inner class created from a static method will (obviously) have no reference to the enclosing instance - because there is no enclosing instance. Though this may be even more cumbersome than just using a named static inner class.