What is the point of allowing type witnesses on al

2019-01-08 23:30发布

问题:

Say we have two methods like the following:

public static <T> T genericReturn() { /*...*/ }
public static String stringReturn() { /*...*/ }

In calling any method, you can supply the type witness regardless of whether or not there is any requirement:

String s;
s = Internet.<String>genericReturn(); //Type witness used in return type, returns a String
s = Internet.<Integer>stringReturn(); //Type witness ignored, returns a String

However I'm not seeing any realistic use for this in Java at all, unless the type cannot be inferred (which is usually indicative of a bigger issue). Additionally the fact that it is simply ignored when it is not appropriately used seems counterintuitive. So what's the point of having this in Java at all?

回答1:

From the JLS §15.2.12.1:

  • If the method invocation includes explicit type arguments, and the member is a generic method, then the number of type arguments is equal to the number of type parameters of the method.

This clause implies that a non-generic method may be potentially applicable to an invocation that supplies explicit type arguments. Indeed, it may turn out to be applicable. In such a case, the type arguments will simply be ignored.

It's followed by justification

This rule stems from issues of compatibility and principles of substitutability. Since interfaces or superclasses may be generified independently of their subtypes, we may override a generic method with a non-generic one. However, the overriding (non-generic) method must be applicable to calls to the generic method, including calls that explicitly pass type arguments. Otherwise the subtype would not be substitutable for its generified supertype.

Along this line of reasoning, let's construct an exmaple. Suppose in Java1.4, JDK has a class

public class Foo
{
    /** check obj, and return it */
    public Object check(Object obj){ ... }
}

Some user wrote a proprietary class that extends Foo and overrides the check method

public class MyFoo extends Foo
{
    public Object check(Object obj){ ... }
}

When Java1.5 introduced generics, Foo.check is generified as

    public <T> T check(T obj)

The ambitious backward comparability goal requires that MyFoo still compiles in Java1.5 without modification; and MyFoo.check[Object->Object] is still an overriding method of Foo.check[T->T].

Now, according to aforementioned justification, since this compiles

    MyFoo myFoo = new MyFoo();

    ((Foo)myFoo).<String>check("");

This must compile too

    myFoo.<String>check("");

even though MyFoo.check is not generic.


That sounds like a stretch. But even if we buy that argument, the solution is still too broad and overreaching. JLS could've tighten it up so that myFoo.<String,String>check and obj.<Blah>toString() are illegal, because type parameter arity doesn't match. They probably didn't have time to iron it out so they just took an simple route.



回答2:

You need the type witness (the type in the diamond) when type inference will not work (see http://docs.oracle.com/javase/tutorial/java/generics/genTypeInference.html)

The example given for this is when daisy-chaining calls like:

processStringList(Collections.emptyList());

where processStringList is defined as:

void processStringList(List<String> stringList) 
{
    // process stringList
}

This will result in an error because it cannot cast a List<Object> to a List<String>. Thus, the witness is required. Albeit, you could do this in multiple steps, but this can be far more convenient.



回答3:

It is because of backward- and/or forward-compatibility (at source level).

Imagine something like the introduction of generic parameters for some classes in Swing in JDK 7. It might happen with methods too (even with restrictions). If something turned out to be a bad idea, you can remove them and the source using it would still compile. In my opinion that is the reason why this is allowed, even if it is not used.

The flexibility though is limited. If you introduced type parameters with n types, you cannot later change to m types (in a source compatible way) if m != 0 or m != n.

(I understand this might not answer your question as I am not the designer of Java, this is only my idea/opinion.)



回答4:

Wondering why "Type Witness" was thrown around in Java? :D

To understand this we should start the story from Type Inference.

Type inference is a Java compiler's ability to look at each method invocation and corresponding declaration to determine the type argument (or arguments) that make the invocation applicable. The inference algorithm determines the types of the arguments and, if available, the type that the result is being assigned, or returned. Finally, the inference algorithm tries to find the most specific type that works with all of the arguments.

If the above algorithm is still not able to determine the type we have "Type Witness" to explicitly state what type we need. For example:

public class TypeWitnessTest {

    public static void main(String[] args) {
        print(Collections.emptyList());
    }

    static void print(List<String> list) {
        System.out.println(list);
    }
}

The above code does not compile:

TypeWitnessTest.java:11: error: method print in class TypeWitnessTest cannot be applied to given types;
            print(Collections.emptyList());
            ^
  required: List<String>
  found: List<Object>
  reason: actual argument List<Object> cannot be converted to List<String> by method invocation conversion
1 error

So, you have Type Witness to rescue from this:

public class TypeWitnessTest {

    public static void main(String[] args) {
        print(Collections.<String>emptyList());
    }

    static void print(List<String> list) {
        System.out.println(list);
    }
}

This is compilable and works fine, however this has been more improved in Java 8:
JEP 101: Generalized Target-Type Inference

PS: I started from fundamentals so that other StackOverflow readers can also benefit.

EDIT:

Type Witness on non-generic Witness!

public class InternetTest {
    public static void main(String[] args) {
        String s;
        s = Internet.<String>genericReturn(); //Witness used in return type, returns a string
        s = Internet.<Integer>stringReturn(); //Witness ignored, returns a string
    }
}

class Internet {
    public static <T> T genericReturn() { return null; }
    public static String stringReturn() { return null; }
}

I tried to simulate @Rogue example using javac 1.6.0_65 but it fails compilation with following error:

javac InternetTest.java 
InternetTest.java:5: stringReturn() in Internet cannot be applied to <java.lang.Integer>()
        s = Internet.<Integer>stringReturn(); //Witness ignored, returns a string
                    ^
1 error

@Rogue: If you were using some prior version than I used, then do let me know your javac version. And if you were then it is not allowed now. :P



标签: java generics