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