web-dev-qa-db-fra.com

Java "Le dernier champ vide n'a peut-être pas été initialisé" Anonymous Interface vs Lambda Expression

J'ai récemment rencontré le message d'erreur "Le champ final vide obj n'a peut-être pas été initialisé".

C'est généralement le cas si vous essayez de faire référence à un champ qui n'est peut-être pas encore affecté à une valeur. Exemple de classe:

public class Foo {
    private final Object obj;
    public Foo() {
        obj.toString(); // error           (1)
        obj = new Object();
        obj.toString(); // just fine       (2)
    }
}

J'utilise Eclipse. Dans la ligne (1) je reçois l'erreur, dans la ligne (2), tout fonctionne. Jusqu'à présent, cela a du sens.

Ensuite, j'essaie d'accéder à obj dans une interface anonyme que je crée dans le constructeur.

public class Foo {
    private Object obj;
    public Foo() {
        Runnable run = new Runnable() {
            public void run() {
                obj.toString(); // works fine
            }
        };
        obj = new Object();
        obj.toString(); // works too
    }
}

Cela fonctionne aussi, car je n’ai pas accès à obj au moment où je crée l’interface. Je pourrais également transmettre mon instance à un autre endroit, puis initialiser l'objet obj, puis exécuter mon interface. (Cependant, il serait approprié de vérifier null avant de l’utiliser). A toujours du sens.

Mais maintenant, je réduis la création de mon instance Runnable à la version burger-arrow en utilisant une expression lambda :

public class Foo {
    private final Object obj;
    public Foo() {
        Runnable run = () -> {
            obj.toString(); // error
        };
        obj = new Object();
        obj.toString(); // works again
    }
}

Et voici où je ne peux plus suivre. Ici, je reçois à nouveau l'avertissement. Je suis conscient que le compilateur ne gère pas les expressions lambda comme des initialisations habituelles, il ne le "remplace pas par la version longue". Cependant, pourquoi cela affecte-t-il le fait que je n'exécute pas la partie de code dans ma méthode run() au moment de la création de l'objet Runnable? Je suis encore capable de faire l'initialisation avant j'appelle run(). Donc, techniquement, il est possible de ne pas rencontrer de NullPointerException ici. (Même s’il serait préférable de vérifier ici null. Mais cette convention est un autre sujet.)

Quelle est l'erreur que je fais? Qu'est-ce qui est traité de manière si différente à propos de lambda que cela influence mon utilisation de l'objet de cette manière?

Je vous remercie pour toutes explications supplémentaires.

20
Steffen T

Je ne peux pas reproduire l'erreur pour votre dernier cas avec le compilateur d'Eclipse. 

Cependant, le raisonnement du compilateur Oracle que je peux imaginer est le suivant: dans un lambda, la valeur de obj doit être capturée au moment de la déclaration. C'est-à-dire qu'il doit être initialisé lorsqu'il est déclaré à l'intérieur du corps lambda.

Mais, dans ce cas, Java devrait capturer la valeur de l'instance Foo plutôt que obj. Il peut ensuite accéder à obj via la référence d'objet Foo (initialisée) et appeler sa méthode. Voici comment le compilateur Eclipse compile votre morceau de code.

Ceci est fait allusion à la spécification, ici :

Le moment choisi pour l'évaluation de l'expression de référence de méthode est plus complexe que celui des expressions lambda (§15.27.4). Lorsqu'une expression de référence de méthode A une expression (plutôt qu'un type) précédant le séparateur :: , Cette sous-expression est évaluée immédiatement. Le résultat de l'évaluation de Est enregistré jusqu'à ce que la méthode du type d'interface fonctionnelle Correspondante soit appelée ; à ce stade, le résultat est utilisé comme référence cible pour l'invocation. Cela signifie que l'expression Précédant le séparateur :: est évaluée uniquement lorsque le programme Rencontre l'expression de référence de la méthode et n'est pas réévaluée lors de Invocations ultérieures sur le type d'interface fonctionnelle. .

Une chose semblable se passe pour 

Object obj = new Object(); // imagine some local variable
Runnable run = () -> {
    obj.toString(); 
};

Imagine obj est une variable locale. Lorsque le code de l'expression lambda est exécuté, obj est évalué et génère une référence. Cette référence est stockée dans un champ de l'instance Runnable créée. Lorsque run.run() est appelé, l'instance utilise la valeur de référence stockée.

Cela ne peut pas arriver si obj n'est pas initialisé`. Par exemple

Object obj; // imagine some local variable
Runnable run = () -> {
    obj.toString(); // error
};

Le lambda ne peut pas capturer la valeur de obj, car il n'a pas encore de valeur. C'est effectivement équivalent à

final Object anonymous = obj; // won't work if obj isn't initialized
Runnable run = new AnonymousRunnable(anonymous);
...
class AnonymousRunnable implements Runnable {
    public AnonymousRunnable(Object val) {
        this.someHiddenRef = val;
    }
    private final Object someHiddenRef;
    public void run() {
        someHiddenRef.toString(); 
    }
}

Voici comment le compilateur Oracle se comporte actuellement pour votre extrait de code.

Cependant, le compilateur Eclipse ne capture pas la valeur de obj, mais la valeur de this (l'instance Foo). C'est effectivement équivalent à

final Foo anonymous = Foo.this; // you're in the Foo constructor so this is valid reference to a Foo instance
Runnable run = new AnonymousRunnable(anonymous);
...
class AnonymousRunnable implements Runnable {
    public AnonymousRunnable(Foo foo) {
        this.someHiddenRef = foo;
    }
    private final Foo someHiddenFoo;
    public void run() {
        someHiddenFoo.obj.toString(); 
    }
}

Ce qui est correct, car vous supposez que l'instance Foo est complètement initialisée au moment où run est invoqué.

10

Vous pouvez contourner le problème en 

        Runnable run = () -> {
            (this).obj.toString(); 
        };

Cela a été discuté lors du développement de lambda. Fondamentalement, le corps de lambda est traité comme un code local lors de analyse d’affectation définitive .

Citant Dan Smith, spec tzar, https://bugs.openjdk.Java.net/browse/JDK-8024809

Les règles prévoient deux exceptions: ... ii) une utilisation à l'intérieur d'une classe anonyme est acceptable. Il n'y a pas d'exception pour une utilisation dans une expression lambda

Franchement, d'autres personnes et moi avons pensé que la décision était mauvaise. Le lambda ne capture que this, pas obj. Ce cas aurait dû être traité de la même manière qu'une classe anonyme. Le comportement actuel est problématique pour de nombreux cas d'utilisation légitimes. Eh bien, vous pouvez toujours le contourner en utilisant l’astuce ci-dessus, heureusement analyse d’assignation définitive n’est pas trop intelligent et nous pouvons le duper.

15
ZhongYu

Vous pouvez utiliser une méthode utilitaire pour forcer la capture de this uniquement. Cela fonctionne aussi avec Java 9.

public static <T> T r(T object) {
    return object;
}

Maintenant, vous pouvez réécrire votre lambda comme ceci:

Runnable run = () -> r(this).obj.toString();
0
Dávid Horváth