Je viens d'apprendre hier Scala, et j'aimerais en savoir plus. Une chose qui m'est venue à l'esprit, cependant, en lisant le site Web Scala est que si Scala s'exécute sur la JVM, alors comment est-il possible que le bytecode compilé à partir de Scala source soit capable de réaliser des choses qui Java ne peut pas faire facilement, comme (mais sans s'y limiter) les génériques réifiés?
Je comprends que le compilateur est ce qui génère le bytecode; aussi longtemps que le compilateur peut masser le code source en un bytecode valide pris en charge par la JVM, alors les deux doivent être équivalents. Mais j'avais l'impression que Java ne pouvait même pas réifier ses propres génériques, alors comment un autre compilateur pourrait-il réussir cela?)
"Tous" les langages de programmation s'exécutent sur x86, alors comment peuvent-ils être très différents les uns des autres?
Brainfuck et Haskell sont tous les deux Turing complet , donc ils peuvent tous les deux faire exactement les mêmes tâches.
Il y a un peu de place pour les changements de syntaxe, le sucre de syntaxe et la magie du compilateur entre les deux. Vous pouvez faire beaucoup de choses là-dedans, mais il y a toujours une limite. Dans votre cas, c'est du code d'octet JVM.
Si Java peut produire le même code d'octet que Scala, ils sont équivalents. Il pourrait cependant arriver qu'une nouvelle fonctionnalité de la JVM soit implémentée uniquement dans Scala. Très peu probable, mais possible .
Pour répondre au problème spécifique que vous soulevez, des génériques réifiés. . .
Dans de nombreux contextes, les paramètres de type sont réellement enregistrés dans des fichiers de classe et exploitables via la réflexion, même malgré l'effacement. Par exemple, le programme suivant imprime 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]);
}
}
Tout "effacement" signifie que lorsque vous créez une instance d'un type paramétré, le paramètre de type n'est pas enregistré comme faisant partie de l'instance; new ArrayList<String>()
et new ArrayList<Integer>()
créent des instances identiques. Mais même en Java, il existe quelques solutions de contournement bien connues, telles que:
new ArrayList<String>() { }
crée en fait une nouvelle instance d'une sous-classe anonyme de ArrayList<String>
, Et la relation de sous-classe ne enregistre le paramètre de type. Vous pouvez donc récupérer le String
de manière réfléchie. (Plus précisément: ((ParameterizedType) new ArrayList<String>() { }.getClass().getGenericSuperclass()).getActualTypeArguments()[0]
Est String.class
.) Ceci est exploité par divers frameworks, tels que Guice et Gson.Un autre langage résidant sur la JVM pourrait réifier les génériques en rendant implicite # 2; chaque fois que vous déclariez une classe générique, le ou les champs de paramètre de type et les paramètres de constructeur étaient ajoutés implicitement, et chaque fois que vous les instanciez ou étendez, les arguments de type étaient implicitement copiés dans constructeur-argument ( s) pour les peupler. Java fait déjà la même chose - champs implicites et paramètres/arguments constructeur - dans un contexte différent, à savoir, pour les classes locales qui font référence à final
variables locales dans leurs méthodes de conteneur .
La principale limitation est que les classes génériques dans le JDK - le framework de collections, Java.lang.Class
, Etc. - n'ont pas déjà cette configuration, et les langages alternatifs exécutés dans la JVM ne peuvent pas y ajouter. De tels langages devraient donc fournir leurs propres équivalents des classes JDK. Mais ils peuvent toujours utiliser la JVM elle-même.
Le bytecode JVM prétend être une sorte de code machine générique, et c'est effectivement le cas, alors ... qu'est-ce qui vous fait penser qu'il ne pourrait pas prendre en charge un autre langage? Le bytecode JVM est un langage complet de Turing, et donc, chaque programme, peu importe la langue dans laquelle il est écrit, peut être compilé/traduit en bytecode.
Il y a beaucoup de langages qui ont déjà un compilateur de bytecode (par exemple Jython pour Python et JRuby pour Ruby) et ils sont également très différents de Java.
Notez que techniquement, chaque langage de programmation complet de Turing peut être compilé dans un autre. Il pourrait être possible de compiler JS en C ou Ruby en Python, par exemple.
En bref, Scala peut le faire parce que le compilateur Scala est un maître dans la transformation/génération de code. Ce qui signifie Java = pourrait le faire aussi (peut-être qu'il le fait déjà) L'astuce n'est pas faite au niveau du bytecode mais au niveau de la source.
Pourriez-vous deviner la sortie de ce code:
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")
(Pas si) Étonnamment, il sort
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 élidé
En raison de l'effacement du type, f.asInstanceOf[T]
ne peut pas échouer, mais il est évident qu'une chaîne n'est pas un int. Fortement Scala apporte des solutions à ce problème:
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")
La principale différence ici est que nous déclarons T
comme Typeable
. Scala propose plusieurs façons de fournir des représentations d'exécution des types.
Les génériques réifiés ne nécessitent pas de prise en charge JVM. Oui, ils seraient plus faciles et plus performants avec le support JVM, mais le support JVM n'est pas nécessaire. Par exemple, un compilateur Scala pourrait, pour chaque classe comportant une variable de type, ajouter un champ qui stocke l'objet correspondant:
class List<T> {
}
void test() {
List<?> list = new List<String>();
List<Integer> intList = (List<Integer>) list;
}
serait compilé pour
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);
}
Il existe bien sûr des stratégies de traduction plus élaborées qui s'intègrent de manière plus transparente au système de type hôte, par exemple en ayant en fait des classes distinctes pour différents arguments de type vers le même type générique:
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;
}
(Il y aurait bien sûr des défis non apparents dans cet exemple trivial, par exemple des arguments de type récursif, une bonne isolation des différents chargeurs de classe, ...)
La raison Java n'a pas de génériques réifiés n'est pas que la réification serait impossible sur la JVM, mais que les génériques réifiés auraient rompu la compatibilité descendante (en particulier la compatibilité binaire) avec les Java - quelque chose Scala n'avait pas à s'inquiéter.