Win a copy of Svelte and Sapper in Action this week in the JavaScript forum!
  • Post Reply Bookmark Topic Watch Topic
  • New Topic
programming forums Java Mobile Certification Databases Caching Books Engineering Micro Controllers OS Languages Paradigms IDEs Build Tools Frameworks Application Servers Open Source This Site Careers Other all forums
this forum made possible by our volunteer staff, including ...
Marshals:
  • Campbell Ritchie
  • Ron McLeod
  • Paul Clapham
  • Bear Bibeault
  • Junilu Lacar
Sheriffs:
  • Jeanne Boyarsky
  • Tim Cooke
  • Henry Wong
Saloon Keepers:
  • Tim Moores
  • Stephan van Hulst
  • Tim Holloway
  • salvin francis
  • Frits Walraven
Bartenders:
  • Scott Selikoff
  • Piet Souris
  • Carey Brown

Wildcards and Method References

 
Bartender
Posts: 1464
32
Netbeans IDE C++ Java Windows
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
I'm starting this new thread since I have a specific question about the code below. Hope someone with a better grasp of generics and method references than I have can help me.

This code compiles fine:



Now, according to the (out-of-date) "Head First Java," this:

is the same as this:

provided you don't need to refer to T from the first version.

However, when I make such a change to Line 15 of my code, so it looks like this:

I get this error message at Line 12:

NetBeans wrote:incompatible types: invalid method reference
 incompatible types: Shape cannot be converted to Round


Method references didn't exist when "Head First Java" came out, so I'm not saying HFJ is wrong. What I am saying is I don't understand this error message.

Can someone help me understand it?
 
Stevens Miller
Bartender
Posts: 1464
32
Netbeans IDE C++ Java Windows
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
One thought I have is that the erasure of the second version is this:


As testRound must provide an implementation of Predicate<Shape>, the error makes sense. This change makes the error go away:

But if erasure replaces the type variables with their bounding types, and that's the reason my second version had an error, why didn't the first version have the same error?
 
author & internet detective
Posts: 40200
816
Eclipse IDE VI Editor Java
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
I think the first version had more context. Let's take method references out of it for a minute.

This lambda works with both the before and after


This one only works with the "before". For the "after", it also complains it is expecting a shape.


The method reference has the same amount of information as my second example. Which shows that Java needs more context than the method reference can provide.
 
Stevens Miller
Bartender
Posts: 1464
32
Netbeans IDE C++ Java Windows
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator

Jeanne Boyarsky wrote:The method reference has the same amount of information as my second example. Which shows that Java needs more context than the method reference can provide.


But how does the first version provide that context? That is, how is this:
able to provide more context than this:
if both have the same erasure?
 
Jeanne Boyarsky
author & internet detective
Posts: 40200
816
Eclipse IDE VI Editor Java
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
Erasure is runtime. We are discussing compilation. That happens *before* erasure occurs.
 
Marshal
Posts: 25823
69
Eclipse IDE Firefox Browser MySQL Database
  • Likes 1
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator

Stevens Miller wrote:if both have the same erasure?



But they don't. In the first example the type variable T means the same thing in both places, Class<T> and Predicate<T>. However in the second example the wildcard ? doesn't mean the same thing in both places; the two wildcards are independent.

Or actually, I guess they do have the same erasure but they don't mean the same thing.
 
Stevens Miller
Bartender
Posts: 1464
32
Netbeans IDE C++ Java Windows
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator

Paul Clapham wrote:

Stevens Miller wrote:if both have the same erasure?

But they don't. In the first example the type variable T means the same thing in both places, Class<T> and Predicate<T>. However in the second example the wildcard ? doesn't mean the same thing in both places; the two wildcards are independent.


But I can make the error happen with only one wildcard:



Whereas, again, the <T extends Shape> version has no error:

 
Stevens Miller
Bartender
Posts: 1464
32
Netbeans IDE C++ Java Windows
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
I was thinking about Jeanne's comments and trying to learn more by tinkering with lambdas instead of method references. That tinkering gave me this version, which no longer imports anything (I'm trying to boil it down to the smallest cases I can):


Line 6 gives me an "incompatible types: Shape cannot be converted to Round" error.

Line 5 does not give me an error.

How.

Can.

That.

Be???
 
Jeanne Boyarsky
author & internet detective
Posts: 40200
816
Eclipse IDE VI Editor Java
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
??? I don't know how that can be either.

But I like that you are doing the experiment with lambdas. Easier to troubleshoot. I do like method references. But when it doesn't work, I get it working with a lambda and then switch back. Easier troubleshooting. And sometimes reveals the problem was that my brain wasn't mapping the method reference right!
 
Stevens Miller
Bartender
Posts: 1464
32
Netbeans IDE C++ Java Windows
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator

Jeanne Boyarsky wrote:??? I don't know how that can be either.


Well, that's comforting, at least   .

I'm still investigating this, but here are two more listings and a citation to a cryptic bit from "Core Java."

First, notice what happens when I pass the Square class as the second parameter to methodA:


Passing a method reference at Line 6 works (though I agree with you that method references are sufficiently opaque that the better practice is to debug with lambdas and convert to method references after everything is working).

However, changing the second parameter from the Round class to the Square class at Line 7 causes the error to return, but in a slightly modified form. Instead of saying, "incompatible types: Shape cannot be converted to Round," now it says, "incompatible types: Square cannot be converted to Round." At both Lines 7 and 8, NetBeans underlines thing as the point where the error is detected.

Changing the type of testRound's parameter from Round to Shape makes both errors go away (one could downcast the parameter inside the method, but I am more interested in knowing why these errors come and go as they do than in a way to get around them). Here's the error-free version:



And now for the cryptic citation. Well, the citation isn't cryptic. What I am citing to here is cryptic. My citation is to "Core Java, Vol. 1," 9th ed., page 715:

Core Java wrote:You cannot use type variables in expression such as new T(...) new T[...], or T.class. For example, the following Pair<T> constructor is illegal:

Type erasure would change T to Object, and surely you don’t want to call new Object(). As a workaround, you can construct generic objects through reflection, by calling the Class.newInstance method.

Unfortunately, the details are a bit complex. You cannot call

The expression T.class is not legal. Instead, you must design the API so that you are handed a Class object, like this:

This method could be called as follows:

Note that the Class class is itself generic. For example, String.class is an instance (indeed, the sole instance) of Class<String>. Therefore, the makePair method can infer the type of the pair that it is making.


It's that part I've put into boldface that I find cryptic. He says the method can "infer" the type.

Apparently, there is something special about passing an object of type Class that allows this "inference." If so, Paul's observation (that wildcards don't constrain the two type parameters passed to methodA to be of the same type) comes back into play, I think.

So what's so special about the Class class that the compiler can use it to make this inference?
 
Stevens Miller
Bartender
Posts: 1464
32
Netbeans IDE C++ Java Windows
  • Likes 1
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
Okay, I think I have figured it out. It does, indeed, have to do with type inference by the compiler. The compiler will attempt to infer the lowest level (that is, greatest descendant from Object) class that it can for each type parameter. Whatever inference it makes must be valid for all appearances of the same parameter. A call to a parameterized method will permit call-specific inferences to be made.

You can have a lot of fun with something like this:


If you call that as, say, ifThenElse(true, "one", "two"), the compiler will infer that T is a String for that call. You can verify this in NetBeans by typing this:

ifThenElse(true, "one", "two").

The presence of the "." at the end of that line will cause the IDE to prompt you with the list of methods available to the inferred class. As you select a method from that list, you will see the javadoc which, at the top, tells you the inferred class (java.lang.String, in this case).

This has its limits, however. For example, ifThenElse(true, 3.14, "two") will compile, run, and return an object of type Double. However, you can't compile this:

That's because the compiler doesn't know that a double will be returned. That won't be known until run-time. The best it can do is infer that an Object will be returned, which is why you can compile this:

If you do, and you System.out.println(o.getClass().getName()), you will see that, indeed, the call returned a java.lang.Double.

Now, interestingly (and disappointingly), if you type this:

ifThenElse(true, 3.14, "two").

you do not get a list of Object's methods from the NetBeans IDE. The compiler apparently infers that it returns (as specifically as the compiler can infer it) an Object. But NetBeans can't.

It gets better/worse. Consider what happens when you pass two arguments that are subclasses of something more specific than Object, like this:

ifThenElse(true, 3.14, 2).

Autoboxing will make that first argument into a Double, the second into an Integer. No way to infer what the returned type will be, except that both of those classes are subclasses of Number. Thus, this is legal:

Again, however, NetBeans doesn't seem to be as good as the compiler at these inferences. Typing this:

ifThenElse(true, 3.14, 2).

does not get you a list of Number's methods, but you can use them, meaning this:

compiles, runs, and prints 2.0, even though you have to go back in time and remember that doubleValue is one of Number's methods.

I'll have more to say tomorrow about why this causes the bizarre behavior we have discussed up-thread, but I want to run a few tests so I have more examples to explain it with first. Meanwhile, it got a lot clearer for me after reading an article by Brian Goetz about generics. Have a look if you can't wait for my next post!
 
Jeanne Boyarsky
author & internet detective
Posts: 40200
816
Eclipse IDE VI Editor Java
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
Interesting and definitely cow worthy!
 
Stevens Miller
Bartender
Posts: 1464
32
Netbeans IDE C++ Java Windows
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator

Jeanne Boyarsky wrote:Interesting and definitely cow worthy!


Thanks! Hope I can do as well when I try to wrap this up tomorrow. It's 2100 where I am now. Been at this for a couple of days and, at my age, 2100 is too late to deal with this shit fascinating Java technicalities. At my age, 2100 is, however the ideal time to pull a brew and watch mindless Japanese anime for an hour before falling asleep.
 
Stevens Miller
Bartender
Posts: 1464
32
Netbeans IDE C++ Java Windows
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
Okay, someone check me on this, but here's what I think is going on. In the case where it works (MethodA), it works because passing Round.class gives the compiler specific information that it can use to infer the type of T. That's because any argument to a method of type Class<T> necessarily results in T being of the class that Class<T> represents. That locks T down as Round when the second argument passed to MethodA is Round.class. Thus, the compiler knows that the first argument is of type Func<Round> and treats the lambda expression the same as it would an anonymous inner class of that type. Thus, all of these work:






Note that all three of those will also work when funcRound is passed to methodB, because all three explicitly declare funcRound to be of type Func<Round>.

Without that explicit declaration, and without any help from an argument of type Class<T>, the best that the compiler can do is infer that T is of type Shape, which has it treating the lambda in the call to MethodB the same it is treated here:


And that produces the exact same "incompatible types: Shape cannot be converted to Round" error. The lambda on the right implements a functional interface that passes an argument of type Round, but the declaration is for a functional interface that passes an argument of Round's superclass, which can't be converted to a Round.

Now, hang onto your seat because you can still get around this problem another way:


The compiler will still not know how to infer anything more specific about the type of T in methodB than that it is Shape, but funcRound is now of type Func<Shape>, which is what methodB expects.

So, Jeanne and Paul were right: this is about what happens at compile-time and, when it can infer the type of a type parameter, the compiler will do so and that type will be the same wherever the same parameter appears. When an argument of type Class<T> is used, the compiler will always know exactly what type T is, because it must always be of the type that Class<T> represents.

Note that this inference appears to be prioritized from right to left in the argument list of a method. Consider this:


This gets you the following error:

The NetBeans IDE wrote:
method classy in class Main cannot be applied to given types;
required: Class<T>, Class<T>
found: Class<Square>, Class<Round>
reason: inferred type does not conform to equality constraint(s)
inferred: Round
equality constraint(s): Round,Square
where T is a type variable
T extends Object declared in method <T>classy(Class<T>,Class<T>)


I take that to mean that the compiler thinks T is Round, which it must have inferred from the right-most argument passed to classy.

Finally, I owe it to Kathy Sierra and Bert Bates to correct a misunderstanding I may have created up this thread. When they said, in "Head First Java," that
is the same as this:
they were not saying that
is the same as this:
even though that's what I extrapolated them to mean, above. In fact, the accurate extrapolation would have been that
is the same as this:
where, returning again to Paul's observation, neither alternative guarantees that list1 and list2 are of the same type.

That's about the best I can do. I welcome comments, corrections, and improvements. (And, to fellow USAens, have a happy Thanksgiving.)

 
Stevens Miller
Bartender
Posts: 1464
32
Netbeans IDE C++ Java Windows
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
Some rabbit holes have no bottom...

There's an obscure (well, it is to me) construct called a "type witness" that can be used to help the compiler resolve types it cannot infer. I would hazard it is of limited usefulness since Java 8 and, if you find yourself using one, it might signal that you should modify your code so you don't need it.

But, it has the odd characteristic that, somewhat akin to a cast, it allows NetBeans to know the type returned by a generic method.

Returning to my example from above:

Again, if you type a dot before the semi-colon at Line 10, you do not see the helpful list of methods available to Number that NetBeans usually provides. You can force NetBeans to recognize that ifThenElse returns a Number with a cast:

Now NetBeans will show Number's methods when you type a dot before the semi-colon.

But there's another way. Try this:

"Main.<Number>" is a "type witness" that explicitly tells the compiler that, for this call to ifThenElse, its type paramter T has type Number. Now, the compiler was able to infer that, so you get nothing new from Java's point of view by adding it. But it does have the quirky effect of letting NetBeans know what the compiller knows: ifThenElse(false, 3.14, 2) is of type Number.

Note that <Number>ifThenElse(false, 3.14, 2); will not compile. The class name is required.

Do you ever really need type witnesses? I thought the answer might be "no," as of Java 8. Prior to Java 8, this did not compile:
Line 9 generated an incompatible types: List<Object> cannot be converted to List<String> error. Using a type witness solved that problem:



Or, a slightly simpler case:

This did not compile in the past, but did compile with a type witness:

As of Java 8, however, both examples above compile without type witnesses.

So, maybe type witnesses are relics of the past.

Except...

Returning to the original question:

I asked how Line 5 could compile while Line 6 does not compile. The answer turned out to be that Line 5 provides an explicit type in its second argument to methodA, allowing exact inference by the compiler, where as Line 6 only gives the compiler enough information to infer that the type parameter is Shape. I am somewhat surprised that a type witness solves this problem:


This last discovery has me a bit dizzy. I hope someone will comment on this and help me know if I am making proper sense of what I'm learning.
 
Stevens Miller
Bartender
Posts: 1464
32
Netbeans IDE C++ Java Windows
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
Sometimes I get like a dog with a bone over this stuff. Here's my summary of what I've drawn from all of the above, and some other investigations, in the form of some code:

 
    Bookmark Topic Watch Topic
  • New Topic