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.