0

Orientação a Objetos com Java

#Java
Gabriel Machado
Gabriel Machado
"A programação Orienta a Objetos impõe disciplina sobre a transferência indireta do controle" Robert "Uncle Bob" Martin livro Arquitetura Limpa
"... a pilha de chamadas funções ... poderia ser movida para HEAP (área de memória não necessariamente ordenada - diferente da stack) possibilitando que as variáveis locais declaradas por uma função existissem muito depois que a função retornasse..." Robert "Uncle Bob" Martin livro Arquitetura Limpa
"A diferença entre um Código Procedural e um O.O é bem simples. Em códigos procedurais (...) escolher o melhor algoritmo é o mais importante (...) Já em linguagens orientadas a objetos (...) pensar no projeto de classes (...) como se encaixam (...) e como serão estendidas é o que mais importa." Maurício Aniche livro Orientação a Objetos e SOLID para Ninjas


Classe

Vamos entender uma classe como um modelo a ser seguido.

Uma classe vai funcionar como uma espécie de molde que nos servirá como base para construir algo.

Por exemplo: Quando pensamos em construir uma casa, nós fazemos uma planta baixa. Ela será o modelo que utilizaremos para construir algo concreto.

As classes funcionam de forma parecida.

Vamos a um exemplo prático.

public class Pessoa {
		private String nome = "Gabriel";
		
		public String getNome() {
				return nome;
		}

		public String falarMeuProprioNome() {
				return "Olá, meu nome é " + getNome();
		}
}


Objeto

Agora entendemos que temos um modelo que podemos seguir. O que podemos fazer com esse modelo?

Bom... Nós fizemos a analogia da casa, certo? Depois de termos a planta baixa, nós começamos a construir.

E o resultado do que nós construímos, vamos chamar de objeto.

Quando nós utilizamos a nossa classe Pessoa - mostrada no código anterior - para criar um objeto, nós diremos que estamos instanciando um objeto da classe Pessoa.

E esse termo é bem simples de entender. O que acontece é que podemos criar vários objetos de uma mesma classe, ou seja, várias instâncias de objetos.

public class ExemploPessoa {
		public static void main(String[] args) {
				Pessoa pessoa = new Pessoa();
				
				System.out.println(pessoa.getNome());
				// Gabriel
		}
}


Atributos

Agora vamos pensar no que nós definimos como nome. Foi tão intuitivo nós pensarmos que uma pessoa teria um nome que nem demos importância a ele.

O nome é uma característica de uma Pessoa e pode ser diferente de pessoa para pessoa.

O nome é um atributo da pessoa.


Métodos

Agora vamos pensar que uma pessoa pode ter ações. Por exemplo, uma pessoa pode falar.

Pensando em um cenário mais específico, uma pessoa pode falar o seu nome.

As ações que nós definimos que uma classe pode ter, nós chamamos de métodos.

public class ExemploMetodos {
		public class void main(String[] args) {
				Pessoa pessoa = new Pessoa();

				System.out.println(pessoa.falarMeuProprioNome());
				// Olá, meu nome é Gabriel
		}
}


Construtores

Podemos entender o termo construtor no sentido literal, afinal vamos construir um objeto.

Por meio de um construtor, criamos um objeto baseado em uma Classe e assim o alocamos em memória.

Ao criarmos um objeto dizemos que estamos instanciando um objeto.

public class Pessoa {
		private String nome;

		public Pessoa() {
		}

		public String getNome() {
				return nome;
		}

		public void setNome(String nome) {
				this.nome = nome;
		}
}

Esse exemplo que acabamos de ver é o exemplo mais comum quando começamos a estudar construtores em Java.

E para instanciar essa classe (criar um objeto dela) fazemos o seguinte:

Pessoa pessoa = new Pessoa();

Quando não temos um construtor padrão (sem parâmetros) definidos em uma classe, é subentendido que temos um construtor desse tipo na classe.

Mas cuidado! Isso só vale quando não tiver outro construtor definido

Também podemos criar construtores parametrizados.

Dessa forma, conseguimos definir um contrato onde sempre será obrigatório passar alguma informação na hora de instanciar a classe.

Nesse exemplo temos dois construtores. Um com passagem de parâmetro e outro sem.

Isso garante que possamos instanciar das duas maneiras.

public class Pessoa {
		private String nome;

		public Pessoa() {
		}

		public Pessoa(String nome) {
				this.nome = nome;
		}

		public String getNome() {
				return nome;
		}

		public void setNome(String nome) {
				this.nome = nome;
		}
}
// instanciando com passagem de parâmetro
Pessoa pessoa = new Pessoa("Gabriel");

// instanciando sem passagem de parâmetro
Pessoa pessoa = new Pessoa();

Já quando definimos nossa classe dessa forma, se tentarmos instanciar a classe sem passar algum parâmetro no construtor, tomaremos erro em tempo de compilação:

public class Pessoa {
		private String nome;

		public Pessoa(String nome) {
				this.nome = nome;
		}

		public String getNome() {
				return nome;
		}

		public void setNome(String nome) {
				this.nome = nome;
		}
}

E existe um destrutor?

Em Java não existe o conceito de destrutor explícito.

Lembra que falamos que quando instanciamos estamos, na verdade, alocando o objeto em memória?

Pois bem... Desalocar esse objeto fica por conta do GC (Garbage Collector).


Encapsulamento

Mais uma vez vamos entender o termo que estamos trabalhando ao pé da letra.

Quando falamos de encapsulamento, estamos falando efetivamente em proteger alguma informação de alguma forma, ou seja, com uma cápsula.

Vamos ver como podemos trabalhar com o encapsulamento nos nossos exemplos anteriores da Classe Pessoa.

Na nossa classe, vamos manipular basicamente 2 atributos:

  • Nome
  • Data de nascimento

Mas afinal o que queremos?

Queremos garantir a nossa implementação e que o acesso a determinados dados esteja realmente protegidos do acesso externo.

Para esse exemplo específico:

  • Queremos que o nome possa ser alterado. Vamos pensar que uma pessoa pode casar e mudar seu nome.
  • Não queremos alterar a data de nascimento. A pessoa nasce com ela e não pode mudar;
  • Queremos de alguma forma retornar a idade da pessoa.
public class Pessoa {
		private String nome;
		private LocalDate dataNascimento;

		public Pessoa(String nome, int dia, int mes, int ano) {
				this.nome = nome;
				this.dataNascimento = LocalDate.of(ano, mes, dia);
		}

		public String getNome() {
				return nome;
		}

		public void setNome(String nome) {
				this.nome = nome;
		}

		public LocalDate getDataNascimento() {
				return dataNascimento;
		}

		public int calculaIdade() {
				return Period.between(dataNascimento, LocalDate.now()).getYears());
		}
}
public class Exemplo001 {
		public static void main(String[] args) {
				Pessoa eu = new Pessoa("Gabriel", 19, 05, 1990);
				
				System.out.println(eu.getNome());
				System.out.println(eu.getDataNascimento());
				System.out.println(eu.calculaIdade());
				
				eu.setNome("Gabriel Amorim");

				System.out.println(eu.getNome());
		}
}


Herança

Vamos agora falar de outro pilar importante da Orientação a Objetos: a Herança.

Como o próprio nome já diz, essa é a capacidade de uma Classe herdar o comportamento de outra.

Exemplo:

public class Veiculo {
		private String modelo;
		private String marca;
}
public class Carro extends Veiculo {
		private int quantidadeDePortas;
}
public class Motocicleta extends Veiculo {
	private String cilindradas;
}

Depois de definir os getters e setters...

public class Exemplo001 {
		public static void main(String[] args) {
				Carro carro = new Carro();
				carro.setMarca("Nissan");
				carro.setModelo("March");
				carro.setQuantidadeDePortas(4);

				Motocicleta moto = new Motocicleta();
				carro.setMarca("DUCATI");
				carro.setModelo("STRETFIGHTER");
				carro.setQuantidadeDePortas(850);
		}
}

Também aproveitamos comportamentos

Nesse cenário, temos o método acelera() na classe veiculo.

public static void main(String[] args) {
		Carro carro = new Carro();
		carro.acelera();

		Motocicleta moto = new Motocicleta();
		moto.acelera();
}


Herança vs Composição

Existe um vasto e antigo debate em relação a utilização de herança. Algumas bibliografias inclusive defendem que ela nunca deve ser utilizada.

E o grande problema tem relação com o nosso tópico anterior: o encapsulamento.

A subclasse necessita conhecer, em muitos casos, a implementação da superclasse, o que cria um acoplamento e quebra a nossa premissa básica do isolamento que vimos no encapsulamento.


Polimorfismo

Quando falamos em herança, o verbo ser é mandatório na nossa forma de falar sobre a classe.

Entendemos, portanto, que um carro é um veículo e uma motocicleta também é um veículo.

Agora no nosso exemplo, nós queremos colocar mais uma característica e uma ação que podem ser comum aos dois, mas com algumas peculiaridades.

Agora vamos querer calcular o valor aproximado do IPVA dos nossos diferentes tipos de veículos.

Tanto carros quanto motos pagam IPVA, certo? E o cálculo é baseado no valor venal do veículo.

Portanto a primeira conclusão que chegamos é que temos uma característica nova na nossa Classe de Veículos, agora temos o valor venal, portanto:

public class Veiculo {
		private String modelo;
		private String marca;

		private double valorVenal;
}

Mas precisamos calcular a nossa precisão de imposto.

Vamos partir do princípio que (valores hipotéticos):

  • Um veículo teria que pagar, no mínimo, 0,01% do valor venal de IPVA.
  • Um carro teria que pagar, no mínimo 0,07% do valor venal de IPVA.
  • Um moto teria que pagar, no mínimo 0,03% do valor venal de IPVA.

Para isso precisaremos definir implementações diferentes de acordo com a classe que estamos trabalhando.

E é onde entraria o Polimorfismo.

Ele nos garantirá a capacidade de um objeto ser referenciado de múltiplas formas.

O Java será capaz de identificar qual objeto foi instanciado e, assim, escolher qual método será utilizado.

Vamos ver como ficaria...

public class Veiculo {
		...

		private double valorVenal;

		...

		public double calculaImposto() {
				return this.valorVenal * 0.01;
		}
}
public class Motocicleta extends Veiculo {
		...

		public double calculaImposto() {
				return this.valorVenal * 0.03;
		}
}
public class Carro extends Veiculo {
		...

		public double calculaImposto() {
				return this.valorVenal * 0.07;
		}
}
public static void main(String[] args) {
		Veiculo generico = new Veiculo();
		generico.setValorVenal(1000.0);
		System.out.println(generico.calculaImposto()); // 10.0
		
		Veiculo carro = new Carro();
		carro.setValorVenal(1000.0);
		System.out.println(carro.calculaImposto()); // 70.0

		Veiculo moto = new Motocicleta();
		moto.setValorVenal(1000.0);
		System.out.println(moto.calculaImposto()); // 30.0
}


This

Quando estamos trabalhando com o termo this, no Java, estamos na verdade, fazendo uma auto referência.

Esse conceito faz mais sentido quando estamos falando de construtores e métodos, exemplo:

public class Veiculo {
		private String modelo;
		
		...

		public String getModelo() {
			return modelo;
		}

		public void setModelo(String modelo) {
				this.modelo = modelo;
		}

		...
}


Super

Analogamente ao This, quando falamos no Super também estamos fazendo uma referência, mas dessa vez estamos fazendo referência a superclasse em um cenário de herança.

Atenção!

Como em Java, todas as nossas classe herdam de Object, se usamos o super em uma classe que não tem um extends explícito, estamos fazendo referência ao Object.

Vamos mudar um pouco o nosso exemplo.

Primeiro vamos transformar a nossa classe veículo.

Ela vai passar a ser uma classe abstrata e, portanto, não poderá mais ser instanciada.

E também vamos definir que o construtor dessa classe sempre irá esperar o modelo, a marca e o valor venal.

public abstract class Veiculo {
		private String modelo;
		private String marca;

		private double valorVenal;

		public Veiculo(String modelo, String marca, double valorVenal) {
				this.modelo = modelo;
				this.marca = marca;
				this.valorVenal = valorVenal;
		}

		...	

}
public class Carro extends Veiculo {
		public Carro(String modelo, String marca, double valorVenal) {
				super(modelo, marca, valorVenal);
		}

		public Carro(String modelo, String marca, double valorVenal, int quantidadeDePortas) {
				super(modelo, marca, valorVenal);
				this.quantidadeDePortas = quantidadeDePortas;
		}

		...
}


Equals

Como sabemos, todas as classes em Java herdam de Object. E, portanto, tem por padrão alguns métodos.

Um deles é o equals que serve para fazer comparações entre objetos.

Entretanto esse método possui algumas peculiaridades.

Por padrão, quando estamos comparando dois objetos, estamos comparando a referência deles. Então se instanciarmos dois carros, por mais que eles tenham as mesmas informações o Java não é capaz de identificar.

Mas poderíamos sobrescrever o método equals() para que nossa lógica funcione do jeito que gostaríamos.

Tenha em mente que é uma boa prática sobrescrever este método.


HashCode

Quando falamos em hashCode, precisamos pensar em um código gerado que garanta um caráter único ao nosso objeto.

Essa pode ser uma forma muito interessante para que possamos comparar se realmente um objeto é igual ao outro.

Temos que garantir que a implementação da lógica de hashCode sempre respeite as mesmas regras, pois quando compararmos os nossos objetos, o nosso fator de comparação será ele.

@Override
public int hashCode() {
		return Objects.hash(modelo, marca, valorVenal);
}
@Override
public boolean equals(Object obj) {
		if (obj == null) {
				return false;
		}
		
		Veiculo comparavel;
		if (obj instanceof Veiculo) {
				comparavel = (Veiculo)obj;
		} else {
				return false;
		}

		if (this.hashCode() == obj.hashCode()) {
				return true;
		} 
		
		return false;
}
0
1

Comentários (2)

1
Eric Kenzo

Eric Kenzo

07/04/2021 10:46

Muito bom artigo Gabriel! Obrigado por compartilhar o conhecimento

1
Rosemeire Deconti

Rosemeire Deconti

06/04/2021 16:10

Gostei muito do seu artigo! Grata por compartilhar!

20 anos / Desenvolvedor na Seidor Brasil

Brasil