Why can't I “static import” an “equals” method

2019-02-05 15:17发布

问题:

I like using this method here:

org.apache.commons.lang.ObjectUtils.equals(Object object1, Object object2)

The only drawback (compared to Google Guava, for instance), is that I cannot static import the method. I.e. this is useless:

import static org.apache.commons.lang.ObjectUtils.equals;

... as my Eclipse compiler will not correctly link that method when writing

equals(obj1, obj2);

The error is:

The method equals(Object) in the type Object is not applicable for the arguments (..., ...)

Why is that? Is my statically imported method not applicable if there is a method with the same name (but not the same signature) in any of the super types? Is this formally specified in the JLS? Or some Eclipse compiler issue?

UPDATE

This doesn't work either:

import static org.apache.commons.lang.ObjectUtils.defaultIfNull;

public class Test {
  void test() {
    defaultIfNull(null, null);
    // ^^ compilation error here
  }

  void defaultIfNull() {
  }
}

javac error message:

Test.java:5: defaultIfNull() in Test cannot be applied to (<nulltype>,<nulltype>)
defaultIfNull(null, null);
    ^
1 error

回答1:

JLS 15.12.1. identifies two reasons, why a method can be "in scope":

  1. "...there is an enclosing type declaration of which that method is a member"
  2. "... due to one or more single-static-import ..."

Now two factors contribute to the surprising result:

  1. At this point only the name of the method is considered, signatures come later.
  2. The two alternatives mentioned above are connected with "otherwise". In the first case we end up looking in the enclosing class of the visible method. In the second case we use the static import.

This "otherwise" implies that the search scope is restricted to only trying either of the two branches. First we have to decide whether we search an enclosing type or using a static import. Enclosing type has higher priority, we find a method of the correct name (Test.defaultIfNull()), search ends here. When later we find this method to be incompatible, there's no going back to trying the static import.

The situation is not uncommon in JLS, also other issues of method lookup are organized in phases, where a partial match in one phase may prevent finding a better match in a subsequent phase. Fixed-arity vs. variable arity matching is another example of this concept. The effect in all cases is that compilers do not search the entire possible solution space, but after certain decisions have been made entire branches are cut off and never visited.

A rule of thumb can be derived from the above: Overloading can only select among methods of the same type hierarchy, it cannot select between methods of types unrelated by inheritance.



回答2:

The collision is actually with Object.equals(). All classes are inherited from Object and therefore have the Object.equals() method which leads to this collision.

You're importing by name, not by signature. You actually can't import a static method named equals because of this. Or rather, you can import it, but not use it. I do agree that this should work though.

(Made my comments my own answer.)



回答3:

As per Java Language Specification

  1. If a single-static-import declaration imports a member whose simple name is n, and the compilation unit also contains a single-type-import declaration that imports a type whose simple name is n, a compile-time error occurs. (This error occurs even if both declarations refer to the same type, on the grounds that it is confusing to use two different mechanisms to redundantly import the same type.)
  2. If a single-static-import declaration imports a member whose simple name is n, and the compilation unit also declares a top level type whose simple name is n, a compile-time error occurs.

So in your case its point 2 mentioned above is the reason you are getting compile time error. so even if method signatures are different if there names are same its a compile time error.

static import JSR and JLS



回答4:

I did a few tests. First thing I noticed is that you only need one static import statement for multiple methods of the same name.

public class EqualsClass {
  public static boolean equals(Object o1, Object o2) {
    return o1 == null ? o2 == null : o1.equals(o2);
  }

  public static boolean equals(Object o1, Object o2, Object o3) {
    return equals(o1, o2) && equals(o2, o3);
  }
}

import static mypackage.EqualsClass.equals;

public class TestClass {
  public static void main() {
    Object o1 = new Object();
    Object o2 = new Object();

    equals(o1, o2); // Compiles - static context

    Object o3 = new Object();

    equals(o1, o2, o3); // No extra static import required
  }

Then I noticed that it doesn't work in an instance context:

  public void someInstanceMethod() {
    Object o1 = new Object();
    Object o2 = new Object();

    equals(o1, o2); // Does not compile - instance context

    Object o3 = new Object();

    equals(o1, o2, o3); // As expected does not compile
  }

}

But then if I clobber the static import with the class's own static method:

public static boolean equals(Object o1, Object o2) {
  return EqualsClass.equals(o1, o2); // Compiles
}

public void someInstanceMethod() {
  equals(new Object(), new Object()); // Compiles!!
  equals(new Object(), new Object(), new Object()); // Doesn't compile!
}

The fact that it works in a static context makes a reasonable amount of sense to me. However, it appears there is a significant difference between the resolution of a statically imported method and a defined static method of the class.

Summary:

  • Methods with the same name as an instance method cannot be accessed when statically imported from an instance context.
  • Static methods from the same class with the same name can be accessed from an instance context.
  • A static import gives you access to all static methods with the same name from that class despite the signature (parameters and return value).

I'd be interested to see the part of the JLS or a compiler spec that specifies the resolution of static imports by the compiler and how they are clobbered by local methods.



回答5:

I also combed through JLS3 and couldn't find a definitive answer.

per 15.12.1, first we need to determine the single class where the equals method is declared/inherited. Here we have two candidate classes, and the spec doesn't seem to have a rule to resolve the conflict.

We can investigate a comparable problem. A simple type name may refer to both an imported type, or an inherited type (a member type of the super class). Javac picks the latter. This is probably because of the procedure in 6.5.2, which gives imports the lowest priority.

If the same principle applies, the imported ObjectUtils.equals should yield to inherited Object.equals. Then per 15.12.2.1, there is no equals method in Object that's potentially applicable to the expression equals(obj1, obj2)

Personally, I'd prefer that import has precedence over inheritance, because import is closer. It also stabilizes the meaning of a name. In the current scheme, suppose Object doesn't have an equals method, the expression equals(obj1, obj2) refers to ObjectUtils.equals; now suppose Object adds the equals method, a totally innocent move, suddenly the subclass doesn't compile. An even worse scenario: the new equals method has a compatible signature; the subclass still compiles, yet the meaning of the expression silently changes.



回答6:

This isnt really an answer (just more questions in a way). This is proof that the compiler does import the methods with signature.

package test;

public class Foo 
{
    public static void equal(Object o1)
    {
        System.out.println("Foo.equal Object");
    }   

    public static void equal(Integer o1)
    {
        System.out.println("Foo.equal Integer");
    }   
}

package test;

public class Bar 
{
    public static void equal(Number o1)
    {
        System.out.println("Bar.equal Number");
    }   
}

import static test.Foo.equal;
import static test.Bar.equal;

public static void main(String args[]) throws Exception
{
    equal((Object)null);
    equal((Number)null);
    equal((Integer)null);
}

Output: 
Foo.equal Object
Bar.equal Number
Foo.equal Integer

This may also be related. A method in an inner class 'hiding' a static method in the outer class with a different signature.

http://ideone.com/pWUf1

It looks like the compiler has different places where to look for methods and it checks them one by one but only searches by name leading to a premature termination of the search.



回答7:

It's a method collision with java.awt, you need to reference the package like this:

 ObjectUtils.equals(a, b);


回答8:

Actually I think this is more of an Eclipse issue than any other thing. If you are using an overloaded version of equals() that receives two arguments, there should be no collision with the default Object.equals().

There are a couple of tricks in Eclipse that you can use to get it to recognize the static import:

1 - Add the static type to Organize Imports Go to:

Window > Preferences > Java > Code Style > Organize Imports 

then click on "New Static", then "Types", then choose your class (in this case org.apache.commons.lang.ObjectUtils)

While still on the Organize Imports panel, deselect the

"Do not create imports for types starting with lowercase letter" 

(do not forget this, it's important)

2 - Add the type to Content Assist Go to:

Window > Preferences > Java > Editor > Content Assist Favorites

then click on "New Type", then choose your class (in this case, again, org.apache.commons.lang.ObjectUtils)

Now with this you should be able to Ctrl+Space anywhere on your method and get the "equals(Object,Object)" method as possible content. If you choose that method, Eclipse should automatically insert the static import for equals.