Isso aqui é só uma definição:
public class Animal{
String nome;
int size, age;
}
Ou seja, você está explicando para o java que, ao criar um animal, existirão 2 variáveis inteiras de nome size e age, e um objeto do tipo string associados. Porém, a definição não cria nada. Aqui, não existe memória sendo alocada, nem endereços. Existe só uma explicação, um plano, para quando uma instância ocorrer.
E quando essa instância ocorre? No momento do “new”:
Animal meuAnimal = new Animal();
Quando o Java vê isso, o que ele faz?
a) Ele verifica que áreas de memória precisará criar: No caso, a definição da classe animal indica que são 3: dois ints e uma referência.
b) Ele cria as três áreas de memória e as associa aos apelidos meuAnimal.nome, meuAnimal.size e meuAnimal.age;
c) Ele procura na definição por um construtor, para inicializar essas áreas de memória com algum valor. No caso dessa classe que não tem, irá fazer com que o java faça a construção padrão, inicializando size a age com 0, e a referência com null.
Note que não há conflito. Se um novo animal for criado,
Animal outroAnimal = new Animal();
Outras 3 variáveis serão criadas:
outroAnimal.nome outroAnimal.size e outroAnimal.age
Agora, você pode se perguntar. Mas e quando eu uso uma dessas variáveis diretamente dentro de um método de animal?
public void fazAlgo() {
System.out.println(nome + " " + age);
}
Novamente, você aqui você está só definindo coisas, ou seja, explicando para o Java o que fazer com as variáveis do animal que for chamado.
Porém, quando você faz:
meuAnimal.fazAlgo();
Ou
outroAnimal.fazAlgo();
Você especifica um animal “concreto”, ou seja, agora o java sabe de que animal específico você está falando.
Assim, ele poderá usar as variáveis corretas desse animal dentro do método.