Curso de Java – Módulo I

Propaganda
Curso de Java – Módulo I
Exceções, Entrada e Saída
Programação Swing
Fábio Mengue – [email protected]
Centro de Computação - Unicamp
Exceções
O termo exception é uma abreviatura da frase “exceptional event”. Sua definição formal é
um evento que ocorre durante a execução de um programa que quebra o fluxo normal dessa
execução.
Vários eventos podem causar exceções. Desde problemas sérios com hardware (como um
crash de disco) até um erro simples de programação, como acessar um elemento de um vetor
com um índice inválido. Quando um erro desses tipos ocorrem em um método Java, o método
cria um objeto do tipo Exception e o envia para o sistema. Esse objeto contém informação sobre
o erro, incluindo seu tipo e o estado em que o programa se encontrava quando o erro aconteceu.
O sistema fica responsável por encontrar alguma maneira de lidar corretamente com o erro. Na
linguagem usada pelos programadores Java, dizemos que o sistema gerou uma exceção (em
inglês, throwed an exception).
Depois que o método gerou uma exceção, o sistema tenta encontrar algum código que
possa ser utilizado para lidar com a exceção. Os candidatos mais prováveis são os que
“chamaram” o método onde o erro aconteceu. Se eles não tiverem condições de lidar com o
erro, o sistema continua a seguir a pilha de chamadas, até encontrar o código apropriado. Por sua
vez, o código é considerado apropriado para lidar com uma exceção quando o tipo da exceção
gerada é o mesmo tipo para o qual ele foi programado. Ao ser encontrado o código correto, o
sistema o executa (em inglês, catch the exception).
Caso o sistema não consiga identificar um código correto para gerenciar a exceção, o
sistema (normalmente representado pelo programa Java) se encerra. Claro que é desnecessário
dizer que isso não é exatamente o que gostaríamos que acontecesse na maioria das vezes.
Pelo fato de utilizar exceções para gerenciar erros, os programas em java possuem
vantagens sobre as linguagens tradicionais. A primeira grande vangatem é poder separar o
gerenciamento de erros do código comum. Outra vantagem é a propagação de erros, que permite
que se crie uma classe especializada apenas no gerenciamento destes eventos. E por fim, é
possível agrupar erros por tipo, tratando-os de uma vez. Veremos exemplos para cada uma das
vantagens a seguir.
1
Nas linguagens de programação tradicionais, a detecção de erros e seu tratamento
normalmente causa uma certa confusão no código, pois tudo está misturado. Por exemplo,
imagine que você tenha uma função que leia um arquivo do disco para a memória. Imagine que
esse programa se parece com isso:
lerArquivo{
abrir o arquivo;
determinar seu tamanho;
alocar memória;
ler o arquivo para a memória;
fechar o arquivo;
}
Parece muito simples. Mas o que acontece se:
•
•
•
•
•
O arquivo não puder ser aberto ?
For impossível determinar o tamanho do arquivo ?
Não for possível alocar a quantidade total de memória ?
A leitura falhar ?
O arquivo não puder ser fechado ?
Então, criamos código a mais para detectar e lidar com esses possíveis erros. Assim,
nosso programa agora ficaria assim:
lerArquivo {
codigoErro=0;
abrir arquivo;
if (arquivoAberto) {
determinar tamanho;
if (tamanhoDeterminado) {
alocar memória;
if (memoriaAlocada) {
ler o arquivo para a memória;
if (leituraFalhou) {
codigoErro=-1;
} else {
codigoErro=-2;
}
} else {
codigoErro=-3;
}
fechar arquivo;
if (arquivoNaoFechado) && codigoErro=0)
{
codigoErro=-4;
}
2
}
}
Desta maneira, seu programa com 7 linhas ficou praticamente 3 vezes maior. O pior é
que o código a mais torna o programa um tanto ilegível, e a lógica se perde no meio dos if's e
else's, dificultando a identificação de quais pedaços do código estão realmente fazendo parte da
linha de raciocínio deste programa. Depois de algumas manutenções, o código deve ficar ainda
pior de ler, e os programadores geralmente irão resolver o problema simplesmente ignorando a
prevenção aos erros; eles irão aparecer quando o programa “der pau”.
A soluçaõ encontrada pelo Java é mais elegante. O programador irá escrever o programa
de maneira usual, mas o tratamento de erros estará separado. Se sua função for reescrita à
maneira Java, ficaria assim:
lerArquivo {
try {
abrir arquivo;
determinar tamanho;
alocar memória;
ler o arquivo para memória;
fechar arquivo;
} catch (falhaAberturaArquivo) {
Código de tratamento
}
} catch (falhaDeterminarTamanho) {
Código de tratamento
}
} catch (falhaAlocacaoMemoria) {
Código de tratamento
}
} catch (falhaLeitura) {
Código de tratamento
}
} catch (falhaFechamentoArquivo) {
Código de tratamento
}
}
Note que o trabalho de criar código para gerenciar o erro continua. O que temos é a
separação dos detalhes do que fazer quando algo acontece. O Java automaticamente executa
apenas o código que se relaciona com o erro, tornando desnecessária a construção de cadeias ifelse para gerenciar os erros. O programa também fica maior, mas mais legivel e ainda sim menor
do que na abordagem tradicional.
A segunda vantagem que existe na utilização de exceções é a habilidade de propagar
erros. Suponha que nossa função lerArquivo é o quarto método em uma série de métodos
3
implementados em sua classe. O metodo1 chama o metodo2, que chama metodo3, que
finalmente chama lerArquivo.
metodo1 {
call method2;
}
metodo2 {
call method3;
}
metodo3 {
call lerArquivo;
}
Suponha que o código em metodo1 seja o único interessado nos erros que podem
acontecer em lerArquivo. Se isso fosse ser implementado usando a notificação tradicional,
metodo3 e metodo2 deveriam ter maneiras de propagar códigos de erro eventualmente
retornados por lerArquivo para os métodos que os invocaram, até chegar a metodo1.
metodo1 {
errorCodeType error;
error = call metodo2;
if (error)
doErrorProcessing;
else
proceed;
}
errorCodeType metodo2 {
errorCodeType error;
error = call metodo3;
if (error)
return error;
else
proceed;
}
errorCodeType metodo3 {
errorCodeType error;
error = call lerArquivo;
if (error)
return error;
else
proceed;
}
4
Como já visto, o sistema Java irá procurar automaticamente um código adequado para o
gerenciamento de erros. Usamos então uma palavra que indica que qualquer exceção ocorrida (e
não tratada) deve ter propagada para a classe que “chamou” a classe corrente. Veja o exemplo:
metodo1 {
try {
call metodo2;
} catch (Exception) {
doErrorProcessing;
}
}
metodo2 throws Exception {
call metodo3;
}
metodo3 throws Exception {
call lerArquivo;
}
Entretanto, como pode ser visto no código, o envio das exceções exige um certo esforço
dos métodos intermediários, que também necessitam repassar a exceção com throws até o
método correto. Mesmo assim, essa técnica obviamente é mais vantajosa que a tradicional, no
sentido que evita confusão no código.
A última mas não menos importante vantagem é que as exceções são agrupadas. Por
exemplo, imagine um grupo de exceções. Todas elas dizem respeito a erros que podem ocorrer
quando se manipula um vetor. Exceção para estouro do vetor, para inclusão de tipo inválido, ou
mesmo para quando o elemento procurado não faz parte do vetor. Cada uma dessas exceções
pode ser tratada de maneira independente, ou você pode ter um método que trate todas as
exceções deste grupo, tornando a coisa mais simples.
Pelo fato das exceções serem classes Java como qualquer outra, a hierarquia presente em
todas as classes Java também está presente aqui. Todas elas são instâncias ou descendentes da
classe Throwable. Cada “folha” da árvore que se inicia em Throwable representa uma exceção
específica, e cada “tronco” representa um grupo relacionado de exceções.
Por exemplo, ArrayException é uma subclasse de Exception (por sua vez, uma subclasse
de Throwable), e possui 3 subclasses: InvalidIndexException, ElementTypeException,
NoSuchElementException. Cada uma delas representa um erro específico, que pode ocorrer na
manipulação de um vetor. Uma maneira de lidar com as exceções é uma a uma:
catch (InvalidIndexException e) {
...
}
Outra maneira seria tratando a exceção de acordo com o grupo ao qual ela pertence. Isso
pode ser feito por vários motivos, o mais forte deles é manter a simpliciadade. No nosso
exemplo, temos ArrayException, que representa qualquer erro que pode acontecer com a
5
manipulação de um vetor. Assim, um método pode lidar com uma exceção baseada em sua
hierarquia. Por exemplo:
catch (ArrayException e) {
...
}
Este método lida com todas as exceções que dizem respeito a vetores. É possível saber
qual a exceção através de uma pergunta ao objeto e, recebido como parâmetro. Também é
possível definir um método que lida com qualquer exceção que chegar até ele, como no exemplo:
catch (Exception e) {
...
}
Seu primeiro encontro com exceções Java.
Veja a mensagem de erro abaixo. Ela é gerada pois a classe que estamos compilando
contém chamadas a métodos que lançam exceções quando um erro acontece.
InputFile.java:8: Warning: Exception java.io.FileNotFoundException
must be caught, or it must be declared in throws clause of this method.
in = new FileReader(filename);
^
A linguagem Java exige que os métodos capturem ou especifiquem o lançamento de
exceções que podem acontecer dentro daquele escopo. Se o compilador identifica que aquele
pedaço de código não possui código adequado para o tratamento de exceções, a mensagem de
erro acima é apresentada e o programa simplesmente não é compilado.
Vamos ver o código do programa InputFile.java e verificar a causa do erro.
import java.io.*;
public class InputFile {
private FileReader in;
public InputFile(String filename) {
in = new FileReader(filename);
}
public String getWord() {
int c;
StringBuffer buf = new StringBuffer();
6
do {
c = in.read();
if (Character.isWhitespace((char)c))
return buf.toString();
else
buf.append((char)c);
} while (c != -1);
return buf.toString();
}
}
O compilador apresenta o erro na linha 8 (in = new FileReader(filename);). Essa
instrução cria um objeto FileReader, usado para abrir um arquivo cujo nome é passado como
parâmetro.
O que acontece se o arquivo não existir no disco ? Os profissionais que desenharam a
classe FileReader não tinham a menor idéia do que o programador gostaria que acontecesse caso
o arquivo não existisse. O programa deve terminar ? Um nome de arquivo diferente deve ser
testado ? O arquivo deve ser criado ? Não existe jeito de saber o que o programador que está
utilizando o FileReader gostaria que acontecesse nessa situação. Assim, eles geram uma exceção
(do tipo java.io.FileNotFoundException). Assim, o método que chamou a instrução decide a
maneira apropriada de tratar do assunto.
No nosso exemplo, pode se ver que o programa ignora o fato de que FileReader pode
gerar uma exceção. Mas o compilador Java não deixa que o fato passe em branco, se recusando a
compilar o programa e gerando uma mensagem de erro que alerta o programador a respeito.
Além da mensagem vista acima, outra mensagem deve ser gerada:
InputFile.java:15: Warning: Exception java.io.IOException
must be caught, or it must be declared in throws clause
of this method.
while ((c = in.read()) != -1) {
^
Agora, o problema está no objeto in, que da classe FileReader. O método read() também
pode gerar uma exceção, desta vez caso algum problema aconteça na leitura. Por exemplo, se as
permissões do arquivo não estejam configuradas de modo que esse programa possa ler seus
dados. A exceção gerada pode esse método é java.io.IOException.
Nesse ponto, você tem duas opções. Ou pode criar o código para capturar as exceções
dentro do programa InputFile ou propaga-las para outros métodos mais acima na pilha de
execução. De qualquer maneira, o programa InputFile deve fazer alguma coisa. E para fazer isso
existe uma maneira correta.
7
O Bloco try
O primeiro passo para construir um programa capaz de gerenciar exceções é colocar os
comandos que podem gerar exceções dentro de um mesmo contexto, usando a palavra reservada
try:
try {
Comandos...
}
Você pode colocar cada um dos comandos em um contexto próprio, ou pode colocar
todos os comandos que podem gerar exceção em apenas um contexto e depois programar
(separadamente) o código que irá lidar com cada uma das exceções.
Por exemplo:
PrintWriter out = null;
try {
out = new PrintWriter(
new FileWriter("OutFile.txt"));
for (int i = 0; i < size; i++)
out.println("Value at: " + i + " = " + victor.elementAt(i));
}
O bloco try neste programa delimita o escopo sobre o qual o código de tratamento de
erros irá atuar. Nesse pedaço de código existem duas instruções que podem gerar exceções; você
consegue identifica-las ?
Uma instrução try deve ser acompanha de pelo menos um bloco catch.
O Bloco catch
A instrução catch define qual o código que irá ser executado, dependendo da exceção
gerada. Você os associa aos blocos definidos pelo try colocando-os logo após o bloco:
try {
...
} catch ( . . . ) {
...
} catch ( . . . ) {
...
}...
Não se deve ter nenhuma outra instrução entre o final do contexto do try e o início do
catch.
8
A forma geral do catch é:
catch (SomeThrowableObject variavel) {
Comandos...
}
Note que ele é muito parecido com um método. Todo bloco catch identifica o tipo de
exceção que ele trata (SomeThrowableObjet, que é o nome da classe-filho de Throwable) e
recebe como parâmetro a exceção recebida.
Essa exceção, como tudo em Java, é um objeto. Sendo subclasse de Throwable, esse
objeto herda uma série de métdos. Um dos mais utilizados é o getMessage(), que serve para
mostrar qual a mensagem de erro. Temos ainda métodos para mostrar a pilha de execução e você
pode definir ainda suas próprias exceções (basta criar uma classe que extends Throwable).
O bloco catch contém normalmente comandos comuns do Java. Muitas vezes o bloco
serve simplesmente para imprimir uma mensagem indicando o erro e parando o programa. O
bloco é executado apenas se a exceção for disparada; os outros blocos catch são ignorados.
Se utilizarmos como exemplo o código apresentado na página anterior, podemos definir
os blocos catch da maneira mostrada abaixo:
try {
...
} catch (ArrayIndexOutOfBoundsException e) {
System.err.println("Caught ArrayIndexOutOfBoundsException: " +
e.getMessage());
} catch (IOException e) {
System.err.println("Caught IOException: " +
e.getMessage());
}
O primeiro bloco irá ser executado quando for disparada uma exceção de índice do vetor,
e o outro quando ocorrer um erro de escrita no arquivo.
A granularidade permitida é muito grande, como podemos ver. Mas é possível definir
apenas alguns (ou mesmo um) blocos para todas as exceções. Criar código diferenciado para
erros de acesso a um vetor ou erro no disco pode ser pouco produtivo. O Java tem a capacidade
de agrupar as exceções, baseado na organização hierárquica das mesmas.
Quanto mais próximo da classe “folha”, mais específica é o tratamento da exceção.
Podemos escolher em tratar as classes mais próximas do “tronco” ou mesmo da “raiz”.
Veja a imagem:
9
Assim, podemos reescrever nosso exemplo de modo que apenas um bloco catch pode
lidar com todas as exceções:
try {
...
} catch (Exception e) {
System.err.println("Exception caught: " + e.getMessage());
}
A classe Exception é a mais alta na hierarquia Throwable. Assim, qualquer exceção que
ocorrer será tratada por esse bloco. A idéia em si é que ele basta para tratar de erros gerais, mas é
um tanto inútil para um tratamento adequado, perdendo um pouco da vantagem de tratamento
que o Java fornece.
Finalmente, temos o bloco finally.
O Bloco finally
Todo try deve ter pelo menos um catch. Existe mais um bloco de código que pode ser
definido, mas ele não é obrigatório. Ele existe para que se possa realizar certas atividades que
devem ser feitas independentemente do curso de execução ter gerado exceções ou não. Um
exemplo é fechar um arquivo ou conexão com o banco de dados.
Lembremos novamente de nosso exemplo da página 8. Três casos ocorrer.
1) O FileWriter falha e lança uma IOException.
2) O victor.elementAt(i) falha e lança uma ArrayIndexOutOfBoundsException.
3) Tudo dá certo, e nenhuma exceção é gerada.
Certas instruções devem ser executadas para qualquer um dos casos. O bloco finally serve
justamente para isso. Suas instruções serão executadas sempre, com exceções ou sem elas. Sua
definição permite que eliminemos código duplicado e facilita o encerramento de uma rotina.
10
Exercícios:
1. O código abaixo é válido ?
try {
...
} finally {
...
}
2.Quais as exceções que serão capturadas pelo bloco abaixo ?
catch (Exception e) {
...
}
Cite uma desvantagem de usar esse tipo de classe de exceção.
3.Quais as exceções que serão capturadas pelo bloco abaixo ?
...
} catch (Exception e) {
...
} catch (ArithmeticException a) {
...
}
Há alguma coisa errada com esses blocos ? O código compila ?
11
Entrada e saida
As vezes um programa necessita trazer informação de uma fonte externa ou enviar
informação para um destino fora do programa. A informação pode estar em qualquer lugar: um
arquivo em disco, na rede, em outro programa. A informação também pode ser de qualquer tipo:
um objeto, um caracter, uma imagem, um som...
Para lidar com essas informações, o Java utiliza o conceito de stream. Ele pode ser
entendido como se fosse um “tubo”, onde as informações são enviadas em fluxo, que são lidas
(ou escritas). Veja o desenho:
Não importa de onde o dado esteja vindo (ou indo), e não importa seu tipo. Os algoritmos
que lidam com streams tratam de maneira sequencial do dados:
abrir o stream
enquanto houver informação
ler informação
fechar o stream
O pacote java.io possui classes que tratam e criam os streams. Basicamente, temos
streams para leitura de caracteres e para leitura de bytes. Os primeiros possibilitam a leitura e
escrita de palavras de 16 bits, e são originários da classe abstrata java.io.Reader e java.io.Writer.
12
Os streams para leitura de bytes lidam com palavras de 8 bits, e são subclasses de
java.io.InputStream e java.io.OutputStream.
Os streams mais fáceis de entender são os relacionados a arquivos. Segue um exemplo de
programa que copia um arquivo de um stream para outro.
import java.io.*;
public class Copy {
public static void main(String[] args) throws IOException {
File inputFile = new File("entrada.txt");
13
File outputFile = new File("saida.txt");
FileReader in = new FileReader(inputFile);
FileWriter out = new FileWriter(outputFile);
int c;
while ((c = in.read()) != -1)
out.write(c);
in.close();
out.close();
}
}
Como podemos ver, o programa é muito simples. Ele abre um FileReader no arquivo
entrada.txt e abre um FileWriter no arquivo saida.txt. O programa lê caracteres enquanto houver
no stream de entrada e os escreve no stream de saída. Quando terminar, ele fecha ambos os
streams.
O assunto é um tanto extenso, e esse curso se propõe a passar apenas a idéia básica sobre
o assunto. Temos capacidade de gerar ou ler streams com arquivos zipados, serializar objetos
(artifício utilizado para comunicação remota) e mesmo acessar arquivos de maneira não
sequencial.
Introducao ao Swing
O pacote Swing é parte da JavaTM Foundation Classes (JFC) na plataforma Java. Existe
na linguagem como pacote de extensão desde a versão 1.1, e serve basicamente para auxiliar as
pessoas a construirem GUI's (Guided User Interface). O Swing fornece todos os componentes
básicos, de botões a tabelas.
Seu primeiro Swing
A maneira mais fácil de ensinar a utilizar o Swing mostrando código. Vamos a um
programa bem simples, que exibe “Alo Mundo”.
import javax.swing.*;
public class HelloWorldSwing {
public static void main(String[] args) {
JFrame frame = new JFrame("HelloWorldSwing");
final JLabel label = new JLabel("Hello World");
frame.getContentPane().add(label);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
14
frame.setVisible(true);
}
}
Esta é a aplicação mais simples que se pode escrever. Vamos analisar o código linha a
linha.
Inicialmente, como sempre, importamos os pacotes que iremos utilizar. No caso,
javax.swing.*. A outra linha interessante é a onde se define nossa raiz, ou container. Ele é o
elemento principal, e onde iremos “dependurar” tudo que queremos que apareca em nossa tela.
No nosso caso, eh o JFrame. Ele permite que outros componentes sejam adicionados a ele, e tem
a capacidade de gerenciar eventos. Existem outros 2 containers Swing: JDialog e JApplet (esse
último, utilizado apenas para Applets).
Cada objeto JFrame implementa uma única janela principal, que possui decorações. Elas
são a borda, o título e botões para maximizar, minimizar e fechar a janela. Uma aplicação Swing
usa normalmente pelo menos um JFrame.
Nosso frame tem um componente. Um label, onde escrevemos “Alo Mundo”. As duas
linhas de código abaixo constroem e adicionam o componente ao frame:
final JLabel label = new JLabel("Hello World");
frame.getContentPane().add(label);
Para fechar a janela quando o botão de fechar é clicado, devemos incluir a linha abaixo
no nosso programa:
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
O comportamento padrão de um frame quando o botão de fechar é clicado é esconder a
janela, coisa que não desejamos.
A linha onde consta o método pack() determina o tamanho do frame de modo que todos
os componentes estejam em um tamanho padrão. Esse método utiliza algumas propriedades do
sistema para criar e colocar o frame em um tamanho considerado adequado.
A última linha, que chama o método setVisible(), faz com que o frame apareça na tela.
Veja o resultado:
15
Notas sobre o Swing:
Para aparecer na tela, todos os componentes devem ser adicionados e estarem presentes
em alguma parte de hierarquia, que se inicia na raiz (JFrame, JDialog ou JApplet).
Todos os componentes raiz possuem um content pane, que contém a parte visível do que
estamos montando, como mostrado na figura abaixo:
Opcionalmente, você pode adicionar um menu a um componente raiz. O menu é
posicionado diretamente na raiz, fora do content pane.
Bibliografia
The Java Tutorial – http://www.sun.com/docs
Core Java – Horstmann, Cay S., Cornell, Gary. Makron Books
Proibida a alteração, reprodução e cópia de parte deste material para qualquer finalidade sem a permissão do Centro
de Computação da Unicamp.
A utilização deste material é permitida desde que conste a autoria do mesmo.
 2002 Centro de Computação da Unicamp.
16
Download