Referências e Objetos

Referências e Objetos

Neste post iremos ver como os objetos que criamos são armazenados na memória e como eles diferem das variáveis “comuns”, as que estamos acostumados até então. Assim, vamos refletir sobre a seguinte situação:


C#


int i1 = 10;
int i2 = 10;

if (i1 == i2)
{
    Console.WriteLine("i1 e i2 são iguais");
}
else
{
    Console.WriteLine("i1 e i2 são diferentes");
}


Java


int i1 = 10;
int i2 = 10;

if (i1 == i2) {
    System.out.println("i1 e i2 são iguais");
} else {
    System.out.println("i1 e i2 são diferentes");
}


Não há nada de especial nesse programa: duas variáveis (i1 e i2) do tipo int (inteiro) são criadas e o valor 10 é atribuído a cada uma delas. Logo em seguida, elas são comparadas para verificar se são iguais. Nesse caso, o resultado será “i1 e i2 são iguais“.

Agora, usando a classe Aluno criada anteriormente, vamos passar para a seguinte situação:


C#


Aluno a1 = new Aluno();
a1.nome = "Maria";
a1.p1 = 7;
a1.p2 = 9;

Aluno a2 = new Aluno();
a2.nome = "Maria";
a2.p1 = 7;
a2.p2 = 9;

if (a1 == a2)
{
    Console.WriteLine("a1 e a2 são iguais");
}
else
{
    Console.WriteLine("a1 e a2 são diferentes");
}


Java


Aluno a1 = new Aluno();
a1.nome = "Maria";
a1.p1 = 7;
a1.p2 = 9;

Aluno a2 = new Aluno();
a2.nome = "Maria";
a2.p1 = 7;
a2.p2 = 9;

if (a1 == a2)
{
    System.out.println("a1 e a2 são iguais");
} else {
    System.out.println("a1 e a2 são diferentes");
}


Veja que esse programa é muito parecido com o anterior: duas variáveis (a1 e a2) da classe Aluno são criadas e dois objetos de mesmos valores iniciais são criados e atribuídos a cada uma delas. Logo em seguida, elas são comparadas para verificar se são iguais.

Feita essas observações, eu lhe pergunto: qual será o resultado desse programa? É provável que você esteja pensando que será igual ao programa anterior, ou seja, “a1 e a2 são iguais“. Mas, o que ocorre é que o resultado será “a1 e a2 são diferentes“!

Tipos Primitivos e Tipo Referência

Por que isso ocorre? Porque no primeiro programa as variáveis são de um tipo que chamamos de primitivo e no segundo programa as variáveis são de um tipo que chamamos de referência.

Nos tipos primitivos, o valor da variável é armazenado dentro dela:

Tipos Primitivos

Já nas referências, o valor é armazenado fora delas. A variável irá conter uma referência a esse valor.

Referências

Mas, esse não é o principal motivo para o programa mostrar que as variáveis são diferentes. A questão é que, no caso do segundo programa, foram criados dois objetos (releia a observação feita logo após o código). Ou seja, apesar de terem os mesmos valores para seus atributos, eles são dois objetos distintos! O comando new aparece duas vezes! Assim, quando comparamos as variáveis, estamos comparando as referências para esses objetos e elas referenciam dois objetos diferentes!

E se quiséssemos que o resultado fosse “a1 e a2 são iguais“? Para isso ocorrer, as variáveis a1 e a2 teriam que referenciar o mesmo objeto:

Referências para o mesmo objeto

Ou, em código:


C# (opção 1)


Aluno a1 = new Aluno();
a1.nome = "Maria";
a1.p1 = 7;
a1.p2 = 9;

Aluno a2 = a1;

if (a1 == a2)
{
    Console.WriteLine("a1 e a2 são iguais");
}
else
{
    Console.WriteLine("a1 e a2 são diferentes");
}


C# (opção 2)


Aluno x = new Aluno();
x.nome = "Maria";
x.p1 = 7;
x.p2 = 9;

Aluno a1 = x;
Aluno a2 = x;

if (a1 == a2)
{
    Console.WriteLine("a1 e a2 são iguais");
}
else
{
    Console.WriteLine("a1 e a2 são diferentes");
}


Java (opção 1)


Aluno a1 = new Aluno();
a1.nome = "Maria";
a1.p1 = 7;
a1.p2 = 9;

Aluno a2 = a1;

if (a1 == a2)
{
    System.out.println("a1 e a2 são iguais");
} else {
    System.out.println("a1 e a2 são diferentes");
}


Java (opção 2)


Aluno x = new Aluno();
x.nome = "Maria";
x.p1 = 7;
x.p2 = 9;

Aluno a1 = x;
Aluno a2 = x;

if (a1 == a2)
{
    System.out.println("a1 e a2 são iguais");
} else {
    System.out.println("a1 e a2 são diferentes");
}


Uma consequência de termos duas referências para um mesmo objeto é que podemos alterá-lo por meio de uma referência e usá-lo por meio de outra:


C#


Aluno a1 = new Aluno();
a1.nome = "Maria";
a1.p1 = 7;
a1.p2 = 9;

Aluno a2 = a1;
a2.nome = "João";

Console.WriteLine(a1.nome);


Java


Aluno a1 = new Aluno();
a1.nome = "Maria";
a1.p1 = 7;
a1.p2 = 9;

Aluno a2 = a1;
a2.nome = "João";

System.out.println(a1.nome);


Ao executar o código acima, veremos que o valor do atributo nome do objeto referenciado por a1 será João, apesar de termos usado a variável a2 para atualizá-lo.

Visto esse conceito, eu gosto de pensar que as referências são a forma que temos para nos comunicar com os objetos que estão na memória (enviamos mensagens, lembra?).

Mas, quais tipos são primitivos e quais são referências? Eu poderia fazer uma lista de cada um dos tipos, mas prefiro simplificar e lhe dizer que toda vez que trabalhamos com objetos, estamos, na verdade, trabalhando com referências a eles. Caso contrário, estaremos usando um tipo primitivo.

E aqui cabe uma observação importante: nas linguagens que estamos usando (C# e Java) as strings e os vetores (arrays) são objetos. Isso significa que os diagramas acima deveriam ser, na verdade, desenhados assim:

String vista como objeto

Comparação de Strings

Conforme vimos, strings são objetos e, portanto, se compararmos duas delas usando o operador ==, estaremos, na verdade, comparando suas referências, ou seja, se referenciam a mesma string e não se elas são iguais. Isso é motivo de dificuldades para muitos iniciantes em Java, mas logo se pega o jeito.

Já em C#, apesar de termos a mesma situação, a linguagem sobrecarrega (assunto que será visto posteriormente) o operador de igualdade para permitir que a comparação seja feita diretamente.

Mas, então, como fazer para comparar duas strings em Java? A resposta é: usando o método equals.


Java (usando ==)


String s1 = "Java";
String s2 = new String("Java");

if (s1 == s2) {
    System.out.println("s1 e s2 são iguais");
} else {
    System.out.println("s1 e s2 são diferentes");
}

O resultado será s1 e s2 são diferentes.


Java (usando equals)


String s1 = "Java";
String s2 = new String("Java");

if (s1.equals(s2)) {
    System.out.println("s1 e s2 são iguais");
} else {
    System.out.println("s1 e s2 são diferentes");
}

O resultado será s1 e s2 são iguais.


Observação: na linha 2 estamos forçando a criação de uma nova string usando o comando
String s2 = new String("Java"); Se isso não for feito, ou seja, se usarmos o comando
String s2 = "Java"; o compilador Java irá otimizar o nosso código, fará as variáveis s1 e s2 referenciarem o mesmo objeto "Java" que está na memória e não veremos o efeito desejado.

É importante notar que todo objeto em Java possui um método chamado equals e que todo objeto em C# possui um método chamado Equals. Veremos o porquê mais à frente, quando formos estudar o assunto “herança”.

Comparação de Objetos

Anteriormente, eu comentei que para que o resultado do programa proposto fosse “a1 e a2 são iguais“, as variáveis a1 e a2 teriam que referenciar o mesmo objeto. No entanto, essa não é a única forma disso acontecer. Imagine que você realmente tenha dois objetos (e não duas referências para o mesmo objeto) e que, quando comparados, você quer que o resultado seja verdadeiro. Como fazer?

Nesse caso, devemos decidir qual o critério que fará dois objetos serem considerados iguais. Por exemplo, no caso da classe Aluno, faz sentido dizer que dois alunos são iguais se eles tiverem o mesmo nome. Assim, para comparar dois alunos, podemos comparar, na verdade, os nomes dos alunos:


C#


Aluno a1 = new Aluno();
a1.nome = "Maria";
a1.p1 = 7;
a1.p2 = 9;

Aluno a2 = new Aluno();
a2.nome = "Maria";
a2.p1 = 7;
a2.p2 = 9;

if (a1.nome == a2.nome)
{
    Console.WriteLine("a1 e a2 são iguais");
}
else
{
    Console.WriteLine("a1 e a2 são diferentes");
}

O resultado será a1 e a2 são iguais.


Java


Aluno a1 = new Aluno();
a1.nome = "Maria";
a1.p1 = 7;
a1.p2 = 9;

Aluno a2 = new Aluno();
a2.nome = "Maria";
a2.p1 = 7;
a2.p2 = 9;

if (a1.nome.equals(a2.nome))
{
    System.out.println("a1 e a2 são iguais");
} else {
    System.out.println("a1 e a2 são diferentes");
}

O resultado será a1 e a2 são iguais.


É muito importante notar os seguintes pontos:

  1. Não estamos mais comparando objetos da classe Aluno diretamente!!! O que estamos comparando são suas características, seus estados, seus atributos!!!
  2. A regra do critério que adotamos ficou do lado de fora da classe, ou seja, toda vez que quisermos comparar dois alunos, deveremos lembrar como é essa regra. Isso está muito longe de ser adequado! Para resolver isso, devemos programar o critério dentro de um método. Adivinha qual um bom nome para ele? Aposto que você já sabe que é Equals no caso do C# e equals no caso de Java.

Mas, como eu disse anteriormente, veremos mais detalhes sobre esse método quando formos estudar sobre “herança”.

O valor null

Muitas vezes, queremos criar uma variável de uma determinada classe, mas não queremos criar o objeto (podemos ainda não ter os dados necessários para iniciá-lo). Nesses casos, qual o valor inicial que podemos atribuir à variável? A resposta é: o valor null. Ele indica que a variável não referencia nenhum objeto:

null

Ou, em código:


C#


Aluno a = null;


Java


Aluno a = null;


Uma outra situação em que podemos usar o valor null é quando queremos que uma variável não referencie mais nenhum objeto:

GC

Ou, em código:


C#


Aluno a = new Aluno();
a.nome = "Maria";
a.p1 = 7;
a.p2 = 9;

a = null;


Java


Aluno a = new Aluno();
a.nome = "Maria";
a.p1 = 7;
a.p2 = 9;

a = null;


Uma boa pergunta para o exemplo acima é: o que acontece com o objeto que estava sendo referenciado pela variável a, uma vez que agora ele está “perdido” na memória, ou seja, sem nenhuma referência para ele (ou, como gosto de pensar, sem nenhum meio de comunicação com ele)? Será que ele fica ocupando espaço na memória? Sim, fica! No entanto o Java e o C# possuem um mecanismo chamado Garbage Collector (GC) ou, em Português, Coletor de Lixo, que fica de tempos em tempos (alguns milissegundos) procurando objetos que não possuem nenhuma referência para eles. Quando os encontra, libera o espaço de memória que ocupavam.

É importante notar que uma variável também deixa de referenciar um objeto quando sai de escopo. Isso significa que, caso o objeto que ela referenciava não possua uma outra referência em um outro escopo maior, ele fica sujeito a ser coletado pelo GC. No exemplo abaixo, são criados dez objetos da classe Aluno, mas todos eles serão coletados pelo GC, pois, a cada iteração, a variável a sai do escopo do laço for deixando de referenciar o objeto criado.


C#


for (int i = 0; i < 10; i++) {
    Aluno a = new Aluno();
    a.nome = "Maria";
    a.p1 = 7;
    a.p2 = 9;
}


Java


for (int i = 0; i < 10; i++) {
    Aluno a = new Aluno();
    a.nome = "Maria";
    a.p1 = 7;
    a.p2 = 9;
}


Note que é importante tomar cuidado para não usarmos uma variável que está nula. Se fizermos isso, o Java irá lançar a exceção NullPointerException e o C# irá lançar a exceção NullReferenceException.


C#


Aluno a = null;
Console.WriteLine(a.calculaMedia());

A saída será algo como: Unhandled Exception: System.NullReferenceException: Object reference not set to an instance of an object


Java


Aluno a = null;
System.out.println(a.calculaMedia());

A saída será algo como: Exception in thread “main” java.lang.NullPointerException


A referência this

Às vezes, durante a execução de algum método, é necessário fazer uma referência ao próprio objeto que está executando aquela ação. Para obter essa referência, pode-se usar a palavra reservada this. O this é como se fosse um atributo que está presente em todo objeto e que sempre contém uma referência para ele mesmo:

this

Iremos ver uma utilidade prática para o this mais à frente…

Bom, esse post ficou gigante com bastante conteúdo importante! Espero que tenha gostado e, como sempre, se tiver alguma dúvida, crítica, sugestão e/ou correção, por favor, deixe um comentário! Até breve!

2 ideias sobre “Referências e Objetos

Se você gostou do post, tem alguma dúvida ou encontrou algum erro, por favor, deixe uma mensagem! Seu feedback é muito importante!