14

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?

assylias
  • 321,522
  • 82
  • 660
  • 783
SpaceTrucker
  • 13,377
  • 6
  • 60
  • 99
  • 1
    That compiles fine with javac - the issue is probably eclipse specific... – assylias Apr 17 '15 at 13:25
  • @assylias well, that's the question. The JLS is the reference and wether javac or eclipse jdt compiler implements it correct. – SpaceTrucker Apr 17 '15 at 13:26
  • 1
    But why? service expects a T extending Derived, but gets a Base object (although it is checked to actually an instance of Derived, and could get casted. – Jens Schauder Apr 17 '15 at 13:27
  • Is service (AbstractService) really meant as a raw type here? – Puce Apr 17 '15 at 13:35
  • We just had similar question a [couple of hours ago](http://stackoverflow.com/questions/29692295/why-does-this-code-fail-with-sourcecompatibility-1-8/29692361), closed as a duplicate, and there are plenty of other duplicates. Any reason the same thing would not apply here? – eis Apr 17 '15 at 13:36
  • @Puce yes, it is. This is because the code originally is from a framework, where we are not supposed to change the code of the framework itself. But since our own code has a dependency on (by copying the framework code into our project and then compiling with all things merged together) this compiler error is a bit annoying when working in eclipse. – SpaceTrucker Apr 17 '15 at 13:38
  • @eis there is no ambigous overload here at play. The service only offers a single method that is considered. – SpaceTrucker Apr 17 '15 at 13:40
  • @eis this question is also about eclipse java compiler not being able to compile something, where http://stackoverflow.com/questions/28466925/java-type-inference-reference-is-ambiguous-in-java-8-but-not-java-7 is about javac not being able to compile something. – SpaceTrucker Apr 17 '15 at 13:46
  • How does the compiler know `base` is a Derived ?. Does it 'see' that `service.service(base);` is inside `if(base instanceof Derived)` ? – baraber Apr 17 '15 at 14:49
  • 1
    @baraber at the statement `service.service(base);` the compiler can deduce `base` is of type `Derived` because of the preceeding if-statement with the `base instanceof Derived` expression. So rephrasing the question to "Can/Should/Must the compiler make use of the information it gains from the instanceof expression?" – SpaceTrucker Apr 17 '15 at 14:53
  • 1
    @SpaceTrucker I don't know what is going on here (it compiles for me), but the method `service` *is* overloaded. If you don't believe me you can do this `for (Method method : FooTest.AbstractService.class.getMethods()) if (method.getName().equals("service")) System.out.println(method);`. Since `AbstractService` implements `Service`, and `Service` has a method `service` accepting a `Base`, a synthetic override of this is created in `AbstractService` accepting a `Base`. This is an overload of your `service` method accepting a `Derived`. – Paul Boddington Apr 17 '15 at 15:12
  • 2
    It *compiles* for me, but IntelliJ shows red. I'm on Java 1.8.0_45. – Makoto Apr 17 '15 at 15:13

2 Answers2

2

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.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • It would be nice if you could reference appropriate sections of jls for your statements. However there is still the question if the compiler can/should/must make use of the informations it gains from the if statement containing the instanceof expression. – SpaceTrucker Apr 17 '15 at 14:46
  • 1
    I looked through the specification but couldn’t find a single, concise paragraph to link to (and linking to twenty places and saying “draw the conclusion” would be a bit pointless). Regarding the `if` statement, the answer is simple: it doesn’t have the slightest influence. Only the compile-time type of the receiver (here the variable `service`) matters. Btw. it’s easy to check that all compilers show the same behavior if you remove the `if` statement. – Holger Apr 17 '15 at 14:51
  • But obviously `javac` does make use of the information in the if statement, because it compiles the code without errors. So `javac` might be wrong since 1.5 and eclipse jdt compiler is correct since it supports java 1.8? – SpaceTrucker Apr 17 '15 at 14:55
  • 2
    @SpaceTrucker: please, read the first part of my answer carefully and try to understand. It’s not that any compiler knows that `base` contains an instance of `Derived`. Instead, the method `Service.service(Base)` will be invoked, because the information that this shouldn’t be possible *has been dropped*, due to the *raw type* usage. – Holger Apr 17 '15 at 15:22
  • The raw type usage is of `AbstractService.service ( base)` and therefore the compiler should require a cast at least to Derived. – redge Apr 19 '15 at 08:14
  • After reading your edited version once again, I do now better understand your point. However I still would be very interested in what exact rules apply from the JLS. As far as I have read the starting point for all this is §15.12.2. I'm trying to figure out this myself, but this is quite a bit of work. – SpaceTrucker Apr 20 '15 at 06:49
0

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());
        }

    }
}
redge
  • 1,182
  • 7
  • 6