Submarino.com.br




Objectos, Variáveis e Referências

Modelo de Memória

A plataforma java especifica um modelo de memória muito simples composto de um stack e um heap. A linguagem java segue este modelo bem de perto. O stack é utilizado para realizar as computações e o heap para guardar os objetos.

Variável

A linguagem java inclui, à semelhança das outras linguagens de programação, o conceito de variável. Uma variável é um pedaço da memória que o programador pode manipular atribuindo valores a ela. O valor dentro da variável, por ser um pedaço de memória tem uma representação binária. É o valor representado pelo arranjo binário dentro da variável que a plataforma java compreende.

Estrutura de uma Variável

Ilustração 1: Estrutura de uma Variável

A linguagem java é fortemente tipada, o que significa que todas as variáveis têm um tipo. O tipo é usado para interpretar o valor da variável.

As variáveis podem existir em diversos escopos e ser utilizadas para diversos fins. Podem ser usadas para guarda informação relevante à instância de um classe ou à própria classe. Podem ser usadas como parâmetros de métodos e podem ser usadas como auxiliares em computação dentro dos métodos. Em java todas as variáveis pertencem a algum escopo e não existe um escopo global. Todas as variáveis estão associadas a alguma classe, seja à classe em si, ou às instâncias dessa classe. As variáveis utilizadas dentro dos métodos têm uma vida curta que só existe durante a execução do método.

As variáveis usadas dentro dos métodos são utilizadas no stack durante a computação e descartadas no final. As variáveis de instância e classe existem no heap onde permanecem até que o coletor de lixo (GC - Garbage Collector) as remover. O GC é uma peça da JVM que através de um algoritmo determina qual espaço de memória deve ser libertado baseado no uso que é feito pelo programa das variáveis do heap.

Tipos Primitivos e não Primitivos

O valor representado pela sequência binária dentro da variável só pode ser interpretado baseado no tipo da variável e o tamanho da sequência binária é determinado pelo tipo da variável.

Familias de Tipos de Variável

Ilustração 2: Familias de Tipos de Variável

Existem duas familias principais de tipos de variável : os tipos primitivos e os tipos de referência. Os primeiros são tipos de valor, em que o arranjo de binário dentro da variável representa um valor por si mesmo. Por exemplo, um número. Os tipos de referência são tipos em que o arranjo binário dentro da variável representa um endereço na memória do heap.

Os tipos primitivos são divididos em três familias : numéricos inteiros, numéricos de vírgula flutuante (também chamados de ponto flutuante) e lógicos. Os tipos primitivos são representados por palavras reservadas começadas por letra minúscula. Cada tipo define , como foi dito, a quantidade de bits usada pela variavel. Os tipos numéricos são (com o seu tamanho em bits entre parênteses) : byte(8), short(16) , char(16), int(32) e long(64). A diferença entre os tipos short e char é que char não aceita valores negativos, apenas positivos. Todos os outros tipos de valor numérico aceitam valores negativos e positivos. Os tipos primitivos em java são todos com sinal, exceto char. Os tipos numéricos de vírgula flutuante utilizam regras especiais para conseguir representar binariamente números decimais. São eles : float(32) e double(64). O tipo lógico é representado por boolean(32).

Os tipos de referência têm um número de bits que depende da implementação da JVM, que por sua vez depende da arquitetura da máquina física e do Sistema Operacional, normalmente 32 em máquinas de 32 bits e 64 em máquinas de 64 bits. Os tipos de referência são representados pelos nomes das classes que referenciam. Por exemplo:

					String nome;
			
Código 1:

Representa uma variável do tipo de referência para uma instância da classe String. Se executarmos o programa seguinte:

					public static main(String[] args) {
					
						int inteiro = 2;
						
						String nome = "Java Building";
						
						System.out.println();
					
					}
			
Código 2:

no momento que invocamos System.out.println() o stack e o heap estariam assim:

O stack e o heap

Ilustração 3: O stack e o heap

Temos a variável 'args' no fundo do stack. Esta é uma variável de tipo de referência que referencia uma objeto do tipo "Array de String", representado por String[]. Depois temos a variável 'inteiro' com valor 2. Esta é uma variável de tipo de valor, e o conjunto de bits dentro da variável representam o valor 2. Finalmente temos a variável "nome" que é outra variável de tipo de referência que referencia um objeto do tipo String no heap. Entenda que os números que são o valor das variáveis "args" e "nome" não têm qualquer significado. São apenas rótulos dos objetos no heap que a JVM usa para saber a que objeto nos estamos referindo.

Operações e Operadores

A linguagem java define um conjunto de operações que podem ser aplicadas a cada tipo. Estas operações são disponibilizadas através de operadores na linguagem. Todos os tipos de variável suportam a operação de designação representada pelo símbolo de igual (=). Este operador permite colocar valor dentro da variável. Também todos os tipos suportam a operação de igualdade de valor, representado pelo operador == e não igualdade de valor, representada pelo operador !=. Estes operadores permitem saber se a representação binária dentro de uma variável é a mesma que em outra, ou não.

Os tipos numéricos suportam operações aritméticas comuns: Adição(+) , Subtração(-), Multiplicação(*), Divisão (/) e Módulo (%). Ainda suporta operadores de Incremento(++) , Decremento (--) O tipo lógico suporta operações lógicas AND(&&) e OR(||). Estes operadores seguem a tabela de verdade dos respectivos operadores lógicos e são munidos de funcionalidade "curto-circuito" (short-circuit) a qual permite que o operador retorne o valor lógico final, sem necessidade de verificar todos os valores da expressão. Por exemplo, em uma operação de AND se a primeira expressão tem valor 'falso', não importa o valor das outras expressões, porque o resultado será sempre falso. Então, o operador simplesmente retorna falso, sem ter avaliado o valor das outras expressões. Os tipos lógico suportam também o operador NOT(!) que retorna o inverso do valor lógico contido na variável.

Os tipos numéricos suportam ainda operadores de relação: Maior (>), Menor (<), Maior Ou Igual (>=) e Menor Ou Igual (<=). Estes operadores utilizam o conceito de ordenação numérica para determinar se o valor contido em uma variável é maior ou menor que o valor em outra variável.

Os tipos numéricos inteiros suportam ainda operações binárias , AND(&) , OR(|) , XOR(^) , Negação (~), Deslocamento para a esquerda (<<), para a direita (>>) e para a direita sem sinal (<<<). O operadores binários AND(&) , OR(|) não seguem as regras de "curto-circuito" como os operadores lógicos AND(&&) e OR(||), e embora possam ser usados com tipos lógicos e produzam os mesmos resultados, não são recomendados para esse fim. Notar que o operador lógico de negação (!) e o operador binário de negação (~) são diferentes e são aplicados a diferentes tipos de variável.

Os tipos de referência não suportam nenhum tipo de aritmética ( diferentemente dos ponteiros do C e C++ por exemplo). Contudo os tipos de referência suportam o operador de "desrreferênciação", representado pelo operador ponto-final(.). Este operador permite acessar a outras variáveis que estão no escopo representado pela variável onde o operador é aplicado. As variáveis de referência sempre representam endereços de instância de classes no heap. Inclusive de instâncias da classe Class o que permite acessar variáveis no escopo tanto da classe, quanto das suas instâncias. Contudo, o acesso é vigiado pela JVM que estabelece regras conforme os operadores de visibilidade (public, private, protected) utilizados.

Passagem de Parâmetros

Tomemos como exemplo o seguinte código:

				// método principal
				public static void main(String[] args){
				
					int a = 2;
					
					int resultado = adicionaQuatro(a);
					
					System.out.println("R=" + resultado + " A=" + a);
				}
				
				
				public static int adicionaQuatro(int valor){
					valor = valor + 4;
					
					return valor;
				}
				
			
Código 3:

Neste código simples estamos tentando somar 4 a 2. O resultado esperado é 6. Mas quando é valor de 'a' depois da operação ?

Este simples programa irá imprimir R=6 A=2. Isto pode parece estranho porque dentro do método adicionaQuatro tínhamos somado 4 a 'a'. Mas na realidade não foi isso que fizemos. Na linguagem java a passagem de parâmetros é feita por valor. Isto significa que sempre que uma variável é enviada para um método, é o seu valor que é enviado, e não a variável em si.

O que acontece de fato é que a JVM cria uma variável chamada "valor" no stack e copia o valor de 'a' para ela. Depois a JVM executa a operação de soma e o valor da variável "valor" é agora 6. Neste ponto o valor 6 é copiado para a variável "resultado" e esse é o valor apresentado na impressão. A variável 'a' nunca esteve dentro do método, e portanto, nunca foi modificada.

Este mecanismo não depende do tipo da variável, e o processo seria o mesmo se usarmos uma variável do tipo de referência. Por exemplo:

				// método principal
				public static void main(String[] args){
				
					String a = "Olá ";
					
					String resultado = adicionaNome(a);
					
					System.out.println("R=" + resultado + " A=" + a);
				}
				
				
				public static String adicionaNome(String valor){
					valor = valor + " JavaBuilding";
					
					return valor;
				}
				
			
Código 4:

O resultado será R=Olá JavaBuilding A=Olá . O valor de 'a' não é modificado.

O que confunde é a afirmação: "Em java, variáveis de referência são passadas por valor". Talvez se escrevermos a regra como : "Em java, variáveis do tipo de referência são passadas por cópia de valor" fique mais claro. É o valor dentro da variável que é passado e não a variável em si. Em outras linguagens é possível passar a variável ela própria. Esse mecanismo é chamado de "passagem por referência". É, os nomes não ajudam muito, mas o que é importante saber é se é passado apenas o valor dentro da variável( passagem por valor), ou a variável como um todo ( passagem por referência) e nunca confundir com o tipo da variável pois ele é irrelevante para este processo.

Portanto, em Java, a passagem de todas as variáveis, não apenas as de referência, acontece copiando o valor entre a variável que enviamos para o método, e a variável de parâmetro dentro do método. Ou seja, o arranjo dos bits dentro da variável que é passada é replicado no arranjo de bits da variável de parâmetro dentro do método. As razões para isto são várias. Há uma maior segurança fazendo desta forma porque se evitam efeitos secundários indesejados mas principalmente porque a JVM mantém o conhecimento do escopo da variável que está associado ao método onde ela foi criada, e por isso passar a variável para outro método iria gerar problemas porque o escopo iria mudar.

Uma palavra sobre o operador == e números de vírgula flutuante

O operador de igualdade == serve, como vimos, para testar se a representação binária dentro de duas variáveis é a mesma. Se é, sabemos que as variáveis representam a mesma coisa. Seja essa coisa, um valor numérico, um valor lógico, ou a referência a um objeto. A JVM garante que um objeto só tem uma referência e é sempre a mesma até que ele seja recolhido pelo GC, logo se a referência é a mesma, o objeto é o mesmo.

Isto é muito simples e é o que esperamos de qualquer linguagem. Contudo existe um detalhe na definição dos tipos numéricos de vírgula flutuante que passa desapercebido e que pode atrapalhar. Para os tipos numéricos de vírgula flutuante é possível que dois arranjos binários diferentes representem o mesmo valor. E mesmo que o arranjo seja o mesmo, o valor pode ser diferente. Esta complexidade é necessária por causa de valores especiais para os tipos numéricos como NaN (Not A Number - Não é um Número). O arranjo binário de NaN é sempre o mesmo, mas a JVM irá retornar falso se tentar fazer NaN == NaN. Ela irá fazer isso porque é uma regra deste tipo de variável e padronizados conforme os padrões IEEE. Não é apenas o java que tem este comportamento.

Os tipos numéricos de vírgula flutuante são complexos de só os deve usar com muito cuidado e apenas quando é necessário. No dia-a-dia é sensato evitar o seu uso. Para cálculos é possível usar a classe BigDecimal em vez. Se tiver mesmo que usar double ou float nunca use o operador ==. Em vez use os métodos Double.compareTo() e Float.compareTo() respectivamente. Estes métodos irão retornar o resultado que seria sensato esperar onde NaN é igual a si mesmo.