Outros resumiram muito bem por que jogar cedo . Em vez disso, deixe-me concentrar no porquê pegar uma parte tardia , para a qual não vi uma explicação satisfatória para o meu gosto.
POR QUE EXCEÇÕES?
Parece haver uma grande confusão sobre por que as exceções existem em primeiro lugar. Deixe-me compartilhar o grande segredo aqui: o motivo das exceções e o tratamento das exceções são ... ABSTRACÇÃO .
Você já viu código assim:
static int divide(int dividend, int divisor) throws DivideByZeroException {
if (divisor == 0)
throw new DivideByZeroException(); // that's a checked exception indeed
return dividend / divisor;
}
static void doDivide() {
int a = readInt();
int b = readInt();
try {
int res = divide(a, b);
System.out.println(res);
} catch (DivideByZeroException e) {
// checked exception... I'm forced to handle it!
System.out.println("Nah, can't divide by zero. Try again.");
}
}
Não é assim que as exceções devem ser usadas. Código como o acima existe na vida real, mas são mais uma aberração e são realmente a exceção (trocadilho). A definição de divisão, por exemplo, mesmo em matemática pura, é condicional: é sempre o "código do chamador" que deve lidar com o caso excepcional de zero para restringir o domínio de entrada. É feio. É sempre uma dor para quem liga. Ainda assim, para tais situações, o padrão de verificação e execução é o caminho natural a seguir:
static int divide(int dividend, int divisor) {
// throws unchecked ArithmeticException for 0 divisor
return dividend / divisor;
}
static void doDivide() {
int a = readInt();
int b = readInt();
if (b != 0) {
int res = divide(a, b);
System.out.println(res);
} else {
System.out.println("Nah, can't divide by zero. Try again.");
}
}
Como alternativa, você pode executar um comando completo no estilo OOP desta maneira:
static class Division {
final int dividend;
final int divisor;
private Division(int dividend, int divisor) {
this.dividend = dividend;
this.divisor = divisor;
}
public boolean check() {
return divisor != 0;
}
public int eval() {
return dividend / divisor;
}
public static Division with(int dividend, int divisor) {
return new Division(dividend, divisor);
}
}
static void doDivide() {
int a = readInt();
int b = readInt();
Division d = Division.with(a, b);
if (d.check()) {
int res = d.eval();
System.out.println(res);
} else {
System.out.println("Nah, can't divide by zero. Try again.");
}
}
Como você vê, o código de chamada tem o ônus da pré-verificação, mas não realiza nenhuma manipulação de exceção depois. Se alguma ArithmeticException
vez surgir uma chamada divide
ou eval
, então é VOCÊ quem deve executar a exceção e corrigir seu código, porque você esqueceu a check()
. Pelas razões semelhantes, pegar um NullPointerException
quase sempre é a coisa errada a se fazer.
Agora, algumas pessoas dizem que querem ver casos excepcionais na assinatura do método / função, ou seja, estender explicitamente o domínio de saída . Eles são os que favorecem as exceções verificadas . Obviamente, alterar o domínio de saída deve forçar a adaptação de qualquer código de chamada direta, o que seria realmente alcançado com exceções verificadas. Mas você não precisa de exceções para isso! É por isso que você tem Nullable<T>
classes genéricas , classes de casos , tipos de dados algébricos e tipos de união . Algumas pessoas OO podem até preferir retornar null
para casos de erro simples como este:
static Integer divide(int dividend, int divisor) {
if (divisor == 0) return null;
return dividend / divisor;
}
static void doDivide() {
int a = readInt();
int b = readInt();
Integer res = divide(a, b);
if (res != null) {
System.out.println(res);
} else {
System.out.println("Nah, can't divide by zero. Try again.");
}
}
Tecnicamente, as exceções podem ser usadas para a finalidade como a acima, mas aqui está o ponto: as exceções não existem para esse uso . Exceções são pró abstração. Exceção são sobre indireção. As exceções permitem estender o domínio "resultado" sem violar os contratos diretos dos clientes e adiar o tratamento de erros para "outro lugar". Se o seu código gerar exceções que são tratadas em chamadores diretos do mesmo código, sem nenhuma camada de abstração no meio, você estará fazendo ERRADO
COMO PEGAR O TARDE?
Então aqui estamos nós. Argumentei meu caminho para mostrar que o uso de exceções nos cenários acima não é como as exceções devem ser usadas. Porém, existe um caso de uso genuíno, em que a abstração e a indireção oferecidas pelo tratamento de exceções são indispensáveis. Entender esse uso também ajudará a entender a recomendação de captura tardia .
Esse caso de uso é: Programação contra abstrações de recursos ...
Sim, a lógica de negócios deve ser programada em abstrações , não em implementações concretas. O código de "fiação" do COI de nível superior instancia as implementações concretas das abstrações de recursos e as transmite à lógica de negócios. Nada de novo aqui. Mas as implementações concretas dessas abstrações de recursos podem potencialmente lançar suas próprias exceções específicas de implementação , não podem?
Então, quem pode lidar com essas exceções específicas de implementação? É possível lidar com alguma exceção específica de recurso na lógica de negócios? Não, não é. A lógica de negócios é programada em abstrações, o que exclui o conhecimento desses detalhes de exceção específicos da implementação.
"Aha!", Você pode dizer: "mas é por isso que podemos subclassificar exceções e criar hierarquias de exceção" (confira Mr. Spring !). Deixe-me dizer, isso é uma falácia. Em primeiro lugar, todo livro razoável sobre OOP diz que a herança concreta é ruim, mas, de alguma forma, esse componente principal da JVM, tratamento de exceções, está intimamente ligado à herança concreta. Ironicamente, Joshua Bloch não poderia ter escrito seu livro Java efetivo antes de obter a experiência com uma JVM em funcionamento, poderia? É mais um livro de "lições aprendidas" para a próxima geração. Em segundo lugar, e mais importante, se você pegar uma exceção de alto nível, como vai lidar com isso?PatientNeedsImmediateAttentionException
: temos que lhe dar um comprimido ou amputar as pernas !? Que tal uma instrução switch sobre todas as subclasses possíveis? Lá se vai o seu polimorfismo, lá se vai a abstração. Você entendeu.
Então, quem pode lidar com as exceções específicas do recurso? Deve ser quem conhece as concreções! Aquele que instanciado o recurso! O código "fiação", é claro! Veja isso:
Lógica comercial codificada contra abstrações ... NENHUM MANUSEIO DE ERROS DE RECURSO DE CONCRETO!
static interface InputResource {
String fetchData();
}
static interface OutputResource {
void writeData(String data);
}
static void doMyBusiness(InputResource in, OutputResource out, int times) {
for (int i = 0; i < times; i++) {
System.out.println("fetching data");
String data = in.fetchData();
System.out.println("outputting data");
out.writeData(data);
}
}
Enquanto isso, em algum outro lugar, as implementações concretas ...
static class ConstantInputResource implements InputResource {
@Override
public String fetchData() {
return "Hello World!";
}
}
static class FailingInputResourceException extends RuntimeException {
public FailingInputResourceException(String message) {
super(message);
}
}
static class FailingInputResource implements InputResource {
@Override
public String fetchData() {
throw new FailingInputResourceException("I am a complete failure!");
}
}
static class StandardOutputResource implements OutputResource {
@Override
public void writeData(String data) {
System.out.println("DATA: " + data);
}
}
E finalmente o código de fiação ... Quem lida com exceções concretas de recursos? Quem sabe sobre eles!
static void start() {
InputResource in1 = new FailingInputResource();
InputResource in2 = new ConstantInputResource();
OutputResource out = new StandardOutputResource();
try {
ReusableBusinessLogicClass.doMyBusiness(in1, out, 3);
}
catch (FailingInputResourceException e)
{
System.out.println(e.getMessage());
System.out.println("retrying...");
ReusableBusinessLogicClass.doMyBusiness(in2, out, 3);
}
}
Agora tenha paciência comigo. O código acima é simplista. Você pode dizer que possui um contêiner de aplicativos / Web corporativo com vários escopos de recursos gerenciados por contêineres IOC e precisa de tentativas e reinicialização automáticas de sessão ou solicitar recursos de escopo, etc. A lógica de fiação nos escopos de nível inferior pode receber fábricas abstratas para crie recursos, portanto, não esteja ciente das implementações exatas. Somente escopos de nível superior saberiam realmente quais exceções esses recursos de nível inferior podem gerar. Agora espere!
Infelizmente, as exceções permitem apenas a indireção sobre a pilha de chamadas, e diferentes escopos com suas cardinalidades diferentes geralmente são executados em vários segmentos diferentes. Não há como se comunicar com isso, com exceções. Precisamos de algo mais poderoso aqui. Resposta: passagem de mensagem assíncrona . Capte todas as exceções na raiz do escopo de nível inferior. Não ignore nada, não deixe escapar nada. Isso fechará e descartará todos os recursos criados na pilha de chamadas do escopo atual. Em seguida, propague mensagens de erro para escopos mais altos, usando filas / canais de mensagens na rotina de tratamento de exceções, até atingir o nível em que as concreções são conhecidas. Esse é o cara que sabe como lidar com isso.
SUMMA SUMMARUM
Então, de acordo com minha interpretação, pegar tarde significa pegar exceções no lugar mais conveniente ONDE VOCÊ NÃO ESTÁ QUEBRANDO A ABSTRACÇÃO MAIS . Não pegue muito cedo! Capture exceções na camada em que você cria a exceção concreta, lançando instâncias das abstrações de recursos, a camada que conhece as concreções das abstrações. A camada "fiação".
HTH. Feliz codificação!