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.
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.
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.
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.
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 ...
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, aClassCastException
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With