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

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

       Совсем недавно вышла ожидаемая многими 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 есть ещё довольно много методов, например, для поиска статических функций, но изучение этих фич остаётся читателю в качестве домашнего задания.
    Метки:
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 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
                Приводить к нужному типу? А если вы не знаете, сколько будет таких типов? У нас была такая задача.
              • +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 местами перепутал.

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