As exceções são usadas na linguagem Java para assinalar que algo não correu como esperado.
São classes que herdam de java.lang.Exception e cujos objetos podem ser atirados (throw) e apanhados (caught).
As exceções que herdam de java.lang.RuntimeException (RTE) são chamadas exceções não verificadas (unchecked exceptions). Neste caso, o compilador não obriga o programador a declarar se apanha ou se atira. Por este motivo, qualquer linha de código pode atirar uma exceção destas. A mais conhecida é a NullPointerException (NPE).
As exceções que herdam de java.lang.Exception são chamadas exceções verificadas (checked exceptions), no sentido, em que a sua utilização é verificada pelo compilador. Nestes casos é preciso explicitar se se apanha a exceção (catch) ou se se lança (throws). Normalmente, se não se vai tentar recuperar a exceção, pode simplesmente dizer que se atira. É preferível atirar do que fazer um falso tratamento de exceção.
Podemos ter as seguintes abordagens em relação às exceções:
A abordagem 'apanhar-e-ignorar' é claramente errada porque perde informação e torna muito mais difícil diagnosticar e resolver problemas.
Os outputs seguintes foram produzidos por uma aplicação que usa sockets para comunicação, e ilustram diversas situações.
Na situação abaixo, o servidor tenta criar o socket com um porto fora do intervalo [0;65535].
java.lang.reflect.InvocationTargetException at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:497) at org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:293) at java.lang.Thread.run(Thread.java:745) Caused by: java.lang.IllegalArgumentException: Port value out of range: 65536 at java.net.ServerSocket.<init>(ServerSocket.java:232) at java.net.ServerSocket.<init>(ServerSocket.java:128) at example.SocketServer.main(SocketServer.java:24) ... 6 more
Na situação abaixo, o cliente tenta ligar ao servidor por meio dum porto incorreto e não consegue.
java.lang.reflect.InvocationTargetException at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:497) at org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:293) at java.lang.Thread.run(Thread.java:745) Caused by: java.net.ConnectException: Connection refused: connect at java.net.DualStackPlainSocketImpl.connect0(Native Method) at java.net.DualStackPlainSocketImpl.socketConnect(DualStackPlainSocketImpl.java:79) at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:345) at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206) at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188) at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:172) at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392) at java.net.Socket.connect(Socket.java:589) at java.net.Socket.connect(Socket.java:538) at java.net.Socket.<init>(Socket.java:434) at java.net.Socket.<init>(Socket.java:211) at example.SocketClient.main(SocketClient.java:32) ... 6 more
As linhas mais importantes dos outputs são as começadas por Caused by. É aí que se podem encontrar as mensagens das exceções. Dado que uma exceção pode ter outra exceção aninhada (cause) pode ser necessário consultar várias linhas para perceber o que causou a exceção de topo.
As linhas começadas por at indicam o contexto de execução. Observando estas linhas é possível ver o conteúdo da pilha de execução do programa, que diz que parte do código estava a chamar que outra parte.
// ANTI-padrão 'apanhar-e-ignorar!' // Ignorar a exceção sem dizer nada a ninguém. // Evitar! try { doSomething(); } catch(Exception e) { } ... // ANTI-padrão 'apanhar-imprimir-e-ignorar!' // Imprimir o stack trace, não resolve nada. // O programa vai "rebentar" mais à frente, onde será mais difícil perceber porquê. // Evitar também! try { doSomething(); } catch(Exception e) { e.printStackTrace(); }
Ignorar a exceção torna muito mais difícil detetar e corrigir erros no código!
Vamos então ilustrar alguns bons exemplos:
// padrão 'deixar-passar' // Se a exceção não vai ser tratada, mais vale lançá-la (throws) public static void main(String[] args) throws Exception { doSomething(); } ... // padrão 'apanhar-imprimir-e-atirar' // Registar onde foi apanhada a exceção, mas voltar a atirá-la para que seja tratada depois try { doSomething(); } catch(MyException e) { System.err.println("Caught exception when doing something: " + e); System.err.println("Rethrowing"): throw e; } // padrão 'apanhar-embrulhar-e-atirar' // Apanhar exceção da camada inferior // Envolver com mais contexto (novo tipo, mensagem de erro melhor) // Atirar try { doSomething(); } catch(MyLowerLevelException e) { System.err.println("Caught exception when doing something: " + e); System.err.println("Wrapping and throwing, adding meaningful message") throw new MyHigherLevelException("Failed to do something.", e); }
Em resumo, um bom tratamento de exceções é muito importante em Sistemas Distribuídos.
As melhores estratégias a seguir são: 'deixar-passar', 'apanhar-*-atirar' e 'apanhar-e-recuperar' quando possível.
© Docentes de Sistemas Distribuídos,
Dep. Eng. Informática,
Técnico Lisboa