А знаете ли Вы, что возвращает .getClass()?

    Я думаю, почти любого Java разработчика когда-то спрашивали на собеседовании: «Какие есть методы у класса Object?»
    Меня, по крайней мере, спрашивали неоднократно. И, если в первый раз это было неожиданностью (кажется, забыл про clone), то потом я был уверен, что уж методы Object'а-то я знаю;)

    И каково же было мое удивление, когда спустя несколько лет разработки я наткнулся на собственное незнание сигнатуры метода getClass()

    Под катом пара слов про Class, .class, .getClass и, собственно, сюрприз, на который я наткнулся.


    Итак, у нас есть класс А и объект этого класса a:
    public class A {
    }
    ...
    A a = new A();
    


    0. A.class vs a.getClass()


    Начнем с простого. При вызове getClass() может отработать полиморфизм, и результатом будет класс-потомок.
    public class B extends A {
    {
    ...
    
    A a1 = new B();
    a1.getClass(); // то же самое, что B.class
    


    Скрытый текст
    Тут была ложь, на которую мне указали в комментариях. class — это не статическое поле, коим может показаться (и даже не нативное-псевдо-статическое поле, как думал я), а особая конструкция языка. И, в отличие от статического поля, обратиться к нему через объект нельзя!
    a.class; // Compile error! Unknown class: "a"
    



    Но это так, цветочки. Идем дальше.

    1. А что такое этот ваш Class?


    A.class — объект класса Class. Смотрим в Class.java:
    public final class Class<T> implements ...
    
    

    Это дженерик. Причем типизирован он, очевидно, этим самым A — классом, у которого вызвали .class

    Если подумать, то понятно зачем это нужно: теперь, в частности, можно написать метод, который возвращает произвольный тип, в зависимости от аргумента:
    public <T> T foo(Class<T> clazz);
    

    A.class возвращает объект класса Class:
    Class<A> result = A.class; // Compilation successfull
    


    2. А что же возвращает a.getClass()?


    Собрав воедино все вышесказанное, можно догадаться, что:
    Class<A> result1 = a.getClass(); // Compilation error!
    

    Действительно, ввиду полиморфизма нужно не забывать, что фактический класс объекта a — не обязательно A — это может быть любой подкласс:
    Class<? extends A> result = a.getClass(); // Compilation successfull
    


    3. А что же написано в Object.java?


    Все эти дженерики — это, конечно, замечательно, но как записать сигнатуру метода getClass синтаксисом java в классе Object?
    А никак:
    public final native Class<?> getClass();
    

    А на вопрос, почему не компилировался пример выше, ответит Максим Поташев джавадок к методу:
    The actual result type is Class<? extends |X|> where |X| is the erasure of the static type of the expression on which getClass is called.


    Так что в Object.java написана одна сигнатура, а компилятор подставляет другую.
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 55
    • 0
      Мсье знает толк в…
      • 0
        Благодарю;)
        • –1
          вы считаете это комплиментом?)
      • +6
        Столько слов, а суть одна: reference type != object type :)
        • +1
          На самом деле, изначально я хотел написать только о сигнатуре в Object.java, про невозможность описать требуемое синтаксисом джавы, но все остальное — потребовалось для полноты картины.
        • +2
          Есть предположения — почему это не компилируется?

          public class HelloWorld{
          public static void main(String []args){
          Class<HelloWorld> aClass = ((HelloWorld)null).class;
          System.out.println(aClass);
          }
          }

          (попробовал здесь www.compileonline.com/compile_java_online.php)

          p.s. прощу прощения, но не тег source не помогает
          • +2
            Вы совершенно правы, я облажался. Такое не скомпилируется. Суть в том, что class — это совсем не статическое поле и даже не пытается им казаться. вызывать class у объекта нельзя. Поправил в посте, премного благодарен.
          • +2
            Недавно столкнулся с такой штукой, раньше не знал.
            Объявляем
            A obj = new A() { @Override public void someOverridenMethod() { ... } };
            И получаем, что obj.getClass() нам вообще класс замыкания вернёт. И тогда надо дёргать getEnclosingClass()
            • +1
              Пару лет назад очень увлекались в проекте анонимными классами. А потом огребли=)
              • +3
                Интересно. Подробности расскажете?
              • +1
                Замыкание (closure) и вложение (enclosure) — разные же понятия, не?
                • +1
                  С enum еще интересней:
                  enum E {
                      first,
                      second {
                          public String toString() {
                              return "not first";
                          }
                      }
                  }
                  

                  E.first.getClass().isEnum() вернет true;
                  E.second.getClass().isEnum() вернет false!
                  • 0
                    Чуть выше уже говорили про getEnclosingClass() :)
                    • +3
                      И чего? :) Как нам это поможет отличить объект вложенного класса от enum константы?

                      enum E {
                          first,
                          second {
                              public String toString() {
                                  return "not first";
                              }
                          };
                      
                          static class Helper { }
                          
                          public static final Helper helper = new Helper();
                      }
                      


                      E.second.getClass().getEnclosingClass().isEnum() вернет true;
                      E.helper.getClass().getEnclosingClass().isEnum() вернет true.

                      Я хотел сказать, что проверять, является ли объект enum константой, через obj.getClass().isEnum() — неправильно.
                      А правильно будет obj instanceof Enum.
                    • +1
                      Красота, не знаю, куда впихнуть в пост, но примерчик возьму на заметку, спасибо;)
                  • 0
                    a.class;

                    Как у вас вообще это скомпилировалось?

                    UPD: Снято. Ответили выше.
                    • +4
                      Автор написал уже что ошибся, a.class больше нету

                      UPD: Снято. Ответили выше, что ответили выше.
                    • 0
                      Автор, после достаточно интересного предыдущего поста пост про .getClass() — это даже не шаг назад, а какой-то прыжок со скалы вниз.
                      ИМХО, такое максимум тянет на мини-шпаргалку для осваивающих Java.
                      • +1
                        Вообще, про getClass я узнал ровно два года назад, т.ч. возможно это действительно прыжок со скалы;) Тем не менее многие мои знакомые удивлялись, что сигнатура в Object.java != реально проверяемая компилятором сигнатура. Возможно, привычка стараться все объяснять максимально подробно и угробила «короткий забавный факт», превратив его в пост-мини-шпаргалку-для-новичков. Буду учиться на ошибках, спасибо.
                        • –1
                          Никакого знака неравенства между сигнатурами там нет. Object — это корневой элемент в любой классовой иерархии, поэтому extends Object в сигнатуре можно не писать. Вы же не пишете в каждом вашем классе, что он extend Object, верно?
                          • +1
                            Как то, что любой класс по дефолту наследуется от Object связано с тем, что в этом самом Object написано
                            Class<?> getClass()
                            , а на самом деле там не совсем такой дженерик?
                            • –1
                              pastebin.com/nB8THEMy
                              Это равнозначные записи. После компиляции это будет одно и то-же. Первый и второй вариант — сокращенные формы записи третьего варианта, синтаксический сахар, как и то, что «любой класс по дефолту наследуется от Object» (вас не заставляют каждый раз явно прописывать extend Object). Не понимаю вашего недоумения.
                              • +1
                                Ещё раз могу повторить. Об «extends Object» речь не идёт вообще, а идёт о том, что если бы компилятор проверял сигнатуру, указанную в Object, то нельзя было бы написать Class<String> cls = String.class.
                                • –1
                                  Как уже сказали, String.class это не поле. Оно возвращает ровно то что должно — дженерик с конкретным типом. Причем тут вообще сигнатура Object#getClass().
                                  • +1
                                    Да, извиняюсь. Нельзя было бы написать Class<? extends String> cls = "".getClass(); (а на самом деле можно).
                                    • –1
                                      Почему нельзя? Все снова сводится к моему комментарию, на который Вы ответили #6859374. Там именно такая сигнатура, которая должна быть, иначе бы #getClass() нельзя было заоверрайдить при наследовании. И компилятор проверяет именно такую сигнатуру, потому что String к Object апкастится отлично. Знака неравенства между сигнатурами нет.
                                      Я все еще не понимаю, что здесь неочевидного.
                                      • +1
                                        иначе бы #getClass() нельзя было заоверрайдить при наследовании

                                        Так и нельзя, он final.
                                        И компилятор проверяет именно такую сигнатуру, потому что String к Object апкастится отлично.

                                        Ну вот попробуйте определить свой метод, который возвращает Class<?>, например

                                        public class Foo { public Class<?> getClass1() { return getClass(); } }

                                        Тогда

                                        Foo foo = new Foo(); Class<? extends Foo> cls1 = foo.getClass(); Class<? extends Foo> cls2 = foo.getClass1();

                                        cls1 компилируется, а cls2 нет. Притом, что сигнатура getClass() (в Object.java) и getClass1() выглядит одинаково.
                                        • –1
                                          > Так и нельзя, он final.

                                          А еще native, и особой магией все таки оверрайдится, что бы возвращать то, что возвращает. В вашем варианте сигнатуры это было бы невозможно.

                                          > cls1 компилируется, а cls2 нет. Притом, что сигнатура getClass() (в Object.java) и getClass1() выглядит одинаково.

                                          Если добавить final, разве не скомпилируется? Тут надо сесть и более детально разобраться, сдается мне сигнатура cls2 только на первый взгляд выглядит одинаково.
                      • –1
                        > Class result1 = a.getClass(); // Compilation error!

                        Ну так разумеется же

                        Class
                        • –1
                          Парсер :(

                          pastebin.com/zGSP0nST
                          • 0
                            Не понял вопрос, если это вопрос=)
                            • –1
                              Нет, это не вопрос. Я пытаюсь понять, в чем ваше недоумение :) Как по мне, все логично. См. мой коммент в ветке выше.
                        • 0
                          > Так что в Object.java написана одна сигнатура, а компилятор подставляет другую.
                          Чего это вдруг?
                          Сигнатура та же. Просто в джавадоке более детально указано что может быть возвращено, чем в дженерике в сигнатуре.

                          Так может каждый
                          
                           /**
                           * return The actual result type is String.
                           */
                           public static <?> getSomething() {
                               return "Hello World";
                           }
                          
                          
                          • 0
                            P.S. А, понял о чем речь.
                            > на вопрос, почему не компилировался пример выше
                            Вопрос не в том был почему не компилируется Class<A> result1 = a.getClass(); , а в том почему компилируется Class<? extends A> result = a.getClass();
                            Здесь да, должен был быть cast, но он не нужен. Действительно, есть какая-то хитрость.
                            • 0
                              Что значит «не нужен». Обычный неявный каст, как здесь:

                              class Object1 { }
                              class Object2 extends Object1 { }

                              Object2 obj2 = new Object2();
                              Object1 obj1 = obj2;
                              • 0
                                Ну это просто upcast, приведение к классу-предку. Это совсем не то, и дженериков нет — что какбы намекает.
                                • 0
                                  Так я и я про upcast — кем бы этот «а» не был в конкретной реализации, он отлично апкастится к A. А в жденерике у нас сказано, что подходит все А и его наследники. Почему же оно не должно компилится?
                              • 0
                                На этот вопрос как мне кажется Эккель отвечает в «Thinking in Java 4 edition» (к великому сожалению читал в переводе).
                                1. В главе про RTTI говорится что:
                                Если обычная ссылка на класс может быть связана с любым объектом Class, параметризованная ссылка может связываться только с объектами типа, указанного при объявлении.

                                Значит код:
                                Class<A> result1 = a.getClass(); //error
                                

                                содержит ошибку — объектная ссылка a может ссылаться (пардон за каламбур) на объект класса-потомка A, что недопустимо. Соответственно, расширение параметра типа проблему решает:
                                Class<? extends A> result = a.getClass();//ok
                                

                                2. А в главе про параметризацию дается объяснение (как мне кажется) всего этого мракобесия:
                                Параметризация в Java реализуется с применением стирания (erasure).

                                А это значит что код:
                                class A{}
                                class B{}
                                List<A> aList = new ArrayList<A>();
                                List<B> bList = new ArrayList<B>();
                                System.out.println(aList.getClass().equals(bList.getClass() )  );
                                

                                выведет:
                                true
                                что говорит о том, что при использовании параметризации вся конкретная информация о типе утрачивается :( и по словам Эккеля это есть проблема Java. Автор указал на это:
                                The actual result type is Class<? extends |X|> where |X| is the erasure of the static type of the expression on which getClass is called.

                                но сидящие здесь гуру (никакого сарказма!), намекают на это настолько неявно и как само собой разумеющееся, что я занервничал и решил-таки побыть К.О. и попозориться немного
                                • 0
                                  > при использовании параметризации вся конкретная информация о типе утрачивается
                                  … в рантайме.
                                  Но речь шла о compilation error.
                                  • 0
                                    Подтвержаю насчет runtime. Далее надеюсь не спороть чепухи. Насколько я понял в Java параметризация реализуется с применением стирания и это учитывается/влияет на компилятор. А вот результат стирания можно увидеть в runtime. И да, мой код совсем не к месту. К месту другой:
                                    class SomeObject{
                                    	public void someMeth(){
                                    		System.out.println("Hello");
                                    	}
                                    }
                                    
                                    class Controller<T>{
                                    	private T mObj = null;
                                    	public Controller(T obj){
                                    		mObj = obj;
                                    	}
                                    	public void control(){
                                    		mObj.someMeth();// <---- compilation error!
                                    	} 
                                    }
                                    
                                    public class Parameterising {
                                    	public static void main(String [] args){
                                    		SomeObject someObj = new SomeObject();
                                    		Controller<SomeObject> controller = new Controller<SomeObject>(someObj);
                                    		controller.control();
                                    	}
                                    }

                                    Из-за стирания параметра типа код внутри дженерика не знает о наличии метода someFn(). Могу предположить что подобная мантра:
                                    Информация о параметрах типов недоступна внутри параметризованного кода
                                    отрабатывает и в методе Object.getClass(). И только ограничения параметра типа
                                    class Controller<T extends SomeObject>{...}
                                    
                                    могут спасти ситуацию
                                    • 0
                                      Почему это надо спасать ситуацию. Я не представляю, как можно знать о типе , не указав от чего он наследуется. Получается какая-то динамика, сложно представить что должна проделать IDE, чтобы работало автодополнение для метода someMeth.
                                      • 0
                                        Все-таки это не имеет отношения к стиранию типа (хоть я и солидарен с Эккелем=))

                                        Пример не скомпилируется не только в джаве, но и в любом другом языке. Дженерик — это обобщение (см. словарь) — это по определению «нечто, работающее с 'произвольным' типом». И крутизна как раз в том, что произвольный тип — это не обязательно Object.
                                        Грубо говоря, вместо дженериков можно работать с Object'ами (в рантайме так и есть), но тогда компилятор и не будет проводить дополнительных проверок.

                                        В вашем примере someObj — с точки зрения Controller \<T\> — объект некоего, заранее неизвестного, класса T. Причем Т — произвольно, никаких ограничений.

                                        То, что Вы неявно просите — это чтобы каждое использование дженерика — в данном случае Controller\<SomeObject\> — накладывало ограничения на дженерик.

                                        В частности, кто-то другой может написать
                                        Controller<Object> controller = new Controller<Object>(new Object());
                                        


                                        Вот откапитанил, так откапитанил;)
                                        • 0
                                          подобный код (с неизвестным типом) легко скомпилируется в с++, например. просто потому что реальный код класса появится только во время явного указания типа (то есть специализации), а пока класса нет, дженерик — просто кусок исходника.

                                          в яве, из-за того что нет технической возможности иметь отдельно объявление и реализацию — получились вот такие, какие есть, дженерики
                                          • 0
                                            Да, Вы правы.
                                            • 0
                                              В C# реализованы Дженерики как надо, там такое будет работать (вроде нет)? В С++ шаблоны, которые довольно сложны в анализе для компиляторов. Ведь это нарушение правила — интерфейс должен быть известен в момент написания кода, в данном случае интерфейс становится известен только когда мы объявим объект. Выглядит это как костыль.
                                              • 0
                                                Не как костыль, а как template. Что собственно и было сделано. Дженерик — это просто удобное следствие.

                                                В с# есть слово where, которое и помогает ему узнать какие методы есть у типа-параметра. Так что как в с++ — c# тоже не умеет
                                                • 0
                                                  Вот этот самый where и есть тот самый интерфейс. В Java можно создать интерфейс и поместить в extends. Интерфейс это лишь описание набора методов без реализации.

                                                  Не нужно брать С++ за эталон, это не простой язык.
                                                  • 0
                                                    java и с# — дженерики. хотя принципиальные отличия у них есть
                                                    c++ — тру-темплейты. собственно поэтому я и решил прокомментировать «В C# реализованы Дженерики как надо, там такое будет работать»
                                  • +2
                                    пользователь return сейчас сильно удивился
                                  • +11
                                    В который раз поражаюсь, как много можно придумать вопросов для собеседования, которые абсолютно никак не показывают навык программиста.
                                    • +1
                                      А много вы знаете вопросов, которые показывают навык программиста?

                                      Понять, на что способен человек можно
                                      а) по коду, который он пишет
                                      б) по разговору

                                      И вот этот конкретный случай лично я считаю прекрасным поводом для разговора на джуниора-мидла
                                      Ex.:
                                      1) поговорили про дженерики и коллекции
                                      2) А какие вы знаете еще примеры дженериков? Если отвечает — прекрасно
                                      3) если задумался — подкидываем удочку: а как насчет Class? Если уверенно отвечает, то фиг с ним, не судьба
                                      4) Если удивился — стимулируем подумать: «А как Вы думаете, зачем?» Намекаем на возвращаемый аргумент метода, просим написать метод, возвращающий произвольный тип
                                      5) Ну и наконец — а что по-вашему должен возвращать getClass? А как это написать? Ну и говорим, что это особенный метод

                                      Я вообще считаю, что добиться на собеседовании, чтобы кандидат рассуждал на тему того, чего не знает — это как раз и есть один из способов оценить навык программиста и опыт. Конкретные вопросы — это скучно. Они быстро устраревают и отлично гуглятся (хотя я бы не сказал, что умение подготовиться — это плохо;), но это другая история)
                                      • +2
                                        Если спрашивать лично мое мнение, то я считаю, что человек от вопросов, с которыми не сталкивался (а редко кто сталкивается с getClass() в работе, а не а разработке собственных ORM, хаках и пр.), может впасть в панику и в худшем случае наговорить глупости, а в лучшем просто почувствовать себя дерьмом и бестолочью (это, к примеру, после дюжины лет опыта и десятка завершенных больших проектов). Конечно, без каверзных вопросов сложно понять уровень и адекватность, но ответы на них могут и быть просто заучены. Думаю, хоть сколько-либо достоверно уровень показать может лишь тестовое задание, или даже испытательный срок.
                                        Но я еще раз повторюсь — это только мое мнение, причем, я очень наивен в этом вопросе, а когда пару раз нанимал программистов, в основном изучал их предыдущие проекты и просил рассказать, какую они роль там выполняли, с какими проблемами сталкивались. «Пытки» же оставлял другим собеседователям :)
                                    • +2
                                      На мой взгляд реализация .class и .getClass в Java вполне логична.
                                      • +1
                                        Возращаемый тип не входит в сигнатуру метода, там только название метода и передаваемые параметры.
                                        • 0
                                          Да, сам влетал в это. Прямое следствие негибких явовских генериков. Вот и пришлось такой костыль прикручивать. Таже абсолютно тема, что и якобы нативные методы в JDK, которые просто на уровне JVM подменяются на платформозависимые реализации, а никакого JNI там нет.

                                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.