O que `someObject.new` faz em Java?

100

Em Java, acabei de descobrir que o seguinte código é válido:

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

Para sua informação, o receptor é apenas uma classe auxiliar com a seguinte assinatura:

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

Eu nunca vi a XYZ.newnotação antes. Como isso funciona? Existe alguma maneira de codificar isso de forma mais convencional?

Café
fonte
7
Para sua referência, classe interna .
Alvin Wong
1
Além disso, eu acreditava que newera um operador em vários idiomas. (Achei que você também pode sobrecarregar newem C ++?) A classe interna de Java é um pouco estranha para mim, no entanto.
Alvin Wong,
5
Não há perguntas tolas no StackOverflow!
Isaac Rabinovitch
2
@IsaacRabinovitch - Não há perguntas estúpidas. No entanto, existem muitos tolos. (E uma ocasional resposta boba também.)
Hot Licks
2
@HotLicks E qual é a sua definição de pergunta boba? Um que você é inteligente demais para perguntar, suponho. Que bom que você tem tanta auto-estima.
Isaac Rabinovitch

Respostas:

120

É a maneira de instanciar uma classe interna não estática de fora do corpo da classe, conforme descrito nos documentos do Oracle .

Cada instância de classe interna está associada a uma instância de sua classe contida. Quando você usa newuma classe interna de dentro da classe que a contém, ela usa a thisinstância do contêiner por padrão:

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

Mas se você deseja criar uma instância de Bar fora de Foo, ou associar uma nova instância a uma instância contida diferente this, você deve usar a notação de prefixo.

Foo f = new Foo(5);
Foo.Bar b = f.new Bar();
b.printVal(); // prints 5
Ian Roberts
fonte
18
E, como você pode ver, isso pode ser incrivelmente confuso. O ideal é que as classes internas sejam detalhes de implementação da classe externa e não sejam expostas ao mundo externo.
Eric Jablow
10
@EricJablow de fato, é um daqueles bits de sintaxe que deve existir para manter a especificação consistente, mas 99,9999% do tempo você realmente não precisa usá-lo. Se pessoas de fora realmente precisarem criar instâncias de Bar, eu forneceria um método de fábrica no Foo em vez de fazer com que usassem f.new.
Ian Roberts,
Corrija-me se eu estiver errado, mas se o publicnível de acesso ativado KnockKnockServer.receiverfor feito private, seria impossível instanciar dessa forma, certo? Para estender o comentário de @EricJablow, as classes internas geralmente devem sempre usar como padrão um privatenível de acesso.
Andrew Bissell
1
@AndrewBissell sim, mas também seria impossível referir-se à receiverclasse de fora. Se eu o estivesse projetando, provavelmente teria a classe pública, mas seu construtor protegido ou privado de pacote, e teria um método KnockKnockServerpara criar instâncias de receptor.
Ian Roberts,
1
@ memória Não estou dizendo isso, eu sei que pode haver razões perfeitamente válidas para querer tornar pública uma classe interna e retornar instâncias da classe interna de métodos externos, mas eu tenderia a projetar meu código de forma que " outsiders "não precisam construir instâncias da classe interna usando diretamente x.new.
Ian Roberts,
18

Dê uma olhada neste exemplo:

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 as instruções geradas 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
}

Construtor de classe interna:

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

}

Tudo é simples - ao invocar o construtor TestInner, java passa a instância de teste como um primeiro argumento main: 12 . Não olhar para aquele TestInner deve ter um construtor sem argumento. O TestInner, por sua vez, salva apenas a referência ao objeto pai, Test $ TestInner: 2 . Quando você invoca o construtor de classe interna a partir de um método de instância, a referência ao objeto pai é passada automaticamente, portanto, você não precisa especificá-lo. Na verdade, ele passa todas as vezes, mas ao invocar de fora, deve ser passado explicitamente.

t.new TestInner(); - é apenas uma maneira de especificar o primeiro argumento oculto para o construtor TestInner, não um tipo

método () é igual a:

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

TestInner é igual a:

class TestInner{
    private Test this$0;

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

Quando as classes internas foram adicionadas ao Java na versão 1.1 da linguagem, elas foram originalmente definidas como uma transformação para o código compatível com 1.0. Se você olhar um exemplo dessa transformação, acho que ficará muito mais claro como uma classe interna realmente funciona.

Considere o código da resposta 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();
  }
}

Quando transformada em código compatível com 1.0, essa classe interna Barse tornaria algo assim:

class Foo$Bar {
  private Foo this$0;

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

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

O nome da classe interna é prefixado com o nome da classe externa para torná-lo único. Um this$0membro privado oculto é adicionado, contendo uma cópia dothis . E um construtor oculto é criado para inicializar esse membro.

E se você olhar para o createBarmétodo, ele seria transformado em algo assim:

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

Então, vamos ver o que acontece quando você executa o código a seguir.

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

Primeiro, instanciamos uma instância de Fooe inicializamos o valmembro em 5 (ou sejaf.val = 5 ).

Em seguida, chamamos f.createBar(), que instancia uma instância de Foo$Bare inicializa o this$0membro com o valor de thispassado de createBar(ou seja,b.this$0 = f ).

Finalmente chamamos o b.printVal()que tenta imprimir o b.this$0.valque éf.val que é 5.

Essa foi uma instanciação regular de uma classe interna. Vejamos o que acontece ao instanciar Barde fora Foo.

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

Aplicando nossa transformação 1.0 novamente, a segunda linha se tornaria algo assim:

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

Isso é quase idêntico à f.createBar()chamada. Novamente, estamos instanciando uma instância de Foo$Bare inicializando o this$0membro para f. Então novamente,b.this$0 = f ,.

E novamente quando você ligar b.printVal(), você está imprimindo b.thi$0.valo que é f.valque é 5.

O principal a ser lembrado é que a classe interna tem um membro oculto que contém uma cópia da thisclasse externa. Quando você instancia uma classe interna de dentro da classe externa, ela é inicializada implicitamente com o valor atual de this. Ao instanciar a classe interna de fora da classe externa, você especifica explicitamente qual instância da classe externa usar, por meio do prefixo da newpalavra - chave.

James Holderness
fonte
4

Pense new receiverem um único token. É como o nome de uma função com um espaço.

Claro, a classe KnockKnockServernão tem literalmente uma função chamada new receiver, mas estou supondo que a sintaxe pretende sugerir isso. Deve parecer que você está chamando uma função que cria uma nova instância de KnockKnockServer.receiveruso de uma instância específica de KnockKnockServerpara qualquer acesso à classe envolvente.

David Z
fonte
Obrigado, sim - agora me ajuda pensar nisso new receivercomo um token! muito obrigado!
Café
1

Sombreamento

Se uma declaração de um tipo (como uma variável de membro ou um nome de parâmetro) em um escopo específico (como uma classe interna ou uma definição de método) tem o mesmo nome de outra declaração no escopo envolvente, então a declaração obscurece a declaração do escopo envolvente. Você não pode se referir a uma declaração obscura apenas pelo seu nome. O exemplo a seguir, ShadowTest, demonstra isso:

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

A seguir está a saída deste exemplo:

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

Este exemplo define três variáveis ​​denominadas x: A variável de membro da classe ShadowTest, a variável de membro da classe interna FirstLevel e o parâmetro no método methodInFirstLevel. A variável x definida como um parâmetro do método methodInFirstLevel obscurece a variável da classe interna FirstLevel. Conseqüentemente, quando você usa a variável x no método methodInFirstLevel, ela se refere ao parâmetro do método. Para se referir à variável de membro da classe interna FirstLevel, use a palavra-chave this para representar o escopo envolvente:

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

Consulte as variáveis ​​de membro que abrangem escopos maiores pelo nome da classe à qual pertencem. Por exemplo, a instrução a seguir acessa a variável de membro da classe ShadowTest do método methodInFirstLevel:

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

Consulte a documentação

JavaTechnical
fonte