it-swarm.com.de

Wenn Scala läuft auf der JVM, wie kann Scala Dinge tun, die Java scheinbar nicht können)?

Ich habe gestern gerade von Scala erfahren, und ich würde gerne mehr darüber erfahren. Eine Sache, die mir jedoch beim Lesen der Website Scala) in den Sinn kam ist, dass wenn Scala läuft auf der JVM, wie ist es dann möglich, dass aus Scala source) kompilierter Bytecode Dinge erreichen kann, die Java kann nicht ohne weiteres, wie (aber nicht beschränkt auf) reifizierte Generika?

Ich verstehe, dass der Compiler den Bytecode generiert. Solange der Compiler den Quellcode in einen gültigen Bytecode einmassieren kann, der von der JVM unterstützt wird, sollten beide gleichwertig sein. Aber ich hatte den Eindruck, dass Java konnte nicht einmal seine eigenen Generika reifizieren, also wie könnte ein anderer Compiler dies schaffen?

31
Mirrana

"Alle" Programmiersprachen laufen auf x86. Wie können sie sich also stark voneinander unterscheiden?

Brainfuck und Haskell sind beide Turing complete , sodass beide genau die gleichen Aufgaben ausführen können.

Dazwischen ist etwas Platz für Syntaxänderungen, Syntaxzucker und Compilermagie. Sie können dort ziemlich viel tun, aber es gibt immer eine Grenze. In Ihrem Fall handelt es sich um JVM-Bytecode.

Wenn Java den gleichen Bytecode wie Scala erzeugen kann, sind sie gleichwertig. Es kann jedoch vorkommen, dass eine neue Funktion in der JVM nur in Scala implementiert wird. Sehr unwahrscheinlich, aber möglich .

32
Filip Haglund

Um das von Ihnen angesprochene spezifische Problem der reifizierten Generika anzugehen. . .

In vielen Kontexten werden Typparameter are tatsächlich in Klassendateien gespeichert und können trotz Reflektion über Reflection ausgenutzt werden. Das folgende Programm druckt beispielsweise class Java.lang.String:

import Java.lang.reflect.Field;
import Java.lang.reflect.ParameterizedType;
import Java.util.ArrayList;

public class ErasureWhatErasure {
    private final ArrayList<String> foo = null;

    public static void main(final String... args) throws Exception {
        final Field fooField = ErasureWhatErasure.class.getDeclaredField("foo");
        final ParameterizedType fooFieldType =
            (ParameterizedType) fooField.getGenericType();
        System.out.println(fooFieldType.getActualTypeArguments()[0]);
    }
}

Alles "Löschen" bedeutet, dass beim Erstellen einer Instanz eines parametrisierten Typs der Typparameter nicht als Teil der Instanz aufgezeichnet wird. new ArrayList<String>() und new ArrayList<Integer>() erstellen identische Instanzen. Aber auch in Java gibt es einige bekannte Problemumgehungen, wie zum Beispiel:

  1. Etwas wie new ArrayList<String>() { } erstellt tatsächlich eine neue Instanz einer anonymen Unterklasse von ArrayList<String>, Und die Unterklassenbeziehung does zeichnet den Typparameter auf. So können Sie das String reflektierend abrufen. (Insbesondere: ((ParameterizedType) new ArrayList<String>() { }.getClass().getGenericSuperclass()).getActualTypeArguments()[0] Ist String.class.) Dies wird von verschiedenen Frameworks wie Guice und Gson ausgenutzt.
  2. Wenn Sie eine eigene Klasse erstellen, können Sie verlangen, dass der Typparameter über den Konstruktor übergeben wird, und ihn in einem Feld speichern. (Der Compiler kann Ihnen dabei helfen, zu erzwingen, dass das Konstruktorargument mit dem Typparameter übereinstimmt. Wenn das Typargument selbst generisch ist, können Sie dies mit der Problemumgehung Nr. 1 kombinieren, um sicherzustellen, dass Sie über das gesamte Typargument verfügen.)

Eine andere Sprache, die sich in der JVM befindet, könnte Generika reifizieren, indem # 2 implizit geschieht. Wenn Sie eine generische Klasse deklarieren, werden die Typparameterfelder und Konstruktorparameter implizit hinzugefügt, und wenn Sie sie instanziieren oder erweitern, werden Typargumente implizit in Konstruktorargument kopiert. s) um sie zu bevölkern. Java macht bereits dasselbe - implizite Felder und Konstruktorparameter/Argumente - in einem anderen Kontext, nämlich für lokale Klassen, die in ihren enthaltenen Methoden auf final lokale Variablen verweisen .

Die Hauptbeschränkung besteht darin, dass generische Klassen im JDK - das Sammlungsframework, Java.lang.Class Usw. - dieses Setup noch nicht haben und alternative Sprachen, die in der JVM ausgeführt werden, es ihnen nicht hinzufügen können. Solche Sprachen müssten also ihre eigenen Entsprechungen zu den JDK-Klassen bereitstellen. Sie können jedoch weiterhin die JVM selbst verwenden.

24
ruakh

JVM-Bytecode gibt vor, eine Art generischer Maschinencode zu sein, und das ist es auch. Warum denken Sie, dass er keine andere Sprache unterstützen kann? JVM-Bytecode ist eine vollständige Turing-Sprache, und somit kann jedes Programm, unabhängig davon, in welcher Sprache es geschrieben ist, kompiliert/in Bytecode übersetzt werden.

Es gibt viele Sprachen, die bereits einen Bytecode-Compiler haben (z. B. Jython für Python und JRuby für Ruby), und es gibt auch ganz andere Sprachen als Java.

Beachten Sie, dass technisch jede vollständige Programmiersprache von Turing in eine andere kompiliert werden kann. Es könnte beispielsweise möglich sein, JS nach C oder Ruby nach Python) zu kompilieren.

7
Eneko

Kurz gesagt, Scala kann dies, weil der Compiler Scala ein Meister in der Code-Transformation/-Generierung ist. Das bedeutet Java) = könnte es auch tun (vielleicht schon). Der Trick wird nicht auf Bytecode-Ebene gemacht, sondern auf Quellenebene.

Könnten Sie die Ausgabe dieses Codes erraten:

def test[T](f : => Any) : T = {
  try { val x = f.asInstanceOf[T]
    println("f.asInstanceOf did not raise an error")
      x
  }
  catch { case e : Throwable =>
    println("f.asInstanceOf did raise an error")
    throw e
  }
}

val x = test[Int]("x")

(Nicht so) Überraschenderweise gibt es aus

f.asInstanceOf did not raise an error
Java.lang.ClassCastException: Java.lang.String cannot be cast to Java.lang.Integer at scala.runtime.BoxesRunTime.unboxToInt(BoxesRunTime.Java:105)

... 33 elidiert

Wegen der Löschung des Typs f.asInstanceOf[T] kann nicht fehlschlagen, aber offensichtlich ist ein String kein Int, also muss er irgendwo fehlschlagen. Fortantely Scala bringt Lösungen für dieses Problem:

import shapeless.Typeable
import shapeless.syntax.typeable._

def test[T : Typeable](f : => Any) : T = {

    f.cast[T] match {
        case Some(x) => {
            println("f.cast[T] succeed")
            x
        }
        case None    => {
            println("f.cast[T] failed")
            throw new RuntimeException("cast failed!")
        }
    }
}

val x = test[Int]("x")

Der Hauptunterschied besteht darin, dass wir T als Typeable deklarieren. Scala bietet verschiedene Möglichkeiten, um Laufzeitdarstellungen von Typen bereitzustellen.

3

Reified Generics benötigen keine JVM-Unterstützung. Ja, sie wären mit JVM-Unterstützung einfacher und leistungsfähiger, aber JVM-Unterstützung ist nicht erforderlich. Zum Beispiel könnte ein Scala Compiler) für jede Klasse mit einer Typvariablen ein Feld hinzufügen, in dem das entsprechende Objekt gespeichert ist:

class List<T> {

}

void test() {
    List<?> list = new List<String>();
    List<Integer> intList = (List<Integer>) list;
}

würde zu kompiliert werden

class List<T> {
    final Class<T> tClass;
    public List(Class<T> tClass) {
        this.tClass = tClass;
    }

    public <O> List<O> castTo(Class<O> oClass) {
        if (tClass == oClass) {
            return (List<O>) this;
        } else {
            throw new ClassCastException("Incompatible type parameter: " + tClass);
        }
    }
}

void test() {
    List<?> list = new List<String>(String.class);
    List<Integer> intList = list.castTo(Integer.class);
}

Es gibt natürlich ausgefeiltere Übersetzungsstrategien, die sich nahtloser in das Hosttypsystem integrieren lassen, beispielsweise indem tatsächlich separate Klassen für verschiedene Typargumente für denselben generischen Typ vorhanden sind:

class List<T> {

}

class List#Integer extends List<Integer> { }

class List#String extends List<String> { }

void test() {
    List<?> list = new List#String();
    List<Integer> intList = (List#Integer) list;
}

(Es würde natürlich einige Herausforderungen geben, die in diesem trivialen Beispiel nicht erkennbar sind, zum Beispiel rekursive Typargumente, die ordnungsgemäße Isolierung verschiedener Klassenlader, ...)

Der Grund Java hat keine reifizierten Generika) ist nicht, dass eine Reifizierung in der JVM unmöglich wäre, sondern dass reifizierte Generika die Abwärtskompatibilität (insbesondere die Binärkompatibilität) mit vorhandenen Java Programme - etwas Scala musste sich keine Sorgen machen.

2
meriton