Tipos anônimos em Java, existe?

No Java por curiosidade estava lendo esse artigo:

Gostaria de saber além da atribuição por inferencia, onde os tipo de dados vem de sua atribuição também se consegue no Java 10 criar tipos dinâmicos como em outras linguagens de programação, exemplo em C# eu posso criar um tipo em tempo de execução, exemplo:

var people = new {
   Id = 1, 
   Name = "Guj"
};

e também retorno de dados vindo de banco de dados pra expor JSON por exemplo ou para o escopo da programação para construir alguma solução?
Em Java 10 também tem esse recurso de criar um dado em tempo de execução, sem que tenha um tipo definido na programação de um determinado projeto?

1 curtida

O Java já possuia classes internas anônimas, mas com o recurso de inferência de tipo através do var, as classes anônimas ficaram bem mais flexíveis. Por exemplo, agora é possível fazer:

public static void main(String[] args) {
    var abc = new Object() {
        String m = "objeto abc";
    };
    System.out.println(abc.m);
}

Toda classe anônima no Java precisa extender explicitamente alguma classe (no caso do exemplo Object) ou implementar uma interface, então antes sem o var você seria obrigado nesse caso a declarar a variável como sendo do tipo Object.

Object abc = new Object() {

Então de fora você não teria como acessar a variável m, já que estaria acessando um objeto do tipo Object, que a princípio não possui essa variável.

5 curtidas

Como já explicado acima, existe algo similar, embora a sintaxe não seja tão sucinta quanto é em C#.

Um caso de uso é criar estes tipos anônimos dentro de lambdas. Por exemplo, supondo que tenho uma classe User com trocentos campos:

public class User {
    private int id;
    private String name;
    private LocalDate dateOfBirth;
    // mais trocentos campos e respectivos getters/setters, etc
}

E eu quero filtrar alguns usuários, mas também quero que o resultado seja uma lista com apenas o id e nome, então eu poderia fazer algo do tipo:

List<User> users = // lista com vários usuários
// filteredUsers é uma lista cujos elementos são do tipo anônimo que vou criar abaixo
var filteredUsers = users.stream()
    .map(u -> new Object() { // cria um tipo anônimo, contendo apenas o id e nome do User
        int codigo = u.getId();
        String nome = u.getName();
    })
    .filter(u -> u.codigo < 3) // aqui posso usar o campo "codigo" do tipo anônimo criado acima
    .collect(Collectors.toList());
filteredUsers.forEach(u -> System.out.println(u.codigo + " - " + u.nome));

Atenção, vale lembrar que o código abaixo já funcionava no Java 8 (exceto a última linha):

// no Java 8 já dava para fazer isso:
List<Object> filteredUsers = users.stream()
    .map(u -> new Object() { // cria um tipo anônimo, contendo apenas o id e nome do User
        int codigo = u.getId();
        String nome = u.getName();
    })
    .filter(u -> u.codigo < 3) // em um lambda posso usar o campo do tipo anônimo
    .collect(Collectors.toList());
// imprimir a lista diretamente funciona
System.out.println(filteredUsers);

// Mas acessar os campos dá erro (só funciona a partir do Java 10):
filteredUsers.forEach(u -> System.out.println(u.codigo + " - " + u.nome)); // não compila em Java < 10

Ou seja, dentro dos lambdas era possível acessar os campos do tipo anônimo, mesmo no Java 8 (como eu fiz com u.codigo dentro do filter).
Mas uma vez criada a lista, não é mais possível acessá-los (ou seja, o filteredUsers.forEach(u -> System.out.println(u.codigo + " - " + u.nome)) nem compila em Java < 10, porque não dava para acessar os campos do tipo anônimo).

Além disso, o tipo da lista deve ser List<Object>, pois antes do Java 10 ainda não existia o var.


Na verdade, no Java 8 dá para obter os campos via reflection, mas está longe de ser o ideal:

List<Object> filteredUsers = // igual ao código anterior
Object first = filteredUsers.get(0);
Class<? extends Object> c = first.getClass();
for (Field f: c.getDeclaredFields()) {
    Object value = f.get(first);
    System.out.println(f.getName() + " = " + value + " - " + value.getClass());
}

E agora um ponto importante → o código acima imprimiu três campos (não deveriam ser dois?):

codigo = 1 - class java.lang.Integer
nome = Fulano de Tal - class java.lang.String
val$u = test.User@378bf509 - class test.User

E esse é um detalhe importante ao se criar tipos anônimos em lambdas: ao se criar o tipo, este captura as referências dos objetos usados na sua inicialização.

Por isso quando eu fiz .map(u -> new Object() { etc..., o objeto u foi capturado, e o tipo anônimo mantém uma referência para ele (no caso, o campo val$u). Se no código acima, dentro do for, compararmos value == users.get(0), veremos que o resultado é true, pois se trata da mesma instância.
Se não usado com cautela, este tipo de situação pode causar vazamentos de memória.

Para evitar isso, nas versões mais novas da linguagem (se não me engano a partir da 16) pode-se usar record no lugar do tipo anônimo. É mais verboso, mas pelo menos não há o risco de vazar memória, já que ele não guardará a referência ao objeto u:

var filteredUsers = users.stream()
    .map(u -> { // cria um record, contendo apenas o id e nome do User
        record CodigoNome(int codigo, String nome) {}
        return new CodigoNome(u.getId(), u.getName());
    })
    .filter(u -> u.codigo() < 3)
    .collect(Collectors.toList());

filteredUsers.forEach(u -> System.out.println(u.codigo() + " - " + u.nome()));
System.out.println(filteredUsers);

// obtendo os campos via reflection só para ver se ainda tem três
Object first = filteredUsers.get(0);
Class<? extends Object> c = first.getClass();
for (Field f : c.getDeclaredFields()) {
    Object value = f.get(first);
    System.out.println(f.getName() + " = " + value + " - " + value.getClass());
}

Coloquei o trecho com reflection só para vermos que agora não tem mais três campos (a referência ao objeto u não é guardada pelo record). Testei com alguns dados aqui e a saída foi:

1 - Fulano de Tal
2 - Ciclano
[CodigoNome[codigo=1, nome=Fulano de Tal], CodigoNome[codigo=2, nome=Ciclano]]
codigo = 1 - class java.lang.Integer / false
nome = Fulano de Tal - class java.lang.String / false

Interessante notar que o record já possui um método toString que mostra todos os campos em um formato pré-definido (o que não acontece com o tipo anônimo). E podemos confirmar que o objeto só possui dois campos, ou seja, ele não guarda a referência ao objeto u.


Vale lembrar ainda que cada vez que um tipo deste é usado, uma nova classe é criada, mesmo que eles sejam idênticos.

Ou seja, se eu fizer:

var x = new Object() {
    String m = "objeto abc";
};
var y = new Object() {
    String m = "objeto abc";
};

System.out.println(x.getClass());
for (Field f : x.getClass().getDeclaredFields()) {
    System.out.println(f.getName());
}
System.out.println(y.getClass());
for (Field f : y.getClass().getDeclaredFields()) {
    System.out.println(f.getName());
}

Ao compilar, são criados dois arquivos .class, mesmo que os tipos sejam idênticos (ambos possuem o mesmo campo, com o mesmo nome e tipo). Eu criei este código dentro de uma classe com o criativo nome de Teste, então a saída foi:

class testes.Teste$1
m
class testes.Teste$2
m

E ao olhar os arquivos criados na compilação, pode-se ver que foram criados Teste$1.class e Teste$2.class, referentes a estes tipos anônimos.


Outro detalhe: em contextos não estáticos, o tipo anônimo ainda captura o this da classe externa.

Para testar, coloquei o seguinte código no construtor da minha classe:

public class Teste {
    public Teste() {
        var x = new Object() {
            String m = "objeto abc";
        };
        System.out.println(x.getClass());
        for (Field f : x.getClass().getDeclaredFields()) {
            System.out.println(f.getName());
        }
    }
}

E ao chamar new Teste(), a saída foi:

class testes.Teste$1
m
this$0

Como pode ver, agora o this da classe externa (no caso, a classe Teste) foi capturado pelo tipo anônimo. O que quer dizer que, se os códigos anteriores estivessem rodando em um contexto não-estático, teríamos mais este campo criado em cada instância do tipo anônimo.

1 curtida