Существуют правила компиляции 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