JRuby - metoda initialize i Java

Opublikowane przez Jarosław Zabiełło Thu, 14 Aug 2008 02:38:00 GMT

Ostatnio, podczas próby użycia javowej biblioteki w JRuby spotkałem problem z kolizją nazw metod Javy z metodami Rubiego. Mianowicie chodzi o metodę o nazwie initialize jaka była zdefiniowana w bibliotece Javy. W JRuby (i Ruby) nazwa ta jest zarezerwowana dla konstruktora. Dzięki temu, że miałem dostęp do źródeł w Javie, mógłbym po prostu je zmodyfikować. Jednakże, takie podejście nie za bardzo mi się podobało. Co w wypadku kiedy miałbym bibliotekę dostępną tylko w postaci skompilowanych klas?

Przeczesując internet, nie znalazłem nic na ten temat. W bugtrackerze JRuby można znaleźć dwa tickety (45 i 2799) które wspominają o tym problemie, lecz nie dostarczają żadnego rozwiązania. W tym drugim podano niby obejście problemu, tylko że błędne (dopisalem tam swoją uwagę). Poprawne rozwiązanie otrzymałem od Marcina Mielżyńskiego (developera JRuby) na grupie pl.comp.lang.ruby.

package pl.foo.bar;
public class X {
    public void initialize() {
        System.out.println("foo");
    }
}

include Java
require 'X.jar'

X = Java::PlFooBar::X
jcls = X.java_class
m = jcls.declared_method("initialize")
m.invoke(X.new.java_object)

Zainspirowany tym podejściem postanowiłem napisać bardziej ogólne rozwiązanie tego typu problemów.

W komentarzach do ticketu 2799 padła sugestia, aby stworzyć dodatkową metodę java_call o sugerowanej, następującej składni:

result = my_inst.java_call :my_method, 'an arg or', 2

Niestety, taki pomyśl raczej nie wypali z powodu specyfiki Javy. W przeciwieństwie do Rubiego, Java nie używa unikalnych nazw metod, ale stosuje ich przeciążanie. Java rozróżnia także typy prymitywne int, float, boolean od obiektów Integer, Float czy Boolean. Mimo, że te drugie są co prawda wrapperami dla prymitywów, jeśli jakaś biblioteka Javy posiada dwie metody o tej samej nazwie ale różniące się typem int vs. Integer, to JRuby nie za bardzo będzie wiedzieć którą metodę ma wybrać (w Ruby nie ma prymitywów, wszystko jest obiektem na tych samych prawach). Np. załóżmy, że mamy bibliotekę o takim kodzie:

public class Tescior {
    public void initialize() {
        System.out.println("initialize is reserved for constructor in JRuby");
    }
    public void initialize(String name) {
        System.out.println("Hello " + name);
    }
    public void initialize(Integer x, Integer y) {
        System.out.printf("%s + %s = %s\n", x,y,(x+y));
    }
    public void initialize(int x, Integer y) {
        System.out.printf("%s + %s = %s\n", x,y,(x+y));
    }
}

Jak niby składnia my_inst.java_call(:initialize, 3, 4) ma wiedzieć czy chodzi o powyższą metodę 3, czy 4? Nie da się! Aby to obejść, należy przekazywać nazwy typów. Dopiero gdy znamy nazwę metody i typy jej argumentów możemy jednoznacznie w Javie określić o jaką metodę nam chodzi.

Skompilujmy z konsoli powyższy kod Javy i stwórzmy jar’a.

$ javac Tescior.java
$ zip Tescior.jar Tescior.class

Kod JRuby i przykład użycia:

include Java

require 'Tescior.jar'
include_class 'Tescior'

class Java::JavaLang::Object
  def java_call(method_name, *params)
    jcls = self.java_class
    m = jcls.declared_method method_name, *params.map{|x| x[0]}
    m.invoke self.java_object, *params.map{|x| Java.ruby_to_java(x[1])}
  end
end

# test
Tescior.new.java_call :initialize
Tescior.new.java_call :initialize, [java.lang.String, "Jarek!"]
Tescior.new.java_call :initialize, [:int, 3], [java.lang.Integer, 4]
Tescior.new.java_call :initialize, [java.lang.Integer, 6], [java.lang.Integer,7]

Jak widać, przekazywane są typy i wartości parametrów. JRuby wszystkie typy prymitywne przekazuje za pomocą symboli. Pozostałe są przekazywane przez java.lang i są zgodne z javowym API. Powyższy kod można troszkę uprościć. Po co pisać takie długie deklaracje, skoro można prościej wiedząc że wszystkie nazwy klas Javy zaczynają się z dużej litery. Mimo, że to nie jest wymóg języka (tak jak jest to w Ruby) ta konwencja jest konsekwentnie stosowana i to wystarczy.

class Java::JavaLang::Object
  def java_call(method_name, *params)
    jcls = self.java_class
    m = jcls.declared_method(method_name, *params.map do |x|
      x[0].to_s =~ /^[A-Z]/ ? eval("java.lang.#{x[0]}"): x[0]
    end)
    m.invoke self.java_object, *params.map{|x| Java.ruby_to_java(x[1])}
  end
end
# test:
Tescior.new.java_call :initialize
Tescior.new.java_call :initialize, [:String, "World!"]
Tescior.new.java_call :initialize, [:int, 7], [:Integer, 8]
Tescior.new.java_call :initialize, [:Integer, 9], [:Integer,10]

Ten zapis jest już dużo lepszy i zgodny z filozofią Javy. Aby wiedzieć o jaką metodę chodzi, potrzebna jest nie tylko jej nazwa ale i typy jej parametrów formalnych. Prawdę mówiąc, dzięki JRuby zaczynam nawet lubić Javę. Dostęp do potężnych bibliotek ale w ludzki, przyjazny sposób.

Tagi ,  | 3 comments

Comments

  1. Avatar Jacek Laskowski powiedział 21 days later:

    Czyż z JRuby nie miało być prościej, tj. piszesz w Javie, ale bez całego bagażu silnego typowania? Widać, że nawet rozwiązania okrzyknięte jako panaceum na utyskiwania związane z brakiem dynamiczności Javy, jakim zdaje mi się okrzyknięto JRuby (czyli zbliżenie Ruby do Javy, albo może i na odwrót, jak kto woli) można znaleźć kilka kwiatków i w JRuby, które odstraszają.

    Wspaniale czytało się ten wpis o JRuby – stawiasz problem, rozwiązujesz go i podsumowanie. Możesz przedstawić na swoim blogu powody, dla których w Twoim przypadku zastosowanie JRuby było bardziej produktywne niż skorzystanie z pewnego javowego szkieletu aplikacyjnego? Chętnie zwróciłbym się po pomoc do JRuby, ale nie wiem jakie sprawy załatwiłby mi elegancko i produktywnie.

    Jacek

  2. Avatar Jarosław Zabiełło powiedział 21 days later:

    Powodów większej produktywności JRuby znalazłoby się więcej, ale ograniczę się do trzech: (1) możliwość interaktywnego testowania kodu, (2) dużo krótszy zapis (wynikający ze składni i dynamizmu Rubiego) i (3) Ruby on Rails / Merb.

  3. Avatar michal powiedział about 1 month later:

    Jacku, jakby nawet mialo Ci pojsc jakims cudem szybciej. Jezeli ktos potem chcialby przegladac Twoj kod – powinien nauczyc sie dwoch jezykow. Java i JRuby. Z mojego punktu widzenia JRuby to jest kolejny framework dla Javy, a szczerze uwarzam ,ze mimo wszytko specyfikacja JEE 5.0 + EJB3.0 + JPA Jest najczytelniejsza. Nie widze pracy w firmie ,gdzie zamiast uczyc sie zasad poprawnego programowania, testowania, projektowania – uczy sie jak latac dziury miedzy dwoma jezykami.

    Poza tym, aby ludzie zaczeli z czegos kozystac – musi to wnosic wiecej niz konkurencja. Jak np Spring w czasach EJB 2.0. lub narzedzia ORM zamiast JDBC.

(leave url/email »)

   Pomoc języka formatowania Obejrzyj komentarz