Java 101

OO: Eu vejo objetos por todos os cantos!

OO: Eu vejo objetos por todos os cantos!


Java tem um fã clube enorme! São pessoas que usam a linguagem no dia a dia e resolvem problemas importantes para a nossa sociedade. Quando Java completou 25 anos houve até a hashtag #MovedByJava para mostrar que o mundo é movido por software desenvolvido em Java, são bilhões de transações em Java em serviços altamente escaláveis.

MAS… existe um pequeno grupo raivoso e ruidoso que odeia Java. Eu não desejaria nem citar esse grupo, mas creio que isso tem que estar em qualquer tutorial de Java, não para dar voz a esse povo, mas para desmentir. Java não é lento, talvez você que não está sabendo usar e vamos mais a frente falar sobre tuning. Essas pessoas usam argumentos bem simples como “tudo tem que estar em objetos”, “eu tenho que escrever um main dentro de um objeto”, “nada disso faz sentido”… Resolvi citar eles aqui, porque eles não odeiam Java, eles odeiam Orientação a Objetos e com esse post eu vou te convencer que além de ser uma ótima forma de pensar, Orientação a Objetos ajudou a pavimentar os outros paradigmas que estão por aqui no ano de 2022.

Um pouco de história

Orientação a Objetos surgiu nos anos 60 e era usado para fazer simulações no Simula 67. Esta linguagem, por sua vez, acabou por influenciar o C++ (1979), que na verdade é uma tentativa de adicionar objetos a linguagem C. Por muitos anos o C++ foi uma das linguagens mais influentes do mercado, ela não era, puramente, uma linguagem orientada a objetos, era até possível intercalar código C com código C++. A primeira linguagem que surge como puramente orientada a objetos e ainda por cima compilada em bytecode para ser executado em uma Máquina Virtual foi…

PAUSA DRAMÁTICA… 🥶

O Smalltalk! O que foi? 🧐 Achou que era o Java? O Java só surge em 1991, e em seu lançamento em 1995, e acaba herdando muitas características do Smalltalk, tanto que muitas pessoas da comunidade Java vieram do mundo Smalltalk. E uma das coisas que Java herda é ser primariamente orientada a objetos.

— Mas porque essa preocupação em ser Orientada a Objetos?!

Porque na verdade a computação não começou com essas linguagens e nem com esses paradigmas, mas como Programação Funcional (ver Conception, evolution, and application of functional programming languages). Linguagens funcionais são excelentes para modelarem problemas matemáticos e alguns problemas computacionais, pois elas são declarativas. Podemos transpor a definição de um problema para a linguagem de programação facilmente, podendo até mesmo aplicar uma lógica equacional, pois, se as funções são puras, o valor de f(x) só precisa ser calculado uma vez. Logica equacional é o mesmo que tratar uma função como uma equação matemática, isso implica que símbolos iguais terão valores iguais.

MAS linguagens funcionais apresentam uma certa dificuldade de modelar alguns tipos de sistemas e, com a popularização da computação, foi necessário outros paradigmas para os novos sistemas que foram sendo desenvolvidos. O primeiro desses paradigmas foi a Programação Procedural, popularizado pela linguagens como C. Nesse tipo de linguagem a lógica de programação pode ser estruturada dentro de procedimentos que podem ser tanto funções quanto procedimentos, a diferença entre os dois é que uma função não altera o valor dos parâmetros e sempre retorna um valor, já os procedimentos alteram o valor dos parâmetros e não retornam nenhum valor. Em C, podemos escrever tanto funções quanto procedimentos.

Linguagens procedurais apresentam bastante dificuldade para encapsular complexidade porque é difícil criar abstrações com ela. Em C, os dados são sempre modelados usando tipos primitivos ou estruturas, que nada mais são que agrupamentos de tipos primitivos. Mesmo quem desenvolve C hoje em dia, não consegue compreender o que era desenvolver nos anos 70, pois a linguagem continuou avançando. Eu tenho uma leve ideia porque, na universidade, desenvolvi programas para um microcontrolador com o compilador bem limitado. O exemplo abaixo eu retirei de um visualizador de CSV que eu desenvolvi por necessidade.

matrix_config_t *matrix_config_initialize(size_t width, size_t height)
{
    matrix_config_t *config = (matrix_config_t *)malloc(sizeof(matrix_config_t));
    config->columns = width;
    config->heights = height;
    config->column_width = (size_t *)calloc(width, sizeof(size_t));
    config->line_height = (size_t *)calloc(height, sizeof(size_t));
    return config;
}

Observe que no código há a preocupação de se alocar a posição de memória necessária para uma determinada estrutura chamada matriz_config_t e que essa alocação é feita através de duas funções diferentes calloc e malloc. Esse código pode parecer simples, mas tem diversidades camadas de complexidades, como simplesmente diferenciar essas duas funções.

— Aonde você quer chegar?!?!

Ora! Qual é o objetivo de um desenvolvedor?

— Escrever código!

Errado! O objetivo de um desenvolvedor é resolver problemas através da escrita de código. Por isso, desenvolvedores não podem e não devem ficar preocupado com complexidades desnecessária. É para remover essa complexidade que surgem linguagens Orientadas a Objetos. As linguagens procedurais são simples e com poucas funcionalidades, por isso toda a informação é armazenada de forma simplória em estruturas. Isso gera complexidade e o objetivo principal de uma linguagem de programação é encapsular complexidade.

Vamos tentar explicar de outra forma…

O que é Programação Orientada a Objetos

Vamos estabelecer algumas hipóteses….

E se eu puder lidar com tipos complexos

Uma linguagem OO irá sempre lidar com tipos de dados cada vez mais complexos pois não estamos apenas falando de programação, mas de encapsulamento de complexidades.

Vamos supor que eu desejo desenvolver uma API para navegação robótica e eu sei que meu robô tem 4 rodas e que posso definir a velocidade de cada roda. Será que eu preciso saber qual é a velocidade de cada roda? Ou eu posso apenas mandar comandos para o meu robô? Um exemplo de comandos são: “Vá para frente”, “Faça uma curva de 30º”, “Pare”.

Quando falamos de Orientação a Objetos, devemos pensar em design de código. Não estamos falando de programação pura, mas de uma modelagem de dados e conceitos. Os detalhes internos devem ser escondidos para quem só sejam visíveis para os próprios times de manutenção.

E se eu puder associar comportamento aos meus tipos complexos

Todo código tem um contexto para ser executado. Quando eu tenho um robô e eu desejo que ele vá para uma posição, se essa ordem é diferente para cada robô e produz diferentes resultados, mas ela sempre está associada a um robô, ou seja, não faz sentido um outro objeto que não seja um robô se mover (ainda falarei de herança).

Mas podem existir outros objetos que se movem, certo? E como fica se a função mover é somente associada a robôs?

Em uma linguagem orientada a objetos, não temos funções e nem procedimentos, mas métodos. A diferença é que uma função transforma dados, procedimentos executa uma série de alterações nos parâmetros, mas um método envia uma mensagem (essas definições não tem caráter acadêmico, se alguém tiver alguma referência me manda no Twitter). Logo um método vai pertencer a um objeto, assim se formos modelar um Avião, podemos criar um outro método mover que existirá somente para um Avião e que será diferente do método mover de um robô.

E se puder compartilhar o comportamento entre tipos diferentes

Objetos tem um tipo especifico, por exemplo nós estamos falando de um robô. Mas eu posso assumir que um robô é um tipo de objeto móvel? Posso eu criar métodos nesse objeto móvel? O que essa implementação desse método faz para um robô é a mesma coisa que se faz para um avião?

Para cada pergunta acima, existe uma resposta no mundo da Programação Orientada a Objetos e é o que vamos ver na próxima sessão.

Orientação a Objetos

Nós falamos um pouco sobre Programação Funcional e Programação Procedural, então vamos definir o que é Programação Orientada a Objetos (POO) antes de ver como Java faz POO.

Programação Orientada a Objetos é um modelo de design, analise e desenvolvimento de software que organiza todo o software ao redor dos dados e suas abstrações. Para que isso seja possível, é criado o conceito de Objeto. Um objeto é um componente de software composto de atributos e comportamento.

Quando falamos de orientação a objeto, focamos na definição do que é um objeto e das operações que esse objeto pode realizar, ao contrário da lógica necessária para realizar a operação. Os principais benefícios da POO é a reutilização de código, escalabilidade e eficiência no desenvolvimento. Então podemos definir que POO vai ter alguns elementos.

Elementos

Abaixo vemos as descrições de cada elemento da POO, elas não se referem a linguagem Java, mas ao paradigma em si.

Classes

Classes são tipos de dados definidos pelo usuário que atuam como modelo para objetos, atributos e métodos.

Objetos

Objetos são instâncias de uma classe criada com dados específicos.

Métodos

Métodos são funções definidas dentro de uma classe que descrevem o comportamento de um objeto. Cada método contido nas definições de classe começa com uma referência a um objeto de instância. Além disso, as sub-rotinas contidas em um objeto são chamadas de métodos de instância. Os programadores usam métodos para reutilização ou para manter a funcionalidade encapsulada dentro de um objeto por vez.

Atributos

Atributos são definidos no modelo de classe e representam o estado de um objeto. Os objetos terão dados armazenados no campo de atributos. Os atributos de classe pertencem à própria classe.

Princípios

Quando falamos em Orientação a Objetos, temos em mente alguns princípios.

Encapsulamento.

Encapsulamento significa que um objeto não é obrigado a expor a sua implementação e nem os seus atributos. Cabe ao design do objeto escolher como será feita essa exposição. Essa característica de ocultação de dados fornece maior segurança ao programa e evita corrupção de dados não intencional.

Abstração

Objetos criam abstrações que tornam possível controlar a complexidade. Ao se criar uma classe, o restante do sistema deverá interagir através da interface que ela propõe não tendo acesso a sua lógica interna.

Herança

As classes podem reutilizar o código de outras classes. Relacionamentos e subclasses entre objetos podem ser atribuídos, permitindo que os desenvolvedores reutilizem a lógica comum enquanto ainda mantêm uma hierarquia única. Essa propriedade da OOP força uma análise de dados mais completa, reduz o tempo de desenvolvimento e garante um maior nível de precisão.

Polimorfismo

Os objetos são projetados para compartilhar comportamentos e podem assumir mais de uma forma. O sistema poderá definir como vê um objeto e como interage por ele baseado na sua própria classe ou em alguma classe pai, reduzindo a complexidade ou a necessidade de duplicar código. Quando uma classe filha é criada, que estende a funcionalidade da classe pai, ambas podem ser tratada pelo mesmo código usando a classe pai como interface. O polimorfismo permite que diferentes tipos de objetos usem a mesma interface.

Como Java faz Programação Orientada a Objetos

Java é uma linguagem primariamente orientada a objetos, logo você deve primeiro entender o que é uma classe. Classe é o arquétipo de um objeto. Arquétipo, resumidamente, é o tipo comum de algo. Por exemplo, se eu falar que existe o tipo Gato, você vai imaginar o formato desse animal e algumas outras características, mas se eu falar que existe o Garfield você vai imaginar que ele é um Gato laranja, gordo e preguiçoso. O Garfield é um indivíduo do arquétipo Gato.

Significado de Arquétipo da Wikipedia

Vamos transpor isso pra Java? Podemos ter uma classe Gato, mas o objeto será um Garfield. Assim, podemos ter…

package org.animais.mamiferos;

import org.fisica.luz.Cor;
import org.animais.psique.Temperamento;

public class Gato {
    private float pesoEmKg;
    private final Cor cor;
    private Temperamento temperamento;
    public Gato(float pesoEmKg, Cor cor, Temperamento temperamento) {
        this.pesoEmKg = pesoEmKg;
        this.cor = cor;
        this.temperamento = temperamento;
    }

    // MÉTODOS
}

Isso significa que podemos modelar qualquer Gato por esse modelo, assim se quisermos ter um Garfield…

Gato garfield = new Gato(15.0, Cor.LARANJA, Temperamento.PREGUICOSO);

No primeiro trecho de código tempo a declaração da classe Gato no pacote org.animais.mamiferos. Isso significa que só pode existir um tipo de Gato nesse pacote, mas isso não implica que eu possa criar o tipo Gato para descrever, por exemplo, instalações elétricas não-oficiais, que obviamente não fazem parte do pacote org.animais.mamiferos, mas org.humanos.civilizacoes.brasil.infraestrutura. Classe é usada para definir o tipo do objeto, mas o pacote é o contexto na qual ele existe. Classe e Pacote tem uma relação umbilical, uma Classe sempre deve estar ligada a um Pacote.

A segunda coisa que vamos detalhar nesse trecho de código são os modificadores de acesso. Como disse uma linguagem orientada a objetos é usada para se encapsular detalhes, logo os modificadores de acesso servem para definir quem pode acessar o quê. Eles podem ser aplicados para Classes, Métodos e Campos e existem os seguintes modificadores de acesso.

Tipo token Descrição
Package Private - Define que o elemento será acessível dentro do pacote. Esse é o modificador padrão, isso significa que nesse caso pode ser omitido.
Privado private Define que o elemento só pode ser acessado dentro da própria classe.
Protegido protected Define que o elemento é acessível dentro do mesmo pacote ou através de herança.
Público public Define que o elemento é acessível em qualquer contexto.
Final final Se aplicada a classe, ela não poderá ser estendida. Se aplicada a um campo ele não poderá ter seu valor alterado. Se aplicado a um método, ele não poderá ser reimplementado em uma classe que herda ele.
Estático static Pode ser usado tanto em campos como em classes internas. Se usado no campo, ele vai ter apenas um valor e está associado a classe. Campos não estáticos são associados a objetos. Se aplicado a classes internas, ela não dependerá de um objeto.

Ainda existem dois mais dois modificadores (volatile e transiente), mas eles não são importantes quando falamos de OO. transiente será importante quando falarmos de serialização e volatile quando falarmos de threads. Dos outros, podemos agrupar o private, protected, public e a ausência de um desses, pois eles são mutualmente excludentes.

O próximo ponto que podemos falar é sobre métodos. Em Java não é comum termos funções puras, nem linguagem está preparada para isso. Temos basicamente dois tipos de métodos. Os métodos de instância são aqueles que são associados a um objeto. E os métodos estáticos são aqueles associados a uma classe, sem depender de uma instância. Conseguimos criar métodos estáticos usando o modificador de acesso static. Quando um método não é estático, podemos usar this para se referir a instância com a qual o método é associado.

Métodos sempre tem parâmetros e valor de retorno (pode ser void que significa um vazio existencial, diferente do vazio de posição que é a palavra empty). Métodos de instância sempre vão te acesso a um objeto específico (usando o this), enquanto métodos estáticos não o são.

Vamos ver melhor como os métodos funcionam? E se nós criássemos 3 métodos na nossa classe gato. O primeiro seria um método para mesclar características de 2 gatos, o segundo seria o método meow e o terceiro o método de reprodução (cruza).

public class Gato {
    public static Gato mistura(Gato gatoA, Gato gatoB) {
        // Mágica acontece
        return gatoC;
    }

    // Campos, construtores, getters e setters

    public void meow() {
        System.out.println("Miau!");
    }

    public Gato cruza(Felino outro) {
        if ((!(outro instanceof Gato)) || sexo == outro.sexo) {
            throw new CruzamentoException("Não é possível gerar filhote!");
        }
        return mistura(this, outro);
    }
}

O método meow é o exemplo clássico que veremos em herança, ele não retorna nada, só executa uma ação. Aqui vamos focar nos métodos cruza e mistura (ok, focar na parte reprodutiva foi péssimo… mas estou falando de gatos!). mistura é um método que aleatoriamente vai gerar um novo gato baseado nas características de dois gatos. Nele podemos ver que o método recebe dois parâmetros e retorna um valor. No caso desse método, estamos retornando um novo objeto, mas nada impede de o retorno ser um dos parâmetros. Outra característica é que os parâmetros são uma passagem por referência e não por valor como vamos ver um pouco mais a frente. Sobre o método cruza, nele podemos acessar os campos do objeto local e campos da referência. Quero ressaltar o uso do this que é a forma de acessar a referência ao objeto pela qual o método é referenciado, o this não pode ser usado para métodos estáticos.

Como Java implementa Herança

Falamos sobre classes e alguns detalhes, mas agora precisamos falar de herança.

Temos 3 tipos de classe: a Classe, a Interface e a Classe Abstrata.

— Peraê! Mas como uma classe pode ser também Interface e Classe Abstrata?!?!? Tem algum erro lógico nessa afirmação!

Não! Segura essa informação que quando formos falar sobre Reflexão trataremos do conceito interno de Classe. Por enquanto aceite que existem três tipos de classe e um deles é classe. 🤷‍♂️

A Interface é quando tempos um contrato de como uma classe deve ser implementada. Ela vai definir a assinatura de alguns métodos. Por assinatura entenda que é a forma como a JVM usa para identificar um método, ela é composta pelo nome do método e a lista de parâmetros. O tipo de retorno não faz parte de uma assinatura e isso vai ser importante mais a frente. Uma interface também pode definir métodos default e métodos static. Uma interface normalmente é usada para definir um tipo, ou comportamento, comum dentro de um sistema.

Uma classe abstrata é uma classe que não pode ser instanciada. Normalmente usamos quase abstrata quando desejamos compartilhar comportamento entre vários tipos. Em uma classe abstrata podemos definir variáveis e métodos, mas também podemos definir métodos abstratos (usando o modificador abstract). Ao se declara um método abstrato, estamos declarando apenas a assinatura, a implementação ficará a cargo de alguma classe que estende nossa classe abstrata.

E por fim uma classe é uma implementação pela qual podemos instanciar objetos. Classes podem ser estendidas também quando queremos modificar um comportamento específico. Por exemplo, e se quisermos modificar a forma como o Garfield mia?

Gato garfield = new Gato(15.0, Cor.LARANJA, Temperamento.PREGUICOSO) {
    public void meow() {
        System.out.println("Miaaaaaaau!");
    }
};

Quando adicionamos um bloco de código lodo após a instanciação da classe, estamos criando uma classe anônima. Esse comportamento será especifico dessa instância. Nós poderíamos evitar isso usando o modificador final no método ou na classe. Se usarmos no método, nenhuma subclasse poderá estender esse método, mas se usarmos na classe, ela não poderá ser estendida.

Quando falamos de herança normalmente usamos as palavras estende e implementa. Estende é quando temos uma classe abstrata sendo estendida, e isso é feito usando a palavra reservada extends. Já implementa é quando temos uma interface sendo implementada pela classe, a palavra reservada implements.

O Java tem algumas limitações em heranças. Uma classe SÓ pode estender uma classe, mas pode implementar quantas interfaces forem necessárias. MAS interfaces com mesma assinatura e tipo de retorno diferentes não são possíveis de serem implementas por uma mesma classe. No caso abaixo, temos que um Gato estende um Felino e implementa as interfaces Miador e Ronronador.

public class Gato extends Felino implements Miador, Ronronador {
    // Implementação
}

Conceitos da Orientação a Objetos

Agora vamos discutir alguns conceitos comuns da orientação a objetos que podem nos auxiliar no dia a dia.

Herança

Para entender herança, podemos pensar em herança genética. Todo objeto ele tem um arquétipo e ele vai possuir uma hierarquia de tipos. Um Gato é um Felino que é um Animal. Cada uma dessas classes podem ter comportamentos associados ou apenas assinaturas de métodos. Se voltarmos no post anterior, sobre a biblioteca Collections, vamos ver o mais comum tipo de herança.

Pacote Java Collections

Vamos ver o caso da LinkedList que estende uma AbstractSequentialList e implementa as interface List, Deque, Cloneable e Serializable.

LinkedList é uma classe, AbstractSequentialList é uma classe abstrata e List uma interface. AbstractSequentialList contém uma implementação de lista que por sua vêz estende uma AbstractList. Podemos dizer que LinkedList herda implementações de AbstractSequentialList e AbstractList. Assim como podemos dizer que LinkedList e ArrayList herdam implementações de AbstractList mesmo tendo comportamentos completamente diferentes.

Da mesma forma LinkedList e ArrayList são tipos de List, enquanto apenas LinkedList é um tipo de Deque.

Quando temos uma classe que herda tipos de outras classe, podemos definir nossos objetos com o tipo que desejarmos. Eu recomendo sempre usar a interface que você deseja usar e não a implementação final. Quer um exemplo? Vamos imaginar que eu quero definir um método que fará uma busca especifica pelo Gato mais gordo. Ao invés de declarar que desejo receber uma LinkedList, posso declarar que desejo receber apenas uma List.

public class Gatos {
    public static Gato maisGordo(List<Gato> gatos) {
        // encontra o Garfield aqui que não tem erro.
    }
}

Uma dúvida clássica é se perguntar porque não devo usar o tipo mais específico. Nunca devemos usar as classes porque isso limita o uso do nosso código. Ao usar um List, eu posso aceitar qualquer implementação de List, mesmo implementações que eu não conheço. Essa preocupação será muito mais real quando estivermos falando de frameworks em que a geração de código ou classes do tipo proxy são comuns.

Override

Chamamos de Override a prática de sobrescrever implementações de métodos em classes filhos. Vamos voltar ao nosso exemplo de Gatos, e se existe uma raça especifica de gatos que não mia, são gatos mudos. Como esse característica é muito especifica mas ele definitivamente são gatos, podemos criar essa nova classe de gatos e sobrescrever o método.

public class GatoMudo extends Gato {

    @Override
    public void meow() {
        System.out.println("."); // . significa silêncio
    }

}

Se tivermos um objeto da classe GatoMudo, mesmo que ele esteja definido como Gato, será chamado o método da classe GatoMudo.

O uso da anotação @Override não é obrigatório, mas é altamente recomendável.

Overload

Chamamos de Overload quando criamos um novo método para um tipo diferente de parâmetros. Essa técnica é excelente quando queremos criar métodos semelhantes para tipos diferentes. Vamos supor que nosso método de mistura vai ser migrado para a classe abstrata de animais e que queremos criar esse método para alguns tipos de animais, não para todos, mas ele será diferente para alguns grupos (tem animal que se divide e não reproduz). Assim podemos criar um método mistura para os tipos Mamifero, Ave, Reptil e Peixe, cada método terá uma implementação completamente diferente.

public class Gato {
    public static Mamifero mistura(Mamifero mamiferoA, Mamifero mamiferoB) {
        // Mágica acontece
        return mamiferoC;
    }

    public static Ave mistura(Ave aveA, Ave aveB) {
        // Mágica acontece
        return aveC;
    }

    public static Reptil mistura(Reptil reptilA, Reptil reptilB) {
        // Mágica acontece
        return reptilC;
    }

    public static Peixe mistura(Peixe peixeA, Peixe peixeB) {
        // Mágica acontece
        return peixeC;
    }

Nós fizemos overload de um método estático, mas poderíamos ter feito de um método de instância.

HashCode, Equals e ToString

Uma outra reclamação constante de quem não gosta de Java é a necessidade de se implementar esses três métodos que as vezes parecem inúteis.

Primeiro devemos esclarecer que hashCode, equals e toString são métodos extremamente úteis e usados constantemente pela JVM. É sempre recomendável a leitura da documentação da classe Object sobre esses três métodos.

hashCode é um método usado para o calculo do Hash do objeto. O hash é um valor inteiro que será usado para identificar cada objeto. Dois objetos iguais devem ter o mesmo hash, mas dois objetos com o mesmo hash não são iguais. Toda e qualquer classe usando o nome Hash usar esse método, assim se você tem um HashMap ou um HashSet, você tem o uso do método.

equals é um método usado para se verificar um objeto é igual a outro. Ele é usado por várias algoritmos da JVM, as vezes associado com o hash ou sem associação. Quando temos um HashMap os dois métodos são usados. O equals é usando quando temos o que chamamos de Colisão de Hash, dois objetos diferentes que tem o mesmo hash.

toString é usado para se criar um valor String para a classe. Sempre implemente o toString para melhorar o rastreamento de erros em logs de execução.

Passagem por valor e Passagem por referência

Quando estudamos linguagem como C, estudar o tipo de passagem como argumento de uma função é muito importante, porque é possível controlar o que queremos fazer ao se escolher o tipo de parâmetro. Já em Java não nos preocupamos muito, mas em ambas a linguagem temos a possibilidade de se passar um argumento como valor ou como referência. Vamos primeiro definir para depois mostrar como pode ser feito?

Falamos de Passagem por valor de um argumento para uma função quando ao se alterar o valor desse argumento dentro de um função, essa alteração não é refletida fora da função. Já quando falamos de Passagem por referência de um argumento, ao se alterar o valor desse argumento dentro da função ele é refletido fora da função. Fácil de entender? Não?!?!

Em C, isso é meio óbvio porque podemos passar o valor ou a referência. Vou tentar mostrar aqui:

#include <stdio.h>

int incrementaValor(int valor) {
    return valor + 1;
}

int incrementaReferencia(int * valor) {
    (*valor)++
    return *valor;
}

int main() {
    int contador = 0;
    printf("Valor: %d\n", incrementaValor(contador));  // Imprime "Valor: 1"
    printf("Valor: %d\n", incrementaValor(contador));  // Imprime "Valor: 1"

    printf("Valor: %d\n", incrementaReferencia(&contador));  // Imprime "Valor: 1"
    printf("Valor: %d\n", incrementaReferencia(&contador));  // Imprime "Valor: 2"
    return 0;
}

O que acontece quando eu chamo a função incrementaValor é que uma cópia do contador é enviado para a função, mas quando chamo incrementaReferencia o próprio contador é enviado para a função.

Em Java só temos passagem por valor quando usamos tipos primitivos (byte, short, int, long, float, double ou char). Quando definimos um objeto, sempre estamos passando a referência do mesmo para funções. Por isso é muito importante entender o que é e como garantir imutabilidade. Quando formos falar de memória, vou explicar o que é o conceito de memória e como isso funciona na prática, mas, resumidamente, tipos primitivos são armazenados na stack do programa enquanto todas as classes são armazenados na memoria heap do programa. Ao se criar um objeto, um ponteiro na stack é criado para um novo espaço de memoria alocado na Heap. Calma, você não tem obrigação de entender isso facilmente!!!

Imutabilidade e Mutabilidade

Chamamos de mutabilidade a capacidade de um objeto ter seu estado interno alterado. Em orientação a objetos mutabilidade é um requisito desejado para quase todas as classes, por isso que só recentemente o Java incorporou o conceito de imutabilidade a linguagem através dos Records. Antes dos Records era comum se usar POJOs em que existia para cada campo um respectivo get e um set.

POJO

POJO é um acrônimo para Plain Old Java Object, que significa velho e simples objeto Java. É um termo usado para referenciar um padrão de classes Java que não dependem da herança de interfaces ou classes de frameworks externos.

Records é o tipo que adiciona o conceito de imutabilidade ao código Java. Abaixo vou definir a classe Usuario três vezes. Na primeira vez ela é mutável, na segunda imutável usando POJO e na terceira usando record.

public class Usuario {
    private int id;
    private String username;
    private String email;

    public Usuario(int id, String username, String email) {
        this.id = id;
        this.username = username;
        this.email = email;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    // Implementa hashCode, equals e toString
}

Para implementar um campo imutável, devemos usar o modificador de acesso final. Um campo final terá seu valor definido no construtor e não poderá ser alterado em todo ciclo de vida do objeto.

public class Usuario {
    private final int id;
    private final String username;
    private final String email;

    public Usuario(int id, String username, String email) {
        this.id = id;
        this.username = username;
        this.email = email;
    }

    public int getId() {
        return id;
    }

    public String getUsername() {
        return username;
    }

    public String getEmail() {
        return email;
    }

    // Implementa hashCode, equals e toString
}

Ao usar records, é como se todos os campos já fossem definidos como final, mas a grande vantagem se dá que não precisamos implementar os métodos hashCode, equals e toString.

public record Usuario(int id, String username, String email) {}

Conclusão

Orientação a Objeto é uma ótima técnica para fazer design de código. Ela é melhor utilizada quando tempos que modelar problemas do mundo real, mas haverá dificuldade se o modelo for mais próximo de um modelo matemático.

O principal ganho com a modelagem a Orientação a Objetos é a capacidade de se encapsular complexidades.

Originally published May 30, 2022