Amigos, na aula 3 (http://linubr.blogspot.com.br/2012/08/aula-3-static-tipos-primitivos-e.html)
fizemos a modelagem UML e codificação do exercício da Colmeia. Como
vimos, trata-se de um simples sistema cujo modelo de domínio é composto
por apenas uma classe chamada Abelha.
O que é um modelo de domínio?
Modelo
de Domínio é um diagrama, geralmente elaborado com base nos Diagramas
de Pacotes ou de Classes da UML, que define todas as classes utilizadas
para representar os dados envolvidos no sistema. Existe versões onde
também são definidos métodos de negócio, mas geralmente as boas práticas
apontam para uma formatação apenas com atributos e métodos de acesso.
No caso do sistema da Colmeia, nós tivemos três classes, porém fixando a
regra de negócio discutida, somente a classe Abelha nos interessa - as
outras duas são auxiliares. Voltaremos a tratar desse assunto no futuro.
Apesar de simples, ao tentarmos codificar o sistema a partir da UML, nos deparamos com diversas dúvidas conceituais e de sintaxe. Nessa aula vamos retomar alguns itens já discutidos sobre as características da linguagem, como também fazer uma representação do funcionamento do sistema Colmeia na Stack e Heap. Caso não se lembrem o que significa Stack ou Heap, peço por favor relembrá-los na aula 2: http://linubr.blogspot.com.br/2012/08/aula-2-instalando-jdk-e-criando-um.html
Clareza na criação de objetos
Vou postar aqui uma situação que ocorreu no código Colmeia.java. Para facilitar vou replicá-lo abaixo.
public class Colmeia {
public static void main(String args[]) {
Util.escrever("Digite a qtde, em mg, de mel:");
int mgMel = Util.lerNumero();
Abelha ze = new Abelha();
int r = ze.buscarMel( mgMel );
Util.escrever("Foram produzidos de mel (mg): " + Integer.toString(r) );
}
}
A linha que nos interessa está grafada de vermelho. O que realmente significa essa linha?

Numerei os elementos desse comando para discutimos com calma.
1 - Java é uma linguagem de programação fortemente tipificada. Mas o que é isso? Uma
linguagem fortemente tipificada nos obrigada, sempre, a definir qual o
tipo da variável que estaremos trabalhando. Só para constar, existem
linguagens (como o PHP, por exemplo) que nos permite omitir o tipo das
variáveis. No Java isso não pode ser feito. O elemento indicado pelo
número 1 define que a minha variável de nome ze é do tipo Abelha. Mas Abelha não é uma classe? Veja bem, é uma classe sim! Você pode usar a palavra “tipo” ou “classe” nesse sentido indiscriminadamente. Agente sabe que é uma classe, mas pode-se dizer que ze é uma variável do tipo Abelha - ou ainda: ze é uma variável da classe Abelha. Enfim, o elemento 1 é o tipo da variável!
2 - Esse elemento é o nome da minha variável. É importante notarmos que estamos falando numa variável e não num atributo. “ze” é um elemento criado dentro do escopo do método main da classe Colmeia. Isso significa que ze não é uma atributo da classe Colmeia e sim uma variável do método main.
3 - Sempre*, no Java, quando vamos instanciar um novo objeto, utilizaremos o comando new. Esse comando cria uma novo objeto na Heap e aloca-o em algum atributo ou variável definida. Neste caso, vamos criar um novo objeto e guardá-lo na variável ze.
*
- por enquanto assuma isso como verdade absoluta, porém o Java oferece
outros recursos para instanciar objetos sem o uso do comando new. Veremos isso quando tratarmos de Strings e arrays.
4 - Sempre surge a pergunta: mas se eu já disse que ze e do tipo Abelha, porque tenho que repetir o nome da classe Abelha nesse ponto?
Aluno rebelde dizendo: O Java é muito tosco mesmo! Ridículo isso! Tenho que ficar repetindo o código toda hora! Que droga!!!! Eu odeio o Java...
Pessoal, calma... O fato de estarmos repetindo o nome da classe aqui, para indicar que a variável ze terá uma instancia do tipo Abelha, só ficará claro quando estivermos trabalhando com Herança e Polimorfismo. Mas como só veremos isso mais à frente, entenda que apesar da definição do tipo Abelha para a variável ze, poderíamos guardar outras instâncias de objetos (na verdade, somente as subclasses de Abelha) dentro do tipo Abelha.
Sabendo que existe essa possibilidade, o Java nos obriga a deixar
explícito qual o tipo de objeto será instanciado. Como aqui nós queremos
instanciar uma Abelha e guardá-la na variável ze que também é do tipo Abelha, o código fica repetido mesmo.
Outra forma de entendimento
Uma boa analogia para entendermos a sintaxe desse comando seria pensarmos na caso clássico da Planta e da Casa.

A figura acima ilustra uma situação que talvez ajude-nos a compreender a utilidade de cada um dos elementos do comando Abelha ze = new Abelha();
Planta da Casa: É razoável entender uma classe como uma planta de uma casa. Uma planta de casa define como será criada a casa, ou seja, quais atributos terão aquela casa quando esta existir. Porém, a partir de uma planta não se pode usufruir do objeto casa em si, pois ele ainda não existe. Quando nós programamos a classe Abelha, estamos mostrando ao Java como será um objeto do tipo Abelha quando este existir.
Endereço da Casa: É bem interessante considerarmos nossa variável ze como sendo o endereço da casa. Pois é exatamente isso que ele é! ze é uma variável de instância que guarda o ID do objeto (objectId) que nos permite acessar o objeto na Heap - ou seja, um endereço para o objeto.
Construindo a Casa: Como falamos, ao utilizar a sintaxe new estamos instanciando (construindo ou criando) um novo objeto.
Instância da Casa: Esse é o objeto instanciado efetivamente. Através dele poderemos acessar seus métodos e trabalhar com seus atributos. Lembrando que esse objeto contém todas as características que nós mesmos definidos na sua planta, ou melhor, na sua classe.
Espero que através dessas analogias tenhamos esclarecido o que significa criar um objeto e porque precisamos de cada um dos comandos apresentados. Isso também nos ajuda a entender que não é possível trabalhar com um objeto diretamente a partir de sua classe - é como se tentássemos entrar numa casa apenas pela planta, sem instanciar a casa em si! Quando queremos trabalhar com o objeto, precisamos instanciá-lo.
Visualizando a Stack e Heap durante a execução da Colmeia
Também conhecido na noite carioca como chinezinho, os próximos passos mostrarão o que ocorre na JVM quando executamos o sistema da Colmeia discutido.
Antes de mostrar os passos, vou representar abaixo uma execução típica desse sistema.

Nessa execução informamos que a Abelha-Rainha deseja obter 12 mg de mel. O sistema automaticamente calculou que seriam necessários 24 mg de néctar e, por fim, a Abelha produziu o mel.
Vamos
agora acompanhar, passo a passo, o que ocorre na memória da JVM. Por
conveniência do aprendizado, sempre replicaremos o código do momento
marcando em negrito o trecho executado. Em paralelo tiraremos uma foto
da memória da JVM para discutirmos o assunto.
Quando chamamos o comando java Colmeia, como sabemos, a JVM busca pelo método main existente nessa classe.
public class Colmeia {
public static void main(String args[]) {
Util.escrever("Digite a qtde, em mg, de mel:");
int mgMel = Util.lerNumero();
Abelha ze = new Abelha();
int r = ze.buscarMel( mgMel );
Util.escrever("Foram produzidos de mel (mg): " + Integer.toString(r) );
}
}
Os métodos são alocados na Stack, portanto, logo de início já teremos a situação abaixo. Atenção: vou omitir elementos que não vão agregar nosso estudo, como por exemplo, o parâmetro String args[]

Veja que o main já foi alocado na Stack, porém não foi necessário criar uma instância da classe Colmeia porque esse método é estático. Como ainda não explicamos os recursos utilizados nos métodos estáticos lerNumero() e escrever() da classe Util, para simplificar, não vou representá-los na Stack. Na sequência, temos:
public class Colmeia {
public static void main(String args[]) {
Util.escrever("Digite a qtde, em mg, de mel:");
int mgMel = Util.lerNumero();
Abelha ze = new Abelha();
int r = ze.buscarMel( mgMel );
Util.escrever("Foram produzidos de mel (mg): " + Integer.toString(r) );
}
}
Aqui uma variável mgMel do tipo int é criada. Nela foi armazenado o número 12 (número que digitei nessa execução).

Chegamos agora no momento em que uma instância da classe Abelha será criada e guardada na variável ze.
public class Colmeia {
public static void main(String args[]) {
Util.escrever("Digite a qtde, em mg, de mel:");
int mgMel = Util.lerNumero();
Abelha ze = new Abelha();
int r = ze.buscarMel( mgMel );
Util.escrever("Foram produzidos de mel (mg): " + Integer.toString(r) );
}
}
Veja que o atributo mgNectar do objeto Abelha é inicializado com zero. A variável ze do método main contém o ponteiro para o objeto que foi criado na Heap.

No próximo passo o método buscarMel() da variável ze é chamado informando como parâmetro o valor da variável mgMel. Ao final da execução desse método, espera-se obter um número inteiro, mas só chegaremos nesse ponto quando o método terminar sua execução.
public class Colmeia {
public static void main(String args[]) {
Util.escrever("Digite a qtde, em mg, de mel:");
int mgMel = Util.lerNumero();
Abelha ze = new Abelha();
int r = ze.buscarMel( mgMel );
Util.escrever("Foram produzidos de mel (mg): " + Integer.toString(r) );
}
}
Como vamos abordar o fluxo de buscarMel(), replicarei o código da classe Abelha abaixo.
public class Abelha {
int mgNectar;
void buscarNectar(int mg) {
if (mg > 40) {
Util.escrever("Uma abelha nao pode carregar mais que 40 mg de nectar!");
} else {
mgNectar = mg;
Util.escrever("A abelha buscou " + Integer.toString( mgNectar ) + " mg de nectar!");
}
}
int fazerMel() {
if ( mgNectar > 0 ) {
int temp = mgNectar;
mgNectar = 0;
return temp / 2;
}
return 0;
}
int buscarMel(int mgMel) {
buscarNectar( mgMel * 2 );
return fazerMel();
}
}
Veja que logo de início, na alocação desse método na Stack, já temos uma variável de nome mgMel - que é justamente o parâmetro do método.

É muito importante entender que, apesar de terem o mesmo nome e valor, as variáveis mgMel do método main e mgMel do método buscarMel() são distintas. Isso significa que caso buscarMel() altere o valor de sua mgMel, a variável de mesmo nome do método main não será alterada. Na figura da Stack acima isso fica claro! Elas possuem o mesmo valor porque na chamada de buscarMel() o valor informado pelo parâmetro é justamente o que está guardado na mgMel do método main - que é 12.
Veja que o primeiro comando de buscarMel() é uma chamada à outro método da instância de Abelha da Heap. Nessa chamada é passado como parâmetro duas vezes o valor de mgMel, que resultará em 24.
public class Abelha {
int mgNectar;
void buscarNectar(int mg) {
if (mg > 40) {
Util.escrever("Uma abelha nao pode carregar mais que 40 mg de nectar!");
} else {
mgNectar = mg;
Util.escrever("A abelha buscou " + Integer.toString( mgNectar ) + " mg de nectar!");
}
}
int fazerMel() {
if ( mgNectar > 0 ) {
int temp = mgNectar;
mgNectar = 0;
return temp / 2;
}
return 0;
}
int buscarMel(int mgMel) {
buscarNectar( mgMel * 2 );
return fazerMel();
}
}
De forma semelhante, o método buscarNectar() foi alocado na Stack - inclusive com o valor de seu parâmetro definido como 24 - que foi o valor passado.

Na execução desse método é feita uma verificação do valor da variável mg de acordo com as regras de negócio definidas no sistema. Como mg é menor que 40, entraremos no else da condição. Ainda omitindo as chamadas de Util.escrever() e Integer.toString(), a única coisa que nos importa agora é entender que na linha sinalizada abaixo, estamos guardando o valor da variável local mg no atributo mgNectar do objeto Abelha.
public class Abelha {
int mgNectar;
void buscarNectar(int mg) {
if (mg > 40) {
Util.escrever("Uma abelha nao pode carregar mais que 40 mg de nectar!");
} else {
mgNectar = mg;
Util.escrever("A abelha buscou " + Integer.toString( mgNectar ) + " mg de nectar!");
}
}
int fazerMel() {
if ( mgNectar > 0 ) {
int temp = mgNectar;
mgNectar = 0;
return temp / 2;
}
return 0;
}
int buscarMel(int mgMel) {
buscarNectar( mgMel * 2 );
return fazerMel();
}
}
Veja na Heap que o valor de mgNectar foi alterado. Nesse caso, todos os ponteiros para essa instância terão acesso ao mesmo valor de mgNectar. Quem poderá acessar esse atributo? Todos seus métodos internos e os métodos externos que tiverem um ponteiro para o objeto na memória. Ou seja, os métodos buscarMel(), buscarNectar() e fazerMel() tem acesso ao atributo mgNectar por serem métodos da própria instância Abelha, já o método main terá acesso porque possui um ponteiro (a variável ze) para essa instância.

Após a definição do valor do atributo, o método buscarNectar() acaba. Isso faz com que o fluxo de execução volte para o método buscarMel(), sendo que o próximo elemento desse método é uma outra chamada, porém agora para o método fazerMel().
public class Abelha {
int mgNectar;
void buscarNectar(int mg) {
if (mg > 40) {
Util.escrever("Uma abelha nao pode carregar mais que 40 mg de nectar!");
} else {
mgNectar = mg;
Util.escrever("A abelha buscou " + Integer.toString( mgNectar ) + " mg de nectar!");
}
}
int fazerMel() {
if ( mgNectar > 0 ) {
int temp = mgNectar;
mgNectar = 0;
return temp / 2;
}
return 0;
}
int buscarMel(int mgMel) {
buscarNectar( mgMel * 2 );
return fazerMel();
}
}
Observe que após a conclusão do buscarNectar(),
ele saiu da pilha Stack (perdoem-me pela redundância, pois Stack é
pilha em inglês - quis apenas enfatizar que os métodos são guardados na
Stack como uma pilha comum, ou seja, como buscarNectar() foi chamado depois de buscarMel(), quando buscarNectar() terminasse ele cairia, por sequência da pilha, no buscarMel() novamente). Veja também que fazerMel() já foi alocado.

O fluxo de fazerMel() verifica inicialmente se existe algum valor de mgNectar para produzir mel. Veja que caso o valor do atributo mgNectar seja igual ou inferior a zero, o método retornará (pelo comando return 0) o valor zero - isso significa que a abelha não produziu mel! Todavia, em nossa execução, veja que o atributo mgNectar da instância de Abelha possui o valor 24. Dessa forma, uma variável interna chamada temp é criada dentro do fazerMel(). Nela guardamos o valor do atributo mgNectar.
public class Abelha {
int mgNectar;
void buscarNectar(int mg) {
if (mg > 40) {
Util.escrever("Uma abelha nao pode carregar mais que 40 mg de nectar!");
} else {
mgNectar = mg;
Util.escrever("A abelha buscou " + Integer.toString( mgNectar ) + " mg de nectar!");
}
}
int fazerMel() {
if ( mgNectar > 0 ) {
int temp = mgNectar;
mgNectar = 0;
return temp / 2;
}
return 0;
}
int buscarMel(int mgMel) {
buscarNectar( mgMel * 2 );
return fazerMel();
}
}
Veja abaixo a foto da memória.

O próximo passo de fazerMel() é zerar o valor do atributo mgNectar. Você deve estar se perguntando porque estamos fazendo isso? Simplesmente pelo fato de que quando a abelha produz mel, ela utiliza todo o seu néctar armazenado. Daí não faz sentido deixarmos seu atributo mgNectar com o valor 24.
public class Abelha {
int mgNectar;
void buscarNectar(int mg) {
if (mg > 40) {
Util.escrever("Uma abelha nao pode carregar mais que 40 mg de nectar!");
} else {
mgNectar = mg;
Util.escrever("A abelha buscou " + Integer.toString( mgNectar ) + " mg de nectar!");
}
}
int fazerMel() {
if ( mgNectar > 0 ) {
int temp = mgNectar;
mgNectar = 0;
return temp / 2;
}
return 0;
}
int buscarMel(int mgMel) {
buscarNectar( mgMel * 2 );
return fazerMel();
}
}
Nesse momento temos o antigo valor do atributo mgNectar em temp e o próprio mgNectar zerado.

Por fim, o método fazerMel() retorna o valor da variável inteira temp dividido por 2. Lembre-se que nossa regra de negócio indica que para se produzir 1 mg de mel, serão necessários 2 mg de néctar. Portanto para se produzir 12 mg de mel, dividimos o valor 24 por 2 - é claro, essa regra se aplica para quaisquer valores.
public class Abelha {
int mgNectar;
void buscarNectar(int mg) {
if (mg > 40) {
Util.escrever("Uma abelha nao pode carregar mais que 40 mg de nectar!");
} else {
mgNectar = mg;
Util.escrever("A abelha buscou " + Integer.toString( mgNectar ) + " mg de nectar!");
}
}
int fazerMel() {
if ( mgNectar > 0 ) {
int temp = mgNectar;
mgNectar = 0;
return temp / 2;
}
return 0;
}
int buscarMel(int mgMel) {
buscarNectar( mgMel * 2 );
return fazerMel();
}
}
Assim o método termina, é retirado da Stack e volta o fluxo para o método buscarMel().

Veja que buscarMel() não guarda o valor de retorno obtido de fazerMel() (que é 12) em nenhuma variável. Ele simplesmente também retorna esse valor para o método que o chamou - que no caso, foi o método main.
public class Abelha {
int mgNectar;
void buscarNectar(int mg) {
if (mg > 40) {
Util.escrever("Uma abelha nao pode carregar mais que 40 mg de nectar!");
} else {
mgNectar = mg;
Util.escrever("A abelha buscou " + Integer.toString( mgNectar ) + " mg de nectar!");
}
}
int fazerMel() {
if ( mgNectar > 0 ) {
int temp = mgNectar;
mgNectar = 0;
return temp / 2;
}
return 0;
}
int buscarMel(int mgMel) {
buscarNectar( mgMel * 2 );
return fazerMel();
}
}
O método main, por sua vez, armazena o valor de retorno do método buscarMel() numa variável inteira chamada r.
public class Colmeia {
public static void main(String args[]) {
Util.escrever("Digite a qtde, em mg, de mel:");
int mgMel = Util.lerNumero();
Abelha ze = new Abelha();
int r = ze.buscarMel( mgMel );
Util.escrever("Foram produzidos de mel (mg): " + Integer.toString(r) );
}
}
Assim, temos a seguinte configuração da memória - veja que buscarMel() já saiu da Stack.

Após a escrita (pelo método escrever() de Util) do valor produzido de mel no prompt, o próprio método main termina e deixa a Stack. Nesse caso, como não existem outros fluxos de execução do sistema em andamento, a JVM termina seu trabalho nesse sistema e libera toda a memória. Caso ainda houvesse outras linhas de execução, o fato de retirar o método main da Stack eliminará também suas variáveis locais mgMel, ze e r. Todavia, a instância do objeto Abelha alocada na Heap permaneceria “viva” até que o Garbage Collection do Java limpe a Heap.
Um pouco sobre Garbage Collection
Mas o que é um Garbage Collection? Isso morde?
Ainda
não falamos sobre Garbage Collection, mas à priori, ele é um recurso
que as linguagens interpretadas oferece para excluir os objetos alocados
na Heap que não podem mais ser utilizados. Um objeto não pode mais ser
utilizado quando não existe, em nenhum ponto na Stack (ou na própria
Heap), um ponteiro para este objeto. No caso acima, por exemplo, assim
que o método main deixa de existir, nós temos a seguinte configuração da memória.

Veja que a instância de Abelha ainda existe na Heap, mas ela ficou inacessível. Isso ocorre porque não existe nenhuma variável pela qual poderemos acessá-la. Dizemos que esse objeto ficou elegível para o Garbage Collection.
Essa foi apenas uma
palinha. Quando estudarmos programas com várias instâncias de objetos,
voltaremos a comentar sobre o Garbage Collection.
Bom, senhores e senhoras, por hora vamos interromper nossa conversa. Espero que esse estudo do tipo “como funciona um programa na JVM” tenha ajudado a esclarecer suas dúvidas sobre o exercício da Colmeia.
Nos vemos na Aula 5!!
Obrigado pela companhia.
Abraços, Guilherme Pontes
Nenhum comentário:
Postar um comentário