30 июля 2011 в 14:10

the Da Vinci Machine Project в Java 7 и борьба с велосипедами

Java*
   Добрый день, уважаемые хабражители!

   Совсем недавно вышла ожидаемая многими Java 7. К сожалению, многих разочаровал состав нововведий, так как в него не попали различные очень ожидаемые вкусности вроде Project Lambda. Однако нововведений всё же много и сегодня я хотел бы немного остановиться на одном из важнейших — the Da Vinci Machine Project, который позволяет пользоваться динамическими языками на JVM более эффективно. Если говорит точнее, то рассматривать мы будем одну из частей the Da Vinci Machine Project — method handle. До конца проникнуться концепциями этой части языка я ещё не успел, но большинство людей вообще не понимают, зачем оно надо :) В статье я рассмотрю один use case, который Java-программистам знаком не по наслышке и родил, думаю, наибольшее число велосипедов ever. Он, конечно, касается перегрузки методов и передачи параметров по интерфейсу.

   Известно, что в Java решение о том, какой из перегруженных методов вызывать производится на этапе компиляции (в отличии, скажем, от C#, где это делается во время исполнения). Предположим, у нас есть следующий код:
  1. class A {
  2. }
  3.  
  4. class B extends A {
  5. }
  6.  
  7. public class Test {
  8.   
  9.   public void call(A a) {
  10.     System.out.println("A");
  11.   }
  12.   
  13.   public void call(B b) {
  14.     System.out.println("B");
  15.   }
  16.   
  17.   public static void main(String[] argv) {
  18.     A b = new B();
  19.     Test test = new Test();
  20.     test.call(b);
  21.   }
  22. }
* This source code was highlighted with Source Code Highlighter.

   По правилам Java будет вызван метод public void call(A a), и соответственно выведено на экран «A», что может показаться странным, потому что реальный тип объекта B. Для того, чтобы вызвать нужный нам метод необходимо переписать код вызова метода так:
  1. if (b instanceof B) {
  2.   test.call((B) b);
  3. }
* This source code was highlighted with Source Code Highlighter.


   Данный подход вполне имеет право на жизнь, но если у вас несколько десятков таких классов, то код становится банально уродливым, появляется дублирование и вообще.

   В Java 7 появились средства, которые могут помочь решить данную проблему, если не на уровне языка, то хотя бы не уровне костылей. Итак, помогут нам классы из пакета java.lang.invoke:
  • MethodHandle — своего рода указатель на метод, эта вещь будет широко применяться в замыканиях и лямбда выражениях (честно говоря, я думал там будет сделано всё наиболее простым образом — лямбды будут преобразовываться к анонимным классам и дело с концом).
  • MethodType — представляет собой сигнатуру метода, а именно возвращаемое значение и список входных параметров.
  • MethodHandles — класс различных утилит для работы с методами.
  • Lookup — класс входит в состав класса MethodHandles и предоставляет интерфейс для поиска методов с заданными параметрами.

   Итак, перейдём к самому интересному — как же нам всё это поможет. Теперь код будет выглядеть так:
  1. import static java.lang.invoke.MethodHandles.*;
  2.  
  3. import java.lang.invoke.MethodHandle;
  4. import java.lang.invoke.MethodType;
  5.  
  6. class A {
  7. }
  8.  
  9. class B extends A {
  10. }
  11.  
  12. public class Test {
  13.   
  14.   public void call(A a) {
  15.     System.out.println("A");
  16.   }
  17.   
  18.   public void call(B b) {
  19.     System.out.println("B");
  20.   }
  21.   
  22.   public static void main(String[] argv) {
  23.     A b = new B();
  24.     Test test = new Test();
  25.         
  26.     try {
  27.       Lookup lookup = lookup();
  28.       MethodType mt = MethodType.methodType(void.class, b.getClass());
  29.       
  30.       MethodHandle mh = lookup.findVirtual(Test.class, "call", mt);
  31.       mh.invokeWithArguments(test, b);
  32.     } catch (Throwable e) {
  33.       e.printStackTrace();
  34.     }
  35.   }
  36. }
* This source code was highlighted with Source Code Highlighter.

   Посмотрим, что же тут происходит. В строке 27 объявляем непосредственно «поисковик» функций. Далее в строке 28 объявляем типы возвращаемого значения и параметров. Важно отметить, в метод methodType сначала передаётся тип возвращаемого значения, затем типы списка параметров искомого метода. Строка 30 — получаем в MethodHandle результаты поиска методов в классе Test, с названием call и типами возвращаемого значения и параметров mt. В строке 31 вызываем найденный метод у объекта test с параметром b и видим на экране ожидаемое «B».

   В классе Lookup есть ещё довольно много методов, например, для поиска статических функций, но изучение этих фич остаётся читателю в качестве домашнего задания.
Илья Ермолов @FlashXL
карма
17,2
рейтинг 0,0
Самое читаемое Разработка

Комментарии (49)

  • +8
    Мне кажеться что первый вариант выглядит понятие и красивее.
    • +3
      Приводить к нужному типу? А если вы не знаете, сколько будет таких типов? У нас была такая задача.
      • 0
        Тоже сталкивался с этим, приходилось решать с помощью аннотаций в аргументах.
      • +2
        > У нас была такая задача.

        То есть все имена сторонних классов были известны внутри вашего класса-диспетчера, а чтобы выполнить какое-нибудь действие с переданным классом, вы сначала должны были узнать, к какому типу принадлежит объект «if (b instanceof B)» и только после этого производить с ним какие-то действия?

        А вы не пробовали провести рефакторинг, заменив условный оператор (гигантский switch по типу передаваемого класса) полиморфизмом, в частности, образцами проектирования State|Strategy? Это даёт две вещи: избавление от необходимости слежения за появлением новых классов, передающихся в условный селектор (селектора больше не будет) во всех местах программы, где такая условнойсть присутствовала и дописываю новых case для них;
        резко сокращается число зависимостей в программе и упрощается сопровождение кода, так как ввашему классу больше не нужно хранить информацию об используемых классах. Книга Мартина Фаулера «Рефакторинг. Улучшение существующего кода», СПб: Символ-Плюс, 2003,. в помощь.

        Диспетчеризации действий c определением типа класса (instanceof) можно и нужно избегать.
        • 0
          Strategy тут не поможет. Вообще тут нужно применять Visitor. Что не сильно красивее switch.
          • 0
            Достаточно хранить Map<Class, StrategyObject>. Потом просто вызывать getClass(), получить стратегию и вызвать ее.
        • +4
          Подписываюсь под каждым словом — если дело в проекте доходит до такого, надо рефакторить. Чем больше работаю с Java, тем больше сознаю, что при выборе решения надо не только придумывать, как это можно сделать, но и учитывать особенности и ограничения языка, с которым работаешь. Java — не Scala и не Jython — и не надо заставлять ее играть роль того или другого. Java — это Java.

          Автор явно хочет от Java динамического поведения, но при этом отказывается от каста в попытке обмануть себя. Мы все принимаем на веру, что каст — это плохо. Но переходить от каста к dynamic dispatch только ради того, чтобы убрать из кода каст — именно это и происходит в коде — имхо, неразумно. Да, предложенный способ дешевле, чем рефлекшн, но он все равно дороже, чем статическое связывание.

          И да, автор, в C#, как и в C++, и в Java, overloading производится на этапе компиляции, учите матчасть: en.wikipedia.org/wiki/Method_overloading
    • +1
      Ну не сказал бы. Как по мне, первый вариант не дает возможности реализовать просто паттерны вроде Фабрики.
    • 0
      Мне кажется, здесь показано не то применение MethodHandle. Я читал статью, где его применяли как замену Reflection в том случае, когда необходимо было вызывать приватные методы. MethodHandle должен работать быстрее Reflection, и он также позволяет динамическим языкам быстрее работать на JVM. В данном случае, как писали ниже, скорее более подойдет просто рефакторинг
  • 0
    Приводить к нужному типу? А если вы не знаете, сколько будет таких типов? У нас была такая задача.
    • 0
      Сорри, не туда.
  • +2
    Может я чего не понимаю, но вы же явно указываете, что b имеет тип A:

    A b = new B();

    но потом пишите:

    > По правилам Java будет вызван метод public void call(A a), и соответственно выведено на экран «A», что может показаться странным, потому что реальный тип объекта B.
    • 0
      Java так работает, да, но нередко бывают задачи, когда надо взывать тот метод, который подходит для реального типа объекта, а не тип ссылки на объект. Здесь в ссылке типа A содержится реально объект типа B, и надо вызвать соответствующий метод, который принимает аргументом B, а не A. Общее решение стало доступно только в Java 7.
  • +2
    А в чем отличие от reflection?
    • 0
      Ну мне кажется этот вариант более лаконичен
      • 0
        download.oracle.com/javase/7/docs/technotes/guides/vm/multiple-language-support.html

        В данном случае нет никакой разницы, классы MethodType и MethodHandler введены для того, что бы обработать вызов invokedynamic «methodname» paramtypes.

        Линкуется все это дело с использованием вызова bootstrap метода.

        Пример. Есть bootstrap метод:
        public static CallSite mybsm(
        MethodHandles.Lookup callerClass, String dynMethodName, MethodType dynMethodType)

        Есть код:
        invokedynamic «add» "(Ljava/lang/Integer;Ljava/lang/Integer;)Ljava/lang/Integer)"

        При выполнении кода (если для «add» он первый раз выполняется) будет вызван bootstrap метод с параметрами dynMethodName=«add», MethodType = MethodType.methodType(Integer.class, Integer.class, Integer.class), callerClass — ссылка на текущий lookup.

        mybsm должен вернуть CallSite со ссылкой на MethodHandler, который соответствует названию (dynMethodName) и типу (dynMethodType). Далее происходит вызов invokeWithArguments, например MethodHandler'a.

        Т.е. метод в invokedynamic линкуется с использованием bootstrap метода.
        Собственно это и есть основная задача «the Da Vinci Machine Project», а не создание «лаконичного» аналога reflection api, как Вы написали.
        • 0
          полностью поддерживаю, имхо, автор несколько недопонял смысл введения этой конструкции.

          Судя по тому что писал John Rose в своем блоге во время разработке, все это дело реализовывалось в рамках «универсализации» JVM.
          В новой модели, предполагается компиляция в байткод всяческих динамически-типизуемых языков. И так как модель вызова методов классов, писаных на этих языках, достаточно непонятна, эти методы будут выносится в constant pool, class-файлов, во время компиляции. Формат class-файлов был специально под это изменен.

          При разгрузке этих классов в JVM, эта информация будет подгружаться на линковке и будет происходить динамической связывание правил вызовов этих методов.

          Детали лучше курить в новой спеки виртуалки.
    • +1
      С рефлекшеном еще бы пришлось динамически кастовать объект к своему реальному типу.
  • 0
    А с новым API можно делать обработку вызовов классом в зависимости от того, как называется метод?
    То есть перейти от вызова методов к message-passing по сути?
  • +4
    "(в отличии, скажем, от C#, где это делается во время исполнения)"

    Но это никак не значит, что в .Net вызовется метод «public void call(B b)», там такое же правило действует — как объявлен объект с такими параметрами метод и вызывается. Кроме рефлекции, обойти в C# можно ещё объявив объект ключевым словом dynamic, но со всеми вытекающими динамических объектов…

    dynamic b = new B();
    Test test = new Test();
    test.call(b);
    Вызовет «public void call(B b)»

    P.S. И если честно, я не понял преимуществ такого количества кода перед рефлекцией. Опять же в C#:
    Type testType = test.GetType();
    MethodInfo mi = testType.GetMethod(«call», new Type[] { b.GetType() });
    mi.Invoke(test, new object[]{ b });
    • –6
      Если в Java рефлекция появилась только в 7 версии мне печально =)
      • +3
        Если мне память не изменяет, оно еще в 1998 году было.
        • 0
          Тогда наверно я не понял в чем заключается новшество «the Da Vinci Machine»
          • 0
            Как я понял из описания проекта, основная его фишка это:

            We are extending the JVM with first-class architectural support for languages other than Java, especially dynamic languages. This project will prototype a number of extensions to the JVM, so that it can run non-Java languages efficiently, with a performance level comparable to that of Java itself.

            Sub-projects with major activity include dynamic invocation, continuations, tail-calls, and interface injection.
            • 0
              Ага поддержка динамических языков + «dynamic invocation, continuations, tail-calls, and interface injection»
              Но тогда последний вопрос в 6-ой версии Java нельзя был ополучить тип класса и по имени метода сам метод и вызвать его? Это можно тока в седьмой версии которыая привнесла
              «the Da Vinci Machine Project»
              • +3
                > Но тогда последний вопрос в 6-ой версии Java нельзя был ополучить тип класса и по имени метода сам метод и вызвать его?

                Что-то типа такого?

                test.getClass().getMethod(«call», B.class).invoke(test, b);
                • 0
                  Ясно. Ваш метод явно лаконичнее предложеного автором статьи.
                  • +1
                    Только, честно, яхз работает ли он так как надо автору статьи :)
                    • 0
                      Нет :)

                      Тут вы явно указываете, что метод нужо взять из класса B, а автора интересует вариант, когда есть объект неизвестного класса и нужно сначала определить, из какого же класса в иерархии нужно вызвать метод.
                      • 0
                        test.getClass().getMethod(«call», b.getClass()).invoke(test, b) не будет работать?
  • 0
    Шикарно!
  • +1
    Так под капотом это будет вызывать рефлексию со всеми вытекающими (медленно, засирается PermGen) или что-то особое?
    • 0
      Честно сказать — не знаю, ещё не ковырял :) Сегодня утром сел разобраться что там вообще есть в седьмой джаве новое и вот решил поделиться результатом.
    • +1
      Что-то особое, в частности новую JVM инструкцию invokedynamic
  • +1
    Тип объекта не определяется, однако метод вызвается верный…

    Ах да… Это стандартная заковыка тестов :) И уже подзабыл… Жаль, что на работе пишу на C#, хотелось бы больше времени уделять Джаве…
  • +2
    Я наверное не совсем понял проблему, описаную в примере и вполне заслуживаю большой минус, но зачем делать кучу методов типа call(A a), call(B b), call(C c), когда можно в классах A, B, C определить метод print().

    Конечно, если эту возможность ввели в java7, то наверное это кому-нибудь нужно, но из приведенного примера это, на мой взгляд, как-то неочевидно.
    • 0
      ну хотя бы потому, что вариантов реализации метода print() может быть несколько. например, понадобилось сделать печать не на экран, а на принтер — и что, теперь у всех классов переписывать print()?
      • 0
        Все равно не понимаю. Если у вас непостредственно выводом на экран/принтер занимаются классы A, B, C (или ф-ции call), то их так и так переписывать придется. А если они лишь вызывают некий API для печати, то для описанного случая вроде шаблон существуют — если не изменяет память вроде он Bridge называется.
        В этом случае не нади ничего переписывать — просто выставляем нужный «printer» и готово.
        • 0
          Ну вот этот вот «вызов API» — это, по моему мнению, лишний код. Если объект просто «вызывает API», то этот самый вызов лучше из объекта вынести в специальный сервис.

          Это бывает полезно, если данные передаются между разными уровнями. Например, на веб-уровне элемент можно отрендерить, а на db-уровне его можно сохранить. Альтернативой будет написание всех методов типа render(), save(), load() и т.д. у каждого объекта, либо использование разных объектов на разных уровнях.

  • +1
    >> } catch (Throwable e) {

    Я понимаю, что это набросок кода, но все-таки так лучше не делать, потому что под этот catch попадают такие ошибки, как OutOfMemoryException и др.
  • 0
    В C# такое делается так:

    test.call((dynamic)b);

    Эта фича тоже появилась как побочный эффект поддержки динамических языков в .net (DLR) и поддержки этого в C# (тип dynamic).
  • 0
    Известно, что в Java решение о том, какой из перегруженных методов вызывать производится на этапе компиляции
    можно здесь поподробнее и про раннее/позднее связывание?
  • 0
    интересно было бы взять готовое решение из commons-lang (которое через reflection работает) и сравнить производительность
  • 0
    > Известно, что в Java решение о том, какой из перегруженных методов вызывать производится на этапе компиляции

    Это неправда. Не всегда однозначно определено, какой метод вызывать. Поэтому почти все вызовы компилируеются в invokevirtual.
    • 0
      в случае, если речь идет о перегрузке в смысле override, а не overload, то на сколько я помню, все обстоит именно так, как сказал автор.
      • 0
        В чем разница между разница между override и overload? Во что еще, кроме invokevirtual, может компилироваться вызов обычного метода?
        • 0
          в примере автора нет никаких виртуальных перегрузок, и судя по всему он подразумевал переопределение.
        • 0
          боюсь что начну заниматься интерпретацией слов автора, а это не есть гуд. но думаю он поправит если что.

          как я понял, подразумевалось что при оверлоаде методов, как приведено в примере, компайлер делает раннее связывание вызовов методов. для манипуляциями с оверлоадами в делфяке например есть специальная дириктива, и некоторые пляски через это. если я все правильно понял, автор об этом и говорил, если же нет, то коммент автора о том что он имел ввиду в студию.

          что касается invokevirtual, есть ещё invokerspecial и invokeinterface, которые к нашей ситуации не подходят вроде, да я о нем и не говорил :)
      • 0
        тьфу блин, overload c override местами перепутал.

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