Class.asSubclass signature

后端 未结 3 1198
有刺的猬
有刺的猬 2021-02-05 10:03

my question is quite theoretic... This is the signature of Class.asSubclass (Javadoc):

public  Class asSubclass(Class clazz)         


        
相关标签:
3条回答
  • 2021-02-05 10:27

    In case of the return type Class<? extends U>. Lets first try understanding, the getClass signature:

    AbstractList<String> ls = new ArrayList<>();
    Class<? extends AbstractList> x = ls.getClass();
    

    Now had the compiler allowed us to do:

    Class<AbstractList> x = ls.getClass();
    

    This would have been wrong. Because at runtime, ls.getClass would be ArrayList.classand not AbstractList.class. Neither can ls.getClass return Class<ArrayList> because lsis of type AbstractList<> and not Arraylist

    So the compiler, now says - Ok! I cant return Class<ArrayList> nor can I return Class<AbstractList>. But because I know ls is an AbstractList for sure, so the actual class object can only be a subtype of AbstractList. So Class<? extends AbstractList> is a very safe bet. Because of wild card: You cannot do:

    AbstractList<String> ls = new ArrayList<>();
    Class<? extends AbstractList> x = ls.getClass();
    Class<ArrayList<?>> xx = x;
    Class<AbstractList<?>> xxx = x;
    

    The same logic applies to your question. say suppose it was declared as:

     public <U> Class<U> asSubClass(Class<U> c)
    

    The below would have compiled:

     List<String> ls = new ArrayList<>();
     Class<? extends List> x = ls.getClass();
     Class<AbstractList> aClass = x.asSubclass(AbstractList.class); //BIG ISSUE
    

    Above aClass at runtime is Class<Arraylist> and not Class<AbstractList>. So this should not be allowed!! Class<? extends AbstractList> is the best bet.



    The first thought I had on looking at the question was, why wasn't it declared as:

     public <U extends T> Class<? extends U> asSubClass(Class<U> c)
    

    it makes more sense to have a compile time restriction on the arguments I can pass. But the reason I think it was not preferred was - this would have broken the backward compatibility of pre-java5 code. For example, code such as below which compiled with pre-Java5 would no longer compile if it asSubClass was declared as shown above.

    Class x = List.class.asSubclass(String.class); //pre java5
    // this would have not compiled if asSubclass was declared above like
    

    Quick check:

        public static <T, U extends T> Class<? extends U> asSubClaz(Class<T> t, Class<U> c){
            return t.asSubClass(c);
        }
        public static <T, U> Class<? extends U> asSubClazOriginal(Class<T> t, Class<U> c){
            return t.asSubClass(c);
        }
        asSubClazOriginal(List.class, String.class);
        asSubClaz(List.class, String.class); //error. So would have broken legacy code
    

    PS: For the edited question, on why asSubClass rather than cast? - Because cast is betrayal. For example:

    List<String> ls = new ArrayList<>();
    Class<? extends List> x = ls.getClass();
    Class<AbstractList> aClass = (Class<AbstractList>) x;
    

    Above would always succeed because generics are erased. So its class cast to a class. But aClass.equals(ArrayList.class) would give false. So definitely cast is wrong. In case you need type safety, you can use asSubClaz above

    0 讨论(0)
  • 2021-02-05 10:28

    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.

    0 讨论(0)
  • 2021-02-05 10:34

    The idea behind this method is exactly to add this wildcard. As far as I understand the purpose of this method is to cast this class object to a class object that represents any subclass of the parameter clazz. That is why the result is declared as Class<? extends U> (the class of something that extends U). Otherwise it wouldn't make sense, because its return type will be exactly the same as its only parameter's type, i.e. it will do nothing but return clazz;. It makes a runtime type check and casts the parameter to the Class<? extends U> (of course it will throw ClassCastException if the target of the invocation, i.e. this, does not represent the class of a U derivative). This is the better approach than simple (Class<? extends U>) type of casting, because the latter makes the compiler to produce a warning.

    0 讨论(0)
提交回复
热议问题