¿Qué hace `someObject.new` en Java?

100

En Java, acabo de descubrir que el siguiente código es legal:

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

FYI, el receptor es solo una clase auxiliar con la siguiente firma:

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

Nunca antes había visto la XYZ.newnotación. ¿Cómo funciona? ¿Hay alguna forma de codificar eso de manera más convencional?

café
fuente
7
Para su referencia, clase interna .
Alvin Wong
1
Además, había creído que newera un operador en muchos idiomas. (¿Pensé que también podrías sobrecargar newen C ++?) Sin embargo, la clase interna de Java es un poco extraña para mí.
Alvin Wong
5
¡No hay preguntas tontas en StackOverflow!
Isaac Rabinovitch
2
@IsaacRabinovitch - No hay preguntas estúpidas. Sin embargo, hay muchos tontos. (Y también una respuesta tonta ocasional.)
Hot Licks
2
@HotLicks ¿Y cuál es tu definición de una pregunta tonta? Uno que eres demasiado inteligente para tener que preguntar, supongo. Qué bueno que tengas tanta autoestima.
Isaac Rabinovitch

Respuestas:

120

Es la forma de crear una instancia de una clase interna no estática desde fuera del cuerpo de la clase contenedora, como se describe en los documentos de Oracle .

Cada instancia de clase interna está asociada con una instancia de su clase contenedora. Cuando tiene newuna clase interna desde dentro de su clase contenedora, usa la thisinstancia del contenedor de forma predeterminada:

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()
  }
}

Pero si desea crear una instancia de Bar fuera de Foo, o asociar una nueva instancia con una instancia contenedora que no sea thisentonces, debe usar la notación de prefijo.

Foo f = new Foo(5);
Foo.Bar b = f.new Bar();
b.printVal(); // prints 5
Ian Roberts
fuente
18
Y, como puede ver, esto puede ser increíblemente confuso. Idealmente, las clases internas deberían ser detalles de implementación de la clase externa y no estar expuestas al mundo exterior.
Eric Jablow
10
@EricJablow de hecho, es uno de esos bits de sintaxis que tiene que existir para mantener la especificación consistente, pero el 99,9999% de las veces no es necesario usarlo. Si los forasteros realmente necesitan crear instancias de Bar, entonces proporcionaría un método de fábrica en Foo en lugar de que lo usen f.new.
Ian Roberts
Corríjame si me equivoco, pero si se hiciera publicel nivel de acceso en , sería imposible crear una instancia de esta manera, ¿verdad? Para extender el comentario de @EricJablow, las clases internas generalmente siempre deben tener un nivel de acceso predeterminado . KnockKnockServer.receiverprivateprivate
Andrew Bissell
1
@AndrewBissell sí, pero también sería imposible referirse a la receiverclase desde fuera. Si lo estuviera diseñando, probablemente tendría la clase public pero su constructor protegido o package-private, y tendría un método KnockKnockServerpara crear instancias de receptor.
Ian Roberts
1
@emory No estoy diciendo eso, sé que puede haber razones perfectamente válidas para querer hacer pública una clase interna y devolver instancias de la clase interna a partir de métodos de la externa, pero tendería a diseñar mi código de tal manera que " los forasteros "no necesitan construir instancias de la clase interna directamente usando x.new.
Ian Roberts
18

Eche un vistazo a este ejemplo:

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();
    }
}

Usando javap podemos ver las instrucciones generadas para este código

Método principal:

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
}

Constructor de clases internas:

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

}

Todo es simple: al invocar el constructor TestInner, java pasa la instancia de prueba como primer argumento principal: 12 . Sin mirar ese TestInner debería tener un constructor sin argumentos. TestInner a su vez solo guarda la referencia al objeto principal, Test $ TestInner: 2 . Cuando invoca el constructor de clase interna desde un método de instancia, la referencia al objeto principal se pasa automáticamente, por lo que no tiene que especificarla. En realidad, pasa cada vez, pero cuando se invoca desde fuera debe pasarse explícitamente.

t.new TestInner(); - es solo una forma de especificar el primer argumento oculto para el constructor TestInner, no un tipo

método () es igual a:

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

TestInner es igual a:

class TestInner{
    private Test this$0;

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

Cuando se agregaron clases internas a Java en la versión 1.1 del lenguaje, originalmente se definieron como una transformación a un código compatible con 1.0. Si miras un ejemplo de esta transformación, creo que aclarará mucho cómo funciona realmente una clase interna.

Considere el código de la respuesta de 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();
  }
}

Cuando se transforma a un código compatible con 1.0, esa clase interna Barse convertiría en algo como esto:

class Foo$Bar {
  private Foo this$0;

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

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

El nombre de la clase interna tiene como prefijo el nombre de la clase externa para que sea único. Se this$0agrega un miembro privado oculto que contiene una copia del externo this. Y se crea un constructor oculto para inicializar ese miembro.

Y si miras el createBarmétodo, se transformaría en algo como esto:

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

Entonces, veamos qué sucede cuando ejecuta el siguiente código.

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

Primero instanciamos una instancia de Fooe inicializamos el valmiembro en 5 (es decir f.val = 5).

A continuación, llamamos f.createBar(), que instancia una instancia de Foo$Bare inicializa el this$0miembro con el valor de thispasado de createBar(es decir b.this$0 = f).

Finalmente llamamos a b.printVal()cuál intenta imprimir b.this$0.valcuál es f.valcuál es 5.

Ahora que era una instanciación regular de una clase interna. Veamos qué sucede cuando se crea una instancia Bardesde el exterior Foo.

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

Aplicando nuestra transformación 1.0 nuevamente, esa segunda línea se convertiría en algo como esto:

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

Esto es casi idéntico a la f.createBar()llamada. Nuevamente, estamos creando una instancia de Foo$Bare inicializando el this$0miembro en f. Así que de nuevo b.this$0 = f.

Y nuevamente cuando llamas b.printVal(), estás imprimiendo b.thi$0.valcuál es f.valcuál es 5.

La clave para recordar es que la clase interna tiene un miembro oculto que tiene una copia de thisla clase externa. Cuando crea una instancia de una clase interna desde dentro de la clase externa, se inicializa implícitamente con el valor actual de this. Cuando crea una instancia de la clase interna desde fuera de la clase externa, especifica explícitamente qué instancia de la clase externa usar, a través del prefijo de la newpalabra clave.

James Holderness
fuente
4

Piense new receiveren una sola ficha. Algo así como el nombre de una función con un espacio en él.

Por supuesto, la clase KnockKnockServerno tiene literalmente una función nombrada new receiver, pero supongo que la sintaxis debe sugerir eso. Está destinado a que parezca que estás llamando a una función que crea una nueva instancia de KnockKnockServer.receiveruso de una instancia particular de KnockKnockServerpara cualquier acceso a la clase adjunta.

David Z
fuente
Gracias, sí, ahora me ayuda a pensar new receiveren una sola ficha. ¡muchas gracias!
Café
1

Sombreado

Si una declaración de un tipo (como una variable miembro o un nombre de parámetro) en un ámbito particular (como una clase interna o una definición de método) tiene el mismo nombre que otra declaración en el ámbito adjunto, entonces la declaración ensombrece la declaración del alcance adjunto. No puede referirse a una declaración sombreada solo por su nombre. El siguiente ejemplo, ShadowTest, demuestra esto:

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);
    }
}

El siguiente es el resultado de este ejemplo:

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

Este ejemplo define tres variables llamadas x: la variable miembro de la clase ShadowTest, la variable miembro de la clase interna FirstLevel y el parámetro en el método methodInFirstLevel. La variable x definida como parámetro del método methodInFirstLevel sombrea la variable de la clase interna FirstLevel. En consecuencia, cuando usa la variable x en el método methodInFirstLevel, se refiere al parámetro del método. Para hacer referencia a la variable miembro de la clase interna FirstLevel, use la palabra clave this para representar el ámbito adjunto:

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

Consulte las variables miembro que encierran ámbitos más grandes por el nombre de clase a la que pertenecen. Por ejemplo, la siguiente instrucción accede a la variable miembro de la clase ShadowTest desde el método methodInFirstLevel:

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

Consulte los documentos

JavaTechnical
fuente