I think that in order to understand this, one must first understand the subtle differences between types and classes: objects have classes, references have types.
Classes vs Types
I think an example is a good way to explain what I mean here. Let assume the existence of the following class hierarchy:
class Mammal {}
class Feline extends Mammal {}
class Lion extends Feline {}
class Tiger extends Feline {}
Thanks to subtype polymorphism we could declare multiple references to the same object.
Tiger tiger = new Tiger(); //!
Feline feline = tiger;
Mammal mammal = feline;
Interestingly, if we asked everyone of these references the name of their class they all would reply with the same answer: "Tiger".
System.out.println(tiger.getClass().getName()); //yields Tiger
System.out.println(feline.getClass().getName()); //yields Tiger
System.out.println(mammal.getClass().getName()); //yield Tiger
This means the actual class of the object is fixed, its class is the one we used to instantiate it when we used the "new" operator in our code above.
The references, on the other hand, may have different types, polymorphically compatible with the actual class of object (i.e. a tiger in this case).
So, objects have a fixed class whereas references have compatible types.
This tends to be confusing, since the class names are the same thing we use to name the types of our references, but semantically speaking there is a subtle difference, as we can see above.
Perhaps the most confusing part is the realization that classes are also objects, and therefore they can have compatible references of their own. For instance:
Class<Tiger> tigerClass = null;
Class<? extends Tiger> someTiger = tigerClass;
Class<? extends Feline> someFeline = tigerClass;
Class<? extends Mammal> someMammal = tigerClass;
In my code above the object being referenced is a class object (which I left null for time being), and these references being used here have different types to reach that hypothetical object.
So, you see, the word "class" here is use to name a "type of reference" pointing to an actual class object whose type is compatible with any of these references.
In my example above I failed to define such class object since I initialized the original variable to null, though. This is intentional, and in a moment we'll see why.
About Invoking getClass
and getSubclass
on References
Following @Jatin's example that considers the getClass
method, now, let's consider the following piece of polymorphic code:
Mammal animal = new Tiger();
Now we know that regardless of the fact that our animal
reference is of type Mammal
, the actual class of the object is, and always will be, Tiger
(i.e. Class).
What should we get if we do this?
? type = mammal.getClass()
Should type
be a Class<Mammal>
or a Class<Tiger>
?
Well, the problem is that when the compiler sees a reference of type Mammal
, and it cannot tell what is the actual class of the object this reference is pointing to. That could only be determined at runtime, right?. It may in fact be a Mammal, but it may just as well be any of its subclasses, like a Tiger, right?
So, when we ask for its class, we don't get Class<Mammal>
because the compiler cannot be sure. Instead we get a Class<? extends Mammal>
, and this makes much more sense, because, after all, the compiler knows that based on the rules of subtype polymorphism the given reference may be pointing to a Mammal or any of its subtypes.
At this point, you can probably see the subtle nuances of using the word class here. It would seem that what we are actually getting out of the getClass()
method is some kind of type reference that we use to point to the actual class of the original object as we had already explained before.
Well, the same thing could be said about the asSubclass
method. For instance:
Mammal tiger = new Tiger();
Class<? extends Mammal> tigerAsMammal = tiger.getClass();
Class<? extends Feline> tigerAsFeline = tigerAsMammal.asSubclass(Feline.class);
When we invoke asSubclass
we are getting a reference to the actual type of class being pointed to by our reference, but the compiler can no longer be sure of what that actual nature should be, and therefore you get a laxer reference like Class<? extends Feline>
. This is the most the compiler can assume about the original nature of the object, and that's why this is all we get.
What about new Tiger().gerClass()?
We might expect that the only way to obtain a Class<Tiger>
(without wirldcards) should be by accessing the original object, right? Like:
Class<Tiger> tigerClass = new Tiger().getClass()
Funny thing, though, we alway reach the tiger object through a reference type, right?. In Java we never have direct access to the objects. Since we are always reaching an object through their references, there is no way the compiler can make assumptions about the actual type of the reference being returned.
That's why even this code would produce Class<? extend Tiger>
Class<? extends Tiger> tigerClass = new Tiger().getClass();
That is, the compiler makes no guarantees about what the new
operator may return here. For all that matters it might return an object compatible with Tiger, but not necessary one whose class is Tiger itself.
This becomes clearer if you change the new
operator for a factory method.
TigerFactory factory = new TigerFactory();
Class<? extends Tiger> tigerClass = tigerFactory.newTiger().getClass();
And our Tiger factory:
class TigerFactory {
public Tiger newTiger(){
return new Tiger(){ } //notice this is an anonymous class
}
}
I hope this somehow contributes to the discussion.