De uma forma simples, como funciona a alocação de memória?

2015/11/09

Acredito que, com este exemplo hipotético e simplificado, ninguém mais vai ter dúvidas sobre o motivo de ocorrer NullPointerException.

Vamos imaginar que esse "desenho textual" aqui embaixo é uma memória com 5 posições:

|       |       |       |       |       |
| vazio | vazio | vazio | vazio | vazio |
|  1    |  2    |  3    |  4    |  5    |

Cada elemento, como esse abaixo, representa um espaço de memória, que tem endereço, representado pelo número que está embaixo, e possui um conteúdo, que inicialmente é "vazio".

|       |
| vazio |
|   1   |

Quando você declara uma variável, você está reservando um espaço em algum lugar aleatório da memória, esse espaço vai receber algum valor. Por exemplo a linha abaixo, reserva espaço para que essa variável possa referenciar um Aluno:

Aluno a1;

Vamos imaginar que a variável "a1" vai ser armazenada no endereço 1 da memória. Agora nossa memória hipotética vai ficar assim:

|  a1   |       |       |       |       |
| vazio | vazio | vazio | vazio | vazio |
|   1   |   2   |   3   |   4   |   5   |

O Endereço 1 corresponde à variável "a1", que ainda não foi inicializada.

Vamos imaginar que um objeto Aluno ocupa 3 espaços de memória, um para seu ponteiro ( ou endereço ) na memória, um para o atributo "nome" e outro para o atributo "matrícula". Quando você usa a instrução new, você está alocando/ocupando memória em algum lugar aleatório da memória. Por exemplo, a linha abaixo aloca memória para um objeto Aluno;

new Aluno();

Vamos supor que o Aluno foi alocado no endereço 5, então nossa memória ficará assim:

|  a1   |       | - - - - - - Aluno - - - - - - |
| vazio | vazio | matricula | nome   | ponteiro |
|   1   |   2   |   3       | 4      |    5     |

Perceba que você no exemplo acima, fizemos "new Aluno();" ao invés de "Aluno a1 = new Aluno();" Dessa forma,ninguém referencia o objeto Aluno que criamos, ele só está lá ocupando memória desnecessariamente. Se observar o "desenho", a variável "a1" continua com "vazio".

Pra você fazer sua variável referenciar o objeto acima, você precisa atribuir ( usando o operador = ) para uma variável, o objeto criado, dessa forma:

Aluno a1 = new Aluno();

Agora nossa memória vai ficar assim:

|  a1   |       | - - - - - - Aluno - - - - - - |
|   5   | vazio | matricula | nome   | ponteiro |
|   1   |   2   |   3       | 4      |    5     |

Perceba que o endereço 1, que corresponde à variável "a1", possui o valor 5, este valor é o endereço do objeto que àquela variável referencia.

Vamos agora declarar mais uma variável do tipo Aluno, a variável "a2";

Aluno a2; 

Após a declaração acima nossa memória vai ficar assim:

|  a1   |  a2   | - - - - - - Aluno - - - - - - |
|   5   | vazio | matricula | nome   | ponteiro |
|   1   |   2   |   3       | 4      |    5     |

O endereço 2 para a ficar reservado para a variável "a2", no entanto ela ainda não foi inicializada.

Se fizermos:

a2 = a1; 

Nossa memória hipotética vai ficar com esse aspecto:

|  a1   |  a2   | - - - - - - Aluno - - - - - - |
|   5   |   5   | matricula | nome   | ponteiro |
|   1   |   2   |   3       | 4      |    5     |

Perceba que tanto a variável "a1", quanto a variável "a2" estão referenciando o endereço 5, que corresponde ao objeto Aluno que foi criado.

Se fizermos:

a1.nome = "Huguinho"; 
a1.matricula = "123";

Nossa memória hipotética vai ficar assim:

|  a1   |  a2   | - - - - - - Aluno - - - - - - |
|   5   |   5   | "123" | "Huguinho" | ponteiro |
|   1   |   2   |   3   |     4      |    5     |

Agora,se fizermos:

a1 = null; 

Nós apenas estaremos anulando a referência de "a1" e nossá memória hipotética vai ficar dessa forma:

|  a1   |  a2   | - - - - - - Aluno - - - - - - |
| vazio |   5   | "123" | "Huguinho" | ponteiro |
|   1   |   2   |   3   |     4      |    5     |

A variável "a2" continua referenciando o objeto Aluno que existe no endereço 5. Entretanto, a variável "a1" voltou a ficar sem referência nenhuma, dessa forma, se alguém tentar acessar fazer isso:

System.out.println(a1.nome);

Vai acontecer uma NullPointerException, pois acabamos de tentar acessar o atributo "nome" de um endereço vazio.

Espero que com esse exemplo fique fácil compreender o que é o tão famigerado NullPointerException, ele simplesmente significa que você está tentando acessar membros de um objeto que não foi inicializado!

Talvez possa surgir a dúvida de porquê nos exemplos acima, o objeto Aluno foi alocado no endereço 5 e não no endereço 3. A resposta é pode parecer idiota: eu quis fazer assim! Não necessariamnete a Máquina Virtual Java tenha um comportamento desses.

No entanto, é comum que na arquitetura de computadores o ponteiro da Heap começar "em uma ponta" da memória e o ponteiro da Stack começar na "outra ponta", dessa forma quando os dois ponteiros atingem um endereço que já foi percorrido pelo outro, significa que houve uma violação de memória.