Programação Orientada por Objectos 87 Tratamento de Erros com Excepções Idealmente os erros de um programa deviam ser apanhados em tempo de compilação porque código errado não deveria ser executado. Mas como nem todos os erros podem ser detectados em tempo de compilação, deve existir um formalismo de tratamento de erros em tempo de execução. Em muitas linguagens o formalismo consiste no retorno de um valor especial ou na colocação de uma flag pela função na qual se verificou o erro, e o programa que chamou a função devia testar o valor retornado ou a flag, e determinar qual o problema. Mas, a maior parte das vezes, os programas não testam as condições de erro. Se testassem todos os potenciais erros sempre que uma função era chamada o código tornar-se-ia ilegível. Benefícios das excepções No ponto do programa onde ocorre um problema, conhecem-se as características do problema que ocorreu, mas em geral, nesse contexto (contexto corrente) não se tem informação suficiente para lidar com o problema. Num contexto mais alto, noutra parte do programa, existe a informação necessária para decidir o que fazer. Outro benefício das excepções resulta num programa limpo de código de tratamento de erros. Não é necessário testar a ocorrência de um erro particular e tratá-lo em vários sítios de um programa. A excepção garante que o erro é tratado. Deste modo o problema só é tratado num sítio. Além de se poupar código, separa-se código com o que se pretende fazer, do código de tratamento de erros. Lançamento de uma excepção Uma excepção (condição excepcional) é um problema que não permite a continuação do método ou scope em execução. Quando ocorre uma excepção não se pode continuar o processamento corrente porque não se tem informação necessária para tratar do problema no contexto corrente e relega-se o problema para um contexto mais alto, lançando uma excepção. Quando se lança uma excepção (usa-se a palavra-chave throw), um objecto excepção é criado (através da chamado de um construtor da respectiva classe) e sai-se do método ou do scope corrente, sendo retornada a referência ao objecto excepção criado. O fluxo de execução corrente é parado e passa do contexto corrente para o local de tratamento da excepção para onde também é passada a referência do objecto excepção. Exemplo de lançamento de uma excepção: if (t == null) throw new NullPointerException(); DEI - ISEP Fernando Mouta 88 Programação Orientada por Objectos Em todas as excepções standard (classes) há 2 construtores: • um sem argumentos, e • outro com 1 argumento do tipo String. Normalmente lança-se uma excepção de uma classe diferente para cada tipo diferente de erro, para que no contexto mais amplo se possa determinar o que fazer (tomar a decisão apropriada) apenas através do conhecimento do tipo de objecto excepção. Quando uma excepção é lançada o mecanismo de tratamento de excepções toma conta do controlo do fluxo do programa e termina a execução do método no qual a excepção é lançada, assim como os métodos dos quais este método foi chamado e a execução continua numa parte do programa que se destina a tratar excepções daquele tipo ( exception handler ). Bloco try - região guardada Para que throw não cause a saída de um método, deve existir dentro desse método um bloco para capturar a excepção Uma excepção lançada é capturada imediatamente a seguir a um bloco try por cláusulas denotadas pela palavra-chave catch, designadas cláusulas catch ou cláusulas de tratamento de excepções (exception handlers). Cada cláusula catch toma 1 e só 1 argumento de um tipo particular. try { código que pode gerar excepções } catch (Type1 id1) { // tratamento de excepções do tipo Type1 } catch (Type2 id2) { // tratamento de excepções do tipo Type2 ... } Normalmente o tratamento da excepção é baseado no tipo da excepção sem necessidade de usar o identificador, mas em qualquer dos casos o identificador tem de ser declarado. As cláusulas catch devem aparecer directamente depois do bloco try, tendo cada cláusula sempre 1 só argumento. Este tratamento de excepções separa o código colocado num bloco try (código com o que se pretende fazer), do tratamento de erros colocado nas cláusulas onde se capturam e tratam as excepções (cláusulas catch). Se uma excepção é lançada, o mecanismo de tratamento de excepções procura a primeira cláusula catch com um argumento do tipo da excepção. Se encontra executa o código dessa cláusula catch e sai. Fernando Mouta DEI - ISEP Programação Orientada por Objectos 89 Lista de Especificação das Excepções Java obriga a declarar as excepções que um método lança na declaração do método depois da lista de argumentos. A especificação das excepções é feita através da palavrachave throws seguida de uma lista de todos os tipos de potenciais excepções. void método() throws excepção1, excepção2 { ... } Se a especificação das excepções declaradas não estiver correcta, o compilador detecta isso e informa que determinada excepção deve ser tratada, ou então indicada na especificação das excepções, significando que pode ser lançada do método. Java contem um classe chamada Throwable que descreve tudo o que pode ser lançado como excepção. Throwable tem 2 subclasses: • Error descreve erros de compilação e do sistema; • Exception é o tipo de erros que podem ser lançados de qualquer método das classes de bibliotecas standard ou de métodos que escrevemos. Todas as excepções que possam ocorrer são objectos de classes que são subclasses do tipo Exception. É possível capturar qualquer tipo de excepção capturando uma excepção do tipo base Exception. catch (Exception e) { System.out.println(“Capturando a excepção “ + e.getMessage()); } Ao objecto excepção podem aplicar-se métodos da classe Throwable, ou ainda métodos da classe Object (superclasse de todas as classes). Métodos da classe Throwable String getMessage() - retorna a mensagem de erro associada com a excepção. String toString() - retorna uma descrição do objecto. void printStackTrace() - imprime para o standard erro. void printStackTrace(PrintStream) - imprime para um stream especificado (por exemplo System.out). printStackTrace() mostra a sequência dos métodos invocados que levaram ao ponto onde a excepção foi lançada. Método da classe Object: getClasse() - retorna um objecto pertencente à classe Class representando a classe desse objecto excepção. A este objecto retornado pode aplicar-se o método getName(), que retorna o nome da classe. DEI - ISEP Fernando Mouta 90 Programação Orientada por Objectos Criação de Excepções Podemos criar as nossas próprias excepções para denotar algum erro especial. Para criar uma classe de excepções é necessário herdar de um tipo existente de excepções. Mostramos, em seguida, um exemplo no qual o método main() chama o método f(), o qual chama o método divide(). O método divide() pode lançar uma excepção. Apresentamos em seguida várias várias situações. Consideremos em todas essas situações, difinida a seguinte classe Excepcao1: class Excepcao1 extends Exception { public Excepcao1() { } public Excepcao1(String msg) { super(msg); } } 1. Sem tratamento das excepções. O programa dá erro de execução. public class Teste0 { public static int divide(int a,int b) { return a/b; } public static int f(int a,int b) { return divide(a, b); } public static void main(String args []) { System.out.println(f(5, 2)); System.out.println(f(3, 0)); System.out.println(f(4, 1)); } } Fernando Mouta DEI - ISEP Programação Orientada por Objectos 91 2. O método divide() pode lançar uma excepção, mas não é capturada nem declarada. Dá erro de compilação. public class Teste { public static int divide(int a,int b) { if (b==0) throw new Excepcao1("Divisao por zero!"); return a/b; } public static int f(int a,int b) { return divide(a, b); } public static void main(String args []) { System.out.println(f(5, 2)); System.out.println(f(3, 0)); System.out.println(f(4, 1)); } } error J0122: Exception 'Excepcao1' not caught or declared by 'int Teste.divide(int a, int b)' 3. O método divide() declara a excepção, mas o método f() não a captura nem a declara. Dá erro de compilação. public class Teste { public static int divide(int a,int b) throws Excepcao1 { if (b==0) throw new Excepcao1("Divisao por zero!"); return a/b; } public static int f(int a,int b) { return divide(a, b); } public static void main(String args []) { System.out.println(f(5, 2)); System.out.println(f(3, 0)); System.out.println(f(4, 1)); } } error J0122: Exception 'Excepcao1' not caught or declared by 'int Teste.f(int a, int b)' DEI - ISEP Fernando Mouta 92 Programação Orientada por Objectos 4. O método divide() e o método f() declaram a excepção, mas o método main() não a captura nem a declara. Dá erro de compilação. public class Teste { public static int divide(int a,int b) throws Excepcao1 { if (b==0) throw new Excepcao1("Divisao por zero!"); return a/b; } public static int f(int a,int b) throws Excepcao1 { return divide(a, b); } public static void main(String args []) { System.out.println(f(5, 2)); System.out.println(f(3, 0)); System.out.println(f(4, 1)); } } error J0122: Exception 'Excepcao1' not caught or declared by 'void Teste.main(String[] args)' 5. Os métodos divide(), f() e main() declaram a excepção. O programa compila mas dá erro de execução. public class Teste { public static int divide(int a,int b) throws Excepcao1 { if (b==0) throw new Excepcao1("Divisao por zero!"); return a/b; } public static int f(int a,int b) throws Excepcao1 { return divide(a, b); } public static void main(String args []) throws Excepcao1 { System.out.println(f(5, 2)); System.out.println(f(3, 0)); System.out.println(f(4, 1)); } } Fernando Mouta DEI - ISEP Programação Orientada por Objectos 93 6. A excepção é capturada no método divide(). O método main() é completado. public class Teste { public static int divide(int a,int b) { try { if (b==0) throw new Excepcao1("Divisao por zero!"); return a/b; } catch (Exception e) { e.printStackTrace(); System.out.println("Resultado nao valido:"); return 0; } } public static int f(int a,int b) { return divide(a, b); } public static void main(String args []) { System.out.println(f(5, 2)); System.out.println(f(3, 0)); System.out.println(f(4, 1)); } } 7. A excepção é capturada no método f(). O método main() é completado. public class Teste { public static int divide(int a,int b) throws Excepcao1 { if (b==0) throw new Excepcao1("Divisao por zero!"); return a/b; } DEI - ISEP Fernando Mouta 94 Programação Orientada por Objectos public static int f(int a,int b) { try { return divide(a, b); } catch (Exception e) { e.printStackTrace(); System.out.println("Resultado nao valido:"); return 0; } } public static void main(String args []) { System.out.println(f(5, 2)); System.out.println(f(3, 0)); System.out.println(f(4, 1)); } } 8. A excepção é capturada no método main(). O método main() não é completado, porque o lançamento da excepção causado pela invocação da 2ª instrução do método main() causa o abandono do código do bloco try. public class Teste { public static int divide(int a,int b) throws Excepcao1 { if (b==0) throw new Excepcao1("Divisao por zero!"); return a/b; } public static int f(int a,int b) throws Excepcao1 { return divide(a, b); } Fernando Mouta DEI - ISEP Programação Orientada por Objectos 95 public static void main(String args []) { try { System.out.println(f(5, 2)); System.out.println(f(3, 0)); System.out.println(f(4, 1)); } catch (Exception e) { e.printStackTrace(); System.out.println("Resultado nao valido:"); // return 0; } } } Excepções em subclasses Quando se reescreve um método só se podem lançar excepções que foram especificadas no método da classe base. Deste modo código que funciona com a classe base também funcionará com qualquer objecto derivado da classe base incluindo excepções. Cláusula finally Por vezes há a necessidade de executar algum código, quer ocorra ou não uma excepção num bloco try, para colocar qualquer coisa no seu estado inicial, tal como um ficheiro aberto ou uma ligação a outro computador. Isso consegue-se usando uma cláusula finally no fim do tratamento das excepções. try { // . . . região guardada } catch ( Excepção1 e1 ) { ... } catch ( Excepção2 e2 ) { ... } finally { ... } DEI - ISEP Fernando Mouta 96 Programação Orientada por Objectos Exemplo: public class Teste1 { static int count = 0; public static void main (String args []) { while (true) { try { if (count++ == 0) throw new Exception(); System.out.println("Excepcao lancada"); } catch (Exception e) { System.out.println("Execepcao capturada"); } finally { System.out.println("Clausula finally"); if (count == 2) break; } } } } Cláusula catch que captura uma excepção lançada Na determinação da cláusula catch que captura uma excepção lançada, o mecanismo de tratamento das excepções procura a primeira cláusula catch para a qual o tipo de excepção é o tipo ou subtipo do argumento da cláusula catch. class A extends Exception { } class B extends A { } public class Teste2 { public static void main(String args []) { try { throw new B(); } catch (A a) { System.out.println("Capturada excepcao A"); } } } Fernando Mouta DEI - ISEP Programação Orientada por Objectos 97 A excepção B é capturada pela cláusula catch(A a). O código seguinte dará erro de compilação porque a cláusula catch (B b) nunca será atingida: class A extends Exception { } class B extends A { } public class Teste { public static void main(String args []) { try { throw new B(); } catch (A a) { System.out.println(“Capturada excepção A”); } catch (B b) { System.out.println(“Capturada excepção B”); } } } Mensagem de erro produzida: error J0102: Handler for 'B' hidden by earlier handler for 'A' Relançamento de uma Excepção (Rethrowing an exception) Para voltar a lançar uma excepção que foi capturada executa-se throw referência. Uma excepção depois de capturada (por uma cláusula catch) pode voltar a ser relançada para ser tratada num contexto mais alto. catch (Exception e) { System.out.println(“Excepção:”); e.printStackTrace(); throw e; } DEI - ISEP Fernando Mouta 98 Programação Orientada por Objectos Exemplo: public class RelancamentoExcepcoes { public static void f() throws Exception { System.out.println("No metodo f()"); throw new Exception("Excepcao lancada em f()"); } public static void g() throws Throwable { try { f(); } catch (Exception e) { System.out.println("No metodo g()"); e. printStackTrace(); throw e; } } public static void main(String args []) throws Throwable { try { g(); } catch (Exception e) { System.out.println("Capturada no main, e.printStackTrace()"); e.printStackTrace(); } System.in.read(); } } Fernando Mouta DEI - ISEP Programação Orientada por Objectos 99 Excepções RunTimeException Java realiza verificações, em tempo de execução, de excepções da classe RuntimeException. Excepções deste tipo são automaticamente lançadas e não é necessário incluí-las nas especificações das excepções. Todos os tipos de potenciais excepções têm de ser incluídos na declaração de um método (lista com a especificação das excepções) excepto excepções do tipo RuntimeException. Excepções do tipo RuntimeException representam erros de programação. Estes erros tais como NullPointException ou ArrayIndexOutOfBoundsException se não forem capturados são reportados na saída do programa auxiliando no processo de depuração (“debugging”). Excepções do tipo RuntimeException ou de classes que herdam deste tipo não necessitam de pertencer à lista de especificação de excepções na declaração de um método. Exemplo: public class RuntimeExcepcao { static void f() { throw new RuntimeException(“Do metodo f()”); } public static void main(String args()) { f(); } } Saída produzida: DEI - ISEP Fernando Mouta 100 Programação Orientada por Objectos Se uma excepção do tipo RuntimeException é lançada e não é apanhada, na saída do programa printStackTrace() é chamado para essa excepção. Exemplos Exemplo 1: Programa que efectua 3 leituras de um número inteiro. Se a entrada não pode ser convertida num número, lança uma excepção que é capturada e imprime a mensagem “Errado.” import java.io.*; class Numero1{ public static String lerString(String msg) throws java.io.IOException { DataInputStream din = new DataInputStream(System.in); System.out.print(msg); return din.readLine(); } public static void main(String args []) throws java.io.IOException { int n; for(int i=0; i<3; i++) { String s = lerString("Digite um numero: "); try { n = Integer.parseInt(s); System.out.println("Numero valido: " +n); } catch (NumberFormatException nfe) { //nfe.printStackTrace(); System.out.println("Errado."); } } System.out.println("Fim."); System.in.read(); } } Fernando Mouta DEI - ISEP Programação Orientada por Objectos 101 Exemplo 2: Programa que efectua a leitura de um número inteiro até a entrada poder ser convertida num número. import java.io.*; class Numero2{ public static String lerString(String msg) throws java.io.IOException { DataInputStream din = new DataInputStream(System.in); System.out.print(msg); return din.readLine(); } public static void main(String args []) throws java.io.IOException { boolean lido = false; int n; do { String s = lerString("Digite um numero: "); try { n = Integer.parseInt(s); lido = true; } catch (NumberFormatException nfe) { //nfe.printStackTrace(); System.out.println("Tente outra vez:"); } } while (!lido); System.out.println("Fim."); System.in.read(); } } DEI - ISEP Fernando Mouta 102 Programação Orientada por Objectos Ficheiros e Streams O armazenamento de dados em variáveis, arrays ou objectos é sempre temporário, porque os dados são perdidos quando as variáveis ou as referências aos objectos saem fora do âmbito de validade ou quando o programa termina. Os ficheiros são usados para retenção dos dados. Dados mantidos em ficheiros designam-se por dados persistentes. Os browsers não permitem aos applets ler ou escrever em ficheiros por razões de segurança. Por isso, programas de processamento de ficheiros são implementados como aplicações. Para realizar processamento de ficheiros em Java temos de importar o package java.io. Apresenta-se a seguir a hierarquia de classes de ficheiros e streams mais importantes: Object File InputStream FileInputStream FiterInputStream DataInputStream BufferedInputStream ObjectInputStream OutputStream FileOutputStream FiterOutputStream DataOutputStream BufferedOutputStream PrintStream ObjectOutputStream RandomAccessFile As entradas e saídas de dados em Java são baseados no uso de “streams”, que são sequências de bytes ou caracteres que fluem de uma fonte para um destino através de um dado caminho de comunicação. InputStream e OutputStream são classes abstractas que declaram métodos para realizar input e output. As classes derivadas destas reescrevem estes métodos. As classes FileInputStream e FileOutputStream destinam-se a criar objectos que abrem ficheiros para leitura ou para escrita, passando aos construtores uma string com o nome do ficheiro. Um ficheiro aberto para escrita, se já existe é inicializado (perdendo-se todo o seu conteúdo) e se não existe é criado. Fernando Mouta DEI - ISEP Programação Orientada por Objectos 103 Não há o conceito de abrir um ficheiro para escrita de texto ASCII ou de dados binários. Pode-se escrever qualquer coisa sem fazer distinção entre caracteres e outros dados. Interface DataInput e interface DataOutput O interface DataInput é implementado pela classe DataInputStream e pela classe RandomAccessFile, declarando métodos para ler tipos primitivos de dados de streams. O interface DataOutput é implementado pela classe DataOutputStream e pela classe RandomAccessFile, declarando métodos para escrever tipos primitivos de dados em streams. Os interfaces DataInput e DataOutput fornecem métodos que suportam o input/output independente da máquina. Escrever Dados num Ficheiro Para abrir um ficheiro para escrita, usa-se um FileOutputStream construído com um objecto String ou File para o nome do ficheiro. Para escrever de um modo formatado temos que construir um objecto DataOutputStream usando como argumento do construtor o objecto FileOutputStream. A classe DataOutputStream fornece a capacidade de escrever objectos String e tipos primitivos para uma stream de saída. Podemos escrever dados num ficheiro criando um objecto do tipo DataOutputStream passando como argumento ao seu construtor um objecto FileOutputStream representando o ficheiro. FileOutputStream fileOutput = new FileOutputStream(“info.dat”); DataOutputStream output = new DataOutputStream(fileOutput); Esta técnica designa-se por encadeamneto de objectos e é normalmente realizada apenas pela instrução: DataOutputStream output = new DataOutputStream( new FileOutputStream(“info.dat”)); Ler Dados de um Ficheiro Para abrir um ficheiro para leitura, usa-se um FileInputStream construído com um objecto String ou File para o nome do ficheiro. Para ler de um modo formatado temos que construir um objecto DataInputStream usando como argumento do construtor o objecto FileInputStream. DEI - ISEP Fernando Mouta 104 Programação Orientada por Objectos A classe DataInputStream fornece a capacidade de ler objectos String e tipos primitivos de uma stream de entrada. Podemos ler dados de um ficheiro criando um objecto do tipo DataInputStream passando como argumento ao seu construtor um objecto FileInputStream representando o ficheiro. FileInputStream fileInput = new File InputStream(“info.dat”); DataInputStream input = new DataInputStream(fileInput); De um modo idêntico à escrita em ficheiros este encadeamneto de objectos é normalmente realizada apenas pela instrução: DataInputStream input = new DataInputStream( new FileInputStream(“info.dat”)); Excepções Quando se cria um objecto FileOutputStream para abrir um ficheiro, o programa testa se a operação de abertura teve sucesso. Se a operação falha (por exemplo se não há espaço de disco disponível) gera-se uma excepção IOException que pode ser capturada pelo programa. Exemplo: try { DatOutputStream output = new DataOutputStream( new FileOutputStream(“info.dat”)); } catch (IOException e) { System.err.println(“Ficheiro não aberto\n” + e.toString()); System.exit(1); } Neste exemplo se a tentativa de abrir o ficheiro gera uma excepção IOException a mensagem de erro é mostrada e o programa termina. O argumento do método exit() é retornado ao ambiente do qual o programa foi invocado, normalmente o sistema operativo. O argumento 0 indica que o programa terminou normalmente e qualquer outro valor indica que o programa terminou devido a um erro. Este valor pode ser usado pelo ambiente que invocou o programa para reagir em conformidade. De um modo semelhante, quando se cria um objecto FileInputStream para abrir um ficheiro para leitura, o programa testa se a operação de abertura teve sucesso. Se a operação falha (ficheiro não existente, ou não permissão de leitura do ficheiro) gera-se uma excepção IOException que pode ser capturada pelo programa. Fernando Mouta DEI - ISEP Programação Orientada por Objectos 105 Exemplo: try { DataInputStream input = new DataInputStream( new FileInputStream(“info.dat”)); } catch (IOException e) { System.err.println(“Ficheiro não aberto\n” + e.toString()); System.exit(1); } Os dados devem ser lidos de ficheiros no mesmo formato no qual foram escritos no ficheiro. Os interfaces DataInput e DataOutput definem métodos que transmitem tipos primitivos de dados através de um stream. As classes DataInputStream e DataOutputStream fornecem uma implementação para cada interface. Os métodos de leitura e escrita existentes aos pares para cada tipo são: Leitura: boolean byte short int long float double char String Escrita: readBoolean() readByte() readShort() readInt() readLong() readFloat() readDouble() readChar() readUTF() void writeBoolean(boolean) void writeByte(byte) void writeShort(short) void writeInt(int) void writeLong(long) void writeFloat(float) void writeDouble(double) void writeChar(char) void writeUTF(String) Tipo: boolean byte short int long float double char String Os métodos readUTF() e writeUTF() leêm e escrevem no formato UTF. UTF (Unicode Transmission Format) é o formato de transmissão Unicode. É uma forma binária que compacta caracteres Unicode de 16 bits em bytes de 8 bits. De um modo semelhante, quando se cria um objecto FileInputStream para abrir um ficheiro para leitura, o programa testa se a operação de abertura teve sucesso. Se a operação falha (ficheiro não existente, ou não permissão de leitura do ficheiro) gera-se uma excepção IOException que pode ser capturada pelo programa. Exemplo: try { DataInputStream input = new DataInputStream( new FileInputStream(“info.dat”)); } catch (IOException e) { System.err.println(“Ficheiro não aberto\n” + e.toString()); System.exit(1); } DEI - ISEP Fernando Mouta 106 Programação Orientada por Objectos Para além destes métodos ainda referimos outro de leitura: String readLine() throws IOException – lê uma String até atingir \n, \r ou o par \r\n. A sequência fim de linha não é incluída na String. Retorna null se atinge o fim do input. E outro de escrita: void writeChars(String s) throws IOException – escreve uma string como uma sequência de char. Uma string escrita com o método writeChars() deve ser lida usando um ciclo com readChar. É necessário escrever primeiro o comprimento da string ou usar um separador para marcar o fim. Os métodos complementares, tais como writeDouble() e readDouble(), permitem recuperar a informação armazenada num ficheiro, mas para os métodos de leitura funcionarem correctamente deve-se conhecer a colocação exacta dos dados no ficheiro. Portanto ou os dados são armazenados no ficheiro num formato fixo ou informação extra deve ser armazenada no ficheiro que permita determinar onde e como os dados estão localizados (por exemplo precedendo cada item de dados por um par de bytes que informam o tipo e comprimento). O método close() fecha uma stream de entrada ou de saída e liberta os recursos associados com a stream. No caso de uma stream de saída qualquer dado escrito para a stream é armazenado antes de a stream ser desalocada. O método available() retorna o número de bytes que podem ser lidos sem bloqueamento, o que para um ficheiro significa até ao fim do ficheiro. Exemplos Exemplo 1: Programa que grava num ficheiro os primeiros 1000 números naturais e em seguida lê o mesmo ficheiro testando o fim de ficheiro com o método available(). import java.io.*; public class File1 { public static void main (String args []) throws java.io.IOException { int [] valores = new int[1000]; for (int i=0; i<1000; i++) valores[i]=i+1; Fernando Mouta DEI - ISEP Programação Orientada por Objectos 107 FileOutputStream fileOutput = new FileOutputStream("out.txt"); DataOutputStream output = new DataOutputStream(fileOutput); for (int i=0; i<valores.length; i++) output.writeInt(valores[i]); output.close(); FileInputStream fileInput = new FileInputStream("out.txt"); DataInputStream input = new DataInputStream(fileInput); int v; while (input.available() != 0) { v=input.readInt(); System.out.println(v); } input.close(); System.in.read(); } } Exemplo 2: Aplicação que grava no ficheiro “nomes.dat” um array de strings, usando o método writeChars() e um separador para marcar o fim de cada string. O tamanho do array é gravado no início para permitir na leitura criar um array do mesmo tamanho. Depois o programa abre o ficheiro para leitura, lê o seu conteúdo para outro array de strings que cria, e mostra essas strings. import java.io.*; public class DataIO { static char SEP = '|'; static String readChars(DataInputStream in, char separador) throws IOException { String s=""; char ch = in.readChar(); while (ch != separador) { s += ch; ch = in.readChar(); } return s; } public static void writeData(String[] s, String fich) throws IOException { FileOutputStream fout = new FileOutputStream(fich); DataOutputStream out = new DataOutputStream(fout); out.writeInt(s.length); for (int i=0; i< s.length; i++) { out.writeChars(s[i]); DEI - ISEP Fernando Mouta 108 Programação Orientada por Objectos out.writeChar(SEP); } out.close(); } public static String [] readData(String fich) throws IOException { FileInputStream fin = new FileInputStream(fich); DataInputStream in = new DataInputStream(fin); String [] s2 = new String[in.readInt()]; for (int i=0; i< s2.length; i++) s2[i] = readChars(in, SEP); in.close(); return s2; } public static void main (String args []) throws java.io.IOException { String [] nomes ={"Miguel", "Ana", "Carlos", "Joaquim"}; writeData(nomes, "nomes.txt"); String [] nomes2; nomes2 = readData("nomes.txt"); for (int i=0; i<nomes2.length; i++) System.out.println(nomes2[i]); System.in.read(); } } Exemplo 3: Aplicação que cria vários objectos da classe Conta, coloca-os num array e grava-os num ficheiro. Em seguida cria um novo vector de referências para objectos Conta e preenche-o com objectos lidos do mesmo ficheiro. Finalmente mostra o conteúdo do vector criado. A classe Conta permite construir uma conta com um dado número (num), primeiro nome (pNome), último nome (uNome), e saldo (saldo), e também gravar num dado stream o conteúdo de um objecto Conta (writeConta()) assim como ler de um stream o conteúdo de um objecto (readConta()) criando o respectivo objecto. import java.io.*; class Conta { private int num; private String pNome, uNome; private double saldo; public Conta(int num, String pNome, String uNome, double saldo) { this.num = num; this.pNome = pNome; Fernando Mouta DEI - ISEP Programação Orientada por Objectos 109 this.uNome = uNome; this.saldo = saldo; } public void writeConta(DataOutputStream out) throws java.io.IOException { out.writeInt(num); out.writeUTF(pNome); out.writeUTF(uNome); out.writeDouble(saldo); } public static Conta readConta(DataInputStream in) throws java.io.IOException { return new Conta( in.readInt(), in.readUTF(), in.readUTF(), in.readDouble() ); } public void print() { System.out.println(num + ": " + pNome + " " + uNome + " -> saldo = " + saldo); } } public class DataIO { static void writeData(Conta [] c, String ficheiro) throws java.io.IOException { DataOutputStream out = new DataOutputStream( new FileOutputStream( ficheiro ) ); out.writeInt(c.length); for (int i=0; i<c.length; i++) c[i].writeConta(out); out.close(); } static Conta [] readData (String ficheiro) throws java.io.IOException { DataInputStream in = new DataInputStream( new FileInputStream(ficheiro)); Conta [] c = new Conta[in.readInt()]; for (int i=0; i< c.length; i++) c[i] = Conta.readConta(in); in.close(); return c; } DEI - ISEP Fernando Mouta 110 Programação Orientada por Objectos public static void main (String args []) throws java.io.IOException { Conta vect[] = new Conta[3]; vect[0] = new Conta (1, "Carlos", "Miguel", 4234.21); vect[1] = new Conta (2, "Jorge", "Silva", 231.15); vect[2] = new Conta (3, "Manuel", "Santos", 8421.5); writeData(vect, "contas.dat"); Conta [] v = readData("contas.dat"); for (int i=0; i<v.length; i++) v[i].print(); System.in.read(); } } O método writeData da classe DataIO abre o ficheiro para escrita e escreve o tamanho do array. Depois escreve, objecto a objecto, o conteúdo do array. Finalmente fecha o ficheiro. O método readData da classe DataIO abre o ficheiro para leitura, lê o tamanho do array e cria um array de objectos. Depois, para cada elemento do array, invoca o método readData da classe Conta que retorna um objecto criado com o conteúdo lido do ficheiro. Finalmente fecha o ficheiro. RandomAccessFile RandomAccessFile é uma classe que descende directamente da classe Object, mas que implementa os interfaces DataInput e DataOutput. Esta classe fornece a capacidade ler ou escrever directamente em localizações específicas de um ficheiro (movendo o apontador do ficheiro para uma posição arbitrária). Este acesso aleatório é suportado pelos seguintes métodos: getFilePointer() - retorna a localização corrente do apontador do ficheiro; seek() - move o apontador do ficheiro para uma nova localização; length() - retorna o tamanho do ficheiro em bytes. O construtor desta classe necessita de dois parâmetros do tipo String: o 1.º com o nome do ficheiro e o 2.º indicando o modo de abertura do ficheiro - só para leitura (“r”) ou para leitura e escrita (“rw”). O acesso só para leitura evita um ficheiro de ser inadvertidamente modificado. import java.io.*; class File1 { public static void main (String args []) throws java.io.IOException { int [] valores = new int[10]; for (int i=0; i<1000; i++) valores[i]=i+1; int v; // escrita dos dados RandomAccessFile fo = new RandomAccessFile("out.txt", "rw"); for (int i=0; i<valores.length; i++) fo.writeInt(valores[i]); // leitura aleatória dos dados fo.close(); Fernando Mouta DEI - ISEP Programação Orientada por Objectos 111 RandomAccessFile fi = new RandomAccessFile("out.txt", "r"); // Exemplo da leitura dos dados armazenados de 8 em 8 bytes for (int i=0; i<fi.length(); i=i+8) { fi.seek(i); v=fi.readInt(); System.out.println(v); } fi.close(); System.in.read(); } } /* Outro exemplo com ficheiro de acesso aleatório */ import java.io.*; class File1 { public static int lerInteiro(String s) throws java.io.IOException { System.out.print(s); BufferedReader d = new BufferedReader( new InputStreamReader(System.in)); int x = Integer.parseInt(d.readLine()); return x; } public static void main (String args []) throws java.io.IOException { int [] valores = new int[20]; for (int i=0; i<20; i++) valores[i]=i+1; RandomAccessFile fo = new RandomAccessFile("out.txt", "rw"); // escrita dos dados for (int i=0; i<valores.length; i++) fo.writeInt(valores[i]); fo.close(); // leitura e escrita aleatoria dos dados RandomAccessFile fi = new RandomAccessFile("out.txt", "r"); int i=lerInteiro("Escreva uma posicao do ficheiro: "); int v; System.out.println( "Leitura e escrita aleatoria dos dados (termine com a posicao -1)"); fi = new RandomAccessFile("out.txt", "rw"); i=lerInteiro("Escreva uma posicao do ficheiro: "); while (i!=-1) { fi.seek(i); v=fi.readInt(); System.out.println("Valor existente: " + v); int j= lerInteiro("Valor a reescrever nessa posicao : "); fi.seek(i); fi.writeInt(j); i=lerInteiro("Escreva uma posicao do ficheiro: "); } fi.close(); } } DEI - ISEP Fernando Mouta 112 Programação Orientada por Objectos /* Aplicação que cria vários objectos da classe Conta, coloca-os num array e grava-os num ficheiro. Em seguida cria um novo array de referências para objectos Conta, preenche-o com objectos lidos do mesmo ficheiro e mostra o conteúdo do vector criado. Ainda é criado mais um objecto da classe Conta que é adicionado ao ficheiro. No fim todo o conteúdo do ficheiro é listado. */ import java.io.*; class Conta { private int num; private String pNome, uNome; private double saldo; public Conta( int num, String pNome, String uNome, double saldo) { this.num = num; this.pNome = pNome; this.uNome = uNome; this.saldo = saldo; } public void writeConta(DataOutputStream out) throws java.io.IOException { out.writeInt(num); out.writeUTF(pNome); out.writeUTF(uNome); out.writeDouble(saldo); } public void writeConta(RandomAccessFile out) throws java.io.IOException { out.writeInt(num); out.writeUTF(pNome); out.writeUTF(uNome); out.writeDouble(saldo); } public static Conta readConta(DataInputStream in) throws java.io.IOException { return new Conta( in.readInt(), in.readUTF(), in.readUTF(), in.readDouble() ); } public void print() { System.out.println( num + ": " + pNome + " " + uNome +" -> saldo = " + saldo); } } Fernando Mouta DEI - ISEP Programação Orientada por Objectos 113 class DataIO { static void writeData(Conta [] c, String ficheiro) throws java.io.IOException { DataOutputStream out = new DataOutputStream( new FileOutputStream( ficheiro ) ); out.writeInt(c.length); for (int i=0; i<c.length; i++) c[i].writeConta(out); out.close(); } static Conta [] readData (String ficheiro) throws java.io.IOException { DataInputStream in = new DataInputStream(new FileInputStream(ficheiro)); Conta [] c = new Conta[in.readInt()]; for (int i=0; i< c.length; i++) c[i] = Conta.readConta(in); in.close(); return c; } public static void main (String args []) throws java.io.IOException { Conta vect[] = new Conta[3]; vect[0] = new Conta (1, "Carlos", "Miguel", 4234.21); vect[1] = new Conta (2, "Jorge", "Silva", 231.15); vect[2] = new Conta (3, "Manuel", "Santos", 8421.5); writeData(vect, "contas.dat"); Conta [] v1 = readData("contas.dat"); for (int i=0; i<v1.length; i++) v1[i].print(); // append de uma nova Conta Conta nova = new Conta (4, "Outra", "Conta", 0.5); RandomAccessFile f = new RandomAccessFile("contas.dat", "rw"); int quant = f.readInt() + 1; f.seek(0); f.writeInt(quant); f.seek(f.length()); nova.writeConta(f); f.close(); Conta [] v2 = readData("contas.dat"); for (int i=0; i<v2.length; i++) v2[i].print(); System.in.read(); } } DEI - ISEP Fernando Mouta 114 Programação Orientada por Objectos Serialização Serialização é o processo de converter objectos para um formato adequado para entrada ou saída de stream. Des-serialização é o processo de voltar a converter um objecto serializado numa instância de um objecto. Para que um objecto possa ser serializado tem de implementar o interface “Serializable”. O mecanismo de des-serialização para objectos restaura o conteúdo de cada campo com o valor e tipo que tinha quando foi escrito. Referências a outros objectos faz com que esses objectos sejam lidos do stream. Grafos de objectos são restaurados correctamente usando um mecanismo de partilha de referências. Novos objectos são sempre alocados quando des-serializados, o que evita que objectos existentes sejam reescritos. Os interfaces DataInput e DataOutput fornecem métodos que suportam o input/output independente da máquina. Os interfaces ObjectInput e ObjectOutput extendem os interfaces DataInput e DataOutput para trabalhar com objectos. Input/Output de Objectos As classes ObjectOutputStream e ObjectInputStream permitem escrever e ler de streams, objectos e tipos primitivos de dados. Estas classes implementam os interfaces ObjectOutput e ObjectInput. Dos métodos especificados por ObjectOutput, o método writeObject() é o mais importante – escreve objectos que implementem o interface Serializable para um stream. O interface ObjectInput declara o método readObject() para ler os objectos escritos para um stream pelo método writeObject(). Quando um objecto é escrito na forma serializado, juntamente com o objecto é armazenada informação que identifica a classe Java a partir da qual o conteúdo do objecto foi gravado, o que permite restaurar o objecto como uma nova instância dessa classe. Quando um objecto é serializado, todos os objectos não estáticos atingíveis a partir desse objecto são também armazenados com esse objecto. O interface Serializable é usado para identificar objectos que podem ser escritos para um stream. Este interface não define quaisquer constantes ou métodos. Fernando Mouta DEI - ISEP Programação Orientada por Objectos 115 import java.io.*; class Conta implements Serializable { private int num; private String pNome, uNome; private double saldo; public Conta( int num, String pNome, String uNome, double saldo) { this.num = num; this.pNome = pNome; this.uNome = uNome; this.saldo = saldo; } public void print() { System.out.println( num + ": " + pNome + " " + uNome +" -> saldo = " + saldo); } } class DataIO { public static void main (String args []) throws java.io.IOException, java.lang.ClassNotFoundException { Conta vect[] = new Conta[3]; vect[0] = new Conta (1, "Carlos", "Miguel", 4234.21); vect[1] = new Conta (2, "Jorge", "Silva", 231.15); vect[2] = new Conta (3, "Manuel", "Santos", 8421.5); String fich = "contas.dat"; ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(fich)); out.writeInt(vect.length); for (int i=0; i<vect.length; i++) out.writeObject(vect[i]); out.close(); ObjectInputStream in = new ObjectInputStream( new FileInputStream(fich)); Conta [] v = new Conta[in.readInt()]; for (int i=0; i< v.length; i++) v[i] = (Conta) in.readObject(); in.close(); for (int i=0; i<v.length; i++) v[i].print(); System.in.read(); } } DEI - ISEP Fernando Mouta 116 Programação Orientada por Objectos /* Versão com a escrita e leitura de um único objecto: o array de objectos Conta */ class DataIO { public static void main (String args []) throws java.io.IOException, java.lang.ClassNotFoundException { Conta vect[] = new Conta[3]; vect[0] = new Conta (1, "Carlos", "Miguel", 4234.21); vect[1] = new Conta (2, "Jorge", "Silva", 231.15); vect[2] = new Conta (3, "Manuel", "Santos", 8421.5); String fich = "contas.dat"; ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(fich)); out.writeObject(vect); out.close(); ObjectInputStream in = new ObjectInputStream( new FileInputStream(fich)); Conta [] v = (Conta []) in.readObject(); in.close(); for (int i=0; i<v.length; i++) v[i].print(); System.in.read(); } } Fernando Mouta DEI - ISEP Programação Orientada por Objectos 117 Threads A estruturação de um programa em classes e a criação de objectos permitem dividir um programa em secções independentes. Por vezes, surge também a necessidade de separar um programa em subtarefas que possam correr independentemente. Cada subtarefa independente designa-se por “thread”. Um processo- é um programa autónomo em execução com o seu próprio espaço de endereçamento. Um sistema operativo multitarefa - é um sistema operativo capaz de correr mais que um processo de cada vez. Um thread - é um fluxo de controlo sequencial único dentro de um processo. Um só processo pode ter múltiplos threads em execução corrente. Usos de Multithreading Suponhamos que temos um botão “quit” que quando pressionado termina o programa. Pretendemos que o botão responda rapidamente quando pressionado. Para não termos que verificar o estado do botão regularmente em todas as partes do código que escrevemos, criámos um thread para fazer essa verificação e colocámo-lo a correr independentemente. Normalmente cria-se um thread para qualquer parte do programa ligada a um determinado evento ou recurso, que corre independentemente do programa principal. O tempo do CPU é repartido entre todos os threads, o que reduz a eficiência da computação, mas o melhoramento no projecto do programa e no balanceamento de recursos compensa. Programas com 1 único thread podem conseguir uma ilusão de múltiplos threads quer através de interrupções ou por “polling” (algumas actividades do programa intercaladas com outras). Mas, num programa, a mistura de 2 funções não relacionadas, resulta num código complexo e de difícil manutenção. Muitos problemas de software são melhor resolvidos usando múltiplos threads de controlo. Um thread de controlo pode actualizar o que é mostrado, outro responde a entradas do utilizador, etc. Criação de Threads Os threads existem definidos numa classe Thread da biblioteca standard Java. Para criar um thread, poder-se-ia criar um objecto thread: ( Thread t = new Thread(); ), e em seguida configurar o thread colocando a prioridade inicial e nome, e invocando o método start() que bifurca um thread de controlo com os dados do objecto thread, e retorna. DEI - ISEP Fernando Mouta 118 Programação Orientada por Objectos Depois a máquina virtual Java (interpretador) invocaria o método run() do thread, tornando o thread activo até que esse método run() retorne, altura em que o thread termina. Mas a implementação do método run() da classe Thread não faz nada. Para ter um thread que faça qualquer coisa temos que reescrever o método run() e para isso é necessário criar uma subclasse de Thread. Outro processo para criar um thread consiste em criar um objecto que implemente o interface Runnable (definindo o método run()) e passando esse objecto Runnable ao construtor da classe Thread. Classe Thread public class Thread extends Object implements Runnable Construtores: public Thread() public Thread(String nome) public Thread(Runnable obj) public Thread(Runnable obj, String nome) Métodos de classe: public static boolean interrupted(); public static void sleep(long milis) throws InterruptedException; Métodos instância: public synchronized void start(); public void run(); public final void suspend(); public final void resume(); public final void stop(); start() inicia a execução de um thread, stop() pára essa execução, suspend() pára o thread temporariamente, resume() retoma a execução do thread, sleep(t) pára o thread durante uma quantidade especificada de tempo em milisegundos. O método run() do thread é o corpo do thread. Começa a executar quando o método start() do objecto thread é chamado. O thread corre até que o método run() retorne ou o método stop() seja invocado. Fernando Mouta DEI - ISEP Programação Orientada por Objectos 119 Exemplos A maneira mais simples de criar um thread é herdar da classe Thread a qual possui os métodos necessários para criar e correr threads. O método mais importante é run(), o qual se deve reescrever com o código que será executado simultaneamente com os outros threads no programa. Exemplo 1: Programa cria 2 threads que imprimem as palavras “sim” e “nao” com cadências diferentes. public class T extends Thread { private String palavra; private int temp; private int cont=0; public T( String p, int t, int c) { palavra = p; temp = t; cont = c; } public void run() { int i = 0; try { while ( i++ <= cont ) { System.out.println(palavra); sleep(temp); } } catch (InterruptedException e) { return; } } public static void main(String args []) throws java.io.IOException { T t = new T("sim", 20, 10); t.start(); t = new T("nao", 100, 5); t.start(); System.in.read(); } } DEI - ISEP Fernando Mouta 120 Programação Orientada por Objectos O método sleep(t) da classe Thread causa uma paragem durante t milisegundos. Este método pode lançar uma excepção do tipo InterruptedException se interrompido. Mas o método T.run() não pode lançar excepções porque reescreve o método Thread.run() e este não lança qualquer excepção. Assim é necessário capturar a excepção que sleep() pode lançar dentro do método T.run(). O método T.main() cria 2 objectos do tipo T (threads) e invoca o método start() para cada objecto. Exemplo 2: Programa cria 3 threads. Cada thread imprime o seu número seguido do valor de um contador que decrementa de 5 até 1. public class A extends Thread { private int cont = 5; private int id; private static int ultId=0; public A() { id = ++ultId; System.out.println("Criado thread n. " + id); } public void run() { while (true) { System.out.println( "Thread n. " + id + "(" + cont + ")"); if (--cont == 0) return; } } Fernando Mouta DEI - ISEP Programação Orientada por Objectos 121 public static void main (String args []) throws java.io.IOException { for (int i=0; i<3; i++) // A a = new A(); // a.start(); new A().start(); System.out.println("Todos os threads iniciados."); System.in.read(); } } Usando Runnable O interface Runnable abstrai o conceito de tudo o que executa código enquanto activo. O interface Runnable declara 1 único método: public void run() A classe Thread implementa o interface Runnable. Thread pode ser estendida para criar threads com execuções específicas, mas esta aproximação pode ser difícil de usar, porque Java só permite herança simples. Se uma classe é estendida (como subclasse de Thread) já não pode ser estendida como subclasse de outra classe, mesmo que se precise. Implementando Runnable é mais simples em muitos casos. Pode-se executar um objecto Runnable num thread próprio passando-o ao construtor Thread. Se um objecto thread é construído com um objecto Runnable, a implementação de Thread.run() invocará o método run() do objecto Runnable. Vejamos uma versão Runnable do 1.º exemplo apresentado. public class RunT implements Runnable { private String palavra; DEI - ISEP Fernando Mouta 122 Programação Orientada por Objectos private int temp; private int cont=0; public RunT( String p, int t, int c) { palavra = p; temp = t; cont = c; } public void run() { int i = 0; try { while ( i++ <= cont ) { System.out.println(palavra); Thread.sleep(temp); } } catch (InterruptedException e) { return; } } public static void main(String args []) throws java.io.IOException { Runnable sim = new RunT("sim", 20, 10); Runnable nao = new RunT("nao", 100, 5); Thread t = new Thread(sim); t.start(); t = new Thread(nao); t.start(); System.in.read(); } } Fernando Mouta DEI - ISEP Programação Orientada por Objectos 123 Este programa é muito idêntico ao 1.º exemplo apresentado, diferindo apenas na superclasse (Runnable versus Thread) e no método main(). A implementação do método run() é a mesma. Nesta implementação criam-se 2 objectos Runnable (RunT) e em seguida 2 objectos Thread, passando cada objecto runnable como argumento ao construtor do objecto Thread. Sincronização Um thread pode realizar uma tarefa independentemente de outro thread. Também dois threads podem partilhar o acesso ao mesmo objecto. Quando dois threads podem modificar o mesmo dado, se ambos os threads realizam a sequência ler-modificar-escrever de um modo intercalado podem corromper esse dado. Como exemplo suponhamos que 2 threads actualizam o saldo de uma conta (objecto c) após um depósito, aproximadamente ao mesmo tempo. thread1 thread2 s1 = c.getSaldo(); s2 = c.getSaldo(); s1 += deposito; s2 += deposito; c.setSaldo(s1); c.setSaldo(s2); Se as sequências ler-modificar-escrever são realizadas intercaladamente só o último depósito afecta o saldo perdendo-se a primeira modificação. Um modo de resolver este problema é não permitir o acesso ao objecto enquanto estiver a ser usado. O 2º thread terá de esperar até o objecto ser liberto pelo 1º thread. Sempre que 2 threads necessitam de usar o mesmo objecto, há a possibilidade de operações intercaladas corromperem os dados. Para sincronizar os acessos de vários threads a um dado objecto de modo a não corromperem os dados, um thread coloca um “lock” (fecho) no objecto, e quando outro thread tenta aceder ao objecto, fica bloqueado até que o primeiro thread termine. DEI - ISEP Fernando Mouta 124 Programação Orientada por Objectos Mas nem todos os métodos de um thread podem corromper os dados (por exemplo métodos de leitura). Por isso só os métodos de um thread que possam interferir com outros são declarados “synchronized”. Se um thread invoca um método sincronizado num objecto, esse objecto fica no estado “locked” (fechado) por esse thread. Se outro thread invocar um método sincronizado para esse mesmo objecto, bloqueará até o objecto ser liberto do “lock”. A invocação de um método não sincronizado prossegue normalmente sem ser afectado por qualquer “lock”. Cada estado “locked” de um objecto existe por thread, por isso a invocação de um método sincronizado num objecto de dentro de outro método sincronizado que colocou esse objecto no estado “locked” prossegue sem bloqueamento, ficando o objecto liberto de “lock” quando o método sincronizado mais externo retorna. Threads sincronizados são mutuamente exclusivos no tempo. O exemplo da actualização do saldo da mesma conta após um depósito por mais que um thread poderia ser feito a partir da seguinte classe Conta: class Conta { private double saldo; public Conta( double depositoInicial ) { saldo = depositoInicial; } public synchronized double getSaldo() { return saldo; } public synchronized void deposito( double deposito ) { saldo += deposito; } } Se o valor de um campo pode mudar, não deve ser permitido ler esse valor ao mesmo tempo que outro thread o escreve, pois a leitura pode retornar um valor inválido. O acesso ao campo deve ser sincronizado e por isso o campo não deve poder ser acedido directamente fora da classe (campo público ou protected). O campo deve ser declarado private e deve existir um método de acesso sincronizado ao valor desse campo. Instruções sincronizadas Uma instrução sincronizada permite executar código sincronizado que coloca um objecto no estado “locked” durante a execução desse código sem necessidade de invocar um método sincronizado para esse objecto. synchronized (expr) { instruções } Fernando Mouta DEI - ISEP Programação Orientada por Objectos 125 expr deve ser colocada dentro de parênteses e deve produzir uma referência para um objecto. Em seguida mostra-se um exemplo que troca cada elemento negativo de um array pelo seu valor absoluto. public static void valorAbs(int [] a) { synchronized (a) { for (int i=0; i<a.length; i++) if (a[i] < 0) a[i] = -a[i]; } } Para usar uma classe em que nenhum método é sincronizado num ambiente de múltiplos threads pode-se criar uma classe estendida (subclasse) e reescrever os métodos que se pretendem que sejam sincronizados declarando-os sincronizados com chamadas aos respectivos métodos da superclasse através da palavra-chave super. DEI - ISEP Fernando Mouta