"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:

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

Código
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:

Código
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
class Cat
{
 protected String getName()
 {
  return "Oscar";
 }
}
class Tiger extends Cat
{
 public String getName()
 {
  return "Oscar Tiggerman";
 }
}
Expandimos a visibilidade do método de protectedpara public.
Código Por que isso é «legal»
public static void main(String[] args)
{
 Cat cat = new Cat();
 cat.getName();
}
Está tudo ótimo. Aqui nem sabemos que a visibilidade foi estendida em uma classe descendente.
public static void main(String[] args)
{
 Tiger tiger = new Tiger();
 tiger.getName();
}
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:
public String getPublicName()
{
super.getName(); //chama o método protegido
}

Em outras palavras, não estamos falando de nenhuma violação de segurança.

public static void main(String[] args)
{
 Cat catTiger = new Tiger();
 catTiger.getName();
}
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
class Cat
{
 public Cat parent;
 public Cat getMyParent()
 {
  return this.parent;
 }
 public void setMyParent(Cat cat)
 {
  this.parent = cat;
 }
}
class Tiger extends Cat
{
 public Tiger getMyParent()
 {
  return (Tiger) this.parent;
 }
}
Substituímos o método getMyParente agora ele retorna um Tigerobjeto.
Código Por que isso é «legal»
public static void main(String[] args)
{
 Cat parent = new Cat();

 Cat me = new Cat();
 me.setMyParent(parent);
 Cat myParent = me.getMyParent();
}
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.

public static void main(String[] args)
{
 Tiger parent = new Tiger();

 Tiger me = new Tiger();
 me.setMyParent(parent);
 Tiger myParent = me.getMyParent();
}
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:
public Tiger getMyTigerParent()
{
return (Tiger) this.parent;
}

Em outras palavras, não há violações de segurança e/ou violações de conversão de tipo.

public static void main(String[] args)
{
 Tiger parent = new Tiger();

 Cat me = new Tiger();
 me.setMyParent(parent);
 Cat myParent = me.getMyParent();
}
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
class Cat
{
 public Cat parent;
 public Cat getMyParent()
 {
  return this.parent;
 }
 public void setMyParent(Cat cat)
 {
  this.parent = cat;
 }
}
class Tiger extends Cat
{
 public Object getMyParent()
 {
  if (this.parent != null)
   return this.parent;
  else
   return "I'm an orphan";
 }
}
Sobrecarregamos o método getMyParent e reduzimos o tipo de seu valor de retorno.

Está tudo bem aqui.

public static void main(String[] args)
{
 Tiger parent = new Tiger();

 Cat me = new Tiger();
 Cat myParent = me.getMyParent();
}
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."