Programação Orientada à Objetos com Java - POO
Tags: oop, java, backend, backenddevelopment
Published: 2023-08-27 14:26:39

POO é um paradigma de programação onde tudo é representado como um objeto. Um programa feito sobre esse paradigma é então composto por uma série de objetos que passam mensagens uns para os outros. Eventualmente, essa troca de mensagens produzirá modificações de estado (dados) nestes objetos.

Os objetos possuem características(atributos) e comportamentos(métodos). Cada objeto deriva de uma classe, que são abstrações de objetos do mundo real. Uma classe é um template, como a "planta" de uma construção, a partir do qual os objetos são criados.

Enquanto a lógica procedural tem analogia comum com “receitas de bolo”, descrevendo apenas “como” as coisas precisam ser feitas, a lógica orientada a objetos inclui “quem”, ou seja, os atores (que desempenham as ações) e os alvos .

Em linguagens procedurais, comportamentos são definidos em procedimentos, funções ou subrotinas. Em programas OO, os diversos comportamentos de um objeto são métodos. Os métodos de um objeto são “invocados” através de mensagens.


- É alocado espaço na memória sempre que um objeto lógico é criado. Classes não consomem espaço na memória..

Ao programar orientado à objetos, temos os benefícios:
- Código mais fácil de compreender, pois o código é estruturado de maneira que simbolizam elementos do mundo real, 
- Mais fácil de dar manutenção (manutenibilidade), pois as responsabilidades estão direcionadas aos objetos
- Reutilização de código: A herança, um dos pilares da POO, permite que os objetos herdem características e comportamentos, evitando a repetição de código.

Os pilares que regem a programação orientada à objetos são: Encapsulamento, Herança, Polimorfismo e Abstração.

- Variável de referência

Podemos acessar um objeto através de uma variável de referência. Uma variável de referência pode ser apenas de um tipo. Após declarada, o tipo da variável de referência não pode ser alterado. 

`Cachorro cachorro = new Cachorro();`

No exemplo acima, cachorro é a variável de referência a um objeto do tipo Cachorro(a classe). Uma variável de referência pode ter uma classe ou uma interface como tipo.

Um único objeto pode ser referenciado por variáveis de referência de diversos tipos (desde que esses tipos estejam na mesma hierarquia), sendo o objeto do mesmo tipo da variável de referência ou de uma superclasse.

Por exemplo, se a classe Cachorro estende da classe Animal, poderíamos dizer:
`Animal rex = new Cachorro();`

Nesse caso, rex seria uma variável de referência do tipo Animal e new Cachorro() criará um novo objeto do tipo Cachorro. Em tempo de compilação, não poderíamos acessar um método ou atributo específico de Cachorro, como por exemplo o método "latir()", porque o compilador conhece apenas o tipo de referência.  Já que latir() não existe na classe do tipo de referência Animal (Só existe na classe Cachorro), não é possível acessá-lo. Em tempo de execução, no entanto, rex será do tipo Cachorro(tipo de tempo de execução/tipo de objeto). Neste caso, podemos convencer o compilador de que o objeto rex será do tipo Cachorro em tempo de execução, realizando o casting, e com isso poderíamos utilizar os métodos específicos de cachorro. 

`Animal rex = new Cachorro();
((Cachorro) rex).latir(); //casting para Cachorro`


- Estado de um objeto
O conjunto de todos os dados “armazenados” em um objeto é designado como seu “estado”. Na terminologia empregada em OO, os diversos dados de um objeto são atributos.
Por exemplo, o objeto “cachorro” tem os atributos: nome, cor, raça, idade etc.

- Os construtores devem definir o “estado inicial válido”
Eventualmente, objetos precisam ser inicializados com um “estado mínimo” para serem considerados válidos. Exemplo:

`Cachorro cachorro = new Cachorro(“Rex”, “marrom”, “Pitbull”,”médio”, “filhote”).`

Nesse caso, sabemos que a classe Cachorro tem um construtor que define que ao criarmos um objeto do tipo Cachorro já devemos informar os atributos de nome, cor, raça e idade.

- O conjunto de atributos e métodos públicos (visíveis) de um objeto constituem sua “interface” (Não confundir com a interface que podemos definir a um tipo de objeto).
- Classes menores são, pelo menos em teoria, modificadas com menor frequência, colaborando para a estabilidade do código e portanto tendem a ser mais coesas.


**<u>Encapsulamento </u>**
Consiste em esconder e proteger detalhes de uma classe, não permitindo que elementos de fora da classe tenham acesso às suas propriedades, evitando o acesso e a modificação direta dos objetos por "qualquer um".  Com isso, temos controle sobre como os dados são manipulados em uma classe, e portanto os detalhes de implementação dos métodos e atributos ficam restritos somente à quem interessa: a classe em si.

Na POO, um atributo ou método que não é visível de fora do próprio objeto é chamado de "privado" e quando é visível, é chamado de "público".
Por exemplo: Ao pisarmos no acelerador de um carro, não precisamos saber os detalhes de como o carro acelera, quais mecanismos são utilizados no processo. Nós só precisamos pisar no acelerador e o carro executa a ação, sem precisar nos expor como ele faz isso.

A capacidade de um objeto “esconder” dados e comportamentos, é conhecido como hiding. Trata-se de um aspecto importante do encapsulamento.

**<u>Herança<u>**
Herança possibilita que um objeto receba comportamentos e estados de outro objeto.
Ao utilizarmos herança em um objeto, herdamos características de todos os objetos acima dele, assim como nós herdamos características de nossos ancestrais. A herança a partir das características do objeto mais acima é considerada herança direta, enquanto as demais são consideradas heranças indiretas. Por exemplo, na família, a criança herda diretamente do pai e indiretamente do avô e do bisavô.


Com isso, quando um objeto herda do outro dizemos que a classe que está sendo utilizado um relacionamento "É um". A classe que está herdando chamamos de subclasse, e a que está compartilhando as características chamamos de superclasse.


Por exemplo: A classe “Cachorro” herda de “Animal”. Dizemos que um cachorro “é um” animal, e nesse caso o cachorro é a subclasse(ou classe filha) e animal é a superclasse, estabelecendo então uma relação de herança entre elas. 

Nesse relacionamento de herança podem ser herdados métodos públicos de instância e variáveis de instância privadas (que podem ser acessadas apenas por meio de métodos getters e setters públicos).

- Em algumas linguagens existe a herança múltipla, onde um objeto pode herdar de diversos outros.
- Cuidado! Modelos com predominância de getters e setters podem estar disfarçando modelos anêmicos.

**<u>Polimorfismo</u>**
A palavra"polimorfismo" vem do grego poli = muitas, morphos = forma. Na programação, consiste na capacidade de um mesmo método assumir diferentes formas.
Ao herdarmos métodos de uma classe pai, por exemplo, é possível alterarmos o comportamento deles para se adequar ao objeto em questão, resultando em classes com métodos iguais mas com comportamentos diferentes. 

Por exemplo, “Carro” e “Moto” herdam de “Automóvel”. Todo “Automóvel” tem o método “acelerar” mas cada um acelera de um jeito, os mecanismos acionados para “Carro” e “Moto” são diferentes. Com isso, ambos irão herdar o método “acelerar” de “Automóvel”, mas irão modificar o seu comportamento e cada um irá acelerar de uma forma diferente.
Ou seja, objetos diferentes que herdam de uma mesma classe e têm um mesmo método que é implementado de formas diferentes

- Como todas as classes do java herdam de Object, todos os objetos do Java são considerados polimórficos.


_<u>Sobrecarga de métodos</u>_
A sobrecarga de métodos (method overload) possibilita que uma mesma classe possua métodos com o mesmo nome, mais de uma vez. Para isso acontecer, sua lista de parâmetros precisa ser diferente, para que o compilador possa diferenciá-los. Os tipos de retorno, modificadores de acesso e exceções lançadas também podem ser diferentes.


Observação: métodos estáticos também podem sofrer sobrecarga.


_<u>Sobrescrita de métodos</u>_
Se uma subclasse tem o mesmo método que foi declarado na superclasse, isso é conhecido como sobrescrita de métodos (method override). Para um método ser sobrescrito, deve manter a mesma assinatura.


A assinatura de um método é composta por: modificador de visibilidade, tipo de retorno do método, parâmetros e exceções.

Observações da sobrescrita de métodos:
- Deve possuir o mesmo tipo de retorno: embora um retorno covariante nos permita alterar o tipo de retorno do método sobrescrito.
- Não pode possuir um modificador de acesso mais restritivo: deve possuir um modificador de acesso menos restritivo.
- Não deve lançar uma exceção verificada (checked exception) nova ou mais ampla: pode lançar exceções verificadas mais restritas e pode lançar qualquer exceção não verificada.


Métodos estáticos e construtores  não podem ser sobrescritos, mas podem ser sobrecarregados. 


_<u>Relacionamentos</u>_
1. Relacionamento É UM: Um relacionamento É UM refere-se à herança ou implementação.
2. Generalização: Generalização usa um relacionamento É UM de uma classe especializada para uma classe generalizada.
3. Relacionamento TEM UM: Uma instância de uma classe TEM UMA referência para uma instância de outra classe.
4. Agregação: Neste relacionamento, a existência de uma classe A e B não são dependentes umas das outras.
Para essa parte de agregação, vamos ver um exemplo da classe Student e da classe ContactInfo. Student (aluno) TEM UMA ContactInfo (informação de contato). ContactInfo pode ser usado em outros lugares – por exemplo, uma classe Employee (funcionário) de uma companhia também poderia utilizar a classe ContactInfo. Assim, Student pode existir sem ContactInfo e ContactInfo pode existir sem Student. Este tipo de relacionamento é conhecido como agregação.
5. Composição: Neste relacionamento, a classe B não pode existir sem uma classe A – mas a classe A pode existir sem a classe B.

Para dar uma ideia sobre composição, vamos ver esse exemplo da classe Student e a classe StudentId. Student TEM UM StudentId. Student pode existir sem StudentId, mas StudentId não pode existir sem Student. Esse tipo de relacionamento é conhecido como composição.


**<u>Abstração</u>**
Abstração é o processo de esconder os detalhes de implementação e exibir apenas as funcionalidades para o usuário.

Um exemplo comum de abstração é o acelerador do carro: pisando mais forte, você aumenta a velocidade. Os motoristas, no entanto, não sabem como essa ação altera a velocidade – eles não precisam saber.

Em Java, podemos obter abstração de duas maneiras: classes abstratas e interfaces.

A palavra-chave abstract pode ser aplicada à classes e métodos. 
- abstract e final ou static nunca podem estar juntas.

Se bem implementado, um código orientado a objetos terá, sempre menos dependência de implementações concretas e mais de abstrações.

1. Classes abstratas: Uma classe é abstrata quando ela contém a palavra reservada abstract.
Classes abstratas não podem ser instanciadas  (não é possível criar objetos de classes abstratas), portanto elas são projetadas para herança. Elas podem ter construtores, métodos estáticos, métodos concretos e métodos finais.
2. Métodos abstratos: Um método é abstrato quando ele contém a palavra chave abstract.
Um método abstrato não possui implementação (não possui um corpo e termina com ponto e virgula). Métodos abstratos não devem ser marcados como private.
3. Classes abstratas e métodos abstratos: 
Se pelo menos um método for abstrato dentro de uma classe, então toda a classe deve ser abstrata.
É possível ter uma classe abstrata sem nenhum método abstrato.
Podemos ter qualquer quantidade de métodos abstratos e não abstratos ao mesmo tempo na mesma classe.
A primeira classe concreta que herde de uma classe abstrata deve prover implementação para todos os métodos abstratos.
Caso a subclasse não implemente os métodos abstratos da superclasse, ela deve também ser marcada como abstrata.
Em um cenário real, a implementação vai ser feita por alguém desconhecido ao usuário final. Usuários não conhecem a classe de implementação nem os detalhes da implementação.

Quando vamos querer marcar uma classe como abstrata?
- Para forçar subclasses a implementar métodos abstratos.
- Para impedir que existam objetos daquela classe.
- Para manter a referência à uma classe.
Para manter código comum.

_Interface_
Uma interface é um template (ou uma "planta" de construção) de uma classe, projetada para ser implementada por outras classes. Ela possui apenas métodos abstratos (não possui métodos concretos) portanto seus métodos não tem “corpo”, apenas a assinatura.

Quando uma classe implementa uma interface, ela é obrigada a implementar estes métodos abstratos da interface. Por isso, dizemos que uma interface é um "contrato", um conjunto de métodos que define que todas as classes que implementarem a interface devem possuir esses métodos. 

Por exemplo, uma interface “Funcionário” que possui os métodos “salario” e “comissao”. Esses métodos dentro da interface não possuem implementação, apenas a assinatura, o seu “nome”. As classes “Gerente”, “Diretor” e “Vendedor” implementam “Funcionário”. Nesse caso, todas elas serão obrigadas a implementar os métodos “salario” e “comissao”, definindo como eles devem funcionar em cada uma delas.


Os métodos da interface são, por padrão, public e abstract. Então, dentro da interface, não precisamos especificar as palavras-chaves public e abstract.  Sendo assim, o modificador de acesso de um método implementado de uma interface deve ser public.

- Um pequeno detalhe: uma interface não pode ser herdada por uma classe, mas sim implementada. No entanto, uma interface pode “herdar” de outra interface, criando uma hierarquia de interfaces. Usando um exemplo completo com carros, dizemos que a classe "Honda Fit Cross" herda da classe "Honda Fit", que por sua vez herda da classe "Carro". A classe "Carro" implementa a interface "Automóvel" que, por sua vez, pode herdar (por exemplo) uma interface chamada "MeioDeTransporte", uma vez que tanto um "automóvel" quanto uma "carroça" são meios de transporte, ainda que uma carroça não seja um automóvel.


- Métodos default e métodos estáticos nas Interfaces
Normalmente, implementamos os métodos de uma interface em classes separadas. Digamos que seja necessário adicionar um novo método à interface. Então, deveremos implementar esse método em todas as outras classes que implementam essa interface também.

Para evitar esse tipo de problema, a partir do Java, 8 foi introduzida a possibilidade de implementar métodos default e estáticos dentro de uma interface, além dos métodos abstratos.


- Interface de marcação
São interfaces vazias. Por exemplo as interfaces Serializable, Cloneable e Remote. Servem apenas para oferecer algum tipo de “marcação” à uma classe.


- Vantagens das interfaces
Nos ajudam a utilizar a herança múltipla no Java.
Elas fornecem abstração.
Elas fornecem baixo acoplamento: os objetos são independentes uns dos outros.


- Quando vamos querer mudar de uma classe para uma interface?
Para forçar subclasses a implementar métodos abstratos.
Para evitar a criação de objetos dessa classe.
Para manter a referência à uma classe.

Caso você queira definir métodos que podem ser necessários e código comum, use uma classe abstrata.
Caso queira apenas definir métodos necessários, use uma interface. 





Fontes:
https://programacao-orientada-a-objetos.online/
https://www.freecodecamp.org/portuguese/news/principios-de-programacao-orientada-a-objetos-em-java-conceitos-de-poo-para-iniciantes/