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

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

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, который:

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

?

Ответы

Ответ 1

Существуют правила, по которым код 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) создаются Getters и Setters. Для класса class X(val i: Int) создается только Getter:

//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 = "~=<>!#%^&|*/+-:\\[email protected]"
ops: String = ~=<>!#%^&|*/+-:\[email protected]

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. Поэтому скалак должен совершать магию. Для объекта object X { val i = 5 } создаются два файла JVM-класса: X.class и X$.class. Первый работает как интерфейс, он включает статические методы для доступа к полям и методам объекта Scala. Последний - это одноэлементный класс, который не может быть создан. Он имеет поле, которое содержит экземпляр singleton класса с именем MODULE$, который позволяет получить доступ к singleton:

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

Примеры классов

Компилятор Scala автоматически генерирует метод apply для класса case и Getters для полей. Класс 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 содержит конкретные элементы признака, но кажется, что с ними невозможно получить доступ. Ни javac, ни eclipse-javac не будут компилировать доступ к этому классу.

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

Функции

Функциональные литералы скомпилированы как анонимные экземпляры классов FunctionN. Объект Scala

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 для f
  • X$$anonfun$g$1.class для g
  • X$$anonfun$h$1$$anonfun$apply$1.class для h
  • X$$anonfun$i$1.class и X$$anonfun$i$1$$anonfun$j$1$1.class для я и j

Для доступа к ним используйте свой метод apply:

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

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

Вы должны знать:

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

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

Option

// 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. Кроме того, он говорит, что класс является абстрактным. Таким образом, может использоваться только метод apply:

Option.apply(3);

Некоторые

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

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

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

None

// 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$ и метод apply 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$. Он имеет метод apply, который ожидает 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

}

Конструктор интересен. Он сообщает нам, что создается массив ints (l. 12), который заполняется 1, 2 и 3. (l 14-25). После этого этот массив доставляется до scala.Predef$.wrapIntArray (l. 26). Этот результат scala.collection.mutable.WrappedArray снова доставляется в наш список (п. 29). В конце список сохраняется в поле (l. 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 компилируется в байт-код. Но я думаю, что с приведенной выше информацией должно быть возможно найти эти правила самостоятельно.

Ответ 2

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

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