Escolar Documentos
Profissional Documentos
Cultura Documentos
Tópicos Avançados
Prof. Carlos Ribeiro
Setembro de 1999
_______________________________________________________________________________________
Índice
TRATAMENTO DE EXCEÇÕES E DEBUG DE PROGRAMAS ..........................................................4
STREAMS E ARQUIVOS..........................................................................................................................28
Streams ......................................................................................................................................................28
Lendo e Escrevendo Bytes ........................................................................................................................28
Tipos de Streams .......................................................................................................................................30
Layering Streams Filters............................................................................................................................32
Object Streams...........................................................................................................................................34
Armazenando Objetos de Tipos Variados .................................................................................................34
O Problema de Salvar Referências a Objetos ............................................................................................39
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 3
_______________________________________________________________________________________
TRATAMENTO DE EXCEÇÕES E DEBUG DE PROGRAMAS
Para situações excepcionais, tais como: digitação de dados errados com o potencial de
“detonar” o programa, java utiliza uma maneira de perceber e tratar o erro denominada
tratamento de exceções.
A primeira parte deste capítulo aborda tratamento de exceções e a segunda parte veremos
técnicas para se encontrar erros antes que eles causem exceções em tempo de execução.
Infelizmente, se você utiliza apenas o JDK, você verá que a detecção de bugs é
exatamente como era nos primórdios da computação. Para diminuir a dor explicaremos
como utilizar o debbuger de linha de comando. Desenvolvedores sérios em Java devem
utilizar produtos tais como Java WorkShop da Sun, Visual Café da Symantec e o Jbuilder
da Inprise que possuem debuggers bastante úteis que por si só já valem o preço do
produto.
Manipulando Erros
Exceções possuem sua própria sintaxe e são parte de uma hierarquia de herança especial.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 5
_______________________________________________________________________________________
A hierarquia Errors descreve erros internos e a exaustão de recursos dentro do Java run-
time system. Pouco você pode fazer se um erro deste tipo ocorre, além de notificar o
usuário e tentar terminar o programa normalmente. Estas situações são raras.
Quando você está programando em Java, você lida com a hierarquia Exception. Esta
hierarquia também se subdivide em dois ramos.: exceções que derivam de
RuntimeException e as que não derivam. A regra geral é:
A regra “Se foi um erro de run-time a culpa é sua” funciona muito bem, isto é, você
poderia ter evitado. Em relação à má formação de URLs, sabe-se que diferentes browsers
podem manipular diferentes tipos de URLs. Por exemplo, Netscape pode lidar com
mailto:URL, enquanto outros browsers não! Logo, a noção de URL mal formado depende
do ambiente e não apenas do código.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 6
_______________________________________________________________________________________
Um método Java pode lançar uma exceção se ele encontra uma situação que não pode
tratar. A idéia é simples: um método não dirá apenas ao compilador que valores ele pode
retornar. Ele também dirá ao compilador o que pode ir errado. Por exemplo, um código
que tenta ler de um arquivo sabe que o arquivo pode não existir ou que o arquivo pode
estar vazio. Logo o código que tenta processar a informação no arquivo necessitará
notificar o compilador que ele pode gerar algum tipo de IOException.
O local onde você anuncia que seu método pode gerar uma exceção é no cabeçalho do
método. O header muda para refletir as exceções que ele pode gerar. Por exemplo: aqui
está um header para um método da classe BufferReader que deve ler uma linha de texto
de um stream. Obs: um stream pode ser proveniente de um arquivo ou uma conexão de
rede.
O header indica que este método retorna um string, mas também possui a capacidade de
dar errado de uma maneira especial – gerando um IOException. Se esta triste situação
acontecer o método não retornará um string e sim uma exceção do tipo IOException. Se
esta exceção ocorrer o run-time system começará a procurar um manipulador de exceções
que saiba como manipular objetos do tipo IOException.
Quando você escreve seus próprios métodos, você não precisa anunciar todo tipo de
throwable object que pode ser gerado pelo seu método. Para entender quando e o que
você deve anunciar na cláusula throws do seu método, mantenha em mente que uma
exceção é gerada em qualquer uma das quatro situações abaixo:
• Você chama um método que gera uma exceção, por exemplo, o método readLine da
classe BufferedReader.
• Você detecta um erro e gera uma exceção com o comando throw. (Veremos este
comando na próxima seção).
Se qualquer dos dois primeiros cenários acontecerem, você deve dizer às pessoas que vão
utilizar o seu método que a utilização do seu método pode causar a geração de uma
exceção.
Porque? Qualquer método que lança uma exceção é uma armadilha de morte em
potencial. Se nenhum handler capturar a exceção, o programa termina. Programadores
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 7
_______________________________________________________________________________________
que utilizam o seu código geralmente não gostam de ver seus programas terminando
inesperadamente como resultado da utilização do seu código tão bem feito.
As exceções que um método pode gerar devem ser declaradas no header no método.
class Animacao
{ . . .
public Image carregaImagem (String S) throws IOException
{ . . .
}
}
Se um método deve manipular mais de uma exceção, você deve indicar todas elas no
header. Separe-as por vírgulas, como no exemplo abaixo:
class Animacao
{ . . .
public Image carregaImagem (String S)
throws EOFException, MalformedURLException
{ . . .
}
}
No entanto, você não necessita anunciar erros Java internos, isto é, exceções
descendentes de Error. Qualquer código potencialmente poderia gerar uma exceção deste
tipo, e elas estão inteiramente fora do nosso controle.
class Animacao
{ . . .
void drawImage (int i)
throws ArrayIndexOutOfBoundsException // Não faça isso!
{ . . .
}
}
Estes erros de run-time estão completamente sob o seu controle. Conserte o erro em vez
de anunciar que ele pode acontecer.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 8
_______________________________________________________________________________________
Resumindo, um método deve declarar todas as checked exceptions que ele gera. Se o seu
método não fizer isso, o compilador java emite uma mensagem de erro.
Quando um método de uma classe declara que ele gera uma exceção que é uma instância
de uma classe particular, então ele pode gerar uma exceção daquela classe ou de qualquer
de suas subclasses. Por exemplo, o método readLine da classe BufferedReader declara
que gera exceções da classe IOException. Nós não sabemos que tipo de IOException.
Poderia ser um IOException puro ou um objeto de um dos seus vários filhos, como
EOFException.
Vamos supor que você possui um método, readData, que está lendo um arquivo cujo
header informa que o tamanho do arquivo (content-length: 1024) é de 1024 bytes, mas
você recebe um EOF após ler 733 caracteres. Você decide que esta situação é tão anormal
que você decide gerar uma exceção.
Você necessita decidir que tipo de exceção gerar. Algum tipo de IOException seria uma
boa escolha. Olhando o arquivo tree.html na documentação das APIs Java, você descobre
um EOFException com a descrição: “Signals that a EOF has been reached unexpectedly
during input.” Perfeito. Aqui está como você gera esta exceção:
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 9
_______________________________________________________________________________________
A exceção EOFException possui um segundo construtor que recebe um argumento string.
Você pode utilizar este string para descrever a exceção que ocorreu.
Como você pode ver, é fácil gerar uma exceção se umas das classes de exceção lhe
atende. Neste caso:
Quando um método gera uma exceção, ele não retorna para quem o chamou. Isto
significa que você não necessita atribuir um valor default de retorno ou copiar um código
de erro.
Pode ocorrer um problema com o seu código que não está adequadamente descrito por
nenhuma das classes de exceções definidas em Java. Neste caso você deve criar sua
própria classe de exceção. É comum se fornecer dois construtores: um default e um que
contenha uma mensagem que descreve detalhadamente o que aconteceu. O método
toString da classe base Throwable imprime a mensagem detalhada, que facilita muito o
processo de depuração.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 10
_______________________________________________________________________________________
Agora você pode gerar o seu próprio tipo de exceção.
import java.util.Calendar;
class Empregado
{ public Empregado (String n, double s, Calendar d)
throws MinhaExcecao
{ if (s > SALARIO_MAXIMO)
throw new MinhaExcecao ("Inserção Inválida. Salário de " + s
+ " > " + SALARIO_MAXIMO);
else
{ nome = n;
salario = s;
dataContratacao = d;
}
}
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 11
_______________________________________________________________________________________
public void setSalario(double sal) throws MinhaExcecao
{ if (sal > SALARIO_MAXIMO)
throw new MinhaExcecao ("Modificação de salário Inválida. "
+ " Salário de " + sal + " > " + SALARIO_MAXIMO);
else
salario = sal;
}
Capturando Exceções
Naturalmente algum código necessita capturar a exceção gerada por um método. Se uma
exceção é gerada e não é capturada em nenhum lugar (em uma aplicação não gráfica), o
programa termina e imprime uma mensagem uma mensagem na console fornecendo o
tipo da exceção e o stack trace. Em um programa gráfico (aplicações e applets), a mesma
mensagem de erro é impressa, mas o controle da execução volta para o loop de
processamento da interface gráfica do usuário. Quando você está debugando um
programa gráfico é uma boa idéia manter a console disponível na tela em vez de
minimizá-la.
Para capturar uma exceção escreve-se um bloco try/catch. A forma mais simples do bloco
try é:
try
{ código
código
}
catch (ExceptionType e)
{ código para manipular uma exceção deste tipo
}
Se uma instrução dentro do bloco try gera uma exceção da classe especificada na cláusula
catch, então:
Se nenhum código dentro do bloco try gerar uma exceção, então o programa salta a
cláusula catch.
import java.util.*;
public class TestaEmpregado
{ public static void main (String[] args)
{ Empregado[] vetEmpregados = new Empregado[2];
try
{ System.out.println
("Tentando inserir Luciana Arruda - Sal: 2500");
vetEmpregados [0] =
new Empregado ("Luciana Arruda", 2500,
new GregorianCalendar (1993, 1, 12));
}
catch (MinhaExcecao e)
{ System.out.println (e.getRazao());
}
try
{ System.out.println
("Tentando inserir Lucio Arruda - Sal: 3500");
vetEmpregados [0] =
new Empregado ("Lucio Arruda", 3500,
new GregorianCalendar (1994, 11, 14));
}
catch (MinhaExcecao e)
{ System.out.println (e.getRazao());
}
try
{ System.out.println ("Tentando conceder aumento a " +
vetEmpregados[0].getNome() + " de 25%");
vetEmpregados[0].aumentaSalario(25);
}
catch (MinhaExcecao e)
{ System.out.println (e.getRazao());
}
}
}
Note que a parte do código está dentro da cláusula try. Observe que a própria classe que
gera um exceção pode tratá-la. No exemplo acima, deixamos o programa chamador tratar
a exceção. Por esta razão tivemos que anunciar no header do construtor da classe
Empregado e no header dos métodos aumentaSalario e setSalario que estes métodos
podiam gerar a exceção MinhaExcecao.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 13
_______________________________________________________________________________________
Qual a melhor alternativa? A regra geral diz que você deve tratar as exceções que você
sabe como tratar e passar a diante as exceções que você não sabe como tratar. Há uma
exceção para esta regra: se você está escrevendo um método que faz um override em um
método da superclasse que não gera exceções então você deve capturar cada checked
exception no código do método. Seu código não pode adicionar novas exceções à lista de
exceções a um método de uma subclasse que está presente na superclasse.
Você pode capturar múltiplos tipos de exceção em um bloco try e tratar cada tipo de
forma diferente. Você utiliza uma cláusula catch separada para cada tipo. Por exemplo:
try
{ código que pode gerar exceções
}
catch (MalformedURLException e1)
{ // Ação a ser tomada em relação a URL mal formadas.
}
catch (UnknownHostException e2)
{ // Ação a ser tomada em relação a Host desconhecido
}
catch (IOException e3)
{ // Ação a ser tomada em relação a problema de I/O
}
O objeto exceção (e1, e2, e3) pode conter informações sobre a natureza da exceção. Para
descobrir mais sobre o objeto utilize:
e3.getMessage()
e3.getClass().getName()
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 14
_______________________________________________________________________________________
Regerando Exceções
Ocasionalmente pode ser necessário capturar uma exceção, para efetuar algum
processamento, mas sem efetivamente resolver o problema. Quando isto ocorre, você
executa alguma ação de emergência em função da exceção que ocorreu e gera novamente
a mesma exceção chamando throw de forma que ela seja passado para o método
chamador.
Exemplo:
Graphics g = image.getGraphics();
try
{ Código que pode gerar exceções
}
catch(MalformedURLException e)
{ g.dispose();
throw e;
}
Embora tendo tomado alguma atitude com g.dispose(), a URL mal formada continua
existindo daí ser repassada ao método chamador que poderá, por exemplo, solicitar ao
usuário que a corrija.
Cláusula Finally
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 15
_______________________________________________________________________________________
Java possui uma solução melhor:
Graphics g = image.getGraphics();
try
{ Código que pode gerar exceções
}
catch(IOException e)
{ done = true;
}
finally
{ g.dispose();
}
Este código executa o código na clausula finally caso a exceção seja ou não capturada.
Isto é, em qualquer circunstância g.dispose será executado.
1. O código não gera exceção. O bloco try é executado e, em seguida, o bloco finally. A
seguir, é executada a primeira linha após o bloco finally.
2. O código gera uma exceção que é capturada pela cláusula catch. O código no bloco
try é executado até a linha que causou a exceção. O código remanescente no bloco try
é saltado. Então o programa executa o código na cláusula catch que captura a exceção
e, em seguida, o bloco finally. A seguir, é executada a primeira linha após o bloco
finally. Se a cláusula catch não gera uma exceção, então o programa executa a
primeira linha após o bloco try-catch-finally. Se na cláusula catch é gerada uma
exceção, o bloco finally é executado e esta exceção é retornada para o programa
chamador.
3. O código gera uma exceção que não é capturada por nenhuma cláusula catch. O
código no bloco try é executado até a linha que causou a exceção. O código
remanescente no bloco try é saltado. Então o programa executa o código na clausula
finally e a exceção é retornada para o programa chamador.
Graphics g = image.getGraphics();
try
{ Código que pode gerar exceções
}
finally
{ g.dispose();
}
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 16
_______________________________________________________________________________________
Tratar uma exceção geralmente demora muito mais do que efetuar um simples teste.
Você escreve um método que chama um método que gera uma exceção uma vez a
cada 100 anos. O compilador reclama porque você não declarou a exceção na lista
throws do seu método. Você não deseja declará-lo na lista throws porque senão o
compilador vai criar caso com todos os métodos que chamam o seu método. Logo,
você simplesmente cala a exceção.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 17
_______________________________________________________________________________________
Técnicas de Debugging
Se x for um número, será convertido para string, e se for um objeto, seu método
toString será executado.
3. Utilize o código genérico para o método toString que imprime para cada campo o seu
valor.
4. Você pode obter uma stack trace de qualquer exceção através do método
printStackTrace da classe Throwable:
try
{ . . .
}
catch (Throwable t)
{ t.printStackTrace();
throw t;
}
5. Não é necessário capturar uma exceção para gerar um stack trace. Apenas insira o
comando abaixo em qualquer lugar do seu código:
new Throwable().printStackTrace();
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 18
_______________________________________________________________________________________
6. Se você projeta seus próprios componentes swing e eles não parecem estar
funcionando corretamente, utilize o debugger do swing. Para ligar o debugger para
um componente swing, utilize o método setDebugGraphicsOptions da classe
JComponent. As seguintes opções estão disponíveis:
Nós (autor do livro CoreJava) descobrimos que para a opção flash funcionar é preciso
desabilitar o “double buffering” , a estratégia utilizada pelo Swing para reduzir o
flicker quando atualiza uma janela. Para ligar a opção flash faça:
RepaintManager.currentManager(getRootPane())
.setDoubleBufferingEnabled(false);
((JComponent)getContentPane())
.setDebugGraphicsOptions(DebugGraphics.FLASH_OPTION);
Simplesmente ponha estas linhas no final do construtor do seu frame e você verá o
content pane sendo preenchido vagarosamente. Ou para um debug mais localizado,
coloque o setDebugGraphicsOptions para um componente específico.
7. Um truque aparentemente pouco conhecido é que você pode colocar um método main
em cada classe pública. Assim você poderá testar cada classe separadamente. Crie
alguns objetos, chame cada método e verifique se eles funcionam corretamente.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 19
_______________________________________________________________________________________
Estas verificações são denominadas assertivas, e para fazer com que o código fique mais
fácil de ler podemos utilizar um método estático check.
Quando o programa estiver testado teremos que remover este código. Uma maneira
simples de fazer isto é acrescentando uma variável boleana que determina se este teste
deve ou não ser executado:
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 20
_______________________________________________________________________________________
Se você executa uma applet dentro de um browser, você pode não conseguir ver
mensagens enviada para System.out. Esperamos que, no futuro, a maioria dos browsers
possuam algum tipo de java console window. O Netscape, assim como o Internet
Explorer, possuem uma. Se você utiliza o Java Plug-In, marque, no painel de
configuração, o check box denominado Show Java Console.
Em alguns browsers a Java Console Window possui barras de rolamento de tal forma que
é possível ver mensagens que rolaram para fora da tela. No ambiente DOS estas
mensagens não podem ser vistas.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 21
_______________________________________________________________________________________
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 22
_______________________________________________________________________________________
public void actionPerformed(ActionEvent evt)
{ String arg = evt.getActionCommand();
Color color = Color.black;
if (arg.equals("yellow")) color = Color.yellow;
else if (arg.equals("blue")) color = Color.blue;
else if (arg.equals("red")) color = Color.red;
pane.setBackground(color);
repaint();
}
Para utilizar o JDB você deve compilar seu programa com a opção –g:
javac –g BuggyButtonTest.java
Quando você compila com esta opção, o compilador adiciona os nomes das variáveis
locais e outras informações de debug nos arquivos .class. Então, você executa o
debugger:
jdb BuggyButtonTest
Uma vez executando-se o debugger, você verá uma mensagem como a que vem abaixo:
Initializing jdb . . .
0x139f3c8:class(BuggyButtonTest)
>
O prompt > indica que o debugger está esperando por um comando. A tabela abaixo
mostra todos os comandos de debug que você pode fornecer. Itens entre colchetes são
opcionais, e o sufixo significam que você pode fornecer um ou mais argumentos
separados por espaços.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 23
_______________________________________________________________________________________
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 24
_______________________________________________________________________________________
Vamos examinar apenas os comandos JDB mais úteis. A idéia básica é simples: você
marca um ou mais breakpoints e então executa o programa. Quando o programa chega no
breakpoint, ele pára. Então você pode inspecionar os valores de variáveis locais.
stop in class.method
Ou
stop at class:line
stop in BuggyButtonTest.actionPerformed
run
O programa será executado, mas o breakpoint só será alcançado quando java começar a
executar o código do método actionPerformed. Para que isto aconteça clique no botão
Amarelo. O debugger pára no início do método actionPerformed. Você verá:
Como o debugger não nos fornece uma janela mostrando a linha corrente que vai ser
executada, utilize o comando list para ver qual é a linha corrente. Por exemplo:
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 25
_______________________________________________________________________________________
Digite locals para ver todas as variáveis locais. Por exemplo:
dump variable
Por exemplo,
dump evt
Existem dois comandos básicos para executar um programa passo a passo. O comando
step que faz com que a execução passe por todos os métodos do programa, passo a passo.
Infelizmente, fica muito confuso entre threads. Achamos que é mais seguro utilizar o
comando next, que executa a próxima linha sem entrar em nenhum método que seja
chamado. Digite next 3 vezes para ver onde você está:
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 26
_______________________________________________________________________________________
Isto não é o que deveria ter acontecido. A variável color deveria ter sido designada para
yellow para depois executar o comando setBackground.
Agora podemos entender o que aconteceu. O valor de arg era “Yellow”, com um y
maiúsculo, mas o teste compara arg com “yellow’:
if (arg.equals(“yellow”))
quit
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 27
_______________________________________________________________________________________
STREAMS E ARQUIVOS
Applets normalmente não podem trabalhar com arquivos no sistema do usuário. Mas
applications normalmente lidam com arquivos. Nesta seção veremos os métodos para
lidar com arquivos bem como os métodos para escrever e ler informações de arquivos.
Este capítulo também apresenta o mecanismo de serialização que nos permite armazenar
objetos tão facilmente quanto armazenamos texto ou dados numéricos.
Streams
Esta seção trata de como obter dados de qualquer fonte de dados que possa enviar uma
seqüência de bytes e como enviar dados para qualquer destino que possa receber uma
seqüência de bytes. Estas fontes e destinos de seqüência de bytes podem ser arquivos,
mas também podem ser conexões de rede e até blocos de memória. Informações
armazenadas em arquivos e informações recuperadas de conexões de rede são
manipuladas essencialmente da mesma forma. Enquanto os dados são armazenados como
uma série de bytes, é geralmente mais conveniente pensar neles como possuindo uma
estrutura de mais alto nível como sendo uma seqüência de caracteres ou objetos. Também
veremos as facilidades que Java provê para entrada e saída de dados em mais alto nível.
Em java, um objeto a partir do qual podemos ler uma seqüência de bytes é denominado
um input stream. E um objeto no qual podemos escrever uma seqüência de bytes é
denominado um output stream. Eles encontram-se implementados nas classes abstratas
InputStream e OutputStream.
Este método lê um byte e retorna o byte que foi lido ou –1 se ele encontra o final da fonte
de dados. O projetista de uma classe input stream concreta faz o override deste método
para prover funcionalidade util. Por exemplo, na classe FileInputStream, este método lê
um byte de um arquivo. System.in é um objeto predefinido de uma subclasse de
InputStream que nos permite ler informações do teclado.
A classe InputStream também possui métodos não abstratos para ler um array de bytes ou
para pular um certo número de bytes. Estes métodos chamam o método abstrato read,
para que as subclasses necessitem fazer o override de apenas um método.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 28
_______________________________________________________________________________________
Da mesma forma, a classe OutputStream define o método abstrato:
Ambos os métodos read e write podem bloquear uma thread até que o byte seja realmente
lido ou escrito. Isto significa que se o byte não pode ser imediatamente lido ou escrito
(geralmente em função de uma conexão de rede ocupada), Java suspende a thread que
contém esta chamada. Isto dá a oportunidade a outras threads de fazer algo útil enquanto
o método está esperando pelo stream para novamente se tornar disponível.
O método available permite que você verifique a quantidade de bytes que está
disponível para leitura.
Java nos fornece muitas classes stream derivadas das classes básicas InputStream e
OutputStream que nos permite trabalhar com dados da forma como geralmente utilizamos
em vez de trabalhar a nível de byte.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 29
_______________________________________________________________________________________
Tipos de Streams
Em Java, por exemplo, não há como escrever em um stream aberto apenas para leitura
uma vez que InputStream não possui métodos para saída de dados.
Existem 4 classes abstratas que formam a base dos Streams: InputStream, OutputStream,
Reader e Writer. Você não cria objetos destes tipos mas outros métodos podem retorná-
los. Por exemplo, a classe URL possui o método OpenStream que retorna um
InputStream. Você então utiliza este objeto InputStream para ler do URL. Como
mencionamos anteriormente, as classes InputStream e OutputStream nos permite ler e
escrever apenas bytes individuais e arrays de bytes. Elas não possuem métodos para ler e
escrever strings e números. Você necessita de subclasses mais capazes para este fim. Por
exemplo, as classes DataInputStream e DatOutputStream nos permitem ler e escrever
todos os tipos de dados básicos da linguagem.
Para código Unicode (dois bytes para representar um) você necessita utilizar as classes
que descendem de Reader e Writer. Os métodos básicos das classes Reader e Writer são
similares aos das classes InputStream e OutputStream:
Eles funcionam como os métodos das classes InputStream e OutputStream exceto que o
método read retorna um caracter Unicode (um número inteiro entre 0 e 65535) ou –1
quando você encontra o final do arquivo.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 30
_______________________________________________________________________________________
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 31
_______________________________________________________________________________________
E existem streams que são úteis, como por exemplo as classes ZipInputStream e
ZipOutputStream que nos permitem ler e escrever arquivos no formato de compressão
ZIP.
Cuidado: Como “\” é um caracter especial em Strings Java, utilize \\ para caminhos de
arquivos estilo windows. Por exemplo: C:\\WINDOWS\\WIN.INI.
Como você verá mais adiante, se nós apenas tivéssemos DataInputStream, então
poderíamos ler tipos numéricos:
DataInputStream din = . . .;
double s = din.readDouble();
Mas assim como FileInputStream não possui métodos para ler tipos numéricos,
DataInputStream não possui nenhum método para obter dados de um arquivo.
É importante entender que o data input stream que criamos com o código acima não
corresponde a um novo arquivo em disco. O mais novo stream criado ainda acessa os
dados do arquivo atachado ao file input stream, mas a questão é que agora este novo
stream possui uma interface mais poderosa.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 33
_______________________________________________________________________________________
Object Streams
Para salvar dados de objetos, você primeiramente necessita abrir um objeto do tipo
ObjectOutputStream:
ObjectOutputStream out =
new ObjectOutputStream(new FileOutputStream(“empregados.dat”);
Agora, para salvar um objeto, você simplesmente utiliza o método writeObject da classe
ObjectOutputStream como no fragmento de código abaixo:
ObjectInputStream in =
new ObjectInputStream(new FileInputStream(“empregados.dat”));
E então, recupere os objetos na mesma ordem em que foram escritos, utilizando o método
readObject.
Empregado e1 = (Empregado)in.readObject();
Empregado e2 = (Empregado)in.readObject();
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 34
_______________________________________________________________________________________
Quando você está recuperando objetos que foram escritos em disco, você deve ter
cuidado com o número de objetos que foram salvos, sua ordem e seus tipos. Cada
chamada a readObject lê um objeto do tipo Object, logo você necessita efetuar o cast para
o tipo correto. Se você não necessita do tipo exato ou você não se lembra dele, então você
pode efetuar o cast para qualquer superclasse, ou pode até mesmo deixá-lo como sendo
do tipo Object. Por exemplo, e2 é uma variável objeto do tipo Empregado embora ela
possa se referir na realidade a um objeto do tipo Gerente. Se você necessita recuperar
dinamicamente o tipo do objeto, você pode utilizar o método getClass, descrito no
capítulo sobre Herança na apostila de Introdução.
Você pode escrever e ler apenas objetos com os métodos writeObject e readObject, isto é,
você não pode ler e escrever números com estes métodos. Para escrever e ler números
você utiliza métodos tais como writeInt/readInt ou writeDouble/readDouble. As classes
Object Stream implementam as interfaces DataInput e DataOutput. Naturalmente
números dentro de objetos (como o campo salário do objeto empregado) são
armazenados e recuperados automaticamente. Lembre-se que, em Java, Strings e arrays
são objetos e podem, conseqüentemente ser restaurados com os métodos
writeObject/readObject.
Existe, no entanto, uma modificação que você necessita fazer com qualquer classe que
você queira salvar e restaurar em um object stream. A classe deve implementar a
interface Serializable:
A interface Serializable não possui métodos, logo você não necessita modificar as suas
classes. Neste aspecto, é similar à interface Clonable. No entanto, para fazer uma classe
cloneable, você ainda tinha que efetuar o override do método clone da classe Object. Para
fazer com que uma classe seja Serializable, você não necessita fazer nada mais. As
classes não são serializable por default por questões de segurança.
De forma similar, ler o resultado também é feito com uma única operação. No entanto,
devemos aplicar um cast ao valor de retorno do método readObject:
Uma vez a informação armazenada , damos a cada empregado um aumento de 100%, não
porque estamos sendo generosos mas porque podemos facilmente distinguir objetos
empregados de objetos gerentes através de seus métodos aumentarSalario. Isto lhe
deveria convencer de que nós restauramos os tipos corretos.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 35
_______________________________________________________________________________________
import java.io.*;
import java.util.*;
public class ObjectFileTest
{ public static void main (String[] args)
{ try
{ Empregado[] vetEmpregados = new Empregado[3];
vetEmpregados [0] = new Empregado ("Luis Claudio", 3500,
new GregorianCalendar (1989, 10, 1));
vetEmpregados [1] = new Gerente ("Vinicius Aguiar", 4500,
new GregorianCalendar (1992, 12, 6));
vetEmpregados [2] = new Empregado ("Luciana Arruda", 2500,
new GregorianCalendar (1993, 1, 12));
ObjectInputStream in = new
ObjectInputStream(new FileInputStream("empregados.dat"));
Empregado[] novoVetEmpregados = (Empregado[])in.readObject();
int i;
for (i = 0; i < novoVetEmpregados.length; i++)
novoVetEmpregados[i].aumentaSalario(100);
for (i = 0; i < novoVetEmpregados.length; i++)
novoVetEmpregados[i].print();
}
catch(Exception e)
{ System.out.print("Erro: " + e);
System.exit(1);
}
}
}
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 36
_______________________________________________________________________________________
class Empregado implements Serializable
{ public Empregado(String n, double s, GregorianCalendar d)
{ nome = n;
salario = s;
dataContratacao = d;
}
public Empregado() {}
public Gerente() {}
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 37
_______________________________________________________________________________________
Observação: O arquivo empregados.dat é salvo em um formato particular. Você pode
utilizar os métodos writeObject e readObject sem ter que conhecer qual é a seqüência de
bytes que representa os objetos em um arquivo.
java.io.ObjectOutputStream
java.io.ObjectInputStream
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 38
_______________________________________________________________________________________
O Problema de Salvar Referências a Objetos
Agora sabemos como salvar objetos que contém números, strings ou outros objetos
simples (como o objeto GregorianCalendar na classe Empregado). No entanto existe uma
importante situação que necessitamos considerar. O que acontece quando um objeto é
compartilhado por vários objetos como parte do seu estado?
Para ilustrar o problema, vamos fazer uma pequena modificação na classe Gerente. Em
vez de armazenar o nome da secretária, vamos salvar uma referência para o objeto
secretária, que é um objeto do tipo Empregado.
public Gerente() {}
Esta é uma abordagem melhor para projetar uma classe Gerente real do que simplesmente
utilizar o nome da secretária – o objeto Empregado referente à secretária pode agora ser
localizado sem a necessidade de fazer uma busca no array vetEmpregados.
Uma vez feito isto, você deve ter em mente que o objeto Gerente agora contém uma
referência para o objeto Empregado, que descreve a secretária, não uma cópia separada
do objeto.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 39
_______________________________________________________________________________________
Em particular, dois gerentes podem compartilhar a mesma secretária, como ocorre na
figura abaixo e no seguinte código:
Agora suponha que o vetor vetEmpregados seja escrito em disco. O que nós não
queremos é que para cada objeto Gerente seja utilizada a seguinte lógica para armazenar
esta informação:
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 40
_______________________________________________________________________________________
Porque com esta lógica os dados de luiza seriam salvos três vezes. Quando recarregados
os objetos teriam a configuração da figura abaixo:
E isto não é o que queremos. Suponha que a secretária receba um aumento. Teríamos que
procurar por todas as outras cópias deste objeto para conceder o mesmo aumento. Na
verdade desejamos salvar e recuperar apenas uma cópia deste objeto secretária. Para fazer
isto devemos copiar e restaurar as referências originais para os objetos. Em outras
palavras, desejamos que o layout dos objetos no disco seja exatamente como o layout dos
objetos na memória. No linguajar da orientação a objetos isto se chama persistência.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 41
_______________________________________________________________________________________
O que java faz nestes casos é utilizar uma abordagem baseada na serialização. Daí o
nome “object serialization” para este mecanismo. Aqui está o algoritmo:
2. Quando um objeto vai ser salvo em disco é verificado se este objeto já foi salvo
anteriormente.
3. Se ele já tiver sido salvo em seu lugar é escrito apenas o seu número de série, caso
contrário, todos os seus dados são salvos.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 42
_______________________________________________________________________________________
Ao ler de volta os objetos, este procedimento é revertido. Para cada objeto que é
carregado na memória é armazenado seu número de seqüência e a posição na memória
onde ele foi colocado. Quando um objeto com um número de seqüência já carregado na
memória é procurado, a referência ao endereço de memória para este objeto é
estabelecida.
Observe que os objetos não necessitam ser salvos em nenhuma ordem particular. A figura
acima mostra o que ocorre quando um gerente ocorre primeiro no array vetEmpregados.
Todo este processo é automático.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 43
_______________________________________________________________________________________
import java.io.*;
ObjectInputStream in = new
ObjectInputStream(new FileInputStream("empregados.dat"));
Empregado[] novoVetEmpregados = (Empregado[])in.readObject();
int i;
for (i = 0; i < novoVetEmpregados.length; i++)
novoVetEmpregados[i].aumentaSalario(100);
for (i = 0; i < novoVetEmpregados.length; i++)
novoVetEmpregados[i].print();
}
catch(Exception e)
{ e.printStackTrace();
System.exit(1);
}
}
}
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 44
_______________________________________________________________________________________
class Empregado implements Serializable
{ public Empregado(String n, double s, GregorianCalendar d)
{ nome = n;
salario = s;
dataContratacao = d;
}
public Empregado() {}
public Gerente() {}
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 45
_______________________________________________________________________________________
ACESSANDO BANCOS DE DADOS
A maioria das principais aplicações de uma empresa envolvem acesso a dados em tabelas
– na sua maioria, bancos de dados relacionais. Com o objetivo de suportar esses tipos de
aplicações, Java conta com a API JDBC. Esta API, que surgiu como um dos avanços da
versão 1.1 do SDK (JDK) da SUN.
JDBC é uma API Java para execução de expressões SQL. Sendo mais específico, JDBC é
um conjunto de classes e interfaces que permitem acesso a bases de dados relacionais de
maneira uniforme, numa solução similar ao ODBC. Existem atualmente mais de 20
implementações de drivers de bancos de dados compatíveis com JDBC, mas basicamente
podemos distinguir quatro categorias de soluções, que são definidas pela JavaSoft:
O cliente (escrito em Java) utiliza a API JDBC. O driver JDBC converte as chamadas
JDBC em chamadas ODBC e passa estas chamadas para o driver ODBC, que por sua vez
acessa a API nativa do banco, que finalmente acessa os dados. A principal vantagem
desta solução é que uma aplicação pode facilmente se conectar a uma fonte de dados:
basta utilizar o driver ODBC correto. Em compensação, este tipo de conexão envolve
uma complexidade adicional e uma sobrecarga considerável na máquina, levando a
problemas de desempenho. Isto sem falar que os drivers ODBC devem estar instalados na
máquina cliente, causando os velhos problemas de administração de versões e
configuração. Por tudo isso a ponte JDBC / ODBC tente a ser uma solução passageira,
apenas para resolver questões imediatas.
Nesta solução o driver ODBC é eliminado e o driver JDBC converte as chamadas JDBC
em chamadas diretas à API nativa do banco. Essa API traduz para uma linguagem
específica do banco de dados as funções JDBC.
Devemos atentar para o fato de que geralmente o driver JDBC é, na verdade, construído
parte em Java e parte em C / C++. O driver deve possuir essa camada de C para que seja
possível fazer as chamadas às APIs nativas (que são escritas em C).
Esse tipo de arquitetura, assim como a ponte JDBC / ODBC, requer que algum código
seja instalado na máquina cliente. Entretanto, esta solução é mais rápida por não precisar
da camada ODBC.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 46
_______________________________________________________________________________________
Tipo 3 – Driver 100% Java + Protocolo de Rede (arquitetura em três camadas)
Como podemos observar, o driver JDBC é escrito totalmente em Java, e sua única função
é implementar a lógica necessária para passar os comandos SQL para o servidor
intermediário de forma conveniente. Por isso esse tipo de driver é bem pequeno
(aproximadamente 200k).
Quanto ao servidor intermediário, pode ter sido escrito em java ou em qualquer outra
linguagem apropriada (C / C++). Se for feito em Java, pode utilizar qualquer driver
compatível com JDBC para chegar ao banco de dados. Se for feito em outra linguagem,
serão utilizadas as bibliotecas nativas do banco, como nos drivers do tipo 2.
Uma grande vantagem desta solução é ter no cliente uma solução 100% Java, sem a
necessidade de instalação de nenhum produto. Uma desvantagem é que o middleware é
geralmente proprietário, junto com o protocolo utilizado para trafegar na rede. Cada
fabricante usa o seu próprio middleware. Isto praticamente inviabiliza o uso de outras
tecnologias mais abertas e promissoras, como por exemplo, CORBA.
Nesta solução, o driver converte as chamadas JDBC diretamente para o protocolo de rede
utilizado pelo banco de dados. Escritos somente em Java, estes drivers não necessitam de
ODBC nem de API nativa, gerando um enorme ganho de desempenho e permitindo o
desenvolvimento de aplicações 100% Java.
Hoje em dia (junho de 1999), a oferta de drivers do tipo 4 ainda é pequena, mas a
tendência é que aumente bastante em breve. Os drivers do tipo 3 também têm seu lugar,
enquanto os drivers do tipo 1 e 2 tendem a morrer.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 47
_______________________________________________________________________________________
Por outro lado, existe uma grande quantidade de desvantagens associadas a essa
arquitetura. A primeira delas é a escalabilidade. Se a máquina corrente ficar
sobrecarregada, a única saída é fazer um upgrade para uma máquina de maior capacidade.
Aplicações one-tier são também extremamente dependentes do hardware e do ambiente
operacional. Essencialmente as companhias ficam presas a um fornecedor específico da
sua plataforma de hardware. Como resultado, não se pode tirar proveito de novas
tecnologias, até que elas sejam disponibilizadas por aquele fornecedor específico.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 48
_______________________________________________________________________________________
Aplicações de duas Camadas
Com o advento dos computadores pessoais, redes locais, bancos de dados relacionais,
poderosas aplicações e ferramentas desktop, a indústria de computação direcionou-se
para o mundo dos sistemas "abertos" e cliente/servidor. Tomadores de decisão podem
gerar seus próprios relatórios e manipular dados com ferramentas desktop poderosas, em
suas próprias estações de trabalho. Esse tipo de arquitetura permite o tratamento de
funções, anteriormente impossíveis de serem manipuladas.
Aplicações Multicamadas
Felizmente, existe uma forma de se obter o melhor das arquiteturas anteriores, sem as
suas desvantagens. Além de suportar os benefícios de ambas as arquiteturas (one e two-
tier), a arquitetura multi-tier também suporta os benefícios de uma arquitetura bastante
flexível.
As três camadas referem-se as três partes lógicas que compõem uma aplicação, e não ao
número de máquinas usadas pela aplicação. O modelo multi-tier divide a aplicação nas
seguintes camadas: lógica da apresentação, 1ógica do negócio e lógica de acesso aos
dados, podendo existir qualquer número de qualquer das camadas numa aplicação. Os
componentes da aplicação comunicam-se entre si utilizando uma interface abstrata, que
funciona como um contrato, em termos do que esta sendo tornado público. Essa mesma
camada abstrata esconde todos os detalhes da implementação das funções desempenhadas
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 49
_______________________________________________________________________________________
por um componente. Ela identifica a operação a ser realizada e define os parâmetros de
entrada e saída necessários a execução da operação. Esse tipo de infra-estrutura
possibilita serviços de localização, segurança e comunicação entre os componentes da
aplicação. São várias as vantagens advindas desta nova “onda” da informática. Algumas
delas são descritas nos parágrafos seguintes.
Reutilização de objetos por outras aplicações - Objetos podem ser compartilhados por
diversas aplicações. De fato, o que está sendo construído não é propriamente uma
aplicação, mas uma coleção de módulos (objetos) clientes e servidores que se comunicam
através de uma interface padronizada e abstrata e que, ao serem combinados, funcionam
como um sistema integrado de aplicações. Cada módulo é, na realidade, um objeto que
pode ser reutilizado e compartilhado por diversos sistemas. Essa versatilidade plug-and-
play é útil quando o setor de tecnologia da informação necessita suportar partes dife-
rentes, mas relacionadas, do negócio.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 51
_______________________________________________________________________________________
O que utilizar quando se pretende acessar um banco de dados a partir de uma aplicação
Java: JDBC ou SQLJ?
Ambas estas APIs são utilizadas no acesso a bancos de dados. Mas porque duas APIs e
não apenas uma? Na realidade você apenas necessita de JDBC, mas você verá que em
função da simplicidade de programação em SQLJ você desejará utilizar esta ferramenta.
SQLJ é um novo padrão para embutir código SQL diretamente em programas Java. É um
produto padrão desenvolvido pela Oracle, IBM, Sybase, Tandem e Informix. Para aqueles
que estão familiarizados com Pro*C (SQL embutido em C), você pode ver SQLJ como
Pro*Java. Então porque não se chama Pro*Java? Pro*qualquer coisa significa uma
tecnologia pro*prietária da Oracle, por exemplo, enquanto SQLJ é um padrão aberto.
JDBC
import java.sql.*;
public class JDBCExample
{ public static void main(String args[]) throws SQLException
{ DriverManager.registerDriver(new
oracle.jdbc.driver.OracleDriver());
Connection conn =
DriverManager.getConnection
("jdbc:oracle:thin:@sbd:1521:orcl", "scott",
"tiger");
// @nome_do_computador:porta:nome_da_instância
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(“SELECT ENAME, EMPNO, SAL “
+ “FROM EMP”);
while (rs.next())
{ String nome = rs.getString(1);
int numero = rs.getInt(2);
double salario = rs.getDouble(3);
System.out.println(nome + “ “ + numero + “ “ +
salario);
}
rs.close();
conn.close();
}
}
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 52
_______________________________________________________________________________________
A primeira linha do método main é:
DriverManager.registerDriver(new oracle.jdbc.driver.OracleDriver());
Tudo o que você realmente necessita saber é que esta chamada faz com que JDBC saiba
que você está utilizando um driver da oracle.
A próxima chamada é:
Esta chamada cria uma objeto que representa uma conexão com o banco de dados. A
primeira parte do string de conexão, jdbc:oracle:thin, indica que deverá ser
utilizado um driver 100% java para estabelecer a conexão com o banco de dados. A
Segunda parte do string de conexão, @sbd:1521:orcl, especifica que a conexão
deverá ser efetuada com a instância orcl na máquina denominada sbd, cujo listener está
na porta 1521. A conexão é efetuada na conta do usuário scott que possui a password
tiger.
Agora que a conexão foi estabelecida podemos emitir comandos SQL para o banco de
dados:
As últimas linhas, rs.close() e conn.close(), permitem que JDBC saiba que você
terminou com o result set e que a conexão com o banco, bem como a correspondente
sessão, devem ser encerradas.
A Oracle fornece três tipos de drivers JDBC: um driver thin (fino) 100% Java, um OCI-
based fat driver e um driver do tipo server-side (também conhecido como KPRB). Todos
estes três drivers atualmente (fevereiro de 1999) atendem à especificação 1.2.2 de JDBC
e estão a caminho de atender à especificação de JDBC 2.0. Estes drivers são compatíveis
com JDK 1.0.2 e 1.1.6. Os drivers thin e fat podem ser utilizados com bancos de dados
Oracle a partir da versão 7.2. Todos estes drivers utilizam as mesmas APIs,
proporcionando flexibilidade máxima em como aplicações JDBC podem ser
desenvolvidas.
Como já mencionado, o driver Oracle do tipo thin é uma implementação 100% Java. Por
esta razão, é ideal para aplicações que necessitam ser 100% Java, tais como applets. Um
outro benefício do driver thin é que ele é pequeno (300k – ou 150k quando comprimido),
sendo interessante a sua utilização em aplicações que necessitam sofrer download, como
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 53
_______________________________________________________________________________________
as applets. Em termos de performance, o driver thin é ligeiramente mais lento do que seu
big-brother OCI-based, que foi implementado em C.
O driver Oracle do tipo fat possui uma camada JDBC implementada em cima da Oracle
Call Interface (OCI). OCI é uma biblioteca C para acesso a bancos de dados Oracle.
Como o driver fat necessita que esta biblioteca (OCI) seja pré-instalada, não pode ser
utilizado em aplicações 100% Java e por aplicações carregadas através de download. Este
driver, no entanto, é o ideal para utilização em aplicações middle-tier, tais como servlets.
O driver Oracle server-side é o último da família. Agora que o Oracle8i permite que
aplicações Java rodem dentro do banco de dados, estas aplicações necessitam de uma
forma de acessar os dados no banco de dados (a principal razão para executar Java no
banco é se o programa manipula muitos dados). Os drivers do tipo server-side foram
criados para este tipo de utilização. Como estes drivers sabem que o código está no banco
eles não necessitam abrir explicitamente uma conexão. Eles também evitam
completamente a latência devido à rede ou devido à comunicação entre processos
acessando diretamente dados SQL. Como você já deve saber, código Java no Oracle 8i é
executado acessando a mesma memória e ocupando o mesmo espaço dos processos que
acessam a instância do banco de dados.
Todos os três drivers mencionados anteriormente utilizam a mesma API. Isto dá aos
desenvolvedores de aplicações Java/JDBC grande flexibilidade em como desenvolver
aplicações Java. Por exemplo, digamos que uma aplicação Client/Server seja escrita
utilizando um driver JDBC-OCI. Neste caso, o cliente seria uma combinação de uma
aplicação Java com um driver e o servidor seria um banco de dados Oracle. Surge então o
Oracle8i e o desenvolvedor percebe que teria um grande ganho de desempenho se
executasse a parte da sua aplicação que faz muito acesso a dados, dentro do banco de
dados. Para efetuar esta modificação o desenvolvedor teria que carregar para dentro do
banco as classes que fazem acesso aos dados e modificar as linhas que estabelecem a
conexão para:
Connection conn =
new oracle.jdbc.driver.OracleDriver().defaultConnection();
Da mesma forma, o desenvolvedor poderia decidir modificar a sua aplicação para uma
applet Java. Neste caso, a única modificação JDBC necessária seria a especificação do
driver thin em vez do driver OCI-based no string de conexão. No mundo de hoje esta
flexibilidade deve ser levada em consideração. Ela permite que os desenvolvedores
possam rapidamente e facilmente adaptar suas aplicações para a configuração de
desenvolvimento do momento.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 54
_______________________________________________________________________________________
JDBC Não é Perfeito
Erros em comandos SQL só podem ser capturados em tempo de execução. Isto porque o
compilador Java utilizado para compilar o código JDBC não conhece a sintaxe e a
semântica de SQL. Em segundo lugar, escrever um comando SQL complexo em JDBC
pode ser bastante trabalhoso. Isto é ilustrado pelo código abaixo:
Este exemplo, nos trás duas questões: Há alguma coisa que pode ser feito para fazer com
que queries SQL em Java 1) sejam mais fáceis de escrever e de ler e 2) possam ser
verificadas (através de debug) em tempo de compilação? A resposta é SQLJ.
SQLJ
Para lhe dar uma idéia rápida de como SQLJ funciona vejamos o exemplo abaixo que faz
o mesmo que as linhas JDBC acima.
Note que no exemplo acima as variáveis qtd e nome foram diretamente embutidas no
comando SQL. Toda variável embutida em código SQL deve ser precedida por “:”.
SQLJ também resolve o problema do debug de código SQL. Como o código SQLJ não é
puro Java, ele necessita ser traduzido antes que possa ser compilado para o bytecode
Java. Este processo de tradução permite que os comandos SQL embutidos sejam
analisados semântica e sintaticamente, capturando erros de SQL em tempo de tradução
em vez de em tempo de execução (como ocorre com JDBC).
Há várias razões porque você deveria conhecer JDBC e SQLJ. JDBC deve ser utilizado
para gerar comandos SQL em tempo de execução enquanto SQLJ é ideal para comandos
SQL estáticos, isto é, quando o formato do comando SQL é conhecido no momento do
desenvolvimento da aplicação. Observe que em aplicações típicas a grande maioria dos
comandos SQL são estáticos.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 55
_______________________________________________________________________________________
Em segundo lugar, a atual encarnação de SQLJ (da Oracle) é executada sobre JDBC. Isto
permite o desenvolvimento de aplicações SQLJ com qualquer configuração de driver,
assim como com JDBC (thin, fat ou server-side) mas isto também significa que o
desenvolvedor que utiliza SQLJ necessita conhecer os vários tipos de drivers JDBC.
import java.sql.*;
import sqlj.runtime.ref.DefaultContext;
import oracle.sqlj.runtime.Oracle;
#sql iterator MyIter (String ename, int empno, float sal);
public class MyExample
{ public static void main (String args[]) throws SQLException
{ Oracle.connect (“jdbc:oracle:thin:@sbd:1521:orcl”,
“scott”, “tiger”);
#sql { INSERT INTO EMP (ENAME, EMPNO, SAL)
VALUES (‘SALMAN’, 32, 20000) };
MyIter iter;
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 56
_______________________________________________________________________________________
A primeira linha do método main é:
Oracle.connect (“jdbc:oracle:thin:@sbd:1521:orcl”,
“scott”, “tiger”);
Esta chamada estabelece uma conexão de banco de dados default para SQLJ utilizar. A
partir deste momento podemos escrever comandos SQLJ. O primeiro comando é:
O comando abaixo executa uma query. E o resultado da query não vai para um result set
como em JDBC. Em SQLJ não utilizamos result sets. Utilizamos iterators. A diferença
entre um result set e um iterator é que um result set pode armazenar o resultado de
qualquer query enquanto um iterator só pode armazenar o resultado de um tipo específico
de query. Por esta razão é preciso definir um iterator type para cada tipo de query
diferente.
Olhando a definição acima podemos ver que este tipo de iterator pode armazenar
resultados com as seguintes características: a primeira coluna deve poder ser mapeada
para um String Java, a segunda para um int Java e a terceira para um float Java. Esta
definição também atribui nomes a estas colunas: ename, empno e sal respectivamente.
MyIter iter;
E finalmente a instância iter é populada com dados. Então podemos acessar estes dados,
linha a linha, com o comando abaixo:
while (iter.next())
{ System.out.println (iter.ename() +“ “+ iter.empno() +“ “+
iter.sal());
}
O método next() de um iterator faz o mesmo que o método next de um result set.
Se o iterator não está na última linha de dados, next() retorna true e aponta o iterator
para a próxima linha. Caso contrário, retorna false. A maneira como os dados de
determinada linha são acessados em um iterator é um pouco diferente da forma como isto
é feito em com um result set em JDBC. O iterator (como você pode ver no exemplo
acima) possui métodos que correspondem aos nomes das colunas no iterator. Chamando
estes métodos, o dado na coluna correspondente é retornado.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 57
_______________________________________________________________________________________
Traduzindo e Compilando um Programa SQLJ
>sqlj MyExample.sqlj
Há muitas opções que podem ser especificadas, mas a chamada acima é suficiente.
Enquanto o tradutor sqlj gera o arquivo MyExample.java ele verifica se o seu código
SQL embutido faz sentido. Ele analisa este código sintática e semanticamente. Ele pode
inclusive verificar se as tabelas, colunas e tipos de dados Java que você está utilizando
são apropriados. Ele efetua uma conexão com o banco de dados para efetuar esta
verificação. Se você deseja que o tradutor se conecte a um banco de dados de teste (e não
com o banco de produção) para efetuar esta verificação, você necessita especificar um
URL de um banco de dados com o qual a conexão deve ser efetuada. Você deverá
fornecer também a conta e a password do usuário.
1) Os arquivos .java gerados pelo tradutor SQLJ efetuam chamadas a uma biblioteca
Java denominada SQLJ runtime. As classes nesta biblioteca utilizam o serialized
object file para descobrir que chamadas JDBC deve efetuar.
2) O serialized object file pode ser utilizado para customizar o programa SQLJ para um
fabricante específico de SGBD (isto é feito automaticamente pelo tradutor SQLJ).
A compilação dos arquivos .java gerados pelo tradutor é efetuada pelo tradutor SQLJ. O
compilador javac do JDK é executado em todos os arquivos .java. O processo de
compilação gera bytecode Java logo, após esta etapa, o programa SQLJ pode ser
executado chamando-se o interpretador java, assim:
>java MyExample
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 58
_______________________________________________________________________________________
JDBC
Inicialmente estudaremos a API JDBC 1.0, que acompanham a versão 1.1 do JDK.
Posteriormente estudaremos a API JDBC 2.0, que acompanham a versão 1.2 do JDK.
No estudo da API JDBC 1.0 aprenderemos como utilizar esta API para criar tabelas,
inserir valores em tabelas, emitir queries, recuperar os resultados de queries, e atualizar
tabelas. Você aprenderá a emitir os comandos statement e prepare statement, e verá,
ainda, como executar uma stored procedure e como capturar exceções e warnings.
No estudo da API JDBC 2.0 aprenderemos a mover o cursor em um scrollable result set,
como atualizar um result set e como fazer atualizações batch.
JDBC 1.0
import java.sql.*;
Se você não incluir “import java.sql.*;” no seu código, você terá de escrever “java.sql. “
mais o nome da classe em frente de todos os métodos e campos JDBC que você utiliza.
1. Carregar o driver.
2. Fazer a conexão.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 59
_______________________________________________________________________________________
Como Carregar o Driver
Para carregar o driver do tipo bridge JDBC-ODBC é preciso escrever a seguinte linha de
código:
Class.forName(“sun.jdbc.odbc.JdbcOdbcDriver”);
A documentação do seu driver lhe informará o nome da classe que você deve utilizar. Se
o nome da classe é jdbc.DriverXYZ, então você deve carregar o diver assim:
Class.forName(”jdbc.DriverXYZ”);
Você não necessita criar uma instância do driver e registrá-lo no DriverManager porque
ao chamar Class.forName isto será feito automaticamente. Uma vez tendo-se carregado o
driver, ele se encontra disponível para uso.
A dificuldade aqui está em saber o que utilizar como url. Se você está utilizando um
driver do tipo ponte JDBC-ODBC, o URL JDBC começará com jdbc:odbc:. O resto do
URL é geralmente o nome do data source. Logo, se você está utilizando ODBC para
acessar um data source ODBC denominado “Uff_ds”, por exemplo, seu URL JDBC
seria: jdbc:odbc:Uff_ds.
Exemplo:
Se você está utilizando um driver desenvolvido por terceiros, a documentação lhe dirá
que subprotocolo utilizar, isto é, o que colocar após jdbc: no URL JDBC. Por exemplo, se
um desenvolvedor de driver registrou o nome acme como o subprotocolo, a primeira e a
segunda parte do URL JDBC será: jdbc:acme: .
Se um dos drivers que você carregou reconhece o URL JDBC fornecido para o método
DriverManager.getConnection, este driver irá estabelecer a conexão com o DBMS
especificado no URL JDBC. A classe DriverManager gerencia todos os detalhes
necessários para estabelecer a conexão. A menos que você esteja escrevendo um driver,
você nunca utilizará os métodos da interface Driver, e o único método da classe
DriverManager que você realmente necessita conhecer é o
DriverManager.getConnection.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 60
_______________________________________________________________________________________
Exemplo:
import java.sql.*;
public class Lista_Empregados
{ public void lista_emp() throws Exception
{ String nome, salario;
Class.forName ("sun.jdbc.odbc.JdbcOdbcDriver");
Connection conn =
DriverManager.getConnection("jdbc:odbc:Oracle","Scott","Tiger");
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 61
_______________________________________________________________________________________
Criando Tabelas
Numero Number(3)
Nome Varchar2(32)
Num_Fornededor Number(4)
Preco Number(5,2)
Qtd_Semana Number(7,2)
Qtd_Total Number(9,2)
Numero Number(4)
Nome Varchar2(40)
Rua Varchar2(40)
Cidade Varchar2(20)
Estado Char(2)
CEP Char(9)
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 62
_______________________________________________________________________________________
O comando abaixo cria a tabela PRODUTOS.
O comando acima não termina com um terminados do SGBD, que varia de SGBD para
SGBD. Por exemplo, o Oracle utiliza um ; para indicar o final de um comando, enquanto
o Sysbase utiliza a palavra go. O driver que você estiver utilizando automaticamente
fornece o terminador apropriado e você não precisará inclui-lo no seu código JDBC.
Os tipos de dados utilizados no comando CREATE TABLE acima são os tipos SQL
genéricos (também denominados tipos JDBC) que são definidos na classe java.sql.Types.
Um objeto statement é utilizado para enviar o seu comando SQL para o SGBD. Você
simplesmente cria um objeto statement e o executa através dos métodos executeQuery ou
excuteUpdate, passando como parâmetro o string SQL que se deseja executar.
É utilizada uma instância da conexão ativa para criar um objeto do tipo Statement. No
exemplo abaixo, utilizamos nosso objeto Connection (con) para criar o objeto Statement
(stmt).
Neste momento stmt existe, mas não possui um comando SQL para passar para o SGBD.
É preciso fornecer este string SQL para o método que utilizaremos para executar o objeto
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 63
_______________________________________________________________________________________
stmt. Para executar um comando SELECT utilizamos o método executeQuery, e para
comandos que criam ou modificam tabelas, utilizamos o método executeUpdate.
Uma vez que criamos um string com o comando SQL que desejamos executar, e
associamos este string à variável createTableProdutos, poderíamos ter escrito o comando
que executa este string SQL, assim:
Stmt.executeUpdate (createTableProdutos);
Segue abaixo o código necessário para inserir uma linha na tabela PRODUTOS:
stmt.executeUpdate (
“INSERT INTO FORNECEDORES(NUMERO, NOME, RUA, CIDADE, ESTADO, CEP) ” +
“VALUES(1001, ‘CIA DO CAFE LTDA’, ‘R. DAS LARANJEIRAS 107’, ‘RIO’, “ +
“‘RJ’, ‘22241-090’)”);
Observe que para inserir novas linhas na tabela PRODUTOS não é preciso instanciar um
novo objeto stmt.
stmt.executeUpdate (
“INSERT INTO FORNECEDORES(NUMERO, NOME, RUA, CIDADE, ESTADO, CEP) ” +
“VALUES(1002, ‘CAFE COLOMBIANO LTDA’, ‘AV. BRASIL, 5240’, ‘RIO’, “ +
“‘RJ’, ‘22348-030’)”);
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 64
_______________________________________________________________________________________
Exemplo:
import java.sql.*;
public class Cria_Tabela
{ public void cria_tab_Produtos() throws Exception
{ String nome, salario;
Class.forName ("sun.jdbc.odbc.JdbcOdbcDriver");
Connection conn =
DriverManager.getConnection("jdbc:odbc:Oracle","Scott","Tiger");
stmt.executeUpdate (
“INSERT INTO FORNECEDORES(NUMERO, NOME, RUA, CIDADE, “ +
“ESTADO, CEP) ” +
“VALUES(1001, ‘CIA DO CAFE LTDA’, “ +
“‘R. DAS LARANJEIRAS 107’, ‘RIO’, ‘RJ’, ‘22241-090’)”);
stmt.executeUpdate (
“INSERT INTO FORNECEDORES(NUMERO, NOME, RUA, CIDADE, “ +
“ESTADO, CEP) ” +
“VALUES(1002, ‘CAFE COLOMBIANO LTDA’, “ +
“‘AV. BRASIL, 5240’, ‘RIO’, ‘RJ’, ‘22348-030’)”);
}
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 65
_______________________________________________________________________________________
Obtendo Dados de uma Tabela
Agora que a tabela Fornecedores possui valores podemos escrever um comando SELECT
para recuperar estes valores.
JDBC retorna os resultados em um objeto do tipo ResultSet, logo é preciso declarar uma
instância da classe ResultSet para armazenar os resultados. O código abaixo declara um
objeto rs do tipo ResultSet e atribui o resultado de uma query a ele.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 66
_______________________________________________________________________________________
import java.sql.*;
public class Lista_Fornecedores
{ public void lista_fornec() throws Exception
{ Class.forName ("sun.jdbc.odbc.JdbcOdbcDriver");
Connection conn =
DriverManager.getConnection("jdbc:odbc:Oracle","Scott","Tiger");
JDBC também permite que se recupere o valor de uma coluna através do seu número:
String s = rs.getString(2);
Observe que o número da coluna se refere ao número da coluna no result set, e não na
tabela original. Usar o número da coluna é ligeiramente mais eficiente.
JDBC permite que vários métodos getXXX sejam utilizados para recuperar certos tipos
de dados SQL. Por exemplo, o método getInt pode ser utilizado para recuperar qualquer
valor SQL numérico ou do tipo caracter. O dado recuperado será convertido para um
número int. Isto é, JDBC tentará converter um varchar em int. O método getInt deve ser
utilizado para recuperar apenas tipos SQL INTEGER, no entanto, não pode ser utilizado
para os tipos SQL BINARY, VARBINARY, LONGBINARY, DATE, TIME, ou
TIMESTAMP.
A tabela abaixo mostra que métodos podem ser legalmente utilizados para recuperar os
vários tipos de dados SQL e quais métodos são recomendados.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 67
_______________________________________________________________________________________
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 68
_______________________________________________________________________________________
Utilizando o Método getString
Embora o método getString possa seja recomendado para recuperar os tipos de dados
SQL CHAR e VARCHAR, é possível recuperar qualquer tipo de dado SQL básico. Você
não pode, no entanto, recuperar os novos tipos de dados SQL3 com ele. Recuperar
valores com getString pode ser muito útil, mas possui limitações. Se for utilizado para
recuperar um tipo numérico, getString converterá o tipo numérico para um objeto String
Java e o valor terá de ser convertido de volta para numérico antes de ser manipulado
como um número. Logo, se você desejar que uma aplicação recupere valores de qualquer
tipo SQL padrão (exceto os tipos SQL3), utilize o método getString.
Atualizando Tabelas
Suponha que após uma semana de trabalho, o proprietário da loja “The Coffee Break”
queira informar, para cada tipo de café, a quantidade (em kilos) vendida na semana.
Utilizando o objeto Statement (stmt) o código abaixo executa o comando SQL contido no
string updateString:
stmt.executeUpdate(updateString);
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 69
_______________________________________________________________________________________
Exemplo:
Para executar este programa é preciso rodar o script CriaCafe.sql existente no diretório
c:\Java\TopicosAvancados\Banco_de_Dados
import java.sql.*;
public class Atualiza_Cafe
{ public void atualizaQtdCafe() throws Exception
{ Class.forName ("sun.jdbc.odbc.JdbcOdbcDriver");
Connection con =
DriverManager.getConnection("jdbc:odbc:Oracle","Scott","Tiger");
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 70
_______________________________________________________________________________________
Utilizando Prepared Statement
Embora objetos PreparedStatement possam ser utilizados para comandos SQL sem
parâmetros, você provavelmente os utilizará mais freqüentemente para comandos SQL
com parâmetros. A vantagem de se utilizar comandos SQL com parâmetros é que você
pode utilizar o mesmo comando fornecendo valores diferentes a cada execução.
Assim como ocorre com objetos Statement, você cria um objeto PreparedStatement com
um objeto connection. Utilizando a nossa conexão aberta (con) dos exemplos anteriores,
você poderia escrever código como o que vem a seguir para criar um objeto
PreparedStatement que possui dois parâmetros:
Você precisará fornecer valores para serem utilizados no local das interrogações (?), antes
de você executar o objeto PreparedStatement. Você efetua esta substituição chamando
um dos métodos setXXX definidos para a classe PreparedStatement. Se o valor que você
quer substituir é um número inteiro Java, então você chama o método setInt. Se o valor
que você deseja substituir é um string Java então você chama o método setString. Existe
um método setXXX para cada tipo de dado Java.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 71
_______________________________________________________________________________________
Para atualizarmos para 75 kilos, a quantidade vendida na semana do produto 101 seria
preciso o seguinte código:
atualizarVendas.setInt(1, 75);
atualizarVendas.setString(2, “CAFE BRASILEIRO”);
atualizarVendas.executeUpdate();
Após estes valores terem sido atribuídos a seus dois parâmetros de entrada, o comando
SQL em atualizaVendas será equivalente ao comando SQL no objeto do tipo String
(comando) abaixo. Logo, os dois fragmentos de código definidos abaixo são equivalentes
(mas não em termos de desempenho, uma vez que o objeto atualizarVendas contém o
comando SQL pré-compilado).
Fragmento de Código 1
Fragmento de Código 2
PreparedStatement atualizarVendas = con.preparedStatement
(“UPDATE PRODUTOS SET QTD_SEMANA = ? WHERE NOME = ?”);
atualizarVendas.setInt(1, 75);
atualizarVendas.setString(2, “CAFE BRASILEIRO”);
atualizarVendas.executeUpdate();
Quando um parâmetro recebe um valor este valor é retido pelo parâmetro até que ele seja
substituído por outro valor ou até que o método clearParameters seja chamado. Utilizando
o objeto atualizarVendas (do tipo PreparedStatement), o seguinte fragmento de código
ilustra a reutilização de um prepared statement após ressetar o valor de apenas um de seus
parâmetros.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 72
_______________________________________________________________________________________
PreparedStatement atualizarVendas = con.prepareStatement
("UPDATE PRODUTOS SET QTD_SEMANA = ? WHERE NOME = ?");
atualizarVendas.setInt(1, 175);
atualizarVendas.setString(2, "CAFE BRASILEIRO");
atualizarVendas.executeUpdate();
Exemplo:
Para executar este programa é preciso rodar o script CriaProd.sql existente no diretório
C:\Java\TopicosAvancados\Banco_de_Dados
import java.sql.*;
public class Atualiza_Duas_Linhas
{ public void atualizaLinhas() throws Exception
{ Class.forName ("sun.jdbc.odbc.JdbcOdbcDriver");
Connection con =
DriverManager.getConnection("jdbc:odbc:Oracle","Scott","Tiger");
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 73
_______________________________________________________________________________________
Utilizando um Loop para Atribuir Valores
Você pode tornar o código mais fácil utilizando um for ou um while para atribuir valores
para parâmetros de entrada:
PreparedStatement atualizarVendas;
String stringAtualizacao = "UPDATE PRODUTOS " +
"SET QTD_SEMANA = ? WHERE NOME = ?";
atualizarVendas = con.prepareStatement(stringAtualizacao);
Observe que em uma aplicação real os dados provavelmente seriam digitados pelo
usuário.
atualizarVendas.setInt(1, 50);
atualizarVendas.setString(2, "CAFE BRASILEIRO");
int n = atualizarVendas.executeUpdate();
Quando o método executeUpdate é utilizado para executar um comando DDL, tal como
um comando CREATE TABLE, ele retorna um int 0.
Se o comando executeUpdate retorna 0, isto pode significar duas coisas: (1) o comando
executado foi um update que afetou zero linhas, ou (2) o comando executado foi um
comando DDL.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 74
_______________________________________________________________________________________
Exemplo:
Para executar este programa é preciso rodar o script CriaProd.sql existente no diretório
c:\Java\TopicosAvancados\Banco_de_Dados
import java.sql.*;
public class Atualiza_Com_Array
{ public void atualizaLinhas() throws Exception
{ Class.forName ("sun.jdbc.odbc.JdbcOdbcDriver");
Connection con =
DriverManager.getConnection("jdbc:odbc:Oracle","Scott","Tiger");
PreparedStatement atualizarVendas;
String stringAtualizacao = "UPDATE PRODUTOS " +
"SET QTD_SEMANA = ? WHERE NOME = ?";
atualizarVendas = con.prepareStatement(stringAtualizacao);
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 75
_______________________________________________________________________________________
Utilizando Joins
Exemplo:
import java.sql.*;
public class Join
{ public void listaJoin() throws Exception
{ Class.forName ("sun.jdbc.odbc.JdbcOdbcDriver");
Connection con =
DriverManager.getConnection("jdbc:odbc:Oracle","Scott","Tiger");
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 76
_______________________________________________________________________________________
Utilizando Transações
Toda conexão é criada no modo auto-commit. Isto significa que cada comando SQL
individual é tratado como uma transação. Para ser mais preciso, um comando SQL é
comitado, por default, quando ele é concluído, não quando é executado. E um comando é
concluído quando todos os seus resultados e contadores de atualizações são recuperados.
Na maioria das situações, no entanto, um comando é concluído, e então comitado, logo
após a sua execução.
A maneira de se permitir que dois ou mais comandos sejam agrupados em uma transação
é desabilitando o modo auto-commit.
con.setAutoCommit(false);
Uma vez tendo-se desabilitado o modo auto-commit, nenhum comando SQL será
comitado até que você chame o método commit explicitamente. O código abaixo ilustra
uma transação:
con.setAutoCommit(false);
PreparedStatement atualizarVendas = con.prepareStatement
("UPDATE PRODUTOS SET QTD_SEMANA = ? WHERE NOME = ?");
atualizarVendas.setInt(1, 17);
atualizarVendas.setString(2, "CAFE BRASILEIRO");
atualizarVendas.executeUpdate();
Neste exemplo, o modo auto-commit é desabilitado para a conexão, o que significa que
dois prepared statements atualizarVendas e atualizarTotal serão commitados quando o
método commit é chamado.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 77
_______________________________________________________________________________________
alterado mas ainda não comitado, e este valor sofre um rollback, você leu um valor
inválido.
Normalmente você não necessita fazer nada a respeito do transaction isolation level.
Você pode apenas utilizar o default para seu SGBD. JDBC permite que você descubra
qual o nível de isolamento que seu SGBD está utilizando, por default. Para fazer isto
utilize o método getTransactionIsolation da classe Connection. JDBC também nos
permite trocar o nível de isolamento com o método setIsolationLevel da classe
Connection. No entanto, para que estas modificações tenham efeito, é preciso que o
driver que você está utilizando dê suporte a níveis de isolamento.
Se você está tentando executar um ou mais comandos DML em uma transação e recebe
uma SQLException, você deveria chamar o método rollback para abortar a transação. Ao
capturar uma exceção SQLException você fica sabendo que algo saiu errado, daí a
necessidade de efetuar o rollback.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 78
_______________________________________________________________________________________
import java.sql.*;
public class Transacoes
{ public void atualizaLinha() throws Exception
{ Class.forName ("sun.jdbc.odbc.JdbcOdbcDriver");
Connection con =
DriverManager.getConnection("jdbc:odbc:Oracle","Scott","Tiger");
con.setAutoCommit(false);
PreparedStatement atualizarVendas = con.prepareStatement
("UPDATE PRODUTOS SET QTD_SEMANA = ? WHERE NOME = ?");
atualizarVendas.setInt(1, 17);
atualizarVendas.setString(2, "CAFE BRASILEIRO");
atualizarVendas.executeUpdate();
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 79
_______________________________________________________________________________________
Stored Procedures
import java.sql.*;
public class CriaStoredProcedure
{ public void criaProcedure() throws Exception
{ Class.forName ("sun.jdbc.odbc.JdbcOdbcDriver");
Connection con =
DriverManager.getConnection("jdbc:odbc:Oracle","Scott","Tiger");
import java.sql.*;
public class ExecutaStoredProcedure
{ public void executaProcedure() throws Exception
{ Class.forName ("sun.jdbc.odbc.JdbcOdbcDriver");
Connection con =
DriverManager.getConnection("jdbc:odbc:Oracle","Scott","Tiger");
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 80
_______________________________________________________________________________________
Alguns SGBDs, ao executarem uma stored procedure, conseguem retornar um conjunto
de dados. Neste caso executaríamos esta stored procedure assim:
Por outro lado, se a Stored Procedure executa apenas um comando DDL ou DML
deveríamos executá-la assim:
No caso do Oracle, que não é capaz de retornar um conjunto de dados em uma stored
procedure, podemos executar todas as stored procedures assim:
Capturando Exceções
No exemplo abaixo utilizamos dois blocos try e dois blocos catch. O primeiro bloco try
contém o método Class.forName, do package java.lang. Este método gera uma exceção
do tipo ClassNotFoundException, e o bloco catch abaixo trata esta exceção. O segundo
bloco try contém métodos JDBC, que geram exceções do tipo SQLException, logo um
bloco catch no final da aplicação pode tratar todas estas exceções, porque todas serão
exceções do mesmo tipo.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 81
_______________________________________________________________________________________
Recuperando Exceções
JDBC nos permite ver exceções e warnings gerados pelo seu SGBD e pelo runtime java.
Exemplo:
try
{ // Código que eventualmente pode gerar uma exceção.
}
catch (SQLException ex)
{ System.err.println (“SQLException: “ + ex.getMessage());
}
try
{ Class.forName(“myDriverClassName”);
}
catch (java.lang.ClassNotFoundException e)
{ System.err.print (“ClassNotFoundException: “);
System.err.println (e.getMessage());
}
Existem, no entanto, três componentes que podem ser impressos: a mensagem (um string
que descreve a mensagem de erro), o SQL state (um string que identifica o erro de acordo
com a CONVENÇÃO x/Open SQLState), e o código de erro do fabricante do SGBD). O
objeto exception (e) é capturado, e seus três componentes são acessados com os métodos
getMessage, getSQLState, e getErrorCode.
O bloco catch abaixo captura mais de uma exceção, caso ocorra. Se existir uma segunda
exceção ela será concatenada à primeira (e), logo, e.getNextException pode ser chamado
para se verificar se há mais alguma exceção. Se existe outra exceção o loop continua e
imprime a próxima mensagem de erro. O loop continua até que não haja mais exceções.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 82
_______________________________________________________________________________________
try
{ // Código que pode gerar uma ou mais exceções.
}
catch (SQLException ex)
{ System.out.print (“\n--- SQLException Capturada ---\n“);
while (ex != null)
{ System.out.print (“Message: “ + ex.getMessage());
System.out.print (“SQLState: “ + ex.getSQLState());
System.out.print (“ErrorCode: “ + ex.getErrorCode());
System.out.print (““);
ex = ex.getNextException();
}
}
Recuperando Warnings
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 83
_______________________________________________________________________________________
Exemplo:
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 84
_______________________________________________________________________________________
JDBC 2.0
O package java.sql que faz parte do JDK 1.2 (conhecido como JDBC 2.0 API) inclui
várias características novas não incluídas no package java.sql que faz parte do JDK 1.1
(conhecido como JDBC 1.0 API). Todos os exemplos de código apresentados até aqui
foram escritos utilizando a API JDBC 1.0.
1. Navegar para frente e para trás em um result set, ou mover-se para uma linha
específica.
2. Fazer atualizações em tabelas do banco utilizando métodos da linguagem Java em vez
de utilizar SQL.
3. Enviar vários comandos SQL para o banco como uma unidade, ou batch.
4. Utilizar os novos tipos de dados SQL3 como valores de colunas.
Até o momento que este material foi escrito (junho de 99) não havia nenhum driver
implementado com as características aqui apresentadas, no entanto, vários drivers
estavam em desenvolvimento.
Scrollable Result Sets tornam possível a criação de aplicações gráficas com grids para a
exibição de result sets. Outra possibilidade é a navegação no ResultSet para atualização
de dados.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 85
_______________________________________________________________________________________
O segundo argumento é uma das duas constantes que indicam se o ResultSet é read-only
ou pode ser atualizado. Estas variáveis são: CONCUR_READ_ONLY e CONCUR_UPDATE. É
preciso especificar os dois parâmetros, no entanto, como ambos são numéricos é preciso
tomar cuidado para não inverter a ordem, uma vez que o compilador não vai detectar o
erro.
Quando você cria um novo objeto do tipo ResultSet, o cursor é inicialmente posicionado
antes do primeira linha. Com um scrollable result set é possível utilizar os métodos next
(assim como com a API JDBC 1.0) e o método previous. Ambos estes métodos retornam
false quando o cursor vai além do result set. (posição após o último ou antes do primeiro).
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 86
_______________________________________________________________________________________
Exemplo:
import java.sql.*;
public class TiposDeResultSet
{ public void lista() throws Exception
{ Class.forName ("sun.jdbc.odbc.JdbcOdbcDriver");
Connection con =
DriverManager.getConnection("jdbc:odbc:Oracle","Scott","Tiger");
Statement stmt =
con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,
ResultSet.CONCUR_READ_ONLY);
String query = "SELECT NOME " +
"FROM FORNECEDORES";
ResultSet rs = stmt.executeQuery(query);
import java.sql.*;
public class TiposDeResultSet
{ public void lista() throws Exception
{ Class.forName ("sun.jdbc.odbc.JdbcOdbcDriver");
Connection con =
DriverManager.getConnection("jdbc:odbc:Oracle","Scott","Tiger");
Statement stmt =
con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_READ_ONLY);
ResultSet rs = stmt.executeQuery("SELECT NOME FROM FORNECEDORES");
rs.afterLast();
System.out.println ("Nome do Fornecedor");
while (rs.previous())
{ String fnome = rs.getString("NOME");
System.out.println (fnome);
}
}
Se o cursor tem 500 linhas a linha de código abaixo move o cursor para a linha 497:
rs.absolute(-4);
Com o método relative você pode especificar quantas linhas você deseja mover o cursor e
em que direção:
O método getRow nos permite verificar em que linha o cursor encontra-se posicionado.
if (rs.isAfterLast() == false)
{ rs.afterLast();
}
while (rs.previous())
{ String fnome = rs.getString("NOME");
System.out.println (fnome);
}
Nas duas próximas seções você verá como utilizar os dois métodos moveToInsertRow e
moveToCurrentRow.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 88
_______________________________________________________________________________________
Atualizando Result Sets
É preciso criar uma Result Set atualizável. Para fazer isso forneça a constante
CONCUR_UPDATABLE da classe ResultSet para o método createStatement. O objeto
Statement criado irá produzir um objeto do tipo ResultSet atualizável cada vez que uma
query for executada. O código abaixo cria um result set atualizável e scrollable. Com um
scrollable result set você pode navegar pelas linhas que deseja modificar e se o tipo for
TYPE_SCROLL_SENSITIVE, você poderá recuperar o novo valor em uma linha do cursor,
após tê-lo atualizado.
import java.sql.*;
public class TiposDeResultSet
{ public void lista() throws Exception
{ Class.forName ("sun.jdbc.odbc.JdbcOdbcDriver");
Connection con =
DriverManager.getConnection("jdbc:odbc:Oracle","Scott","Tiger");
Statement stmt =
con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,
ResultSet.CONCUR_UPDATABLE);
ResultSet rs = stmt.executeQuery("SELECT * FROM FORNECEDORES");
. . .
Uma modificação é uma alteração de um valor de uma coluna na linha corrente. Vamos
supor que desejamos aumentar o preço de um produto. Com JDBC 1.0 fazemos:
stmt.executeUpdate(“UPDATE PRODUTOS “ +
“SET PRECO = 10.99 “ +
“WHERE NUMERO = 101”);
rs.first();
rs.updateFloat(“PRECO”, 10.99);
Uma operação de update em JDBC 2.0 afeta a linha corrente do cursor. Existe um método
updateXXX para cada tipo de dado. updateString, updateBigDecimal, updateInt, etc.
Até este momento, o preço do produto número 101 é 10.99 em rs, mas o preço na tabela
PRODUTOS ainda não foi alterado. Para que esta atualização seja efetuada no banco e
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 89
_______________________________________________________________________________________
não apenas no result set é preciso chamar o método updateRow da interface ResultSet,
conforme vem abaixo:
rs.first();
rs.updateFloat(“PRECO”, 10.99);
rs.updateRow();
Se você tivesse movido o cursor para outra linha antes de executar rs.updateRow(),
a alteração teria sido perdida.
rs.first();
rs.updateFloat(“PRECO”, 10.99);
rs.first();
rs.updateFloat(“PRECO”, 10.99);
rs.cancelRowUpdates();
rs.updateFloat(“PRECO”, 11.99);
rs.updateRow();
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 90
_______________________________________________________________________________________
do result set. Você pode pensar nela como um buffer separado no qual uma nova linha é
composta.
O primeiro passo será mover o cursor para a insert row, chamando-se o método
moveToInsertRow. O próximo passo é designar um valor para cada coluna na linha. Isto
deve ser feito chamando-se os métodos updateXXX para cada valor. E fianlmente chama-
se o método insertRow. Este método simultaneamente insere a linha no result set e na
tabela a partir da qual o result set foi montado.
O código abaixo cria um objeto ResultSet scrollable e atualizável que contém todas as
linhas e colunas da tabela produtos.
Connection con =
DriverManager.getConnection("jdbc:odbc:Oracle","Scott","Tiger");
Statement stmt =
con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,
ResultSet.CONCUR_UPDATABLE);
ResultSet rs = stmt.executeQuery("SELECT * FROM FORNECEDORES");
E o fragmento de código abaixo, utiliza o objeto ResultSet (rs) para inserir um novo
produto.
rs.moveToInsertRow();
rs.updateInt(“NUMERO”, 107);
rs.updateString(“NOME”, “CAFÉ BOLIVIANO”);
rs.updateInt(“NUM_FORNECEDOR”, 1003);
rs.updateFloat(“PRECO”, 10.44);
rs.updateFloat(“QTD_SEMANA”, 0);
rs.updateFloat(“QTD_TOTAL”, 0);
rs.insertRow(); // Insere no result set e no banco ao mesmo tempo.
Como você pode utilizar o número da coluna em vez de seu nome, poderíamos fazer o
seguinte:
rs.updateInt(1, 107);
rs.updateString(2, “CAFÉ BOLIVIANO”);
...
Após chamar o método insertRow, você pode começar a construir outra linha para ser
inserida, ou você pode mover o cursor de volta para uma linha do result set. Para por o
cursor em uma linha específica você pode utilizar os métodos: first, last, beforeFirst,
afterLast, e absolute. Você também pode utilizar os métodos previous, relative e
moveToCursorRow. Você pode chamar o método moveToCurrentRow apenas quando o
cursor está na insert row.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 91
_______________________________________________________________________________________
insert row para a linha que era a corrente antes do método moveToInsertRow ser
chamado.
O result set rs acima é atualizável, scrollable, e sensível a modificações efetuadas por ele
próprio e por outros. Embora seu tipo seja TYPE_SCROLL_SENSITIVE, é possível que um
método getXXX chamado após a inserção de uma linha não recupere valores para a nova
linha inserida. Existem métodos da interface DatabaseMetaData que nos indicarão o que
é visível e o que é detectado nos diferentes tipos de result set para o seu driver e SGBD.
Estes métodos são discutidos em detalhe em JDBC Database Access with Java, mas eles
estão fora do escopo deste curso.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 92
_______________________________________________________________________________________
import java.sql.*;
public class InsereLinha
{ public static void main (Strings args[])
{ try
{ Class.forName ("sun.jdbc.odbc.JdbcOdbcDriver");
}
catch (java.lang.ClassNotFoundException e)
{ System.err.print(“ClassNotFoundException: “);
System.err.println(e.getMessage());
}
try
{ Connection con =
DriverManager.getConnection
("jdbc:odbc:Oracle","Scott","Tiger");
Statement stmt =
con.createStatement
(ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_READ_ONLY);
ResultSet rs = stmt.executeQuery
("SELECT NOME FROM PRODUTOS");
rs.moveToInsertRow();
rs.updateInt(“NUMERO”, 107);
rs.updateString(“NOME”, “CAFÉ BOLIVIANO”);
rs.updateInt(“NUM_FORNECEDOR”, 1003);
rs.updateFloat(“PRECO”, 10.44);
rs.updateFloat(“QTD_SEMANA”, 0);
rs.updateFloat(“QTD_TOTAL”, 0);
rs.insertRow(); // Insere no result set e no banco
// ao mesmo tempo.
rs.updateInt(“NUMERO”, 108);
rs.updateString(“NOME”, “CAFÉ PILAO”);
rs.updateInt(“NUM_FORNECEDOR”, 1001);
rs.updateFloat(“PRECO”, 7.48);
rs.updateFloat(“QTD_SEMANA”, 0);
rs.updateFloat(“QTD_TOTAL”, 0);
rs.insertRow(); // Insere uma Segunda linha
rs.beforeFirst();
System.out.println ("Nome do Fornecedor");
while (rs.previous())
{ String fnome = rs.getString("NOME");
System.out.println (fnome);
}
rs.close();
stmt.close();
con.close();
}
catch (SQLException ex)
{ System.err.println(“SQLexception: “ + ex.getMessage());
}
}
}
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 93
_______________________________________________________________________________________
Deletando uma Linha
Para deletar uma linha basta mover o cursor para a linha que se deseja remover, para
então, chamar o método deleteRow. Por exemplo:
rs.absolute(4);
rs.deleteRow();
Alguns drivers JDBC removem a linha deletada do result set. Outros drivers JDBC
utilizam uma linha em branco para demonstrar que ali havia uma linha que foi removida.
Desta forma, ainda é possível utilizar o método absolute para posicionar-se em uma linha
específica. Isto é, o número absoluto de uma linha não muda com a deleção de uma linha
anterior.
Lembre-se que os drivers funcionam de maneira diferente. Se você pretende escrever uma
aplicação para rodar com diferentes bancos de dados, você não deveria escrever código
que dependa da existência de uma linha em branco no local de uma linha deletada.
A questão é se você verá as modificações efetuadas por você e por outros usuários
enquanto o result set ainda está aberto. (Geralmente você está mais interessado nas
modificações efetuadas pelos outros usuários). A resposta a esta questão depende do
SGBD, do driver, e do tipo de objeto ResultSet que você possui.
Você pode, até certo ponto regular que modificações são visíveis aumentando ou
diminuindo o transaction isolation level para a sua conexão com o banco de dados. Por
exemplo, a seguinte linha de código (onde con é um objeto de uma conexão ativa)
designa TRANSACTION_READ_COMMITTED para o isolation level da conexão.
con.setTransactionIsolation(TRANSACTION_READ_COMMITTED);
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 94
_______________________________________________________________________________________
Com este nível de isolamento, o seu objeto ResultSet não mostrará nenhuma modificação
antes que elas sejam comitadas.
Você pode utilizar o método refreshRow para obter os últimos valores para uma linha
diretamente do banco de dados. Este método pode ser muito custoso especialmente se o
banco de dados retorna várias linhas quando o refresh é executado.
Mesmo quando um result set é sensitive e as modificações são visíveis, uma aplicação
pode nem sempre ver a última modificação efetuada em determinada linha, se o driver
recupera várias linhas de cada vez e faz um cache delas. Neste caso, a utilização do
método refreshRow é a única maneira de se ter certeza que você está vendo a versão
mais atualizada do dado.
Para utilizar o método refreshRow, o result set deve ser sensitive, caso contrário nada
acontecerá. Um exemplo da necessidade de um refresh seria, por exemplo, para verificar
se o assento que você está reservando (em uma passagem aérea) ainda está disponível.
import java.sql.*;
public class VerificaPreco
{ public static void main (Strings args[])
{ Class.forName ("sun.jdbc.odbc.JdbcOdbcDriver");
Connection con =
DriverManager.getConnection
("jdbc:odbc:Oracle","Scott","Tiger");
Statement stmt = con.createStatement
(ResultSet.TYPE_SCROLL_SENSITIVE,
ResultSet.CONCUR_UPDATABLE);
ResultSet rs = stmt.executeQuery("SELECT * FROM PRODUTOS");
rs.absolute(4);
Float preco1 = rs.getFloat(“PRECO”);
// faz algo . . .
rs.absolute(4);
rs.refreshRow();
Float preco2 = rs.getFloat(“PRECO”);
if (preco2 > preco1)
{ // Faz algo . . .
}
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 95
_______________________________________________________________________________________
Fazendo Atualizações em Modo Batch
Com a API JDBC 1.0 você pode submeter atualizações ao banco de dados
individualmente com o método executeUpdate. Múltiplos comandos executeUpdate
podem ser enviados em uma mesma transação, mas embora sejam comitados ou sofreram
rollback como uma unidade, eles ainda são processados individualmente.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 96
_______________________________________________________________________________________
Exemplo:
import java.sql.*;
public class VerificaPreco
{ public static void main (Strings args[])
{ Class.forName ("sun.jdbc.odbc.JdbcOdbcDriver");
Connection con =
DriverManager.getConnection
("jdbc:odbc:Oracle","Scott","Tiger");
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 97
_______________________________________________________________________________________
Tratamento de Exceções em Batch Update
Há duas exceções que podem ser geradas durante uma operação de Batch Update:
SQLException e BatchUpdateException.
Quando cada comando é executado ele deve retornar um contador de atualização que
pode ser adicionado a um array de contadores de atualização. A tentativa de colocar um
result set em um array de contadores de atualização causará um erro e fará com que
executeBatch gere uma exceção do tipo SQLException. Em outras palavras, apenas
comandos que retornam um contador de atualização (INSERT, DELETE, UPDATE, CREATE
TABLE, DROP TABLE, ALTER TABLE, etc) podem ser executados como um batch com o
comando executeBatch.
Se nenhuma SQLException foi gerada, você sabe que não ocorreram problemas de acesso
ao banco de dados e que todos os comandos produziram contadores de atualização. Se um
dos comandos não pôde ser executado por alguma outra razão, o método executeBatch
gera uma exceção do tipo BatchUpdateException. Além das informações que todas as
exceções possuem, esta exceção contém um array com os contadores de atualização para
os comandos que executaram com sucesso antes da exceção ser gerada. Como os
contadores de atualização estão na mesma ordem dos comandos que os produziram, você
pode saber quantos comandos foram executados com sucesso e quais são eles.
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 98
_______________________________________________________________________________________
try
{ // Faz algumas atualizações
}
catch (BatchUpdateException b)
{ System.err.println(“SQLException: “ +b.getMessage());
System.err.println(“SQLState: “ +b.getSQLState());
System.err.println(“Message: “ +b.getMessage());
System.err.println(“Fabricante do SGBD: “ +b.getErrorCode());
System.err.println(“Contadores de Atualização: “);
int [] contadoresDeAtualizacao = b.getUpdateCounts();
for (int i = 0; i < contadoresDeAtualizacao.length; i++)
{ System.err.print(contadoresDeAtualizacao [i] + “ “);
}
}
_______________________________________________________________________________________
Apostila de Java – Tópicos Avançados 99