Ответ 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 компилируется в байт-код. Но я думаю, что с приведенной выше информацией должно быть возможно найти эти правила самостоятельно.