Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring can't determine generic types when lambda expression is used instead of anonymous inner class

I'm playing around with Spring's ConversionService, adding a simple converter to convert a ZonedDateTime (Java 8) to String:

@Bean
public ConversionServiceFactoryBean conversionServiceFactoryBean() {
    ConversionServiceFactoryBean conversionServiceFactoryBean =
        new ConversionServiceFactoryBean();

    Converter<ZonedDateTime, String> dateTimeConverter =
        new Converter<ZonedDateTime, String>() {
            @Override
            public String convert(ZonedDateTime source) {
                return source.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
            }
        };

    conversionServiceFactoryBean.setConverters(
        new HashSet<>(Arrays.asList(dateTimeConverter)));
    return conversionServiceFactoryBean;
}

This works fine. But my IDE (IntelliJ) suggests replacing the anonymous inner class with a lambda expression:

Converter<ZonedDateTime, String> dateTimeConverter =
    source -> source.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);

If I do that, then it doesn't work anymore, I get an error about Spring not being able to determine the generic types:

Caused by: java.lang.IllegalArgumentException: Unable to the determine sourceType <S> and targetType <T> which your Converter<S, T> converts between; declare these generic types.
    at org.springframework.util.Assert.notNull(Assert.java:112)
    at org.springframework.core.convert.support.GenericConversionService.addConverter(GenericConversionService.java:100)
    at org.springframework.core.convert.support.ConversionServiceFactory.registerConverters(ConversionServiceFactory.java:50)
    at org.springframework.context.support.ConversionServiceFactoryBean.afterPropertiesSet(ConversionServiceFactoryBean.java:70)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1627)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1564)

The Class object that represents the lambda expression is apparently different enough from the Class for the anonymous inner class that Spring can't determine the generic types anymore. How does Java 8 do this exactly with lambda expressions? Is this a bug in Spring that is fixable or does Java 8 just not provide the necessary information?

I'm using Spring version 4.1.0.RELEASE and Java 8 update 20.

like image 941
Jesper Avatar asked Sep 07 '14 15:09

Jesper


People also ask

Which is better lambda expression or anonymous inner class?

Lambda expression can be used where a class implements a functional interface to reduce the complexity of the code. An inner anonymous class is more powerful as we can use many methods as we want, whereas lambda expression can only be used where an interface has only a single abstract method.

What is difference between lambda expression and anonymous class?

Anonymous class is an inner class without a name, which means that we can declare and instantiate class at the same time. A lambda expression is a short form for writing an anonymous class. By using a lambda expression, we can declare methods without any name.

What is the difference between and lambda expression and an inner class instance?

Anonymous inner class can be instantiated. Lambda Expression can't be instantiated. Inside Anonymous inner class, “this” always refers to current anonymous inner class object but not to outer object. Inside Lambda Expression, “this” always refers to current outer class object that is, enclosing class object.

How do I change my anonymous class with Lambda?

Since the most common use of Anonymous class is to provide a throwaway, stateless implementation of abstract class and interface with a single function, those can be replaced by lambda expressions, but when you have a state field or implementing more than one interface, you cannot use lambdas to replace the anonymous ...


1 Answers

This post linked in the comments by Alan Stokes explains the issue well.

Basically, in the current JDK, the actual implementation of the lambda is compiled into the declaring class and the JVM produces a Lambda class whose method is the erasure of the method declared in the interface.

So

Converter<ZonedDateTime, String> dateTimeConverter =
    source -> source.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);

produces a synthetic method like

private static java.lang.String com.example.Test.lambda$0(java.time.ZonedDateTime source)  {
    return source.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
}

which gets invoked by the generated lambda class instance. Internally, the functional interface method simply casts to the parameter type of the method above. The JLS states

If the erasure of the type of a method being overridden differs in its signature from the erasure of the function type of U, then before evaluating or executing the lambda body, the method's body checks that each argument value is an instance of a subclass or subinterface of the erasure of the corresponding parameter type in the function type of U; if not, a ClassCastException is thrown.

The VM itself produces an overriding method which is the raw equivalent of the method declared in the interface.

The only information you have about the types is in the static method above. Since this method is part of the declaring class, there's no way for Spring to retrieve it given an instance produced from the lambda expression.

However, you can do

interface ZonedDateTimeToStringConverter extends Converter<ZonedDateTime, String> {
}

and

Converter<ZonedDateTime, String> dateTimeConverter = (ZonedDateTimeToStringConverter)
    source -> source.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);

or

ZonedDateTimeToStringConverter dateTimeConverter =  source -> source.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);

This forces the lambda to declare a method like

public String convert(ZonedDateTime zdt);

and Spring will be able to find it and resolve the target and source types.

like image 166
Sotirios Delimanolis Avatar answered Oct 13 '22 21:10

Sotirios Delimanolis