Как узнать об использовании scala.None из Java с помощью javap?

В предыдущем вопросе Доступ к scala.None из Java , кажется, люди использовали javap, чтобы выяснить, как получить доступ к scala.None из Java. Я хотел бы знать, как они это сделали. К вашему сведению, ответ:

scala.Option$.MODULE$.apply(null);

который можно замкнуть на:

scala.Option.apply(null);

Учитывая эту программу (OptionTest.scala):

object OptionTest extends App {
  val x = scala.None
  val y = scala.Some("asdf")
}

Я запускал javap на нем так:

javap -s -c -l -private OptionTest

Это часть вывода javap:

public static final scala.None$ x();
  Signature: ()Lscala/None$;
  Code:
   0:   getstatic  #11; //Field OptionTest$.MODULE$:LOptionTest$;
   3:   invokevirtual  #55; //Method OptionTest$.x:()Lscala/None$;
   6:   areturn

Я также запускал javap на scala.None и scala.Option. Как понять из вывода javap, что:

  1. None — это единственный объект типа None.type, расширяющий Option.
  2. Требуется метод apply() для объекта-компаньона.

?


person Mike Slinn    schedule 19.02.2012    source источник


Ответы (2)


Существуют правила компиляции Scala-кода в JVM-байткод. Из-за возможных конфликтов имен сгенерированный код не всегда интуитивно понятен, но если известны правила, можно получить доступ к скомпилированному коду Scala в Java.

Внимание: Пока я писал это, я заметил, что javac и eclipse-javac ведут себя по-разному при доступе к коду Scala из Java. Возможно, приведенный ниже код компилируется с одним из них, но не с другим.

Классы, конструкторы, методы

Здесь нет особых правил. Следующий класс Scala

class X(i: Int) {
  def m1 = i*2
  def m2(a: Int)(b: Int) = a*b
  def m3(a: Int)(implicit b: Int) = a*b
}

можно получить доступ как к обычному классу Java. Он скомпилирован в файл с именем X.class:

X x = new X(7);
x.m1();
x.m2(3, 5);
x.m3(3, 5);

Обратите внимание, что для методов без списка параметров создается пустой список параметров. Несколько списков параметров объединяются в один.

Поля, значения

Для класса class X(var i: Int) созданы геттеры и сеттеры. Для класса class X(val i: Int) создается только геттер:

//Scala
val x = new X(5)
x.i = 3 // Setter
x.i // Getter

//Java
X x = new X(5);
x.i_$eq(3); // Setter
x.i(); // Getter

Обратите внимание, что в Java идентификатор не может содержать специальные знаки. Поэтому scalac генерирует для каждого из этих специальных знаков определенное имя. Существует класс scala.reflect.NameTransformer который может кодировать/декодировать операции:

scala> import scala.reflect.NameTransformer._
import scala.reflect.NameTransformer._

scala> val ops = "~=<>!#%^&|*/+-:\\?@"
ops: String = ~=<>!#%^&|*/+-:\?@

scala> ops map { o => o -> encode(o.toString) } foreach println
(~,$tilde)
(=,$eq)
(<,$less)
(>,$greater)
(!,$bang)
(#,$hash)
(%,$percent)
(^,$up)
(&,$amp)
(|,$bar)
(*,$times)
(/,$div)
(+,$plus)
(-,$minus)
(:,$colon)
(\,$bslash)
(?,$qmark)
(@,$at)

Класс class X { var i = 5 } транслируется по той же схеме, что и при создании поля в конструкторе. Прямой доступ к переменной i из Java невозможен, потому что она приватная.

Объекты

В Java нет такого понятия, как объект Scala. Поэтому scalac должен творить чудеса. Для объекта object X { val i = 5 } создаются два файла класса JVM: X.class и X$.class. Первый работает как интерфейс, он включает в себя статические методы для доступа к полям и методам объекта Scala. Последний представляет собой одноэлементный класс, который не может быть создан. У него есть поле, которое содержит экземпляр синглтона класса с именем MODULE$, который разрешает доступ к синглтону:

X.i();
X$.MODULE$.i();

Классы случаев

Компилятор Scala автоматически генерирует метод применения для класса case и геттеры для полей. Доступ к классу case case class X(i: Int) легко получить:

new X(3).i();
X$.MODULE$.apply(3);

Черты

Трейт trait T { def m }, который содержит только абстрактные члены, компилируется в интерфейс, который помещается в файлы классов с именем T.class. Поэтому его можно легко реализовать с помощью класса Java:

class X implements T {
  public void m() {
    // do stuff here
  }
}

Если трейт содержит конкретные члены, в дополнение к обычному интерфейсу создается файл класса с именем <trait_name>$class.class. Черта

trait T {
  def m1
  def m2 = 5
}

также может быть легко реализован в Java. Файл класса T$class.class содержит конкретные члены трейта, но кажется, что к ним невозможно получить доступ из Java. Ни javac, ни eclipse-javac не скомпилируют доступ к этому классу.

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

Функции

Литералы функций компилируются как анонимные экземпляры классов FunctionN. Скала-объект

object X {
  val f: Int => Int = i => i*2
  def g: Int => Int = i => i*2
  def h: Int => Int => Int = a => b => a*b
  def i: Int => Int => Int = a => {
    def j: Int => Int = b => a*b
    j
  }
}

компилируется в обычные файлы классов, как описано выше. Кроме того, каждый литерал функции получает свой собственный класс-файл. Итак, для значений функций генерируется файл класса с именем <class_name>$$anonfun$<N>.class, где N — непрерывное число. Для методов функций (методов, возвращающих функцию) создается файл класса с именем <class_name>$$anonfun$<method_name>$<N>.class. Части имени функции разделены знаками доллара, а перед идентификатором anonfun также есть два знака доллара. Для вложенных функций имя вложенной функции добавляется к внешней функции, это означает, что внутренняя функция получит файл класса, например <class_name>$$anonfun$<outer_method_name>$<N>$$anonfun$<inner_method_name>$<N>.class. Когда внутренняя функция не имеет имени, как показано в h, она получает имя apply.

Это означает, что в нашем случае мы получаем:

  • X$$anonfun$1.class для ф
  • X$$anonfun$g$1.class для г
  • X$$anonfun$h$1$$anonfun$apply$1.class для ч
  • X$$anonfun$i$1.class и X$$anonfun$i$1$$anonfun$j$1$1.class для i и j

Чтобы получить к ним доступ, используйте их метод применения:

X.f().apply(7);
X.g().apply(7);
X.h().apply(3).apply(5);
X.i().apply(3).apply(5);

Ответь на вопрос

Ты должен знать:

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

Некоторые примеры

Вариант

// javap scala.Option
public abstract class scala.Option extends java.lang.Object implements ... {
  ...
  public static final scala.Option apply(java.lang.Object);
  public scala.Option();
}

javap говорит, что у него есть конструктор и метод применения. Кроме того, он говорит, что класс является абстрактным. Таким образом, можно использовать только метод apply:

Option.apply(3);

Немного

// javap scala.Some
public final class scala.Some extends scala.Option implements ... {
  ...
  public scala.Some(java.lang.Object);
}

У него есть конструктор и метод применения (потому что мы знаем, что у Option есть один, а у Some extends Option). Воспользуйтесь одним из них и будьте счастливы:

new Some<Integer>(3);
Some.apply(3);

Никто

// javap scala.None
public final class scala.None extends java.lang.Object{
  ...
}

Он не имеет конструктора, метода применения и не расширяет Option. Итак, рассмотрим None$:

// javap -private scala.None$
public final class scala.None$ extends scala.Option implements ... {
  ...
  public static final scala.None$ MODULE$;
  private scala.None$();
}

Ага! Мы нашли поле MODULE$ и метод применения Option. Кроме того, мы нашли частный конструктор:

None$.apply(3) // returns Some(3). Please use the apply-method of Option instead
None$.MODULE$.isDefined(); // returns false
new None$(); // compiler error. constructor not visible

Список

scala.collection.immutable.List является абстрактным, поэтому мы должны использовать scala.collection.immutable.List$. У него есть метод применения, который ожидает scala.collection.Seq. Итак, чтобы получить список, нам сначала нужен Seq. Но если мы посмотрим на Seq, там нет метода применения. Кроме того, когда мы смотрим на суперклассы Seq и на scala.collection.Seq$, мы можем найти только методы применения, которые ожидают Seq. Так что делать?

Мы должны посмотреть, как scalac создает экземпляр List или Seq. Сначала создайте класс Scala:

class X {
  val xs = List(1, 2, 3)
}

Скомпилируйте его с помощью scalac и посмотрите на файл класса с помощью javap:

// javap -c -private X
public class X extends java.lang.Object implements scala.ScalaObject{
...
public X();
  Code:
   0:   aload_0
   1:   invokespecial   #20; //Method java/lang/Object."<init>":()V
   4:   aload_0
   5:   getstatic   #26; //Field scala/collection/immutable/List$.MODULE$:Lscala/collection/immutable/List$;
   8:   getstatic   #31; //Field scala/Predef$.MODULE$:Lscala/Predef$;
   11:  iconst_3
   12:  newarray int
   14:  dup
   15:  iconst_0
   16:  iconst_1
   17:  iastore
   18:  dup
   19:  iconst_1
   20:  iconst_2
   21:  iastore
   22:  dup
   23:  iconst_2
   24:  iconst_3
   25:  iastore
   26:  invokevirtual   #35; //Method scala/Predef$.wrapIntArray:([I)Lscala/collection/mutable/WrappedArray;
   29:  invokevirtual   #39; //Method scala/collection/immutable/List$.apply:(Lscala/collection/Seq;)Lscala/collection/immutable/List;
   32:  putfield    #13; //Field xs:Lscala/collection/immutable/List;
   35:  return

}

Конструктор интересный. Это говорит нам о том, что создается массив целых чисел (л. 12), который заполняется 1, 2 и 3 (л. 14-25). После этого этот массив доставляется в scala.Predef$.wrapIntArray (л. 26). Этот результат scala.collection.mutable.WrappedArray снова доставляется в наш Список (л. 29). В конце Список сохраняется в поле (л. 32). Когда мы хотим создать список в Java, мы должны сделать то же самое:

int[] arr = { 1, 2, 3 };
WrappedArray<Object> warr = Predef$.MODULE$.wrapIntArray(arr);
List$.MODULE$.apply(warr);

// or shorter
List$.MODULE$.apply(Predef$.MODULE$.wrapIntArray(new int[] { 1, 2, 3 }));

Это выглядит некрасиво, но это работает. Если вы создадите красивую библиотеку, которая обертывает доступ к библиотеке Scala, будет легко использовать Scala из Java.

Резюме

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

person kiritsuku    schedule 19.02.2012
comment
Потрясающий ответ. Возможно, вам будет интересно разместить ссылку на NameTransformer, который искажает/расшифровывает имена операторов. - person Daniel C. Sobral; 25.02.2012
comment
@DanielC.Sobral: Спасибо за подсказку. Я не знал этого класса. - person kiritsuku; 25.02.2012

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

scala> :paste
// Entering paste mode (ctrl-D to finish)

object OptionTest extends App {
  val x = scala.None
  val y = scala.Some("asdf")
}

// Exiting paste mode, now interpreting.

defined module OptionTest

scala> :javap -v OptionTest$
Compiled from "<console>"
public final class OptionTest$ extends java.lang.Object implements scala.App,scala.ScalaObject
  SourceFile: "<console>"
  Scala: length = 0x

  [lots of output etc]   

  public scala.None$ x();
    Code:
     Stack=1, Locals=1, Args_size=1
     0: aload_0
     1: getfield    #65; //Field x:Lscala/None$;
     4: areturn
person psp    schedule 24.02.2012