Java: Passagem por Valor vs. Passagem por Referência
Por que Java é sempre passagem por valor, mesmo ao passar referências de objetos, e o que isso significa na prática.
Java é passagem por valor. Sempre. O que quer que você passe para um método — um int, uma String, um objeto personalizado, um array — o método recebe uma cópia do valor que você entregou. Esse valor pode ser um número, ou pode ser uma referência a um objeto, mas ainda é uma cópia.
Isso confunde muita gente porque um método pode alterar o conteúdo de um objeto que você passou, e essa alteração é visível para quem chamou. Então parece que o objeto foi passado por referência. Não foi. O que foi passado foi uma cópia da referência. Este capítulo mostra exatamente o que isso significa.
Esta página aborda como primitivos, objetos, arrays e strings se comportam quando entregues a um método, o modelo mental que explica cada caso e as consequências práticas para seus próprios parâmetros de método.
Primitivos são simples
Passar um primitivo copia seu valor para o parâmetro:
public static void doubleIt(int n) {
n = n * 2;
}
int x = 5;
doubleIt(x);
System.out.println(x); // 5O método tem seu próprio n, separado de x. Reatribuir n não tem efeito sobre x. Todo mundo concorda que isso é passagem por valor.
Argumentos de objeto: a referência é copiada
Para tipos de objetos, a variável não contém o objeto — ela contém uma referência (um endereço que aponta para o objeto em algum lugar na memória). Quando você passa essa variável para um método, Java copia a referência, não o objeto:
Caller's variable → [ref to Dog A]
|
v
{ Dog A: name="Rex" }
^
|
Method's parameter → [ref to Dog A]Ambas as variáveis agora apontam para o mesmo objeto Dog. É por isso que modificar o objeto por meio do parâmetro é visível no local da chamada:
public static void rename(Dog d) {
d.setName("Buddy"); // mutates the shared object
}
Dog rex = new Dog("Rex");
rename(rex);
System.out.println(rex.getName()); // BuddyMas atribuir uma nova referência ao parâmetro não altera a variável do chamador:
public static void replace(Dog d) {
d = new Dog("Buddy"); // parameter now points at a new Dog
}
Dog rex = new Dog("Rex");
replace(rex);
System.out.println(rex.getName()); // Rex — unchangedO método atualizou sua própria cópia da referência. O rex do chamador ainda aponta para o Dog original.
O modelo mental dos dois ponteiros
Sempre que um método recebe um parâmetro de objeto, visualize duas setas no início: uma da variável do chamador, outra do parâmetro do método, ambas apontando para o mesmo objeto.
- Modificar o objeto por qualquer uma das setas altera o que a outra seta enxerga.
- Redirecionar uma seta para outro lugar não tem nenhum efeito na outra seta.
Essa única regra resolve todas as perguntas do tipo "Java é passagem por referência?".
Arrays seguem a mesma regra
Arrays são objetos, então passar um para um método copia a referência, não o conteúdo:
public static void zeroFirst(int[] xs) {
xs[0] = 0; // mutates the shared array
}
int[] data = {1, 2, 3};
zeroFirst(data);
System.out.println(data[0]); // 0O método alterou um elemento por meio de sua cópia da referência, e o chamador vê a mudança porque ambas as referências apontam para o mesmo array.
Mas reatribuir o parâmetro a um array completamente novo não tem efeito sobre o chamador:
public static void resetArray(int[] xs) {
xs = new int[]{0, 0, 0}; // parameter only
}
int[] data = {1, 2, 3};
resetArray(data);
System.out.println(data[0]); // 1 — unchangedStrings: a imutabilidade esconde o problema
Strings também são objetos, mas são imutáveis — não existe método que altere o conteúdo de uma String. Então o caso de mutação não pode acontecer:
public static void uppercase(String s) {
s = s.toUpperCase(); // creates a new String
}
String name = "ada";
uppercase(name);
System.out.println(name); // ada — unchangeds.toUpperCase() retorna uma nova String; atribuí-la a s apenas atualiza o parâmetro. name ainda aponta para a "ada" original. Para "alterar" uma string, retorne a nova e deixe o chamador atribuí-la.
Por que isso importa
Três consequências práticas:
-
Um método não pode "alterar para fora" um primitivo nem reatribuir a referência do chamador. Se você precisa desse efeito, retorne o novo valor:
x = doubleIt(x);ou use um objeto wrapper que o chamador possa ler após a chamada. -
Um método pode modificar um objeto compartilhado — o que às vezes é desejado (preencher um array, popular uma lista) e às vezes é uma surpresa (os chamadores não esperam que sua lista mude).
-
Cópia defensiva. Se um método não deve alterar o objeto do chamador, ou não mute o parâmetro, ou copie-o primeiro:
Arrays.copyOf(xs, xs.length). Da mesma forma, se você retorna um array ou lista internos, os chamadores podem modificá-los pela referência, a menos que você retorne uma cópia.
Um exemplo prático
O que vem a seguir
Você entende como argumentos individuais fluem para os métodos. Às vezes você não sabe de antemão quantos argumentos o chamador vai fornecer — pense em String.format, ou um max(...) que aceita qualquer número de valores. É para isso que servem os varargs.