Override mechanics - unintentionally overriding newer implementations

Kevin Campbell kevin_rec at 12doors.com
Wed Nov 28 21:57:23 PST 2012


Right now, methods *always* override matching inherited methods. It's not
conditional or optional. Even though new methods and implementation
can be added to the interface (or superclass) after the implementing
class is written, if the implementing class contains a matching method,
then that old method automatically overrides the new one. Even though
that is not what the author intended and the old method may not adhere
to the same behavior and possible side effects that the new method does.

The introduction of default methods in interfaces is going to magnify the
problem. Perhaps it is time to bite the bullet and make a better way
to do this.

To truly solve the problem, the override mechanism would have to
change from "always" to "only if the author specifies it". The author could
specify an override with an annotation. The linker would have to enforce
that only methods with the proper annotations overrode the inherited
methods. (i.e. invoke interface would change.) This would ensure that
new methods and implementations could be added to existing interfaces
without causing any conflicts or undesired behavior.

The downside of making overrides optional is that calling the same
method on the same object, from two different reference types,
can have two different behaviors.
obj.someMethod() can be different from ((Interface1)obj).someMethod().


interface Interface1 {
    public void printHello();
    // Interface1 could have added "doSomething()" after class Test was
written.
    public default void doSomething() { System.out.println("Interface1");
}
interface Interface2 { public default void doSomething() {
System.out.println("Interface2"); }

class Test extends Base implements Interface1, Interface2, Interface3 {
    // Compiler already knows this method must override
Interface1.printHello().
    //@Override Interface1    // Warning or automatic insertion of
@Override.
    public void printHello() { System.out.println("Hello"); }

    // Tell the compiler and linker which implementations to override.
    @Override Interface2, Interface3 // does not override Interface1
    public void doSomething() { System.out.println("Test"); }

    static public void main(String[] args) {
        Test obj = new Test();
        obj.doSomething(); // prints "Test"
        ((Interface1)obj).doSomething(); // prints "Interface1"
        ((Interface2)obj).doSomething(); // prints "Test"
    }
}

The current @Override syntax could be extended easily so it could
specify which inherited interfaces the method should override.

The added complexity of declaring every override method annotation
can be somewhat mitigated by help from the compiler. It can
automatically enforce that methods that match an inherited method
without an implementation always have an appropriate @Override
annotation. (Making the method override the missing implementation)
The compiler can generate a warning if the annotation is missing or
it could create the @Override annotation automatically and insert it
into the class file.

The compiler could also generate warnings for other senarios. For
methods that match the signature of an inherited method but do not
provide an @Override annotation at all, and for methods that do
provide an @Override annotation, but ommit an interface. (The
@Override syntax could be extended to allow an author to
explicitly state which interfaces the method does not override.
This would only be a warning supression, as which interfaces a
method does not override would not be saved in the class file.)

A special rule would have to apply to Java 7 and earlier class files to
make them behave the same as they do now. The linker would know
that methods in these class files implicitly override any matching
inherited methods that do not have an implementation.The lack of
an appropriate @Override annotation in the class file would be ignored
in this case.

Interface methods that do have a default implementation must be
newer than the potential override method in the Java 7 class file. The
matching method in the class file could not have been written to
intentionally override the default implementation, as the implementation
didn't exist when the class was created. Therefore, invoke interface
should call the default implementation.

This also holds true for the case where an existing interface method
without an implementation is updated to include an implementation.
Again, the new default interface implementation must be newer than
the potential override method in the Java 7 class file. Therefore the
method in the Java 7 class file should not override the default interface
implementation.


On Tue, Nov 27, 2012 at 8:01 AM, <brian.goetz at oracle.com> wrote:

>
> Date: Tue, 27 Nov 2012 10:16:09 -0500
> From: Brian Goetz <brian.goetz at oracle.com>
> Subject: Re: Evolving interfaces: source/binary compatibility issues
> To: Gernot Neppert <mcnepp02 at googlemail.com>
> Cc: lambda-dev <lambda-dev at openjdk.java.net>
> Message-ID: <50B4D939.5070801 at oracle.com>
> Content-Type: text/plain; charset=ISO-8859-1; format=flowed
>
> Yes, this is a known risk.  Note that this is not a new problem
> introduced by default methods; this risk has been around since day 1, in
> that adding a method to a superclass (such as AbstractList) carries this
> exact same risk.  What is new is that we are turning down the
> conservatism knob on adding methods to commonly used supertypes.  This
> is a tradeoff between making the platform more usable for everyone, and
> causing some source incompatibilities.
>
> Unless we're willing to freeze the platform in concrete (we're not),
> what we can do is try and reduce the surface area of potential
> conflicts.  One way we've already done this is by not adding
> filter/map/etc to Collection, but instead adding a stream() method to
> Collection which is less likely to conflict.
>
> Conflicts are also more likely to occur on nilary methods; List.sort()
> is more likely to conflict than List.reduce(Supplier<T>,
> BinaryOperator<T>).  Similarly, conflicts are more likely to occur on
> the "obvious" short names like "sort".
>
> The comment about Comparator.reverse() is well-taken and I'll bring that
> up in our next round of bikeshedding.
>
>
> On 11/23/2012 10:23 AM, Gernot Neppert wrote:
> > Hi all,
> >
> > when I browsed the Java 8 docs and found that java.util.List finally has
> > a method "void sort(Comparator<? super E> comparator)",
> > I was quite happy at first.
> > Unfortunately, that bliss lasted only until I found that one of my own
> > implementations of java.util.List already has such a method - albeit
> > "protected".
> > This has at least two consequences:
> >
> > 1. I can't compile my old code against the JDK 8, since it tries to
> > implement an interface method with weaker access privileges.
> >
> > 2. Even worse: if I link my old compiled code against a JDK 8, it may
> > raise an "java.lang.IllegalAccessError" at Runtime, since code from the
> > new codebase may innociously call back my "sort" method via the
> interface.
> >
> > Here are some other methods that are likely to cause trouble, because
> > they are obvious extensions likely to be already present in user-code:
> >
> > java.util.Comparator.reverse()
> >
> > java.util.Collection.addAll(Iterable)
> >
> > Do you consider this a real danger or does it seem esoteric?
> >
> >
> >
> >
> >
> >
> >
> >
>
>
>


More information about the lambda-spec-observers mailing list