Hoje, enquanto trabalhava em um projeto para um curso de “Design Patterns” da faculdade (Java 11 necessário), descobri um problema com a restrição de acesso do modificador de acesso que pode ser ignorado declarando var. Eu sei como var é usado, é apenas um açúcar sintático que deixa a inferência de tipo para o compilador.
Não consigo descobrir que tipo de alias a var realmente é aqui:
- é "Child.InnerChild"? Isso não seria uma incompatibilidade de tipo?
- "InnerParent"? Isso não ignora o restritor de acesso protegido?
Aqui está meu código simplificado:
public abstract class Parent {
protected abstract static class InnerParent {
public InnerParent self() {
return this;
}
}
}
public class Child extends Parent {
public static class InnerChild extends InnerParent {}
}
import anotherpackage.Child;
/**
* Compiling with Java 11:
*/
public class Main {
public static void main(String[] args) {
// As we expected a compilation error: The returned static type does not match the expected type
// Child.InnerChild innerChild = new Child.InnerChild().self();
// As we expected a compilation error: Parent.InnerParent is package visible (protected)
// Parent.InnerParent innerChild = new Child.InnerChild().self();
// Why does it compile and run correctly here?
// var is just syntactic sugar for the compiler type, it should be a Parent.InnerParent alias here,
// why is var allowed to transgress the protected access restriction?
var innerChild = new Child.InnerChild().self(); // perhce' non da' errore ? var e' un alias di cosa ?
System.out.println(innerChild);
System.out.println(innerChild.getClass().getName());
}
}
Também perguntei ao ChatGPT, mas ele não está respondendo tão bem quanto eu gostaria e não tenho certeza se está correto:
Por que
var
funciona
- Tipo inferido : O tipo inferido para
var innerChild
éParent.InnerParent
.- Regras de acesso : como o tipo é inferido e não escrito explicitamente no código, o compilador não impõe restrições de acesso para a variável declarada.
Encontrei um novo problema: por que não consigo acessar getClass()
?
No entanto, é possível compilar dessa maneira.
System.out.println(((Object) innerChild).getClass().getName());
// OUTPUT: com.github.lorenzoyang.anotherpackage.Child$InnerChild
A resposta é:
var
pode representar qualquer coisa que o compilador possa raciocinar sobre como um tipo, mesmo os não-denotáveis: Ele permite que você adie a questão. O erro que você acabou tendo existe desde antes da introdução dovar
que prova que isso não é um problema com var especificamente; é um efeito pretendido (mas um tanto estranho) que corresponde à especificação de linguagem Java.Vamos entrar em detalhes e decompô-los. Mas primeiro:
Lembrete – GPT é inútil
GPT é uma ferramenta que produz uma resposta autoritativa. Como em, ele vai continuar tentando responder sua pergunta até que algo apareça que pareça autoritativo. Pode ser um monte de besteira completa e neste caso, de fato é. Geralmente pedir ao GPT por respostas objetivas é uma ideia muito ruim: Claro, para perguntas simples parece magicamente incrível, mas então - era uma pergunta simples. Existem muitas maneiras de obter a resposta para uma pergunta simples. Não é para isso que você deve otimizar - seriam respostas para perguntas complexas. E o GPT é muito ruim nisso. No sentido de que você não consegue dizer. Ele lhe dará uma resposta que soa muito bem.
Lembre-se disto: Perguntar a um GPT sobre questões esotéricas de linguagem é ridículo. Nunca faça isso.
Não vou abordar mais as respostas do GPT. Elas são inúteis (possivelmente erradas, possivelmente não erradas. Qualquer dica que elas dão pode ser relevante, ou não. Portanto, inúteis).
Tipos não denotáveis
É uma questão de linguagem/compilador, então nos referimos à fonte, o JLS:
JLS21 §14.4.1 :
E observe também este esclarecimento na caixa de exemplo:
A última parte está implícita na definição real (pois a definição não declara em nenhum momento que o tipo que o compilador infere
var
precisa ser denotado, portanto você deve inferir que ele não precisa ser; a nota na caixa de exemplo destaca isso).E é essencial para entender o que está acontecendo aqui. Aqui está um exemplo de uso de tipo não denotável que é mais fácil de seguir do que o que esta questão encontrou:
Cole o acima em um arquivo, compile-o - erro na linha que diz 'compiler error'. Marque-o, compile novamente, execute-o - funciona bem.
O que é bizarro no sentido de que não há nada que você possa substituir
var
que faça isso funcionar . O que está acontecendo é que op
tipo de é inferido como o tipo da classe local anônima e esse tipo tem umtest()
método . Isso sempre fez parte do Java (bem, desde a versão 1.2 ou algo assim, décadas atrás neste ponto), só quevar
agora permite que você declare variáveis locais com esse tipo. OasAReminder()
método mostra isso; que o código 'funciona' (compila e roda como você esperaria) em qualquer versão Java das últimas 2 décadas.Portanto,
var
pode denotarInnerParent
muito bem aqui. Não é 'denotável' - se você literalmente substituirvar
em seu exemplo porInnerParent
ele falharia na compilação porqueInnerParent
não é acessível, masvar
meramente representa o tipo da expressão da maneira como o compilador a trata. E o compilador tem que suportá-lo . Afinal, isso é perfeitamente legal:O compilador terá que lidar com o fato de que a expressão atribuída
o
aqui éInnerParent
(self()
afinal, a definição de diz isso) e, portanto, terá que descobrir no contexto que isso é aceitável.Nós também podemos provar isso!
Prova de que isso não tem nada a ver com
var
Tente substituir sua
main
definição pela seguinte:Esse código não será compilado, com exatamente o mesmo erro:
Em outras palavras, no Java 8 (que não suporta
var
nada -var
como um recurso foi adicionado no Java 10) podemos ter o mesmo conceito: Uma expressão que é, ela própria, legal, e cujo tipo não é acessível no contexto em que a escrevemos. Ela 'funciona', mas o tipo é extremamente venenoso: Se você 'tocar' nela, quase tudo que você fizer com ela será um erro do compilador; mesmo invocandofinal
métodos herdados diretamente do obviamente acessíveljava.lang.Object
. Praticamente a única coisa que você pode fazer com eles é tratá-los como um tipo acessível primeiro (por meio de cast ou atribuição a uma variável) ou passá-los para outro método na íntegra.var
apenas permite que você adie a solução.var innerChild = new Child.InnerChild().self();
simplesmente não viola nada do que a Especificação da Linguagem Java diz.Na Seção 6.6 , é dito que o controle de acesso é aplicado quando você usa um nome qualificado .
Você não usou o nome
InnerParent
aqui, então as regras de controle de acesso não são aplicadas.Alguns tipos de expressões também impõem controle de acesso. Por exemplo, uma chamada de método em uma expressão primária precisa garantir que o método que está sendo chamado seja acessível. É por isso que a chamada
innerChild.getClass()
produz um erro. Especificamente, esta cláusula proíbe o acesso:InnerParent
não é acessível, então seu métodogetClass
também não é acessível.Veja as letras pequenas antes do cabeçalho da seção 6.6.1 para mais informações.
var
não é um açúcar sintático simples que é simplesmente substituído/reescrito com o nome do tipo da expressão no RHS. A expressão no RHS pode nem ter um nome (por exemplo, classes anônimas) ou pode não ter um tipo (por exemplo, expressões lambda e referências de método). O tipo de umavar
variável é especificado em 14.4.1Ao escrever
var
, você realmente não está usando o nome deInnerParent
.