Пользователь
0,0
рейтинг
16 октября 2013 в 15:21

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

JAVA*
Я думаю, почти любого 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 написана одна сигнатура, а компилятор подставляет другую.
Алексей Фомин @Fomka
карма
15,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (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 там нет.

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