4

I am aware that we can cast a function to be Serializable where we need this.

However, I would like to move this casting to a generic method, to make the using code less cluttered. I do not manage to create such a method.

My specific problem is that the below map is not Serializable:

final Map<MyObject, String> map =
        new TreeMap<>(Comparator.comparing(MyObject::getCode));

I can fix this by using:

final Map<MyObject, String> map =
        new TreeMap<>(Comparator.comparing((Function<MyObject, String> & Serializable) MyObject::getCode));

But I would like to be able to do something like:

final Map<MyObject, String> map =
        new TreeMap<>(Comparator.comparing(makeSerializable(MyObject::getCode)));

public static <T, U> Function<T, U> makeSerializable(Function<T, U> function) {
    return (Function<T, U> & Serializable) function;
}

For the compiler this is fine, but at runtime, I get a ClassCastException:

java.lang.ClassCastException: SerializableTest$$Lambda$1/801197928 cannot be cast to java.io.Serializable

I also tried the following alternatives, without success:

// ClassCastException
public static <T extends Serializable, U extends Serializable> Function<T, U> makeSerializable(Function<T, U> function) {
    return (Function<T, U> & Serializable) function;
}

// No ClassCastException, but NotSerializableException upon Serializing
public static <T, U> Function<T, U> makeSerializable2(Function<T, U> function) {
    return (Function<T, U> & Serializable) t -> function.apply(t);
}

Is it possible to create such a method?

Implementation of MyObject:

static class MyObject implements Serializable {

    private final String code;

    MyObject(String code) {
        this.code = code;
    }

    public String getCode() {
        return code;
    }

}
Ward
  • 2,799
  • 20
  • 26
  • Can you add your test case (or an [MCVE](https://stackoverflow.com/help/mcve)) so that we can test potential solutions? – Jacob G. Apr 09 '18 at 15:43
  • @JacobG. he already did... – Eugene Apr 09 '18 at 15:43
  • @Eugene Where? I don't see code that serializes anything in the question. Specifically, I'd like to see how he's serializing his object. – Jacob G. Apr 09 '18 at 15:44
  • 1
    @JacobG. my bad - it was in the now deleted answer `new ObjectOutputStream(new ByteArrayOutputStream()).writeObject(map);` – Eugene Apr 09 '18 at 15:45
  • @Eugene Oh, thanks! I need to get to 10k more quickly so I could have seen that! – Jacob G. Apr 09 '18 at 15:45
  • @FedericoPeraltaSchaffner You should post that comment you made earlier (regarding creating a functional interface) as an answer. I had that idea too, but I'd feel bad stealing it now that it's in the comments :P – Jacob G. Apr 09 '18 at 15:52
  • Given all the gymnastics needed to make this work, I would opt to forego lambdas and method references altogether, and would create an explicit class that implements both `Comparator` and `Serializable`. A possible name for it might be ByCodeComparator. – VGR Apr 09 '18 at 17:19
  • I see where you heading at, but then I think the use of the nice comparator factory methods added in Java 8 get's a bit lost? I think these are really nice additions to create combined/chained comparator in a clear understandable way. – Ward Apr 09 '18 at 18:01

2 Answers2

7

Yes, it's possible, as long as you don't use Function<T, U> as the type of either the argument or the result.

Instead, you could create your own functional interface that is both a Function and Serializable:

interface SerFunc<T, U> extends Function<T, U>, Serializable { }

As @M.Prokhorov cleverly suggests in the comments, you could create a method that resembles your method, but that receives and returns an instance of SerFunc instead of Function:

public static <T, U> SerFunc<T, U> makeSerializable(SerFunc<T, U> function) {
    return function;
}

The only goal of this method would be to provide a Serializable target type for the method reference or lambda expression passed as an argument. This is why we're just returning the argument, i.e. doing nothing.

Now you can use the method as in your code, and everything will work fine:

Map<MyObject, String> map =
    new TreeMap<>(Comparator.comparing(makeSerializable(MyObject::getCode)));

As to why your attempts failed, I think you could find the reason in an answer to this question (link provided by @Eugene), where Brian Goetz explains that this behavior is by design:

This is correct, and by design. Just as you cannot take a non-serializable object and make it serializable after instantiation, once a lambda is created, its serializability is set.

A lambda is serializable if its target type is serializable (and its captured arguments are serializable.)

In your attempts, the original function (received by the makeSerializable method as an argument) is not Serializable, so any lambda we create that uses this non-serializable function (which would actually be a captured argument) will be non-serializable as well.

Community
  • 1
  • 1
fps
  • 33,623
  • 8
  • 55
  • 110
  • 1
    Comments are not for extended discussion; this conversation has been [moved to chat](https://chat.stackoverflow.com/rooms/168595/discussion-on-answer-by-federico-peralta-schaffner-how-to-make-function-serializ). – Samuel Liew Apr 09 '18 at 23:24
1

Well, if you are willing to change your parameters (I am not sure the needed information could be extracted from input Function, but I could try to dig something up), you could dynamically create a serialized Function, not sure if this is doable for you...

public static <T, U> Function<T, U> makeSerializable(
        Class<T> targetType, // MyObject
        Class<U> returnType, // String
        String methodName) throws Throwable {

    MethodHandles.Lookup lookup = MethodHandles.lookup();
    MethodType methodType = MethodType.methodType(returnType, targetType);
    Function<T, U> fun = (Function<T, U>) LambdaMetafactory.altMetafactory(
            lookup,
            "apply",
            MethodType.methodType(Function.class),
            methodType,
            lookup.findVirtual(targetType, methodName, MethodType.methodType(returnType)),
            methodType,
            1) // this signals for serialization 
            .getTarget().invokeExact();
    return fun;
}

This could take a boolean and made to work with a static method reference...

Invocation would be:

Map<MyObject, String> map = new TreeMap(Comparator.comparing(makeSerializable(
            MyObject.class, String.class, "getCode")));
Eugene
  • 117,005
  • 15
  • 201
  • 306
  • I like this answer, it's a good alternative if you don't want to create a `Serializable` functional interface as I've done in my answer. – fps Apr 09 '18 at 17:45
  • Actually, I don't really like this, this seems to fabricated for me – Ward Apr 09 '18 at 17:54
  • @Ward you asked the question so it's entirely up to you, but this is what happens behind the scenes when you are using a method reference anyway – Eugene Apr 09 '18 at 18:11