Reputation: 13556
given the following class:
public class FooTest {
public static class Base {
}
public static class Derived extends Base {
}
public interface Service<T extends Base> {
void service(T value);
}
public abstract class AbstractService<T extends Derived> implements Service<T> {
public void service(T value) {
}
}
private AbstractService service;
public void bar(Base base) {
if(base instanceof Derived) {
service.service(base); // compile error at this line
}
}
}
When building the class with the following pom.xml
:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mgm-tp</groupId>
<artifactId>java-compiler-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<compilerId>eclipse</compilerId>
</configuration>
<dependencies>
<dependency>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-compiler-eclipse</artifactId>
<version>2.5</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
in maven 3.4 it produces the following compile error:
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.3:compile (default-compile) on project java-compiler-test: Compilation failure [ERROR] C:\Users\abrieg\workingcopy\java-compiler-test\src\main\java\FooTest.java:[25] The method service(FooTest.Base) in the type FooTest.Service is not applicable for the arguments (FooTest.Base)
When setting source and target level to 1.7 for the eclipse compiler or when using javac
as the compiler there is no compile error reported.
The question is wheter JLS 1.8 is more specific about type inference such that this code is really not allowed as supposed by eclipse compiler for java 1.8 or if this is a regression in the eclipse compiler.
Based on the text of the compiler error I tend to say its a regression, but I am not sure.
I have identified the following two bugs already reported to jdt, but I think they do not apply exactly:
https://bugs.eclipse.org/bugs/show_bug.cgi?id=432603
https://bugs.eclipse.org/bugs/show_bug.cgi?id=430987
If this is a regression, has this already been reported to jdt?
Upvotes: 14
Views: 1096
Reputation: 1192
This of course is about typesafety
As others have said you need to cast base to Derived to get it to satisfy the requirements for service(T value) because the compiler knows that argument to AbstractService.service must extend Derived and so if it is not Derived it cannot fit.
Unfortunately this gets rid of the compiler error but it does not fix the problem. which is why you get the type safety warning.
Due to the definition of AbstractService
, service
is actually AbstractService<? extends Derived>
and if you fix this omission you will just get the error back.
This is because AbstractService<? extends Derived>
does not extend AbstractService<Derived>
and casting base to Derived fixes the problem for AbstractService<Derived>
.
Look at the classes below to see an example of this. ConcreteService
does not extend AbstractService<Derived>
it extends AbstractService<RealDerived>
. You cannot pass just any Derived
into ConcreteService.service(RealDerived base)
, for instance, you cannot pass a FalseDerived
object because that does not satisfy the argument type.
The truth is that somewhere something has to actually define what T is so that we can fix the type safety problem.
Doing it generically requires some trickery like a class that ties together the actual type of the argument with the actual type of the service so that the casting can be done in a typesafe way.
As follows
public class FooTest {
public static class Base {
}
public static class Derived extends Base {
}
public interface Service<T extends Base> {
void service(T value);
}
public abstract static class AbstractService<T extends Derived> implements Service<T> {
public void service(T value) {
}
}
// The following class defines an argument B that and ties together the type of the service with the type of the referenced stored class which enables casting using the class object
public abstract static class Barrer<B extends Derived>{
private AbstractService<B> service;
private Class<B> handledClass;
protected Barrer(Class<B> handledClass, AbstractService<B> service){
this.handledClass = handledClass;
this.service = service;
}
public void bar(Base base) {
if(handledClass.isAssignableFrom(base.getClass())) {
service.service(handledClass.cast(base)); // compile error at this line
}
}
}
// the following classes provide concrete implementations and the concrete class to perform the casting.
public static class RealDerived extends Derived{}
public static class FalseDerived extends Derived{}
public static class ConcreteService extends AbstractService<RealDerived>{
}
public static class ConcreteBarrer extends Barrer<RealDerived> {
protected ConcreteBarrer() {
super(RealDerived.class,new ConcreteService());
}
}
}
Upvotes: 0
Reputation: 298469
To my understanding, this code should compile, but of course not without an unchecked warning.
You have declared a variable service
of the raw type AbstractService
which is a subtype of the raw type Service
which has a method void service(Base)
which is the erasure of void service(T)
.
So the invocation service.service(base)
may invoke that method void service(Base)
declared in Service
, of course, with an unchecked warning as the method is generic and no verification of the type parameter T
happened.
This might be counter-intuitive as the type AbstractService
overrides that method with a method whose erasure is void service(Derived)
but this method can only override the other method in a generic context, not in a raw type inheritance relationship.
Or, in other words, a type can’t override a method in a way that it is more restrictive regarding parameter types than the overridden supertype method.
This also applies to the generic type inheritance but to a different outcome. If your variable had the type AbstractService<X>
, then X
must be assignable to Derived
due to the constraint of the type parameter. This type AbstractService<X>
is a subtype of Service<X>
which has a method void service(X)
(as T
:= X
) which is overridden (implemented) by AbstractService<X>
with a method void service(X)
which accepts the same argument types.
Since there seems to be some confusion on your site, I want to emphasize that this has nothing to do with your if(… instanceof Derived)
statement. As explained above, this behavior is due to the raw type usage, which means you are using AbstractService
without an actual type argument and basically switching off the Generics type checking. This would even work if you had written
public void bar(Base base) {
service.service(base); // UNCHECKED invocation
}
If you changed the declaration of the variable to
private AbstractService<Derived> service;
it won’t be a raw type anymore and the type checking will happen and service.service(base)
will generate a compiler error, regardless of whether you enclose it with if(base instanceof Derived) { … }
or not.
Raw types exist for compatibility with pre-Generics code only and you should avoid using them and not ignore warnings provoked by raw type usage.
Upvotes: 2