Java utiliza passagem por referência ou por valor? 🧐
Agora você vê a referência, agora não vê mais...
Em Java, um dos conceitos fundamentais que os programadores precisam entender é como a linguagem lida com a passagem de argumentos para métodos. O debate entre "passagem por referência" e "passagem por valor" pode parecer intimidante no começo, mas é mais simples do que parece. Nesse breve artigo, vamos mergulhar nessas explicação com exemplos em código e diagramas para entender o assunto.
Se gostar do conteúdo, por favor, compartilhe e deixe seu like no post! Isso me ajuda e incentiva a continuar a trazer conteúdos em forma de texto também!😄
Os Fundamentos!
Imagine que você tem um livro favorito e quer compartilhar algumas informações dele com um amigo. Você pode fazer isso de duas maneiras:
Passagem por Valor (Anotações no Livro):
Você faz anotações diretamente no seu livro e envia uma cópia dessas páginas para o seu amigo. Seu amigo pode ler as anotações, mas se ele fizer alterações, elas estarão apenas na cópia dele. Seu livro original permanece inalterado.
No Computador: Isso é como passar tipos primitivos em Java. Quando você passa um tipo primitivo para um método, você está enviando uma cópia do valor. Alterações feitas nessa cópia não afetam o valor original, que está seguro em sua própria área da memória (stack).
Passagem por Referência (Endereço da Biblioteca):
Em vez de enviar cópias das páginas, você dá ao seu amigo o endereço da biblioteca onde seu livro está. Com esse endereço, seu amigo pode ir até lá e ler o mesmo livro. Se ele fizer anotações no livro da biblioteca, essas mudanças serão vistas por qualquer pessoa que ler o livro depois.
No Computador: Isso é como passar objetos em Java. Você passa a referência (o endereço na memória) para o objeto. Qualquer alteração feita no objeto através dessa referência afeta o objeto original, que está armazenado na heap.
Essa analogia nos ajuda a entender um pouco como Java lida com a passagem de argumentos. Embora muitas vezes se diga que Java passa objetos por referência, tecnicamente, Java passa a referência por valor. Isso significa que a referência (como o endereço da biblioteca) é uma cópia, mas aponta para o mesmo objeto original na memória (heap). Calma jovem ansioso! Vamos ver um exemplo e desenhos e muito mais para entender isso melhor e claramente.
Gostaria de explicar esses dois conceitos fundamentais também, afinal o que é um heap e uma stack?
O que é o Stack?
O stack, ou pilha, é uma área da memória usada para armazenar variáveis locais e informações de controle de fluxo (como endereços de retorno de chamadas de função). Pense no stack como uma pilha de livros:
Acesso Rápido e Ordem: Cada vez que uma função é chamada, um "livro" (um frame de stack) é adicionado ao topo da pilha. Este livro contém todas as variáveis locais e outras informações necessárias para a execução da função. Quando a função termina, o livro é removido. Tudo é feito em ordem: o último a entrar é o primeiro a sair.
Tamanho Limitado: O stack tem um tamanho limitado, determinado no início da execução do programa. Se você colocar muitos livros na pilha (ou seja, muitas chamadas de função e alocações de variáveis locais), você pode esgotar o espaço, levando a um erro conhecido como "stack overflow".
Armazenamento na Stack
Tipos Primitivos: Em linguagens como Java, os tipos primitivos (como
int
,double
,char
,boolean
, etc.) são armazenados diretamente na stack. Quando você declara uma variável primitiva dentro de um método, o valor dessa variável é armazenado na stack.Referências a Objetos: Além dos tipos primitivos, a stack também armazena referências a objetos. No entanto, é importante notar que a stack armazena apenas a referência (ou o endereço) do objeto, enquanto o objeto em si é armazenado na heap.
Portanto, a stack desempenha um papel crucial na gestão de memória para tipos primitivos e referências a objetos.1
O que é o Heap?
O heap, ou monte, é uma área de memória usada para alocação dinâmica. Ao contrário do stack, o heap não tem uma ordem estrita para armazenamento e remoção de dados. É mais como uma caixa de brinquedos:
Alocação Dinâmica: Quando você precisa de espaço para um objeto grande ou cujo tamanho não é conhecido em tempo de compilação, você o coloca no heap. Você pode adicionar e remover itens a qualquer momento, sem seguir uma ordem específica.
Gerenciamento Manual: Enquanto o stack é gerenciado automaticamente pelo sistema, o heap requer um gerenciamento mais manual. Em linguagens como C++, você precisa alocar e desalocar memória explicitamente. Em Java, a Garbage Collector ajuda a gerenciar o heap, removendo objetos que não são mais necessários.
Mais Espaço, Mais Lentidão: O heap geralmente tem muito mais espaço do que o stack, mas acessar e gerenciar esse espaço é mais lento.2
Podemos visualizar isso melhor na imagem abaixo, foi retirada do site baeldung:
No diagrama, são mostrados três blocos de memória de pilha, cada um correspondendo a uma das funções da pilha de chamadas. As variáveis armazenadas na pilha incluem:
id
- Um valor inteiro (tipo primitivo).name
- Uma referência a um objetoString
.this
ouperson
- Referências a objetos do tipoPerson
.
No diagrama, há duas áreas no heap:
String Pool - Uma área especial do heap onde as instâncias de
String
são armazenadas, seguindo o padrão de internação de strings (reutilização de instâncias de strings imutáveis para economizar memória).Person Object - Um objeto do tipo
Person
que está armazenado no heap. Ele contém o valor23
para um campo (presumivelmenteid
) e uma referência ao objetoString
"John".
Fluxo de Execução
Quando o programa é executado, aqui está o que geralmente acontece:
O método
main
é chamado e um bloco é criado na pilha de chamadas.Dentro do
main
, a funçãobuildPerson
é chamada, criando um novo bloco na pilha.A função
buildPerson
provavelmente cria um novoPerson
usando o construtorPerson(int, String)
, que adiciona outro bloco à pilha de chamadas.O construtor de
Person
inicializa um novo objetoPerson
com os valores fornecidos (id
ename
). Esse objeto é alocado no heap.A referência ao objeto
Person
é armazenada na variávelperson
(outhis
dentro do construtor) na memória de pilha.O valor
23
é armazenado diretamente na pilha, pois é um tipo primitivo.A referência ao
String
"John" é armazenada na pilha, enquanto o próprioString
está no String Pool do heap.
A compreensão do gerenciamento de memória é crucial para responder a pergunta se Java utiliza passagem por referência ou por valor, porque a maneira como os dados são passados nas funções/métodos afeta diretamente como os programas são escritos, como eles funcionam e como os recursos são gerenciados.
No próximo tópico, vamos aprofundar como esse mecanismo de passagem de argumentos afeta a maneira como gerenciamos dados e objetos em nossos programas Java.
Mecanismo de Passagem de Argumentos em Java
Tipos Primitivos: Passagem por Valor
Quando falamos de tipos primitivos em Java, como int
, double
, char
, entre outros, estamos nos referindo a valores básicos que não são objetos. Eles são armazenados diretamente na memória stack. Vamos entender isso com um exemplo simples:
public void alterarValor(int numero) {
numero = 10;
}
Se você chamar alterarValor(5)
, o que acontece é que o valor 5
é copiado e passado para o método. Dentro do método, numero
é uma cópia independente do valor original. Qualquer modificação feita em numero
não afeta o valor original fora do método. Isso é a essência da passagem por valor: o valor real é copiado e passado, e não o próprio item.
Objetos: Passagem de Referência por Valor
Agora, quando se trata de objetos, a história é um pouco diferente. Em Java, objetos são armazenados na memória heap, e o que temos na stack são referências a esses objetos. Quando passamos um objeto para um método, estamos passando a referência a esse objeto por valor. Isso significa que o método recebe uma cópia da referência, mas essa cópia aponta para o mesmo objeto na memória heap. Vejamos um exemplo:
public void modificarObjeto(MeuObjeto obj) {
obj.atributo = "novo valor";
}
Ao chamar modificarObjeto(meuObjeto)
, meuObjeto
é uma referência ao objeto na heap. O método recebe uma cópia dessa referência, e qualquer modificação feita no objeto através dessa referência afeta o objeto original, pois a cópia da referência ainda aponta para o mesmo objeto na heap.
Eu quero que isso fique bem claro, por isso depois de algum tempo pensando em um exemplo melhor em código cheguei nisso:
// Define uma classe chamada Person com um único campo 'name'.
class Person {
String name; // Campo que guarda o nome da pessoa.
// Construtor que inicializa o objeto Person com um nome.
public Person(String name) {
this.name = name;
}
// Um método para mudar o nome da pessoa.
public void changeName(String newName) {
this.name = newName;
}
}
public class Main {
// Método que tenta mudar o nome da pessoa.
public static void modifyPerson(Person p) {
p.changeName("Alice"); // Muda o nome da pessoa para Alice.
p = new Person("Bob"); // Tenta reatribuir p para um novo objeto Person.
// Esta reatribuição não tem efeito fora deste método porque Java passa por valor.
}
// Método que muda o valor de um inteiro.
public static void modifyNumber(int number) {
number = 10; // Tenta mudar o valor do número.
// Esta mudança é local a este método e não tem efeito na variável original.
}
public static void main(String[] args) {
Person person = new Person("John"); // Cria um novo objeto Person com o nome John.
int number = 5; // Inicializa um inteiro com o valor 5.
System.out.println("Antes da modificação: " + person.name); // Saída: John
modifyPerson(person); // Passa a referência do objeto Person para o método.
System.out.println("Depois da modificação: " + person.name); // Saída: Alice
// O nome da pessoa foi alterado para Alice, mostrando que o objeto foi modificado.
System.out.println("Antes da modificação: " + number); // Saída: 5
modifyNumber(number); // Passa o valor do número para o método.
System.out.println("Depois da modificação: " + number); // Saída: 5
// O valor do número permanece 5, mostrando que o tipo primitivo não foi modificado.
}
}
O exemplo acima pode ser compilado em uma IDE com Java. 👆🏼
Parece muita coisa pra entender, por isso vou fragmentar a explicação, vamos iniciar com o que ocorre com o objeto Person.
Vou explicar o que acontece com o ponteiro de memória em cada uma dessas operações:
p.changeName("Alice");
- Este método é chamado no objetoPerson
referenciado porp
. O métodochangeName
altera o camponame
do objeto para "Alice". Neste ponto, a referênciap
ainda aponta para o mesmo objetoPerson
na memória heap que foi passado para o método. Não há mudança no ponteiro de memória; apenas o conteúdo do objeto na memória é atualizado. Portanto, qualquer outra referência que aponte para este objeto verá a mudança no camponame
.p = new Person("Bob");
- Aqui,p
é reatribuída para apontar para um novo objetoPerson
criado na memória heap com o nome "Bob". Agora, a referênciap
aponta para um local de memória diferente do original. No entanto, essa mudança no ponteirop
é local ao métodomodifyPerson
. A variável original que foi passada paramodifyPerson
(por exemplo,person
no métodomain
) ainda aponta para o objeto original na memória heap. A reatribuição não afeta a variável fora do métodomodifyPerson
, porque a referênciap
é uma cópia do valor da referência original; mudarp
não muda a referência original.
Para visualizar vou tentar ajudar com isso:
Variável original (person) em main
↓
[Memória Heap]
[Objeto Person] ("John")
Após p.changeName("Alice");
:
Variável original (person) em main
↓
[Memória Heap]
[Objeto Person] ("Alice") ← Referência `p` dentro de `modifyPerson` aponta para aqui
Após p = new Person("Bob");
:
Variável original (person) em main. Nova referência `p` dentro de `modifyPerson`
↓ ↓
[Memória Heap] [Memória Heap]
[Objeto Person] ("Alice") [Objeto Person] ("Bob")
A variável person
no método main
ainda aponta para o objeto Person
que agora tem o nome "Alice". A referência p
dentro do método modifyPerson
agora aponta para um novo objeto Person
com o nome "Bob", mas isso não tem efeito fora do método modifyPerson
, pois apenas o valor da referência p
foi alterado, não a referência em si que main
possui.
Passagem por valor com tipos primitivos na prática!
No caso do método modifyNumber
, estamos lidando com um tipo primitivo (int
) em Java. Os tipos primitivos são passados por valor, o que significa que qualquer alteração feita ao valor dentro do método é feita em uma cópia local do valor original e não afeta a variável original fora do método.
Vamos mergulhar linha a linha no que ocorre:
O valor da variável
number
é passado para o métodomodifyNumber
.Dentro do método
modifyNumber
, o parâmetronumber
é uma cópia do valor original. Qualquer modificação a este valor só afeta a cópia dentro do método.A linha
number = 10;
altera o valor da cópia local denumber
para 10. Este é um valor completamente independente da variável que foi passada; é armazenado em um local diferente na stack memory.Quando o método
modifyNumber
termina, a cópia local denumber
(com o valor 10) é descartada, pois a execução do método está completa.A variável original que foi passada ao método permanece inalterada.
Para visualizar:
Antes de modifyNumber
ser chamado:
Stack Memory
+-------------+
| main frame |
| number = 5 | ← A variável original 'number' no método 'main'
+-------------+
Durante a execução de modifyNumber
:
Stack Memory
+-----------------+
| modifyNumber |
| number = 10 | ← Cópia local do valor de 'number' dentro de 'modifyNumber'
+-----------------+
+-----------+
| main frame|
| number = 5|← A variável original 'number' permanece inalterada no método main
+-----------+
Após modifyNumber
terminar:
Stack Memory
+-------------+
| main frame |
| number = 5 | ← A variável original 'number' ainda é 5 em 'main'
+-------------+
A variável number
no método main
ainda tem o valor 5 porque a alteração para 10 ocorreu apenas na cópia local que foi criada quando o método modifyNumber
foi chamado.
Por que tudo isso é importante?
Entender esses detalhes sobre a passagem por valor e por referência em Java é fundamental por várias razões que impactam diretamente a programação e o design do software:
Comportamento do Programa: Compreender como as variáveis são passadas ajuda a prever como o programa irá se comportar. Por exemplo, saber que modificar um objeto dentro de um método também refletirá essas mudanças fora do método permite que você manipule objetos de forma eficaz.
Gerenciamento de Memória: Conhecer a diferença entre passagem por valor e por referência ajuda a entender como a memória é utilizada, o que pode influenciar a performance e a eficiência do programa. Por exemplo, evitar a criação desnecessária de objetos pode reduzir a carga sobre o coletor de lixo e melhorar o desempenho.
Depuração de Código: Quando um bug ocorre devido a um objeto inesperadamente alterado (ou não alterado), entender esses conceitos ajuda a identificar rapidamente a causa do problema.
Imutabilidade: Em alguns casos, pode-se desejar que os objetos sejam imutáveis. Compreender a passagem por referência é crucial para garantir que objetos não sejam alterados quando não devem ser.
Design de API: Ao criar APIs ou bibliotecas, é importante entender como os objetos são passados para que você possa projetar sua API para evitar efeitos colaterais indesejados e tornar claro para os usuários da API como os objetos serão afetados por chamadas de método.
Padrões de Projeto: Alguns padrões de design, como o padrão Prototype, dependem do conhecimento de como a clonagem de objetos funciona e como as referências a objetos são tratadas.
Paralelismo e Concorrência: Ao trabalhar com threads e concorrência, é crucial entender como as referências aos objetos são passadas e compartilhadas. Isso pode ajudar a evitar condições de corrida e outros problemas de sincronização.
Otimização: Esse conhecimento pode levar a otimizações de código, como passar objetos grandes e complexos com eficiência, ou escolher estruturas de dados apropriadas para evitar cópias desnecessárias.
O que podemos concluir?
Depois de mergulharmos nos detalhes e exemplos, a resposta direta é: Java utiliza passagem por valor.
Eu sei que isso pode causar um pouquinho de confusão, principalmente porque quando passamos objetos para métodos, parece que estamos passando a própria referência, certo? Mas a questão crucial aqui é que o que está sendo passado é o valor da referência ao objeto, e não a referência em si. Isso significa que estamos passando por valor o endereço onde o objeto pode ser encontrado na memória heap. Se modificarmos o objeto através dessa referência, o objeto original será afetado porque estamos modificando o conteúdo no endereço para o qual a referência aponta.3
Por outro lado, se tentarmos fazer a referência apontar para um novo objeto, como no caso do p = new Person("Bob");
, isso não tem efeito fora do método, porque apenas a cópia local da referência é alterada.
Com tipos primitivos, como int
, double
, char
, etc., a coisa é mais direta: uma cópia do valor real é passada ao método, e quaisquer mudanças nesse valor só existem dentro do escopo do método.
Então, para concluir, embora possamos manipular objetos através de suas referências passadas para métodos e ver essas mudanças refletidas fora dos métodos, isso não muda o fato de que o que passamos foi o valor da referência, não a própria referência. Isso é uma distinção sutil, mas muito importante para entender como Java funciona e como gerenciar o estado e o comportamento dos objetos em seus programas. Pode parecer um pouco como um truque de mágica: "Agora você vê a referência, agora não vê mais", mas é apenas Java sendo consistente com sua escolha de passagem por valor.
Analogias para ajudar no entendimento 👇🏼
Stack: Restaurante de Fast Food
Pense no stack como a linha de montagem de um restaurante de fast food. Cada bandeja representa um frame de stack:
Ordem de Operação: Os clientes fazem pedidos (funções são chamadas), e cada pedido é uma bandeja que entra na linha de montagem (é empilhada no stack). Os trabalhadores (CPU) completam o pedido mais recente antes de retirá-lo da linha (o frame de stack é removido quando a função retorna), seguindo sempre a ordem do último pedido feito ao primeiro a ser completado.
Eficiência e Limitações: Este sistema é rápido e eficiente porque cada trabalhador sabe exatamente onde encontrar os ingredientes (variáveis locais) para cada pedido. No entanto, há espaço limitado na linha de montagem. Se houver pedidos demais (muitas chamadas de função e alocação de variáveis locais), os pedidos não cabem na linha, causando um transtorno (erro de stack overflow).
Heap: Estacionamento Aberto
Por outro lado, o heap pode ser comparado a um grande estacionamento aberto:
Alocação Conforme a Necessidade: Quando você vai a um estacionamento, pode escolher qualquer vaga aberta para estacionar seu carro (objeto). Você pode chegar e sair a qualquer momento, sem a necessidade de seguir a ordem de chegada ou saída (alocação e liberação dinâmicas).
Gerenciamento e Espaço: Você é responsável por lembrar onde seu carro está estacionado (gerenciamento de memória). Em Java, você tem o benefício de um serviço de valet (Garbage Collector) que lembra por você e eventualmente pode retirar o carro se perceber que você o abandonou (limpa a memória).
Flexibilidade com Custos: O estacionamento tem muito mais espaços do que a linha de montagem do restaurante de fast food (mais memória no heap do que no stack). No entanto, encontrar seu carro pode levar mais tempo do que simplesmente pegar o próximo pedido na linha de montagem, especialmente se o estacionamento estiver cheio (o acesso ao heap é mais lento que o acesso ao stack).
Última analogia, prometo! 😂
Analogia: Link de um Documento Compartilhado
Referência/Link: O link para o documento é como a referência ao objeto. Você pode enviar este link por e-mail (passagem de argumento) para várias pessoas (outros métodos).
Documento/Objeto: O documento real que o link abre é o objeto na memória heap. Não importa quantas pessoas têm o link; todas veem e interagem com o mesmo documento.
Modificações: Se alguém com o link edita o documento, como adicionar um parágrafo (modificar o objeto), todos com o link verão essa adição. O documento não é duplicado; apenas a visão de todos sobre ele é atualizada.
Cópia do Link/Passagem por Valor: Quando você envia o link, você não está enviando o próprio documento. Você está apenas passando uma cópia do link que leva ao documento. Se você "modifica" o link em seu e-mail, como ao encurtá-lo, o documento original não é afetado, similar a reatribuir uma referência em Java que não impacta o objeto original.
Esta analogia reflete o comportamento das referências em Java: enviar um link para alguém não duplica o conteúdo ao qual ele aponta; da mesma forma, passar uma referência não duplica o objeto.