What is the reasoning behind not allowing supertypes on Java method overrides?

前端 未结 7 1512
悲哀的现实
悲哀的现实 2021-01-31 10:23

The following code is considered invalid by the compiler:

class Foo {
    void foo(String foo) { ... }
}

class Bar extends Foo {
    @Override
    void foo(Obje         


        
7条回答
  •  囚心锁ツ
    2021-01-31 11:16

    (A rewrite, from a different angle ... my original answer contained an error. :( )

    why can't the parameter in the subtype (Bar) be a supertype of the parameter in the supertype (Foo).

    I believe that technically it could, and it wouldn't break ancestor contracts following type substition (Liskov Substitution Principle).

    • Types are passed-by-value (including reference types).
    • The caller can never be forced to deal with different parameter types than what it passes in.
    • A method body can swap a parameter type, but can't return it back to the caller (no such thing as 'output parameter types').
    • If your proposal was allowed, and a call was made to the ancestor method signature, a decendent class could override the method with broader types, but it's still impossible to return a broader type than what the caller sets.
    • The override could never break a client that uses the narrow ancestor method contract.

    From my analysis below, I surmise/guess the rationale for not allowing your scenario:

    • Performance-related: allowing broader types in override would impact runtime performance, a major issue
    • Functionality-related: it only adds a minor amount of functionality. As it stands, you could add your 'broader method' as an overloaded method without overriding. Then you could additionally override the original method with an exact signature match. The net result: you achieve something quite similar, functionally.

    Compiler Requirements for Method Override - JLS 7

    The compiler's required to act accordancing to your experience. 8.4 Method Declarations:

    A method in a subclass can override a method in an ancestor class iff:

    • the method names are identical (8.4.2 and 8.4.8.1)
    • the method parameters have identical types, after erasure of generic type parameters (8.4.2 and 8.4.8.1)
    • the return type is type-substitutable for the return type in the ancestor class, i.e. the same type or narrower (8.4.8.3)

      Note: subsignature does not mean the overriding method uses subtypes of the overridden method. The overriding method is said to have a subsignature of the overridden method when the overriding method has exactly the same type signature except that generic types and the corresponding raw types are considered equivalent.


    Compiler v Runtime Processing for Method Matching & Invocation

    There's a performance hit matching method signatures via polymorphic type matching. By restricting override method signatures to exact match of ancestor, the JLS moves much of this processing to compile time. 15.12 Method Invocation Expressions - summarised:

    1. Determine Class or Interface to Search (Compile-Time Determination)

      • Obtain the base type T, on which the method is being invoked.
      • This is the reference type declared to the compiler and not the runtime type (where a subtype may be substituted).
    2. Determine Method Signature (Compile-Time Determination)

      • Search the compile-time base type T, for applicable methods matching the name and parameter & return types consistent with the call.
        • resolve generic parameters for T, either explicitly passed or implicitly inferred from the types of invocation method arguments
        • stage 1: methods applicable via consistent types/subtypes ('subtyping')
        • stage 2: methods applicable via automatic boxing/unboxing plus subtyping
        • stage 3: methods applicable via automatic boxing/unboxing plus subtyping plus variable 'arity' parameters
        • determine the most specific matching method signature (i.e. the method signature that could successfully be passed to all other matching method signatures); if none: compiler/ambiguity error
    3. Check: Is the Chosen Method Appropriate? (Compile-Time Determination)

    4. Evaluation of Method Invocation (Runtime Determination)

      • Determine runtime target reference type
      • Evaluate arguments
      • Check accessibility of method
      • Locate method - an exact signature match against the compile-time matched signature
      • Invoke

    Performance Hit

    The breakdown, in terms of text in the JLS:

    Step 1: 5% Step 2: 60% Step 3: 5% Step 4: 30%

    Not only is Step 2 volumous in text, it's surprisingly complex. It has complex conditions and many expensive test conditions/searches. It's advantageous to maximise the compiler's execution of the more complex and slower processing here. If this was done at runtime, there would be a drag on performance, because it would occur for each method invocation.

    Step 4 still has significant processing, but it is as streamlined as possible. Reading through 15.12.4, it contains no processing steps that could be moved to compile time, without forcing the runtime type to exactly match the compile-time type. Not only that, but it does a simple exact match on the method signature, rather than a complex "ancestor type match"

提交回复
热议问题