Comment étendre Java pour introduire le passage par référence?
Java est pass-by-value. Comment pourriez-vous modifier le langage pour introduire la passe par référence (ou un comportement équivalent)?
prendre par exemple quelque chose comme
public static void main(String[] args) {
String variable = "'previous String reference'";
passByReference(ref variable);
System.out.println(variable); // I want this to print 'new String reference'
}
public static void passByReference(ref String someString) {
someString = "'new String reference'";
}
qui (sans le ref
) se compose au suivant bytecode
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String 'previous String reference'
2: astore_1
3: aload_1
4: invokestatic #3 // Method passByReference:(Ljava/lang/String;)V
7: return
public static void passByReference(java.lang.String);
Code:
0: ldc #4 // String 'new String reference'
2: astore_0
3: return
le code à 3:
charge la référence sur la cheminée à partir de la variable variable
.
une possibilité que je considère est d'avoir le compilateur déterminer une méthode est de passer par référence, peut-être avec ref
, et de changer la méthode pour accepter un objet Holder qui stocke la même référence que notre variable. Lorsque la méthode est terminée, et modifie éventuellement cette référence dans le holder, la variable de la valeur de l'appelant est remplacée par la valeur de la référence du holder.
il devrait compiler à un équivalent de ce
public static void main(String[] args) {
String variable = "'previous String reference'";
Holder holder = Holder.referenceOf(variable);
passByReference2(holder);
variable = (String) holder.getReference(); // I don't think this cast is necessary in bytecode
System.out.println(variable);
}
public static void passByReference(Holder someString) {
someString.setReference("'new String reference'");
}
où Holder
pourrait être quelque chose comme
public class Holder {
Object reference;
private Holder (Object reference) {
this.reference = reference;
}
public Object getReference() {
return this.reference;
}
public void setReference(Object reference) {
this.reference = reference;
}
public static Holder referenceOf(Object reference) {
return new Holder(reference);
}
}
Où cela peut-il échouer ou comment pourriez-vous améliorer?
10 réponses
pour répondre à votre question:
Où cela peut-il échouer?
- variables finales et constantes enum
- références "spéciales" telles que
this
- références qui sont retournées à partir d'appels de méthodes, ou construites en ligne en utilisant
new
- littérales (cordes, entiers, etc.)
...et peut-être d'autres. Fondamentalement, votre mot-clé ref
ne doit être utilisable que si le paramètre source est un champ non-final ou une variable locale. Toute autre source devrait générer une erreur de compilation lorsqu'elle est utilisée avec ref
.
exemple de (1):
final String s = "final";
passByReference(ref s); // Should not be possible
exemple de (2):
passByReference(ref this); // Definitely impossible
exemple de (3):
passByReference(ref toString()); // Definitely impossible
passByReference(ref new String("foo")); // Definitely impossible
exemple de (4):
passByReference(ref "literal"); // Definitely impossible
et puis il y a des expressions d'affectation, qui me semblent comme quelque chose d'un appel de jugement:
String s;
passByReference(ref (s="initial")); // Possible, but does it make sense?
il est également un peu étrange que votre syntaxe nécessite le mot-clé ref
pour la définition de méthode et l'invocation de méthode. Je pense que la définition de la méthode serait suffisant.
l'idiome habituel que j'ai vu pour pass-by-reference en Java est de passer un tableau à un seul élément, qui préservera à la fois le type d'exécution-sécurité (contrairement aux génériques qui subissent l'effacement) et d'éviter la nécessité d'introduire une nouvelle classe.
public static void main(String[] args) {
String[] holder = new String[1];
// variable optimized away as holder[0]
holder[0] = "'previous String reference'";
passByReference(holder);
System.out.println(holder[0]);
}
public static void passByReference(String[] someString) {
someString[0] = "'new String reference'";
}
votre tentative de modifier le langage ignore le fait que cette" fonctionnalité " a été explicitement laissée de côté pour empêcher les bogues à effets secondaires bien connus d'être en mesure de se produire en premier lieu. Java recommande de faire ce que vous tentez d'archiver en utilisant les classes data-holder:
public class Holder<T> {
protected T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
une version sans fil serait la AtomicReference .
stocker maintenant une seule chaîne dans une classe semble exagéré et la plupart probablement il est, cependant habituellement vous avez une classe de détenteur de données pour plusieurs valeurs liées au lieu d'une chaîne simple.
le grand avantage de cette approche est que ce qui se passe à l'intérieur de la méthode est très explicite . Donc, même si vous programmez un lundi matin après un week-end mouvementé et que la machine à café vient de tomber en panne, vous pouvez toujours dire facilement ce que le code fait ( KISS ), empêchant plusieurs bogues de même se produire en premier lieu, juste parce que vous avez oublié cette caractéristique de la méthode foo.
si vous pensez à ce que votre approche peut faire que la version de data-holder ne peut pas, vous vous rendrez vite compte que vous mettez en œuvre quelque chose juste parce qu'il est différent, mais effectivement il n'a pas de valeur réelle.
utilisant la classe AtomicReference comme objet porteur.
public static void main(String[] args) {
String variable="old";
AtomicReference<String> at=new AtomicReference<String>(variable);
passByReference(at);
variable=at.get();
System.out.println(variable);
}
public static void passByReference(AtomicReference<String> at) {
at.set("new");
}
assez curieusement, j'ai pensé à ce problème moi-même récemment. Je me demandais s'il serait amusant de créer un dialecte de VB qui fonctionnerait sur le JVM - j'ai décidé que ce ne serait pas le cas.
quoi qu'il en soit, il y a deux cas principaux où cela est susceptible d'être utile et bien défini:
- variables locales
- attribut (s) d'objet
je suppose que vous écrivez un nouveau compilateur (ou l'adaptation d'un produit existant) pour votre nouveau dialecte de Java.
les variables locales sont typiquement gérées par un code similaire à ce que vous proposez. Je connais très bien Scala, qui ne supporte pas pass-by-reference, mais supporte les fermetures, qui ont les mêmes problèmes. À Scala , il y a une classe scala.runtime.ObjectRef
, qui ressemble à votre classe Holder
. Il existe également des classes similaires {...}Ref
pour les primitives, les volatiles et les variables, et similaires.
si le compilateur doit créer une fermeture qui met à jour une variable locale, il" met à niveau "la variable en final ObjectRef
(qui peut être passé à la fermeture dans son constructeur), et remplace les utilisations de cette variable par get
s et mises à jour par set
s, sur le ObjectRef
. Dans votre compilateur, vous pouvez mettre à jour les variables locales à chaque fois qu'elles sont passées par référence.
vous pourriez utiliser un truc similaire avec les attributs de l'objet . Supposons que Holder
implémente une interface ByRef
. Lorsque votre compilateur voit un attribut objet passer par référence, il peut créer une sous-classe anonyme de ByRef
qui lit et met à jour l'attribut objet dans ses méthodes get
et set
. Encore une fois, Scala fait quelque chose de similaire à cela pour les paramètres évalués paresseusement (comme les références, mais en lecture seule).
pour des points supplémentaires, vous pourrait étendre la technique aux propriétés Javabéennes et même Map
, List
et Array
éléments.
un effet secondaire de ceci est qu'au niveau de la JVM, vos méthodes ont des signatures inattendues. Si vous compilez une méthode avec la signature void doIt(ref String)
, au niveau du bytecode, vous finirez avec la signature void doIt(ByRef)
(vous pourriez vous attendre à ce que ce soit quelque chose comme void doIt(ByRef<String>)
, mais bien sûr les génériques utilisent le type erasure). Cela peut causer des problèmes avec méthode de surcharge, comme tous les paramètres by-ref compilent à la même signature.
Il peut être possible de le faire avec le bytecode de la manipulation, mais il ya des pièges, comme le fait que la JVM des demandes de permis de ré-utiliser des variables locales - donc au niveau du bytecode, il peut ne pas être clair si un paramètre est en cours de ré-attribuée, ou sa fente re-utilisé, si la demande a été compilé sans les symboles de débogage. En outre, le compilateur peut éluder aload
instructions s'il n'y a aucune possibilité qu'une valeur ait changé dans la méthode externe - si vous ne prenez pas de mesures pour éviter cela, les changements à votre variable de référence peuvent ne pas se refléter dans la méthode externe.
je pense que vous pouvez accomplir la plupart de ce que vous voulez en construisant un agent et en utilisant cglib.
beaucoup des exemples donnés ici peuvent fonctionner. Je recommande d'utiliser le modèle que vous avez proposé car il sera compilé avec le compilateur normal.
public void doSomething(@Ref String var)
puis dans les coulisses vous utilisez cglib pour réécrire les méthodes annotées, ce qui est facile. Vous devrez aussi réécrire l'interlocuteur, ce qui sera beaucoup plus compliqué dans cglib. javassist utilise plus une approche axée sur le "code source", et pourrait être mieux adapté pour réécrire les appelants.
pensez à la façon dont il pourrait être mis en œuvre avec un type primitif, dites int
. Java - la JVM, et pas seulement le langage-n'a pas de type "pointeur" vers une variable locale, sur le frame (method stack) ou l'operand stack. Sans cela, il n'est pas possible de vraiment passer par référence.
D'autres langues qui prennent en charge pass-by-reference utilisent des pointeurs (je crois, mais je ne vois pas d'autre possibilité). Les références C++ (comme int&
) sont des pointeurs déguisé.
j'ai pensé à créer un nouvel ensemble de classes qui étendent Number
, contenant int
, long
, etc. mais pas immuable. Cela pourrait donner l'effet de passer des primitives par référence - mais elles ne seront pas auto-boxe, et d'autres fonctionnalités pourraient ne pas fonctionner.
sans support dans la JVM, vous ne pouvez pas avoir de vrai pass-by-reference. Désolé, mais c'est ma compréhension.
BTW, il existe déjà plusieurs classes de type de référence (comme vous le souhaitez pour Holder). ThreadLocal<>
(qui a get()
et set()
), ou les Reference
Extenseurs, comme WeakReference
(qui je pense ont seulement get()
).
Edit:
Après avoir lu d'autres réponses, je suggérerais que ref
soit une forme d'auto-boxe. Ainsi:
class ReferenceHolder<T> {
T referrent;
static <T> ReferenceHolder<T> valueOf(T object) {
return new ReferenceHolder<T>(object);
}
ReferenceHolder(T object) { referrent = object; }
T get() { return referrent; }
void set(T value) { referrent = value; }
}
class RefTest {
static void main() {
String s = "Hello";
// This is how it is written...
change(s);
// but the compiler converts it to...
ReferenceHolder<String> $tmp = ReferenceHolder.valueOf(s);
change($tmp);
s = $tmp.get();
}
// This is how it is written...
static void change(ref Object s) {
s = "Goodbye"; // won't work
s = 17; // *Potential ClassCastException, but not here*
}
// but the compiler converts it tothe compiler treats it as:
static <T> void change(ReferenceHolder<T> obj) {
obj.set((T) "Goodbye"); // this works
obj.set((T) 17); // *Compiler can't really catch this*
}
}
mais voir où il est possible de mettre le mauvais type de dans le ReferenceHolder
? Si vous généralisez correctement, le compilateur peut être capable de prévenir parfois, mais comme vous voulez probablement que le nouveau code ressemble au code normal autant que possible, il y a la possibilité d'un CCEx avec chaque appel auto-ref.
Vous répondant à la question sur la façon d'étendre la langue de mon choix serait: - En utilisant divers supports techniques comme plusieurs autres réponses décrivent - Utilisez les annotations pour joindre les métadonnées concernant les arguments qui doivent être passés par référence et puis commencer à jongler avec une bibliothèque de manipulation de code octet, comme cglib afin de remplir vos idées en code octet lui-même.
bien que toute cette idée semble étrange.
il y a plusieurs façons d'écrire du code Java comme pass-by-reference, même dans les conventions standard pass-by-value.
une approche consiste à utiliser des variables statiques ou d'instance dont le champ d'application inclut une méthode particulière, au lieu de paramètres explicites. Les variables qui sont modifiées pourraient être inclus dans les commentaires, si vous voulez vraiment parler de leurs noms au début d'une méthode.
l'inconvénient avec ce approche est que la portée de ces variables devrait englober l'ensemble de la classe en question, plutôt que de la méthode. Si vous souhaitez restreindre les portées des variables plus précisément, vous pouvez toujours les modifier en utilisant les méthodes getter et setter plutôt que comme paramètres.
ayant travaillé à la fois avec Java et C / C++, Je ne pense pas que la rigidité supposée de Java à être pass-by-value seulement soit un problème--pour tout programmeur qui sait ce qui arrive à la variables, il y a des solutions de rechange raisonnables qui peuvent accomplir les mêmes choses fonctionnellement.
Java est (en fait) passage par référence. Lorsque la méthode est appelée, la référence(pointeur) de l'objet est passé et quand vous modifiez l'objet, vous pouvez voir la modification lorsque vous êtes de retour de la méthode. Le problème avec votre exemple est que java.lang.La corde est immuable.
et ce que vous réalisez réellement avec votre exemple est les paramètres de sortie.
Voici une version légèrement différente de Jeffrey Hantin:
public static void main(String[] args) {
StringBuilder variable = new StringBuilder("'previous String reference'");
passByReference(variable);
System.out.println(variable); // I want this to print 'new String reference'
}
public static void passByReference(StringBuilder someString) {
String nr = "'new String reference'";
someString.replace(0, nr.length() - 1, nr);
}