Que fait `someObject.new` en Java?

100

En Java, je viens de découvrir que le code suivant est légal:

KnockKnockServer newServer = new KnockKnockServer();                    
KnockKnockServer.receiver receive = newServer.new receiver(clientSocket);

Pour info, le receveur est juste une classe d'assistance avec la signature suivante:

public class receiver extends Thread {  /* code_inside */  }

Je n'ai jamais vu la XYZ.newnotation auparavant. Comment ça marche? Existe-t-il un moyen de coder cela de manière plus conventionnelle?

café
la source
7
Pour votre référence, classe interne .
Alvin Wong
1
De plus, j'avais cru que c'était newun opérateur dans de nombreuses langues. (Je pensais que vous pourriez également surcharger newen C ++?) La classe interne de Java est un peu étrange pour moi, cependant.
Alvin Wong
5
Il n'y a pas de questions idiotes sur StackOverflow!
Isaac Rabinovitch le
2
@IsaacRabinovitch - Il n'y a pas de questions stupides. Il y en a cependant beaucoup de stupides. (Et une réponse idiote occasionnelle aussi.)
Hot Licks
2
@HotLicks Et quelle est votre définition d'une question idiote? Vous êtes trop intelligent pour avoir besoin de le demander, je suppose. C'est bien que vous ayez une telle estime de soi.
Isaac Rabinovitch

Réponses:

120

C'est le moyen d'instancier une classe interne non statique depuis l'extérieur du corps de la classe conteneur, comme décrit dans la documentation Oracle .

Chaque instance de classe interne est associée à une instance de sa classe contenante. Lorsque vous newcréez une classe interne à partir de sa classe thisconteneur, elle utilise l' instance du conteneur par défaut:

public class Foo {
  int val;
  public Foo(int v) { val = v; }

  class Bar {
    public void printVal() {
      // this is the val belonging to our containing instance
      System.out.println(val);
    }
  }

  public Bar createBar() {
    return new Bar(); // equivalent of this.new Bar()
  }
}

Mais si vous souhaitez créer une instance de Bar en dehors de Foo, ou associer une nouvelle instance à une instance contenant autre que celle-ci, thisvous devez utiliser la notation de préfixe.

Foo f = new Foo(5);
Foo.Bar b = f.new Bar();
b.printVal(); // prints 5
Ian Roberts
la source
18
Et, comme vous pouvez le constater, cela peut être incroyablement déroutant. Idéalement, les classes internes doivent être des détails d'implémentation de la classe externe et ne pas être exposées au monde extérieur.
Eric Jablow
10
@EricJablow en effet, c'est l'un de ces bits de syntaxe qui doit exister pour que la spécification reste cohérente, mais 99,9999% du temps, vous n'avez pas réellement besoin de l'utiliser. Si des étrangers ont vraiment besoin de créer des instances Bar, je fournirais une méthode d'usine sur Foo plutôt que de les utiliser f.new.
Ian Roberts
Corrigez-moi si je me trompe, mais si le publicniveau d'accès KnockKnockServer.receiverétait défini, privateil serait impossible de l'instancier de cette manière, non? Pour étendre le commentaire de @EricJablow, les classes internes doivent généralement toujours utiliser par défaut un privateniveau d'accès.
Andrew Bissell
1
@AndrewBissell oui, mais il serait également impossible de faire référence à la receiverclasse de l'extérieur. Si je le concevais, j'aurais probablement la classe publique mais son constructeur protégé ou package-private, et j'aurais une méthode KnockKnockServerpour créer des instances de récepteur.
Ian Roberts
1
@emory Je ne dis pas cela, je sais qu'il peut y avoir des raisons parfaitement valables de vouloir rendre publique une classe interne et renvoyer des instances de la classe interne à partir de méthodes externes, mais j'aurais tendance à concevoir mon code de telle sorte que " outsiders "n'ont pas besoin de construire des instances de la classe interne directement en utilisant x.new.
Ian Roberts
18

Jetez un œil à cet exemple:

public class Test {

    class TestInner{

    }

    public TestInner method(){
        return new TestInner();
    }

    public static void main(String[] args) throws Exception{
        Test t = new Test();
        Test.TestInner ti = t.new TestInner();
    }
}

En utilisant javap, nous pouvons afficher les instructions générées pour ce code

Méthode principale:

public static void main(java.lang.String[])   throws java.lang.Exception;
  Code:
   0:   new     #2; //class Test
   3:   dup
   4:   invokespecial   #3; //Method "<init>":()V
   7:   astore_1
   8:   new     #4; //class Test$TestInner
   11:  dup
   12:  aload_1
   13:  dup
   14:  invokevirtual   #5; //Method java/lang/Object.getClass:()Ljava/lang/Class;
   17:  pop
   18:  invokespecial   #6; //Method Test$TestInner."<init>":(LTest;)V
   21:  astore_2
   22:  return
}

Constructeur de classe interne:

Test$TestInner(Test);
  Code:
   0:   aload_0
   1:   aload_1
   2:   putfield        #1; //Field this$0:LTest;
   5:   aload_0
   6:   invokespecial   #2; //Method java/lang/Object."<init>":()V
   9:   return

}

Tout est simple - lors de l'appel du constructeur TestInner, java passe l'instance de test comme premier argument main: 12 . Ne pas regarder cela TestInner devrait avoir un constructeur sans argument. TestInner à son tour enregistre simplement la référence à l'objet parent, Test $ TestInner: 2 . Lorsque vous appelez le constructeur de classe interne à partir d'une méthode d'instance, la référence à l'objet parent est transmise automatiquement, vous n'avez donc pas à le spécifier. En fait, cela passe à chaque fois, mais lors de l'invocation de l'extérieur, il doit être passé explicitement.

t.new TestInner(); - est juste un moyen de spécifier le premier argument caché du constructeur TestInner, pas un type

method () est égal à:

public TestInner method(){
    return this.new TestInner();
}

TestInner est égal à:

class TestInner{
    private Test this$0;

    TestInner(Test parent){
        this.this$0 = parent;
    }
}
Mikhail
la source
7

Lorsque des classes internes ont été ajoutées à Java dans la version 1.1 du langage, elles ont été initialement définies comme une transformation en code compatible 1.0. Si vous regardez un exemple de cette transformation, je pense que cela rendra beaucoup plus clair comment une classe interne fonctionne réellement.

Considérez le code de la réponse d'Ian Roberts:

public class Foo {
  int val;
  public Foo(int v) { val = v; }

  class Bar {
    public void printVal() {
      System.out.println(val);
    }
  }

  public Bar createBar() {
    return new Bar();
  }
}

Une fois transformée en code compatible 1.0, cette classe interne Bardeviendrait quelque chose comme ceci:

class Foo$Bar {
  private Foo this$0;

  Foo$Bar(Foo outerThis) {
    this.this$0 = outerThis;
  }

  public void printVal() {
    System.out.println(this$0.val);
  }
}

Le nom de la classe interne est précédé du nom de la classe externe afin de le rendre unique. Un this$0membre privé masqué est ajouté qui contient une copie de l'externe this. Et un constructeur caché est créé pour initialiser ce membre.

Et si vous regardez la createBarméthode, elle serait transformée en quelque chose comme ceci:

public Foo$Bar createBar() {
  return new Foo$Bar(this);
}

Voyons donc ce qui se passe lorsque vous exécutez le code suivant.

Foo f = new Foo(5);
Foo.Bar b = f.createBar();                               
b.printVal();

Tout d'abord, nous instancions une instance de Fooet initialisons le valmembre à 5 (ie f.val = 5).

Ensuite, nous appelons f.createBar(), qui instancie une instance de Foo$Baret initialise le this$0membre à la valeur de thispassé de createBar(ie b.this$0 = f).

Enfin, nous appelons b.printVal()qui essaie d'imprimer b.this$0.valce f.valqui vaut 5.

Maintenant, c'était une instanciation régulière d'une classe interne. Regardons ce qui se passe lors de l'instanciation Barde l'extérieur Foo.

Foo f = new Foo(5);
Foo.Bar b = f.new Bar();
b.printVal();

En appliquant à nouveau notre transformation 1.0, cette deuxième ligne deviendrait quelque chose comme ceci:

Foo$Bar b = new Foo$Bar(f);

C'est presque identique à l' f.createBar()appel. Encore une fois, nous instancions une instance de Foo$Baret initialisons le this$0membre à f. Encore une fois, b.this$0 = f.

Et encore une fois, lorsque vous appelez b.printVal(), vous imprimez b.thi$0.valce f.valqui est de 5.

La chose clé à retenir est que la classe interne a un membre masqué contenant une copie de thisde la classe externe. Lorsque vous instanciez une classe interne à partir de la classe externe, elle s'est implicitement initialisée avec la valeur actuelle de this. Lorsque vous instanciez la classe interne depuis l'extérieur de la classe externe, vous spécifiez explicitement l'instance de la classe externe à utiliser, via le préfixe du newmot - clé.

James Holderness
la source
4

Pensez new receiverà un seul jeton. Un peu comme un nom de fonction avec un espace dedans.

Bien sûr, la classe KnockKnockServern'a pas littéralement une fonction nommée new receiver, mais je suppose que la syntaxe est censée le suggérer. Cela donne l'impression que vous appelez une fonction qui crée une nouvelle instance d' KnockKnockServer.receiverutilisation d'une instance particulière de KnockKnockServerpour tout accès à la classe englobante.

David Z
la source
Merci, oui - cela m'aide maintenant à penser new receivercomme un jeton! Merci beaucoup!
Café du
1

Ombrage

Si une déclaration d'un type (telle qu'une variable membre ou un nom de paramètre) dans une portée particulière (telle qu'une classe interne ou une définition de méthode) a le même nom qu'une autre déclaration dans la portée englobante, alors la déclaration masque la déclaration de la portée englobante. Vous ne pouvez pas faire référence à une déclaration masquée par son seul nom. L'exemple suivant, ShadowTest, illustre ceci:

public class ShadowTest {

    public int x = 0;

    class FirstLevel {

        public int x = 1;

        void methodInFirstLevel(int x) {
            System.out.println("x = " + x);
            System.out.println("this.x = " + this.x);
            System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);
        }
    }

    public static void main(String... args) {
        ShadowTest st = new ShadowTest();
        ShadowTest.FirstLevel fl = st.new FirstLevel();
        fl.methodInFirstLevel(23);
    }
}

Voici la sortie de cet exemple:

x = 23
this.x = 1
ShadowTest.this.x = 0

Cet exemple définit trois variables nommées x: la variable membre de la classe ShadowTest, la variable membre de la classe interne FirstLevel et le paramètre de la méthode methodInFirstLevel. La variable x définie comme paramètre de la méthode methodInFirstLevel masque la variable de la classe interne FirstLevel. Par conséquent, lorsque vous utilisez la variable x dans la méthode methodInFirstLevel, elle fait référence au paramètre de méthode. Pour faire référence à la variable membre de la classe interne FirstLevel, utilisez le mot clé this pour représenter la portée englobante:

System.out.println("this.x = " + this.x);

Faites référence aux variables membres qui entourent de plus grandes étendues par le nom de la classe à laquelle elles appartiennent. Par exemple, l'instruction suivante accède à la variable membre de la classe ShadowTest à partir de la méthode methodInFirstLevel:

System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);

Reportez-vous à la documentation

JavaTechnique
la source