Informações
| Tipo: | Tutorial |
|---|---|
| Data de Publicação: | 01/01/2004 |
| Revisado em: | 01/01/2004 |
Vote!
Tags Relacionadas
Comentários ( 1 )
Imprimir
Fundamentos da Linguagem Java
por:
Antônio Theóphilo (actcj@yahoo.com.br)
Este capítulo inicial tem o objetivo de passar em revista conceitos fundamentais da linguagem Java. Ele não trata de nenhum assunto específico mas fala um pouco de muitos itens que são essenciais para o entendimento da linguagem. Alguns assuntos tratados aqui superficialmente (propositadamente) terão um capítulo totalmente dedicado a cada um deles aonde os seus detalhes serão discutidos.
Como este capítulo trata de conceitos fundamentais da linguagem, é imprescindível que o leitor compreenda-o totalmente, não deixando nenhuma dúvida para capítulos posteriores. Dúvidas podem ser tiradas pelo endereço actcj@yahoo.com.br.
1. Arquivos Fonte
Todo arquivo fonte em java deve possuir uma extensao .java. Um arquivo fonte pode conter a definição de
muitas classes no entanto uma importante nota sobre este assunto é que caso haja uma classe pública no nível mais
alto de aninhamento (podem existir classes aninhadas) ela deve ser única. Caso uma classe pública esteja presente,
ela deve ter o mesmo nome (diferenciando maiúsculas e minúsculas) que o arquivo fonte sem a extensão. Ou seja, caso
haja uma classe pública chamada MinhaClasse no nível mais alto de aninhamento, então ela além
de ser a única classe pública neste nível de aninhamento, deve estar contida em um arquivo fonte que tem o nome
MinhaClasse.java. Este não é requisito da linguagem mas das implementações dos compiladores,
inclusive dos compiladores referência da Sun.
Existem três elementos principais em um arquivo .java, são eles:
- Declaração de Pacote
- Declaração de Importação de Classes
- Definições de Classes
Nenhum destes três elementos é obrigatório mas caso algum deles esteja presente ele deve estar na ordem mostrada acima.
A declaração de pacote define a que pacote as classes definidas neste arquivo pertencem. Ela começa com
a palavra chave package, segue-se o nome do pacote e por fim o caracter ponto-e-vírgula. O nome do pacote é
composto por um ou mais identificadores separados pelo caracter ponto (.). Cada identificador representa um diretório
respeitando a hierarquia do nome do pacote, por este motivo é recomendável que o nome do pacote não contenha
caracteres como espaço, barra, barra invertida, ...
A declaração de importação de classes pode ser feita de duas formas: pode-se importar uma classe específica ou
pode-se importar todo um pacote. A forma geral de uma declaração de importação é a palavra chave import
seguida pelos identificadores do pacote/classe separados pelo caracter ponto e por fim seguida pelo caracter ponto-e-vírgula.
O identificador pode representar uma classe específica ou um pacote inteiro. Para importar um pacote inteiro basta
inserir um asterisco (*) no lugar do nome da classe. Segue abaixo um exemplo de um arquivo fonte:
|
Uma importante nota a este respeito é que existem classes com o mesmo nome em diferentes pacotes (por exemplo a classe
Date dos pacotes java.sql e java.util). Caso tente-se importar dois pacotes inteiros
e ambos contenham classes de mesmo nome, ao utilizá-las o compilador vai apresentar uma mensagem de erro devido à
ambiguidade. A saída para este problema é importar apenas um pacote inteiro e utilizar o nome totalmente qualificado
(por exemplo java.util.Date) para a classe do outro pacote. Outra saída mais clara é utilizar o nome
totalmente qualificado para as duas classes ambíguas.
Um outro ponto importante sobre a utilização da palavra "import" é que o compilador não carrega
as classes declaradas a menos que necessite dela. Ou seja, o carregamente das classes é feito sob
demanda. Assim, quando o trecho import java.util.* é utilizado, o compilador não
necessariamente carrega todas as classes daquele pacote. O que ocorre é que você indicou ao
compilador onde ele deve tentar procurar as classes caso precise delas. Para clarear ainda mais,
vamos supor que dentro de seu programa seja necessário utilizar uma estrutura de dados java.util.Vector.
É possível importar essa classe de duas formar: import java.util.Vector ou import java.util.*.
Não existe qualquer diferenças entre as duas soluções. O compilador irá carregar apenas a classe Vector.
2. Palavras Chave e Identificadores
A linguagem Java possui 51 palavras que são classificadas entre palavras-chave ou palavras reservadas. Estas palavras não podem ser utilizadas como identificadores em programas Java (identificador é uma palavra utilizada para nomear uma variável, método, classe ou um rótulo/label). Abaixo segue uma tabela delas:
abstract |
boolean |
break |
byte |
case |
catch |
char |
class |
const |
continue |
default |
do |
double |
else |
final |
finally |
float |
for |
goto |
if |
implements |
instanceof |
int |
interface |
import |
extends |
false |
long |
native |
new |
null |
package |
private |
protected |
public |
return |
short |
static |
strictfp |
super |
switch |
synchronized |
this |
throw |
throws |
transient |
true |
try |
void |
volatile |
while |
As palavras goto e const são palavras reservadas que não têm nenhum significado em Java. Apesar de não
possuir nenhum significado, estas palavras não podem ser utilizadas como identificadores.
Um identificador pode começar com três tipos de caracter: uma letra, o caracter cifrão ("$") ou o caracter sublinhado
("_"). Os caracteres subsequentes podem ser de quatro categorias: as mesmas três do primeiro caracter (letra, "$", "_")
ou um dígito. Os identificadores são sensíveis ao caso, por exemplo: o identificador temp é diferente de
Temp. Segue abaixo alguns exemplos de identificadores:
Identificadores aceitos:
|
|
3. Tipos de Dados Primitivos
Java possui oito tipos de dados primitivos que são:
booleancharbyteshortintlongfloatdouble
Estes tipos de dados primitivos podem ser classificados em três categorias: tipos booleanos -
boolean; tipos inteiros - char, byte, short,
int, long; e por fim tipos de ponto flutuante - float, double.
Segue abaixo uma tabela com os tipos primitivos e sua representação efetiva. Por representação efetiva queremos dizer qual é o comportamento aparente no nível Java das variávies. O tamanho real do armazenamento de cada variável e o seu layout podem variar de implementação para implementação e não está sob a tutela da especificação da linguagem. O que a linguagem especifica é o comportamento das variáveis em operações de shift, mascaramento, etc... no nível Java. Se você escreve código nativo, os valores da tabela abaixo provavelmente serão diferentes.
Tipo | Representação Efetivo em bits |
boolean | 1 |
char | 16 |
byte | 8 |
short | 16 |
int | 32 |
long | 64 |
float | 32 |
double | 64 |
Sobre as variáveis do tipo booleano tem-se pouco a se falar, elas podem possuir apenas dois valores: true
ou false.
Sobre as variáveis de tipos inteiros (char, byte, short, int e
long), há um pouco a se falar. Destes apenas o
tipo char não possui sinal. Ele pode conter valores que vão de 0 a 2^16 - 1. Estes valores representam um
caracter no código Unicode, que é um sistema de codificação utilizado para representar uma vasta quantidade de
caracteres internacionais. O sistema de codificação ASCII está contido dentro deste sistema e equivale a colocar
zero nos nove bits mais significativos e o código ASCII nos sete bits restantes.
Os outros tipos de variáveis inteiras são todas com sinal, representadas em complemento de dois. A faixa de valores de cada uma está mostrada na tabela abaixo.
Tipo | Tamanho | Mínimo | Máximo |
byte | 8 | -2^7 | 2^7-1 |
short | 16 | -2^15 | 2^15-1 |
int | 32 | -2^31 | 2^31-1 |
long | 64 | -2^63 | 2^63-1 |
Uma regrinha fácil para memorizar estes valores é notar que a partir do tipo
byte (que como o próprio nome indica contém oito bits) os tipos
que se seguem dobram o número de bits, e que o expoente da faixa de valores é sempre uma unidade
a menos que o número de bits. A última coisa a se lembrar é que a faixa positiva
tem um elemento a menos do que a faixa negativa devido ao algarismo zero.
Por fim temos dois tipos de ponto flutuante: float e
double. As variávies destes tipos têm seus valores
representados conforme a especificação IEEE 754. Uma observação importante a ser feita sobre estes tipos primitivos
é que eles podem assumir padrões de bits que não representam números mas resultados atípicos de expressões (como o
infinito negativo por exemplo). Estes padrões de bits estão representados como constantes nas classes Float
e Double e podem ser referenciadas do seguinte modo:
Float.NaN //NaN significa: Not a NumberFloat.NEGATIVE_INFINITYDouble.NaNDouble.NEGATIVE_INFINITYDouble.POSITIVE_INFINITY
A seguinte expressão no trecho de código abaixo atribui à variável x o valor Float.NEGATIVE_INFINITY:
float x = -3.0/0.0;
Você pode depois fazer a comparação x == Float.NEGATIVE_INFINITY
para checar isso.
Apenas uma curiosidade: se você tentasse escrever:
int x = -3/0;
haveria um erro de compilação pois não existe uma representação em números inteiros para este resultado.
4. Literais
Um literal é um valor que pode ser determinado em tempo de compilação, ao contrário de variáveis aonde o valor só pode ser conhecido em tempo de execução. Em Java só pode haver literais de tipos primitivos ou de strings. Por representarem valores fixos os literais só podem aparecer em lados direitos de expressões de atribuição e em chamadas a métodos.
4.1 Literais Booleanos
Como é de se esperar só existem dois literais válidos para os tipos booleanos que são
true e false. Abaixo estão
alguns exemplos de uso de literais:
|
4.2 Literais Caracter
Um literal caracter pode ser expresso colocando o caracter em questão entre aspas simples. É claro que nem todos os
caracteres estão disponíveis ao programador via teclado, para estes caracteres uma forma de expressá-los é colocando
entre aspas simples a expressão \u seguida do valor (Unicode) em notação hexadecimal do caracter em questão. Alguns
caracteres podem ser expressos de forma especial utilizando o caracter de escape \. Abaixo eles estão
listados logo após de um exemplo de literal expresso em notação hexadecimal e de um caracter expresso de maneira
usual com as aspas.
'a' | 'a'notacao normal |
'\u00FF' | notação em hexadecimal |
'\n' | nova linha |
'\t' | caracter de tabulação |
'\r' | return |
'\f' | formfeed |
'\b' | backspace |
'\\' | contrabarra |
'\'' | aspas simples |
'\'" | aspas duplas |
4.3 Literais Inteiros
A representação de literais inteiros é trivial. Eles podem ser expressos pelo seu valor em decimal que é a
representação padrão. Também pode-se representá-los em notação octal ou hexadecimal. Para usar a representação
octal basta acrescentar o literal 0 como prefixo do literal e para utilizar a representação hexadecimal
basta acrescentar o literal 0x ou 0X como prefixo do literal. Na representação em hexadecimal as
letras podem ser minúsculas ou maiusculas. O sufixo L ou l pode ser acrescentado ao literal para
indicar um literal do tipo long já que o tipo default para literais é int. Desaconselha-se o uso
do caracter l visto que ele es parece muito com o número um (1) e pode confundir os leitores do código.
A seguir estão listadas as diversas representações para o literal de valor decimal 44:
44 - notação decimal
054 - notação octal
0x2C - notação hexadecimal
0X2C - notação hexadecimal
0x2c - notação hexadecimal
0X2c - notação hexadecimal
|
Uma importante nota sobre literais numéricos é que ao atribuir um literal a uma variável, o complilador molda
(ou define) o tamanho do literal de acordo com o tipo da variável. Assim na atribuição short i = 0xff; o
compilador irá criar um literal de tamanho short. Apesar do exemplo anterior estar correto, a expressão short
n = 9 + i; irá causar um erro de compilação visto que o tipo da expressão é int.
4.4 Literais de Ponto Flutuante
Para um literal de ponto flutuante ser interpretado como tal ele deve ser representado da seguinte maneira:
- conter um ponto decimal, ou;
- conter a letra
eouEnuma representação do valor do literal em notação científica, ou; - conter a letra
fouFcomo sufixo indicando que o literal é do tipofloat, ou; - conter a letra
douDcomo sufixo indicando que o literal é do tipodouble.
Um importante fato a ser lembrado é que um literal de ponto flutuante sem nenhum sufixo é por default do tipo
double. A seguir estão alguns exemplos de literais de ponto flutuante:
10.56 - literal de ponto flutuante double
4.28e+13 - literal de ponto flutuante double em notação científica
3.00E12 - literal de ponto flutuante double em notação científica
57.18f - literal de ponto flutuante float
12.14d - literal de ponto flutuante double
15.33E-13f - literal de ponto flutuante float em notação científica
|
4.5 Literais String
Um literal string representa uma cadeia de caracteres que está contida entre aspas duplas. Os mesmos
caracteres de escape utilizados para literais do tipo caracteres podem ser utilizados dentro de um literal
string, inclusive para o caracater " que pode ser inserido utilizando a seqüência de escape \".
Java possui diversas facilidades para a manipulação de strings que serão vistas no capítulo 8. Abaixo está um
exemplo de utilização de um literal String:
String teste = " literal de teste com aspas duplas: \".\n ";
5. Arrays
Um array em Java pode ser visto como uma coleção ordenada de elementos do mesmo tipo. Esses elementos podem ser tipos primitivos, referências a objetos ou até mesmo referências a outros arrays. Array é uma coleção homogênea de elementos e por homogênea leia-se que os elementos devem ter o mesmo tipo. Isto não significa que os elementos devem ser da mesma classe, um array de uma determinada classe pode conter referências de objetos que são subclasses da classe em questão.
Existem três passos básicos que devem ser seguidos antes de se utilizar um array, são eles: Declaração, Construção e Inicialização.
A Declaração é o passo em que o programador informa ao compilador Java o nome do array e o tipo de elementos que ele vai conter. Abaixo estão alguns exemplos de declarações de arrays:
|
O exemplo acima ilustra os três elementos que um array pode conter: tipos primitivos, referências a objetos e referências a outros arrays.
Diferente de outras linguagens como C/C++, em Java os colchetes de uma declaração de array podem vir tanto
depois do tipo do array como depois do nome, logo as declarações int[] arrayUm; e int arrayUm[];
possuem o mesmo efeito. Um método que retorne um array pode ser declarado como int[] metodo() ou como
int metodo()[].
Como já foi dito, a declaração apenas especifica o tipo e o nome do array. A Construção, ou seja, a
alocação de memória, só é realizada em tempo de execução através do operador new. É neste passo que o
programador especifica o tamanho do array. Abaixo está um exemplo de construcão:
float[] arrayDeFloat; //Declaração
arrayDeFloat = new float[10]; //Construção
|
Por ser realizada em tempo de execução, o tamanho do array pode ser especificado utilizando uma variável ao
invés da utilização de constantes ou literais (como em C/C++). Outra nota a ser feita é que a declaração e a
construção podem ser realizadas em uma única linha de código. Abaixo está um exemplo de uso de variável para
especificar o tamanho de um array e como declarar e construir um array em uma única linha:
int tamanho; //Variável
tamanho = 10;
float[] arrayDeFloat = new float[tamanho]; //Declaração e Construção
|
Após a construção, os elementos do array contém valores default. Estes valores dependem do tipo do
elemento e estão listados na tabela abaixo.
Tipo | Valor Inicial |
byte | 0 |
short | 0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0d |
char | '\u0000' |
boolean | false |
referência | null |
Caso o programador deseje utilizar outros valores (e quase sempre ele o quer) deve realizar o terceiro passo que é a Inicilização. Neste passo o programador define valores para cada elemento do array que ele tem interesse. A inicialização pode ser realizada para cada elemento em separado após a construção ou pode ser realizada juntamente com a declaração e construção. Veja exemplos abaixo:
int[] arrayDeInt; //Declaração
arrayDeInt = new int[3]; //Construção
for(int i=0; i < arrayDeInt.length; i++)
arrayDeInt[i] = i+3; //Inicialização
float[] arrayDeFloat = {1.0f, 1.5f, 2.0f, 2.5f, 3.0f}; //Declaração,
//Construção e
//Inicialização
|
No caso acima não é necessário especificar o tamanho do array pois esta informação já está implícita no número de elementos dentro das chaves.
Foi utilizado no exemplo acima o termo arrayDeInt.length. Arrays em Java são objetos como outros quaisquer
e portanto é permitido utilizar suas referências para acessar atributos ou métodos. No caso acima length
nao é um atributo mas um caso especial da linguagem. É uma informação do objeto que o compilador permite o acesso e que indica o
tamanho do array. Apesar de ser uma classe, o programador não pode criar uma subclasse da classe Array.
Duas notas importantes devem ser feitas em relação à arrays: 1. Os índices dos arrays em Java começam em 0, ou
seja, um array de 100 elementos tem índices que vão de 0 até 99; 2. A última observação a ser feita é que como os
arrays em Java são objetos e arrays multidimensionais são nada mais nada menos do que arrays de arrays, é
possível criar arrays não-retangulares. Arrays não-retangulares são arrays que possuem uma ou mais dimensões
variávies (veja exemplo abaixo).
int[][] arrayDeArrayDeInt;
arrayDeArrayDeInt = new int[3][]; //Primeira Dimensão - Fixa
for(int i=0; i < arrayDeArrayDeInt.length; i++)
arrayDeArrayDeInt[i] = new int[i+3]; //Segunda Dimensão - Variável
|
Figura 1.1
No exemplo acima usamos a seguinte linha de código: new int[3][] que é legal, significa que estamos
criando um array de três elementos que por sua vez são arrays de inteiros ainda não construídos. Seria ilegal se
escrevêssemos new int[][3] pois essa construção não faz sentido: é um número desconhecido de arrays para
três inteiros. Essa primeira parte criou a primeira dimensão do array que é fixa, seus elementos conterão
referências a outros arrays. Esses outros arrays podem ter o tamanho que quisermos e representarão a segunda
dimensão do array. É a partir da segunda dimensão que podemos variar o tamanho dela. É uma característica
interessante da linguagem porém perigosa pois o programador deve-se lembrar do tamanho de todos os arrays.
6. Classes
Nesta parte iremos dar algumas poucas informações sobre um conceito fundamental em Java que são classes. Um maior detalhamento sobre este assunto será feito em um capítulo posterior.
6.1 O método main()
O método main é a porta de entrada padrão para qualquer aplicação Java. Toda aplicação Java deve conter
uma classe que contenha o método main e para dar ínicio à aplicação deve-se escrever "java" e o nome da classe que
contém o referido método. A assinatura do método é a seguinte:
static void main(String[] args)
<%--Não é obrigatório mas é costume acrescentar a essa assinatura o modificador de acesso public.--%>
O modificador public determina que esse método poderá ser acessado por um classe externa. Isso faz
com que a JVM consiga acessar tal método quando acionado através da chamada "java NomeDaClasse".
O modificador static é necessário pois permite a chamada do método main sem a criação de uma instância da
classe. Se o tipo de retorno for outro a classe será compilada, no entanto a máquina virtual não conseguirá
encontrar um método main que retorne void e portanto haverá um erro em tempo de execução. O mesmo motivo se
aplica à lista de parâmetros. Podemos mudar o nome do array de strings mas se mudarmos o tipo dos parâmetros a
máquina virtual não conseguirá dar início à aplicação. Este array de strings é uma forma de comunicação entre a
aplicação Java e quem a chamou. Ele contém os argumentos passados na linha de comando. Ao contrário de outras linguagens
(C/C++) em Java o primeiro elemento deste array não é o caminho/nome da aplicação, mas o primeiro argumento. Numa
chamada: java aplic bola casa porta, o elemento args[0]
representa a string "bola" enquanto que o elemento args[1] representa a string "casa" a
assim por diante.
6.2 Variáveis e Inicialização
Em relação ao tempo de vida, existem dois tipos de variáveis em Java: variáveis membro e variáveis automáticas.
- Variáveis Membro: São as variáveis que são membros de alguma classe, também chamadas de atributos. Estas variáveis são criadas quando uma instância da classe é criada e destruída assim que o objeto é destruído. O acesso a elas depende de uma referência ao objeto ao qual ela é membro e está sujeito a regras de acessibilidade.
- Variáveis Automáticas: São as variáveis criadas na entrada de um método e existem apenas durante a execução daquele método. Também são chamadas "method local".
Todas as variáveis membro que não são explicitamente inicializadas têm seus valores inicializados para um valor padrão que depende do seu tipo. A tabela abaixo mostra os valores default para cada tipo de variável.
Tipo | Valor Inicial |
byte | 0 |
short | 0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0d |
char | '\u0000' |
boolean | false |
referência | null |
No caso de uma variável membro ser inicializada na mesma linha da declaração int x = 20; ela terá seu
valor definido logo antes do construtor da classe ser chamado. Caso esta variável seja estática (através do uso do
modificador static) ela terá seu valor definido no momento que a classe fosse carregada.
Variáveis automáticas não são inicializadas pelo sistema de execução e o compilador Java exige que todas elas
sejam inicializadas antes de serem utilizadas. Caso uma variável seja usada sem talvez ter sido inicializada
(inicialização dentro de um if por exemplo) o compilador acusará um erro em tempo de compilação (exibirá a mensagem
"Variable ___ may not have been initialized"). Uma boa prática é inicializar todas as variáveis automáticas
não-inicializadas para o valor default exposto na tabela acima. Abaixo está um exemplo de variável automática que
receberá um erro em tempo de compilação por não ter a sua inicialização garantida antes do seu uso:
public int metodo(int arg) {
int temp;
if(arg > 0)
temp = arg*10;
return temp;
}
|
7. Passagem de Argumentos
Toda a passagem de argumentos em Java é feita por valor, isto significa que ao passar argumentos para algum
método você esta passando uma cópia para o método chamado. No entanto o efeito disto depende da informação que
este argumento possui. No caso de tipos primitivos (int, double, char, ...) o valor das variáveis é o padrão de
bits que representam o valor da variável. Ao passarmos uma variável de tipo primitivo como argumento estamos
passando uma cópia deste valor e ao modificarmos esta cópia dentro do método estamos modificando a cópia e
deixando intacta a variável original.
public void metodo1() {
int temp = 5;
metodo2(temp);
System.out.println(temp);
}
public void metodo2(int arg) {
arg = 10;
}
|
No exemplo acima uma cópia do valor da variável temp é passada para o
método2 e toda a modificação feita dentro do
metodo2 é feita na cópia deixando a variável
temp intacta.
No caso de variáveis do tipo referência também uma cópia do valor é feita durante a passagem de argumentos, no entanto o efeito é um pouco diferente. O que é passado para o método é uma cópia da referência original, no entanto esta cópia "aponta" para o mesmo objeto e caso ela seja usada para chamar algum método que modifique o estado do objeto, o objeto original será modificado. Referências geralmente são implementadas como endereços de objetos, quando uma referência é passada como parâmetro, uma cópia deste endereço é criada e apesar de ser uma cópia este endereço aponta para o mesmo objeto, podendo assim modificá-lo. Abaixo está um exemplo do uso de referêcias:
public void metodo1() {
Label lbl = new Label();
lbl.setText("testo 1");
metodo2(lbl);
System.out.println(lbl.getText());
}
public void metodo2(Label arg) {
arg.setText("texto 2");
}
|
No exemplo acima é criado um objeto da classe Label com uma referência chamada
lbl. Esta referência é passada como parâmetro, o que significa que uma cópia
dela é passada para o método2. Agora temos 2 referências apontando
para o mesmo objeto, o que significa que ambas as chamadas setText irão modificar
o mesmo objeto. É importante notar que a cópia aqui é entre as referências e não entre objetos. Mais uma vez
a passagem de argumentos é por valor mas o valor neste caso não é uma grandeza numérica mas um endereço.
Mas como fazer para passarmos um tipo primitivo para um método por referência para que o mesmo possa modificá-lo? Uma técnica usada para este fim é criar um array de um único elemento e passar este array. Como arrays são objetos ao passá-los a um método estamos passando uma referência a eles, possibilitando ao método chamado o acesso á variável original (veja exemplo abaixo).
public void metodo1() {
int[] arr = new int[1];
arr[0] = 5;
metodo2(arr);
System.out.println(arr[0]);
}
public void metodo2(int[] arg) {
arg[0] = 10;
}
|
No caso acima estamos modificando a variável original através de uma cópia da referência ao array.
Uma última nota a ser feita é quanto a implementação de referências em Java. A especificação da Máquina Virtual Java é muito ampla em relação a como se implementar referências a objetos. Em algumas implementações ela é o endereço do objeto mas na maioria das implementações ela é um endereço para o endereço de um objeto. Este nível de indireção a mais (chamada dupla indireção) visa permitir ao Coletor de Lixo realocar os objetos para reduzir a fragmentação de memória.
8. Coletor de Lixo
Quando criamos um objeto dentro de um método da seguinte maneira: Label lbl = new Label(); nós alocamos
um espaço na pilha para a variável referência lbl que será liberado ao final do método. No entanto o objeto
Label será armazenado em outra área da memória chamada heap e a desalocação de espaço nesta área é
uma ação que necessita de um pouco mais de critério. Podíamos pensar que ao final do método poderíamos desalocar tanto
a referência da pilha quanto o objeto da heap, no entanto imagine que antes do final do método uma cópia da referência
a este objeto foi feita e passada para outra thread, não podemos desalocar o objeto pois ao fazer isso a outra thread
irá acessar dados inexistentes. Em muitas linguagens (C/C++, Pascal, ...) é responsabilidade do programador a alocação e
desalocação de dados na heap. Essa abordagem já provou ser muito suscetível a erros como: desalocação de mémoria
antes do tempo (provocando acesso a dados inexistentes) e estouro da heap pela não desalocação de memória. Java
possui uma abordagem um pouco diferente dessas outras linguagens. Ainda é responsabilidade do programador o momento de
alocação de memória, no entanto existe uma thread de baixa prioridade que varre a memória em busca de objetos não
mais referenciados para terem sua região de memória desalocada. Agora não é mais responsabilidade do programador a
desalocação de memória, ela é feita automaticamente por uma thread que é denominada Coletor de Lixo ou
Garbage Collector. Agora o problema de desalocar memória enquanto existem referências a ela está resolvido
pois o coletor de lixo apenas libera espaço de memória depois de não haver mais nenhuma referência apontando para
ela. O segundo problema de falta de espaço pela não liberação de memória é também resolvido pela abordagem de
coleta automática de lixo embora não de forma ótima. O coletor de lixo é, como já foi dito, uma thread de baixa
prioridade, logo ela é executada em intervalos de tempo e entre esses intervalos pode acontecer de existir espaço
em memória que pode ser liberado mas que só irá ser definitivamente limpo quando o coletor de lixo for executado. O
que realmente acontece é um compromisso entre desempenho e utilização de recursos. Em uma aplicação aonde a memória
está sendo muito utilizada o coletor de lixo irá ser executado com uma maior frequência do que em uma aplicação
com maior disponibilidade de memória. Também deve-se notar que o coletor de lixo não pode denegrir o desempenho de
certas aplicações. Por exemplo, uma aplicação de controle em tempo-real precisa ter garantias de que a resposta a
determinada ação seja imediatamente disparada sem que o coletor de lixo tenha influencia sobre o tempo de resposta.
O coletor de lixo hoje em dia é muito ligado a Máquina Virtual Java, o objetivo é que em futuras versões o
programador possa escolher diferentes algoritmos para o coletor de lixo que atenda às diferentes necessidades.
A execução do coletor de lixo é agendada pela JVM (Máquina Virtual Java), no entanto o programador pode interferir
neste agendamento. Através das chamadas System.gc(); ou Runtime.gc(); o programador pede à JVM para
que execute o coletor de lixo assim que possível. É importante salientar que não se pode garantir que após essa
linha de código o coletor de lixo irá ser executado, uma thread de maior prioridade pode entrar na frente do
coletor de lixo na fila de execução da JVM. Essa chamada é apenas uma "sugestão" do programador para a JVM.
É importante notar também que o coletor de lixo pode demorar um pouco a coletar espaço de um objeto não mais utilizado
mas ele nunca irá liberar espaço de um objeto ainda referenciado. Por isso é uma boa prática de programação
atribuir o valor null a variáveis referências não mais utilizadas pois isso fará os
objetos desnecessários serem liberados mais cedo.
Comentários (1)
- Muito bom esse site, me ajudou com algumas dúvidas que vão surgindo.... valew
- postado por Suellen em 03/10/2007 às 23:21
