• Post Reply Bookmark Topic Watch Topic
  • New Topic

Generic return types  RSS feed

 
Des Robin
Ranch Hand
Posts: 30
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
Hi

I'm struggling to understand a feature of generics. Specifically:



If I replace the code at // 1 with 'return a.getT();', the compiler generates an type error relating to 'Priv' being incompatible with 'T'. What I don't understand is why...

It is possible to invoke a.getT() and assign the return value to 'a2' or another Priv variable. It is not possible to assign it as follows:

T t = a.getT();

In both the return and the direct assignment above the compiler complains about a type mismatch ('T' vs 'Priv'). However, under type erasure, the method signature is converted to be Priv. Why the complaint? While a cast to 'T' solves the problem, I really want to understand what is causing this error.

Could someone help clarify the issue?

Thanks
 
Jesper de Jong
Java Cowboy
Sheriff
Posts: 16060
88
Android IntelliJ IDE Java Scala Spring
  • Likes 2
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
You're using the raw type Priv in the very first line:

Try changing it to this:
 
Winston Gutkowski
Bartender
Posts: 10575
66
Eclipse IDE Hibernate Ubuntu
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
Des Robin wrote:I'm struggling to understand a feature of generics. Specifically:...

Here's a little tip for you when trying to get your head around generics:

Try to imagine that the entire class has been edited with a copy/replace function that changes each occurrence of 'T' to whatever class you're thinking of, and that THAT is what the compiler is looking at.

The analogy breaks down sometimes (particularly when wildcards get involved), but it's a good place to start. Just image that every 'T' you're looking at is, in fact a <Class> (whatever that is).

HIH

Winston
 
Des Robin
Ranch Hand
Posts: 30
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
@Jesper/Winston

Thanks for the quick replies. It compiled with <T extends Priv<T>>, but my thinking regarding this is still bemused. Doing a javap -v on the class reveals that the only difference between the two versions, is the class signature:

vs

The actual method signatures are identical in both versions of the class:

and

Interesting...

Thanks again.
 
Matthew Brown
Bartender
Posts: 4568
9
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
Remember type erasure - looking at the byte code won't tell you much because most of the generic information doesn't make it that far. It's just used by the compiler for type checking.
 
Des Robin
Ranch Hand
Posts: 30
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
@Matthew

Ironically it was my application of type erasure that was confusing me. The compiler was telling me that I was trying to assign a 'Priv' to a 'T'. The output from javap showed that the return types from getT() and doStuff() was identical. If the types for the two methods were erased to Priv (sans the type parameter info which as you say does not make it to the byte code), then Priv passed back from a call to getT() should surely be the same no matter which variable was being used to call getT() (either 'a' or 'a2')? I was really chasing my tail when I started to put the 'a.getT()' and 'a2.getT()' calls into println statements. Both worked as expected producing the underlying object info. Invoking 'a.get();' on a line by itself without assigning it to anything was fine.

So... what had I learnt to that point. 'a.getT()' worked as did 'a2.getT()'. Both should (surely) return the same return type so I was really getting confused. Rightly so... the compiler was not replacing the return type the way I thought it was/should.

The syntax is misleading to me because the type of Priv has no impact on the on the method being invoked and it is the method that defines the return type (you'd think). Instead, the reference type used to access the method appears to provide the compiler with info which it seems to be using to complete the assignment (type inference??). For example:


// 1 works.
// 2 does not.
Both refer to the same object on // 3 which has been created as a raw type.

So, if calling getT() using 'a' returns a 'Priv', what does calling getT() using 'a2' return. I changed the the call to 'String t = a2.getT();' and the compiler complained it had found a String when it was returning a 'T'. Hmmm...

If I am understanding the compiler correctly here it is doing something like:

1. When the class is defined as <T extends Priv>, the compiler knows that any T is actually a Priv, so it short circuits any type checking for occurrences of T and treats them all as Priv (why bother with all that type checking code in the compiler). It now starts the return or assignment (in doStuff()) and compares the current type (Priv) to the specified return type (T) for the method doStuff(). At this point it does not appear to have changed the return type for doStuff() to a Priv and so issues the compiler error.
2. When the class is defined as <T extends Priv<T>>, the compiler only knows that T could be a Priv<Priv> or any subclass of Priv<Priv>. It does not supplant T with Priv<Priv> or Priv<SubPriv> but it leaves it as T. While the byte code will ultimately be Priv, the compilation phase can only enforce the range check defined.

All in all, interesting behaviour. I'm busy playing around with subclasses now to see what that throws into the equation. Also been scrolling around the JLS, but unfortunately they don't have a 'simple examples for stupid questions' section. Finding the info I want is time consuming.

Any thoughts from anyone on points 1 and 2 above. Am I off in the bush again???

Thanks
 
Steve Luke
Bartender
Posts: 4181
22
IntelliJ IDE Java Python
  • Likes 1
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
Des Robin wrote:The compiler was telling me that I was trying to assign a 'Priv' to a 'T'. The output from javap showed that the return types from getT() and doStuff() was identical. If the types for the two methods were erased to Priv (sans the type parameter info which as you say does not make it to the byte code), then Priv passed back from a call to getT() should surely be the same no matter which variable was being used to call getT() (either 'a' or 'a2')?

Type checking is done before conversion to byte code and type erasure is applied (if it weren't then there would be no point, how can you type check on erased types?). This lets the compiler check for type safety even when the types aren't assigned until runtime (and works within the byte-code definitions available prior to generics).
The syntax is misleading to me because the type of Priv has no impact on the on the method being invoked and it is the method that defines the return type (you'd think). Instead, the reference type used to access the method appears to provide the compiler with info which it seems to be using to complete the assignment (type inference??). For example:


// 1 works.
// 2 does not.
Both refer to the same object on // 3 which has been created as a raw type.

The object doesn't exist at compile time, it exists at run time. The compiler can not predict what will be assigned to a variable at run time, unless the variable is a compile-time constant (because the value could change by being called by code the compiler doesn't have access to, or over time...). So knowing what object both references point to is something you know but the compiler can't.

So, if calling getT() using 'a' returns a 'Priv', what does calling getT() using 'a2' return.

The variable a has the the unparameterized type Priv, since Priv is generic, then the unparameterized version is akin to Priv<Object>. Since getT() returns the type of the parameterization, and the compiler knows the type of the parameterization, then the expected type of a.getT() is Object. But the type expected by the compiler is referred to a T, and that T has been defined as extending Priv. So the compiler checks the type returned from a.getT() (Object) and the type T the variable holds (Priv or a subclass) and knows they are incompatible (because a.getT() can legally return any Object, but the variable can only hold subclasses of Priv).

This checking is done during compilation, and before the code is converted to byte code. The byte code does not have the type information (because of type erasure) - and so neither does the run time.

On the other hand, the variable a2 has the parameterized type Priv<T>, and the type T has already been defined as T extends Priv, so the type of a2 is akin to Priv<T extend Priv>, and that means the return type of a2.getT() is Priv or some subclass. This is the same thing that the variable refers to, and so everything works. And as with a.getT() these checks are made while compiling, before the byte code is made, and not visible at run time.

1. When the class is defined as <T extends Priv>, the compiler knows that any T is actually a Priv, so it short circuits any type checking for occurrences of T and treats them all as Priv (why bother with all that type checking code in the compiler).

You want to find all your errors at compile time, since it runs once and in your hands. Run time errors require lots of testing to make sure you follow all different paths through your code, and are likely to happen at unexpected times in your customer hands. Run time errors are hard to find and track down. Compile time errors are easy and quick. Compile time type checking makes type errors visible when they are easiest to fix. Generics make it so you can have compile-time type safety but still allow classes to work with arbitrary, run-time only, types. You just have to make sure the expected types of assignments match.

It now starts the return or assignment (in doStuff()) and compares the current type (Priv) to the specified return type (T) for the method doStuff(). At this point it does not appear to have changed the return type for doStuff() to a Priv and so issues the compiler error.

I am not sure I follow this at all. I think you are mixing run time knowledge (the actual type of the T iT variable) with compile time knowledge (the allowable types for T). Remember these things are separated by space and time, compile first, just once, then run many times in different locations (and possibly different methods calling doStuff(...).
 
Des Robin
Ranch Hand
Posts: 30
  • Mark post as helpful
  • send pies
  • Quote
  • Report post to moderator
@Steve - Thanks for your feedback.

I think I have been complicating things by making reference to type erasure and runtime info. A short summary of my understanding is this:

Input parm a2 is defined as Priv<T>. The reason 'return a2.getT()' compiles is because:

1. The compiler checks to see if a2 actually has a method getT(), that it can use.
2. It then checks to see the return type of getT() - in this case T.
3. It then looks at the definition of a2 to see what types it supports and whether or not the underlying type for a2 is compatible.
4. In the case of a2, the type is T and as the return type is also T, compilation works.

In the case of parm a:

1. As for a2
2. As for a2
3. As for a2
4. In the case of a, there is no type associated as T is defined as Priv (and not Priv<T> as pointed out by Jesper). The compilation then fails as the return type T does not match Priv.

Thanks for everyone's help!

 
It is sorta covered in the JavaRanch Style Guide.
  • Post Reply Bookmark Topic Watch Topic
  • New Topic
Boost this thread!