Classes em Java
Como antes dito, tudo em Java são classes, é possível ver isso em prática logo na método main(String args[])
, esse método reside em uma classe como pode ser visto na linha 2.
// Program.java
public class Program {
public void main(String[] args) {
System.out.println("Hello World");
}
}
Nós podemos ainda criar nossas próprias classes e métodos, basta criar um arquivo com o nome da classe desejada, com a extensão .java
e preenche-lo com a estrutura que já vimos ao redor do método main.
// HelloWorld.java
public class HelloWorld {
}
Para instanciar:
// Program.java
public void main(String[] args) {
HelloWorld hello = new HelloWorld();
}
Vale ressaltar que é uma boa prática utilizar PascalCase no identificador(nome) das classes.
Variáveis de classe.
Uma classe é um conjunto de atributos e métodos, no Java podemos definir os atributos de uma classe através de suas variáveis. Para definir uma variável pertencente a uma classe, devemos declarar seu tipo e em seguida seu identificador Tipo Identificador;
, como visto abaixo:
// HelloWorld.java
public class HelloWorld {
String message;
int count;
}
É uma boa prática utilizar camelCase nos identificadores de atributos.
Neste exemplo, definimos dois atributos para a classe HelloWorld: message do tipo String e count do tipo int. Os tipos em Java podem ser primitivos, ou Wrapper Classes, que incluem métodos para lidar com os dados, as Wrapper Classes consistem no tipo em CamelCase, com exceção de int, sua wrapper class é Integer, String, o qual não possui uma versão primitiva e char cuja Wrapper Class é Character. Os tipos disponíveis são:
- String: Cadeia de caracteres.
- short: Números inteiros(2 bytes).
- int: Números inteiros(4 bytes).
- long: Inteiros de até 8 bytes.
- float: Números de ponto flutuante(4 bytes).
- double: Números ponto flutuante de dupla precisão(8 bytes).
- byte: Representa 8 bits.
- char: Representa um único caractere.
- Boolean: Representa um valor lógico 1 - True ou 0 - False.
Quando lidamos com classes que serão persistidas, é uma boa prática utilizar Wrapper Classes ao invés de tipos primitivos, para manter o código mais próximo do que será persistido.
Modificadores.
Além do tipo, também é possível definir o escopo de um atributo ou classe através de Modificadores.
Modificadores são responsáveis por gerir o acesso ou alterar a funcionalidade de classes, atributos e métodos. Eles são divididos em 2 grupos:
- Modificadores de acesso - Controlam o nível de acesso.
- Não modificadores de acesso - Proveem outras funcionalidades.
Modificadores de Acesso.
Para classes podemos usar:
- public: A classe é acessível por qualquer outra classe.
- default: A classe é acessível somente a outras classes no mesmo package. É utilizado quando não se especifica um modificador.
Para atributos, métodos e construtores, podemos utilizar os seguintes:
- public: É acessível a todas as classes.
- private: É acessível apenas pela classe que o declarou.
- protected: É acessível apenas no mesmo package e classes filhas.
- default: É acessível apenas no mesmo package.
Por exemplo:
// HelloWorld.java
public class HelloWorld {
public String message;
private int count = 5;
}
Modificadores de Não Acesso.
Para classes, podemos utilizar:
- final: A classe não pode ser herdada por outras classes.
- abstract: A classe não pode ser utilizada para criar objetos (Para acessar uma classe abstrata, ela deve ser herdada por outra classe).
Para atributos e métodos, podemos utilizar um dos seguintes:
- final: Atributos e métodos não podem ser sobrescritos/alterados.
- static: Atributos e métodos são da classe e não do objeto, ou seja, para utilizá-los não é necessário instanciar um objeto.
- abstract: Podem ser utilizados apenas em métodos de classes abstratas. Um método com este modificador não possui um corpo, o seu corpo é provido pela classe que o herda.
- transient: Atributos e métodos são ignorados quando o objeto que os contem é serializado.
- synchronized: Metódos podem ser acessados por apenas uma thread de cada vez.
- volatile: O valor deste atributo deve ser armazenado memória principal.
Podemos também encadear modificadores de acesso com modificadores de não acesso:
// HelloWorld.java
public class HelloWorld {
public static final String message = "Hello World";
}
Métodos.
Os métodos definem o comportamento de uma classe, e podem também gerir a modificação dos atributos(getters
e setters
). Para definir um método basta definir o tipo de seu retorno (Qualquer tipo primitivo, wrapper class, ou classe), o nome do método, e os parâmetros que o método recebe entre parênteses... as instruções executadas pelo método são definidas entre chaves logo após fechar o parênteses dos parâmetros.
// HelloWorld.java
public class HelloWorld {
public static final String message = "Hello World";
// Método sem parâmetros
public void sayHello() {
System.out.println(message);
}
}
Podemos também, definir um método com parâmetros ou com retorno. Podemos definir os parâmetros de uma função logo após seu identificador, entre parênteses, basta definir o tipo e o nome do parâmetro. Para retornar um valor a partir de um método, precisamos antes definir o tipo que será retornado pelo método, isso ocorre antes da definição do nome do método, no exemplo anterior utilizamos void
, que define que o método não retornará valores. Após definir o tipo de retorno, podemos utilizar a palavra-chave return
para retornar um valor. Vejamos um exemplo:
// HelloWorld.java
public class HelloWorld {
public static final String message = "Hello World";
private int count;
public String sayHello(String message) {
return "Hello " + message;
}
}
Construtores.
Mackogneur Bulbapedia
Até agora, os atributos de uma classe foram definidos de forma "hard-coded", ou seja, estão fixas na classe, e quem for utiliza-la só pode alterar esses valores se eles forem públicos, e não há nenhuma tratativa dos dados a serem alterados.
Podemos alterar este comportamento através de construtores. Construtores são métodos chamados no momento de instanciação de um objeto, eles servem para iniciar variáveis ou ainda chamar métodos importantes para a instanciação da classe, como seria o caso de uma classe BancoDeDados que precisa se conectar ao banco no momento de instanciação.
Um construtor é obrigatoriamente público e não deve ter o tipo de retorno definido, veja o exemplo:
// HelloWorld.java
public class HelloWorld {
public static final String message = "Hello World";
private int count;
public HelloWorld(int count) {
this.count = count;
}
}
Na instanciação:
// Program.java
import HelloWorld;
public class Program {
public void main(String[] args) {
HelloWorld hello = new Hello(5); // Parâmetros do construtor vão entre os parênteses.
}
}
No exemplo, é possível notar a utilização da palavra-chave this
, esta palavra-chave é utilizada para diferenciar os valores pertencentes a classe e sua instância, dos parâmetros, neste caso se fizéssemos count = count
estaríamos alterando o parâmetro do método e não o atributo da classe.
O
this
também é utilizado para retornar o próprio objeto.
Method Overloading.
Bulbapedia
Aproveitando os construtores, podemos também citar a sobrecarga de métodos. A sobrecarga de métodos é alcançada através de vários métodos de mesmo nome, mas com parâmetros diferentes, por exemplo, podemos ter um construtor de HelloWorld que aceite um int count
, e um que não aceite, dessa forma, na instanciação podemos definir um count ou não, e o método correto (com ou sem count) será chamado. Vejamos:
// HelloWorld.java
public class HelloWorld {
public static final String message = "Hello World";
private int count;
public HelloWorld() {
count = 0;
}
public HelloWorld(int count) {
this.count = count;
}
}
// Program.java
import HelloWorld;
public class Program {
public void main(String[] args) {
HelloWorld hello = new HelloWorld(); // Válido
HelloWorld hello2 = new HelloWorld(4); // Válido
}
}
Encapsulamento
Pokemon.com
Já citamos todo o básico de POO em Java, agora podemos nos aprofundar nos pilares da programação orientada a objetos. O primeiro pilar que trataremos é o Encapsulamento.
Em Java, podemos atingir o Encapsulamento utilizando o modificador de acesso private
que faz com que o atributo ou método não possa ser acessado por outras classes, assim como um Whirlipede utiliza seu casco para se proteger!
No exemplo abaixo obteremos um aviso, que diz que as variáveis privadas não foram utilizadas, mas isto é esperado. Quando declaramos uma variável pública o compilador assume que ela será utilizada, já em uma variável privada o compilador espera que em algum método exista uma referência a variável criada.
// HelloWorld.java
public class HelloWorld {
private String message;
private int count;
}
Uma das formas de retirar este warning é inicializando as variáveis por meio do construtor:
// HelloWorld.java
public class HelloWorld {
private String message;
private int count;
public HelloWorld(String message, int count) {
this.message = message;
this.count = count;
}
}
Também podemos ter métodos encapsulados, métodos privados podem ser chamados apenas por outros métodos da classe, veja:
// HelloWorld.java
public class HelloWorld {
private String message;
private int count;
private void sayHello() {
System.out.println("Hello")
}
public void say() {
SayHello();
}
}
// Program.java
import HelloWorld;
public class Program {
public void main(String[] args) {
HelloWorld hello = new HelloWorld("World", 2);
hello.SayHello(); // Erro
hello.Say(); // "Hello"
}
}
É uma boa prática manter todos os atributos privados, e caso estes necessitem de acesso externo, utilizar métodos
get
.
Métodos Getter e Setter.
Sharing is caring, by chibipirate
Ao Encapsular enfrentamos um "problema" de acesso aos dados, esse comportamento já é esperado, mas podemos querer que uma atributo seja acessível mas não alterável, alterável mas não acessível, ou ainda alterável e acessível.
Os Getters
e Setters
vêm para prover essa funcionalidade. Através de um método get
podemos gerenciar a forma com que um atributo é apresentado fora de seu escopo, com um método set
podemos tratar o dado antes de armazena-lo, por exemplo, aplicando um imposto sobre um atributo salário, ou convertendo uma moeda para outra antes de armazenar em uma variável de saldo bancário.
- Por convenção, os métodos
getters
esetters
devem ser chamadosgetNomeDaVariavel
ou seja, iniciando comget
ouset
e então a variável a qual eles estão relacionados.
O get
precisa retornar um valor do tipo da variável relacionada, o set
precisa receber um parâmetro do tipo da variável com a qual está relacionado.
// HelloWorld.java
public class HelloWorld {
private String message;
private int count;
public String getMessage() {
return this.message;
}
public void setMessage(String message) {
this.message = message;
}
public int getCount() {
return this.count;
}
public void setCount(int count) {
this.count = count;
}
}
Herança.
A Herança é uma das formas da POO reduzir a repetição de código. Através da herança, podemos fazer com que uma subclasse herde os atributos e métodos de uma superclasse.
Para utilizar a herança, basta incluir a palavra-chave extends
e a superclasse a qual herdaremos. Vejamos um exemplo com uma superclasse Account
e duas subclasses, ContaFisica
e ContaIndividual
:
A superclasse:
// Conta.java
public class Conta {
private Integer numero;
private String dono;
private Double saldo;
private Double limiteSaque;
public Conta() {
}
public Conta(Integer numero, String dono, Double saldoInicial, Double limiteSaque) {
this.numero = numero;
this.dono = dono;
this.limiteSaque = limiteSaque;
this.saldo = saldoInicial;
}
// getters e setters
// ...
public void deposito(double amount) {
quantia += amount;
}
public void saque(double quantia) {
quantia -= quantia;
}
}
As subclasses:
// IndividualAccount.java
public class ContaFisica extends Conta {
}
// ContaJuridica.java
public class ContaJuridica extends Conta {
}
Somente a palavra extends
já nos garante acesso aos métodos definidos em Account. Porém temos um problema, apesar de a subclasse poder acessar os métodos, os atributos estão invisíveis a ela, pois a superclasse os definiu como private
. Para a subclasse poder acessar os atributos de uma superclasse, o modificador de acesso deve ser protected
.
Além disso, se tentarmos inserir novos atributos para a subclasse, teremos que recriar o construtor, o que fará com que o construtor da superclasse seja ignorado e por consequência, as variáveis da superclasse não serão inicializadas. Para evitar este problema, temos acesso a função super()
, através do super
podemos chamar o construtor da superclasse, ou qualquer outro método, passando os parâmetros.
Para exemplificar, criaremos um atributo imposto na classe ContaFisica
:
// ContaFisica.java
public class ContaFisica extends Conta{
private double imposto;
public ContaFisica(Integer numero, String dono, Double saldoInicial,
Double limiteSaque, double imposto) {
// super é chamado passando os parâmetros.
super(numero, dono, saldoInicial, limiteSaque);
this.tax = tax;
}
}
Como podemos ver, o código nas subclasses foi extremamente reduzido graças a herança. A superclasse armazena os atributos e métodos comuns, e a subclasse armazena os métodos específicos. Vale ressaltar que o Java não permite herança múltipla, ou seja, não podemos estender de mais de uma classe, o motivo disto é o Problema do Diamante.
O Problema do Diamante.
Este problema está relacionado diretamente a herança múltipla, podemos pensar nele da seguinte forma:
Se uma classe Button
é subclasse de uma classe Rectangle
e uma Classe Clickable
ao mesmo tempo, e ambas implementam um método equals
qual implementação desse método a subclasse deverá herdar? A de Rectangle
ou a de Clickable
? Para evitar este conflito, o Java não permite heranças múltiplas, mas existem algumas gambiarras a qual explicaremos mais adiante.
Polimorfismo.
Gengar Ditto By Devin
A Herança, citada anteriormente, é o que permite o funcionamento deste pilar. O Polimorfismo é a capacidade que uma subclasse tem de "se passar" por sua superclasse. Para exemplificar, podemos dizer que todo Squirtle é um Pokémon, e como tal, pode se alimentar, se reproduzir, usar habilidades, e também entrar em pokébolas.
Para demonstrar isto, criamos 2 classes, além de um método main para testar:
// Nossa superclasse
public class Pokemon {
protected String nome;
protected Integer forca;
public Pokemon(String nome, Integer forca) {
this.nome = nome;
this.forca = forca;
}
public void comer() {
System.out.println(nome + " está comendo!");
}
}
// Nossa subclasse
public class Squirtle extends Pokemon {
public Float multiplicadorAgua = 0.25f;
public Squirtle(String nome, Integer forca) {
super(nome, forca);
}
@Override
public void comer() {
System.out.println("Squirtle come uma berry!");
}
}
Podemos observar que Squirtle é um Pokemon, pois o "estende". Vamos ver o Polimorfismo em funcionamento agora:
public class Program {
public static void main(String[] args) {
// Instanciação Normal
Squirtle squirtleDeOculos = new Squirtle("Squirtle de Óculos", 10);
squirtleDeOculos.usarWaterCannon();
// Squirtle de Óculos causou 10 pontos de dano!
squirtleDeOculos.comer();
// Squirtle come uma berry!
System.out.println(squirtleDeOculos.multiplicadorAgua);
// 0.25
// Upcast
Pokemon pokemon = squirtleDeOculos;
pokemon.comer();
// Causam erro
// pokemon.usarWaterCannon();
// System.out.println(pokemon.multiplicadorAgua);
//Downcast
Squirtle mesmoSquirtle = (Squirtle) pokemon;
mesmoSquirtle.usarWaterCannon();
mesmoSquirtle.comer();
System.out.println(mesmoSquirtle.multiplicadorAgua);
}
}
No começo do método instanciamos um novo Squirtle "Squirtle de óculos". Squirtle de óculos usa uma habilidade, come uma berry e tem acesso ao seu multiplicador de dano aquático.
Mas repetindo, todo Squirtle é um pokémon, certo? Então fazemos um "Upcast", armazenando a superclasse em uma variável, com isto, perdemos acesso aos métodos e atributos da classe Squirtle, mas mantemos os Overrides(sobreescrita de métodos)... Ou seja, Squirtle pode ainda pode comer uma berry, mas é incapaz de usar sua habilidade e ter acesso ao seu multiplicador de dano aquático.
Mais a frente, realizamos o processo inverso, e os olhos mais atentos podem observar uma sintaxe diferente, por quê? Porque o compilador é capaz de ver que um Squirtle é um Pokémon, pois sabe que esta é sua superclasse, mas como ele irá ter certeza de que um Pokémon é um Squirtle? Talvez ele seja um Charmander... Para dizer ao compilador que sabemos que este pokémon específico é um Squirtle, temos que passar a subclasse entre parênteses, forçando a conversão.
Todo gato é um animal, mas nem todo animal é um gato!
Professores de Ciências da Computação e áreas correlatas.
Beleza, mas porquê eu iria querer perder dados com um Upcast?
by tctreasures
Existem situações, em que precisamos agrupar todas as subclasses de determinada superclasse. Como uma Box do PC Pokémon, utilizada para armazenar Pokémons. Em Java, como poderíamos utilizar uma List para representar uma Box? Não podemos dizer que o List é do tipo Squirtle, pois assim ela só poderia armazenar Squirtles, muito menos poderíamos dizer que o List é do tipo Object, pois desta forma a Box poderia também armazenar sapatos, ou qualquer outra coisa! Neste caso, o melhor é utilizar uma superclasse comum entre os objetos:
import java.util.ArrayList;
import java.util.List;
public class Program {
public static void main(String[] args) {
// Instanciação Normal
Squirtle squirtle = new Squirtle("Squirtle de Óculos", 10);
Charmander charmander = new Charmander("Charmander de tutu", 15);
squirtle.comer();
// Squirtle come uma berry!
charmander.cuspirChama();
// Charmander use Ember!
List<Pokemon> box1 = new ArrayList<>();
box1.add(squirtle);
box1.add(charmander);
((Squirtle) box1.get(0)).comer();
// Squirtle come uma berry!
}
}
Acima podemos ver dois exemplos, um Charmander e um Squirtle são adicionados a mesma box, o que só é possível graças ao tipo "Pokémon" desta lista! Ainda podemos ver que é possível realizar o Downcast para recuperar a classe original após o Upcast. Também podemos observar que, sempre que utilizamos uma lista, fazemos uso do Polimorfismo!
Abstração.
Pokemon.com
O ultimo pilar da POO é a abstração, ela consiste em definir contratos que uma classe deve seguir, por exemplo uma subclasse de Formaobrigatoriamente deve ter um tamanho(largura e altura ou raio) e uma forma de calcular sua área. Temos duas formas de definir um contrato Forma, e passaremos por cada um deles. A Abstração ajuda a esconder e separar os detalhes e mostrar apenas o que é necessário.
Abstract classes.
Uma classe abstrata consiste em uma classe com o modificador abstract
, uma classe abstract pode ter tantos métodos sólidos e abstratos quanto forem necessários mas, se houver um método abstrato que seja, a classe também deve ser abstrata.
Um método abstrato consiste na assinatura de um método, ou seja, seus modificadores + seu tipo de retorno + identificador + parâmetros, sem o corpo da função(as chaves e seu conteúdo).
No caso da classe Forma, poderíamos torná-la abstrata, veja:
// Forma.java
public abstract class Forma{
public abstract double area();
}
Para utilizarmos esta classe abstrata, basta herdá-la:
// Retangulo.java
public class Retangulo extends Forma {
private Double largura;
private Double altura;
public Retangulo(Double largura, Double altura) {
this.largura = largura;
this.altura = altura;
}
@Override
public double area() {
return altura * largura;
}
}
Também podemos ter uma classe Circulo
:
public class Circulo extends Forma{
private Double raio;
public Circulo(Double raio) {
this.raio = raio;
}
@Override
public double area() {
return 3.1415 * Math.pow(raio, 2);
}
}
Podemos ver que todas as classes que herdam Shape são obrigadas a implementar o método abstrato a sua maneira. Vale lembrar que classes abstratas não podem ser instanciadas, seu intuito é servir como uma superclasse apenas.
Por quê utilizar uma classe abstrata?
Podemos, em algum momento desejar que classes sigam algum padrão, mas tenham alguns de seus próprios métodos. No nosso exemplo, queremos que qualquer classe que estenda Forma seja capaz de calcular sua própria área, mas não sabemos quais serão suas formas de medida, por isso passamos a ela a responsabilidade de implementar isto. Uma classe Circulo que estende Forma, é incumbida da responsabilidade de dizer como calcular sua área, neste caso, utilizando seu atributo raio; Assim como uma classe Quadrado, que estende forma, deve dizer sua área de acordo com o tamanho de seu lado.
Interfaces.
By Supersirius
Interfaces são como classes abstratas, porém elas permitem implementação múltipla, ou seja, podemos implementar diversas interfaces(A gambiarra a qual citamos sobre o problema do diamante 😅).
Diferentemente das classes abstratas, interfaces possuem apenas métodos sem corpo. Uma interface não pode definir variáveis, a menos que sejam final
.
Vejamos um exemplo de Interface para Pagamentos Online, vejamos:
// ServicoInvestimentoOnline.java
public interface ServicoInvestimentoOnline{
public double taxa(double quantia);
public double lucro(double quantia, int meses);
}
Agora vejamos um exemplo da implementação:
// ServicoInvestimento.java
public class ServicoInvestimento implements ServicoInvestimentoOnline{
private static final double PORCENTAGEM_TAXA= 0.02;
private static final double LUCRO_MENSAL = 0.01;
@Override
public double taxa(double quantia) {
return quantia*PORCENTAGEM_TAXA;
}
@Override
public double lucro(double quantia, int meses) {
return quantia * LUCRO_MENSAL * meses;
}
}
Podemos ver acima que, não utilizamos a palavra-chave extends
e sim implements
. Vale notar também que todos os métodos de uma interface devem ser implementados.
Ao utilizar interfaces podemos implementar varias delas em uma subclasse, basta separar por ,
. Também é possível estender uma classe, basta manter a ordem extends
e depois implements
.
public class Circle extends Shape implements Colored, Drawable {
// ...
}
So tell me WHYYYY????
Acho que me confundi, U've been RickRolled while Backstreet Boyed.
Não é um dor no chifre, interfaces facilitam a leitura de um código, podendo ser utilizadas como descrições de métodos. Mas não se resumem a isso, uma biblioteca de terceiros pode ter um método que aceita um objeto filho de determinada interface, e através do polimorfismo sabemos que qualquer classe que seja recebida no método, terá todos os métodos da interface.
Considerações Finais
Esse artigo começou a ser escrito mais ou menos no dia 16/11/2020, na semana em que começou meu estágio como desenvolvedor Java EE, no periodo de treinamento. Foi me dado um prazo de 2 semanas para dominar POO(mas eu trapaceei e comecei a estudar 2 semanas antes, deal with it). Felizmente, consegui um resultado satisfatório em 1 semana e passei a mergulhar no mundo JSF com Spring. Por volta do dia 27 de Novembro, ouvi do meu Gestor que havia a intenção de me efetivar e, em 05 de Janeiro, eu fui efetivado como Desenvolvedor Assistente, agora após 45 dias como efetivo, tomei coragem de terminar este artigo.
Agradeço muito por participar de uma equipe prestativa, que não poupa esforços em demonstrar satisfação com meu trabalho. Diariamente aprendo milhares de coisas novas, não só durante os momentos de código e pesquisa, mas também nas conversas com os colegas de equipe. Sem dúvidas, isso não seria possível sem o esforço que fiz até o momento, foram 8 anos estudando mais de 7 Linguagens de programação diferentes, inúmeras ferramentas e 2 anos entregando currículos. A intenção desse Post é poupar de você, algum tempo que perdi sem entender Programação Orientada a Objeto, sem dúvidas alguma, esse paradigma me ajudou a entender melhor os Design Patterns, e melhorar a qualidade do meu código.
Como exemplo de que nunca sabemos tudo, na primeira versão deste artigo, eu havia dito que Polimorfismo era análogo a Method Overloading, fui descobrir a verdade durante o treinamento do estágio e desde então venho procrastinando para terminar isso aqui.
Espero que você tenha gostado, tentarei driblar a falta de tempo e trazer mais alguns conteúdos que considero importantes, tanto para quem está começando, quanto para quem já está na área.
Gostou do que viu? Considere um café :)
https://ko-fi.com/immurderer
Top comments (0)