"Vou falar sobre « modificadores de acesso ». Já falei sobre eles uma vez, mas a repetição é um pilar do aprendizado."
Você pode controlar o acesso (visibilidade) que outras classes têm aos métodos e variáveis de sua classe. Um modificador de acesso responde à pergunta «Quem pode acessar este método/variável?». Você pode especificar apenas um modificador para cada método ou variável.
1) modificador « público ».
Uma variável, método ou classe marcada com o modificador público pode ser acessada de qualquer lugar no programa. Este é o mais alto grau de abertura: não há restrições.
2) modificador « privado ».
Uma variável, método ou classe marcada com o modificador private só pode ser acessada na classe em que foi declarada. O método ou variável marcado está oculto de todas as outras classes. Este é o mais alto grau de privacidade: acessível apenas por sua classe. Esses métodos não são herdados e não podem ser substituídos. Além disso, eles não podem ser acessados em uma classe descendente.
3) « Modificador padrão ».
Se uma variável ou método não estiver marcado com nenhum modificador, será considerado marcado com o modificador "padrão". Variáveis e métodos com este modificador são visíveis para todas as classes no pacote onde são declaradas e somente para essas classes. Esse modificador também é chamado de acesso " pacote " ou " pacote privado ", sugerindo que o acesso a variáveis e métodos é aberto a todo o pacote que contém a classe.
4) modificador « protegido ».
Este nível de acesso é um pouco mais amplo do que o pacote . Uma variável, método ou classe marcada com o modificador protegido pode ser acessada de seu pacote (como "pacote") e de todas as classes herdadas.
Esta tabela explica tudo:
Tipo de visibilidade | Palavra-chave | Acesso | |||
---|---|---|---|---|---|
Sua classe | Seu pacote | Descendente | todas as classes | ||
Privado | privado | Sim | Não | Não | Não |
Pacote | (sem modificador) | Sim | Sim | Não | Não |
Protegido | protegido | Sim | Sim | Sim | Não |
Público | público | Sim | Sim | Sim | Sim |
Existe uma maneira de lembrar facilmente esta tabela. Imagine que você está escrevendo um testamento. Você está dividindo todas as suas coisas em quatro categorias. Quem pode usar suas coisas?
Quem tem acesso | modificador | Exemplo |
---|---|---|
só eu | privado | diário pessoal |
Família | (sem modificador) | Fotos de família |
Família e herdeiros | protegido | propriedade familiar |
Todo mundo | público | Memórias |
"É como imaginar que as aulas de um mesmo pacote fazem parte de uma família."
"Também quero contar algumas nuances interessantes sobre a substituição de métodos."
1) Implementação implícita de um método abstrato.
Digamos que você tenha o seguinte código:
class Cat
{
public String getName()
{
return "Oscar";
}
}
E você decidiu criar uma classe Tiger que herda esta classe e adicionar uma interface à nova classe
class Cat
{
public String getName()
{
return "Oscar";
}
}
interface HasName
{
String getName();
int getWeight();
}
class Tiger extends Cat implements HasName
{
public int getWeight()
{
return 115;
}
}
Se você apenas implementar todos os métodos ausentes que o IntelliJ IDEA diz para você implementar, mais tarde você pode acabar gastando muito tempo procurando por um bug.
Acontece que a classe Tiger tem um método getName herdado de Cat, que será considerado a implementação do método getName para a interface HasName.
"Não vejo nada de terrível nisso."
"Não é tão ruim, é um lugar provável para erros se infiltrarem."
Mas pode ser ainda pior:
interface HasWeight
{
int getValue();
}
interface HasSize
{
int getValue();
}
class Tiger extends Cat implements HasWeight, HasSize
{
public int getValue()
{
return 115;
}
}
Acontece que você nem sempre pode herdar de várias interfaces. Mais precisamente, você pode herdá-los, mas não pode implementá-los corretamente. Olhe para o exemplo. Ambas as interfaces exigem que você implemente o método getValue(), mas não está claro o que ele deve retornar: o peso ou o tamanho? Isso é bastante desagradável de se lidar.
"Concordo. Você quer implementar um método, mas não pode. Você já herdou um método com o mesmo nome da classe base. Está quebrado."
"Mas há boas notícias."
2) Expandir a visibilidade. Ao herdar um tipo, você pode expandir a visibilidade de um método. Isto é o que parece:
código Java | Descrição |
---|---|
|
|
|
Expandimos a visibilidade do método de protected para public . |
Código | Por que isso é «legal» |
---|---|
|
Está tudo ótimo. Aqui nem sabemos que a visibilidade foi estendida em uma classe descendente. |
|
Aqui chamamos o método cuja visibilidade foi estendida.
Se isso não fosse possível, sempre poderíamos declarar um método no Tiger: Em outras palavras, não estamos falando de nenhuma violação de segurança. |
|
Se todas as condições necessárias para chamar um método em uma classe base ( Cat ) forem satisfeitas, então elas certamente serão satisfeitas para chamar o método no tipo descendente ( Tigre ). Porque as restrições na chamada do método eram fracas, não fortes. |
"Não tenho certeza se entendi completamente, mas vou lembrar que isso é possível."
3) Restringindo o tipo de retorno.
Em um método substituído, podemos alterar o tipo de retorno para um tipo de referência restrito.
código Java | Descrição |
---|---|
|
|
|
Substituímos o método getMyParent e agora ele retorna um Tiger objeto. |
Código | Por que isso é «legal» |
---|---|
|
Está tudo ótimo. Aqui nem sabemos que o tipo de retorno do método getMyParent foi ampliado na classe descendente.
Como o «código antigo» funcionava e funciona. |
|
Aqui chamamos o método cujo tipo de retorno foi reduzido.
Se isso não fosse possível, poderíamos sempre declarar um método em Tiger: Em outras palavras, não há violações de segurança e/ou violações de conversão de tipo. |
|
E tudo funciona bem aqui, embora tenhamos ampliado o tipo das variáveis para a classe base (Cat).
Devido à substituição, o método setMyParent correto é chamado. E não há com o que se preocupar ao chamar o método getMyParent , pois o valor de retorno, embora seja da classe Tiger, ainda pode ser atribuído à variável myParent da classe base (Cat) sem problemas. Os objetos Tiger podem ser armazenados com segurança nas variáveis Tiger e Cat. |
"Sim. Entendi. Ao substituir métodos, você deve estar ciente de como tudo isso funciona se passarmos nossos objetos para um código que só pode manipular a classe base e não sabe nada sobre nossa classe. "
"Exatamente! Então a grande questão é por que não podemos restringir o tipo do valor de retorno ao substituir um método?"
"É óbvio que neste caso o código da classe base pararia de funcionar:"
código Java | Explicação do problema |
---|---|
|
|
|
Sobrecarregamos o método getMyParent e reduzimos o tipo de seu valor de retorno.
Está tudo bem aqui. |
|
Então este código vai parar de funcionar.
O método getMyParent pode retornar qualquer instância de um Object, porque na verdade ele é chamado em um objeto Tiger. E não temos um cheque antes da missão. Portanto, é totalmente possível que a variável myParent do tipo Cat armazene uma referência String. |
"Maravilhoso exemplo, Amigo!"
Em Java, antes de um método ser chamado, não há verificação se o objeto possui tal método. Todas as verificações ocorrem em tempo de execução. E uma chamada [hipotética] para um método ausente provavelmente faria com que o programa tentasse executar um bytecode inexistente. Isso levaria a um erro fatal e o sistema operacional fecharia o programa à força.
"Uau. Agora eu sei."
GO TO FULL VERSION