Por que o Java não permite subclasses genéricas de Throwable?

146

De acordo com o Java Language Sepecification , 3ª edição:

É um erro em tempo de compilação se uma classe genérica for uma subclasse direta ou indireta de Throwable.

Desejo entender por que essa decisão foi tomada. O que há de errado com exceções genéricas?

(Até onde eu sei, os genéricos são simplesmente açúcar sintático em tempo de compilação e serão traduzidos de Objectqualquer maneira nos .classarquivos, declarar com eficiência uma classe genérica é como se tudo nela fosse um Object. Por favor, corrija-me se estiver errado .)

Hosam Aly
fonte
1
Os argumentos de tipo genérico são substituídos pelo limite superior, que por padrão é Object. Se você tem algo parecido com List <? estende A>, então A é usado nos arquivos de classe.
Torsten Marek
Obrigado @Torsten. Eu não pensei nesse caso antes.
395 Hosam Aly
2
É uma boa pergunta para entrevista, essa.
31410 skaffman
@TorstenMarek: Se alguém ligar myList.get(i), obviamente getainda retorna um Object. O compilador insere uma conversão Apara capturar algumas das restrições no tempo de execução? Caso contrário, o OP está certo de que, no final, se resume a Objects no tempo de execução. (O arquivo de classe certamente contém metadados sobre A, mas é apenas metadados AFAIK.)
Mihai Danila

Respostas:

155

Como mark disse, os tipos não são passíveis de devolução, o que é um problema no seguinte caso:

try {
   doSomeStuff();
} catch (SomeException<Integer> e) {
   // ignore that
} catch (SomeException<String> e) {
   crashAndBurn()
}

Ambos SomeException<Integer>e SomeException<String>são apagados para o mesmo tipo, não há como a JVM distinguir as instâncias de exceção e, portanto, não há como saber qual catchbloco deve ser executado.

Torsten Marek
fonte
3
mas o que significa "reifiable"?
aberrant80
61
Portanto, a regra não deve ser "tipos genéricos não podem subclassificar Throwable", mas "cláusulas catch sempre devem usar tipos brutos".
19411 Archie
3
Eles poderiam simplesmente proibir o uso de dois blocos catch do mesmo tipo juntos. Para que somente o SomeExc <Integer> seja legal, somente o SomeExc <Integer> e o SomeExc <String> juntos seriam ilegais. Isso não causaria problemas, ou seria?
Viliam Búr 14/02
3
Oh, agora entendi. Minha solução causaria problemas com o RuntimeExceptions, que não precisam ser declarados. Portanto, se SomeExc for uma subclasse de RuntimeException, eu poderia lançar e capturar explicitamente SomeExc <Integer>, mas talvez alguma outra função esteja silenciosamente jogando SomeExc <String> e meu bloco de captura para SomeExc <Integer> acidentalmente capturaria isso também.
Viliam Búr 14/02
4
@ SuperJedi224 - Não. Faz direito - dada a restrição de que os genéricos precisavam ser compatíveis com versões anteriores.
Stephen C
14

Aqui está um exemplo simples de como usar a exceção:

class IntegerExceptionTest {
  public static void main(String[] args) {
    try {
      throw new IntegerException(42);
    } catch (IntegerException e) {
      assert e.getValue() == 42;
    }
  }
}

O corpo da instrução TRy lança a exceção com um determinado valor, que é capturado pela cláusula catch.

Por outro lado, a seguinte definição de uma nova exceção é proibida, pois cria um tipo parametrizado:

class ParametricException<T> extends Exception {  // compile-time error
  private final T value;
  public ParametricException(T value) { this.value = value; }
  public T getValue() { return value; }
}

Uma tentativa de compilar o que foi mencionado acima relata um erro:

% javac ParametricException.java
ParametricException.java:1: a generic class may not extend
java.lang.Throwable
class ParametricException<T> extends Exception {  // compile-time error
                                     ^
1 error

Essa restrição é sensata porque quase qualquer tentativa de capturar essa exceção deve falhar, porque o tipo não é reificável. Pode-se esperar que um uso típico da exceção seja algo como o seguinte:

class ParametricExceptionTest {
  public static void main(String[] args) {
    try {
      throw new ParametricException<Integer>(42);
    } catch (ParametricException<Integer> e) {  // compile-time error
      assert e.getValue()==42;
    }
  }
}

Isso não é permitido, porque o tipo na cláusula catch não é reificável. No momento da redação deste artigo, o compilador Sun reporta uma cascata de erros de sintaxe nesse caso:

% javac ParametricExceptionTest.java
ParametricExceptionTest.java:5: <identifier> expected
    } catch (ParametricException<Integer> e) {
                                ^
ParametricExceptionTest.java:8: ')' expected
  }
  ^
ParametricExceptionTest.java:9: '}' expected
}
 ^
3 errors

Como as exceções não podem ser paramétricas, a sintaxe é restrita, de modo que o tipo deve ser gravado como um identificador, sem o seguinte parâmetro.

IAdapter
fonte
2
O que você quer dizer quando diz "reifiable"? 'reifiable' não é uma palavra.
ForYourOwnGood
1
Eu não sabia que a palavra eu, mas uma rápida pesquisa no google me fez isto: java.sun.com/docs/books/jls/third_edition/html/...
Hosam Aly
13

É essencialmente porque foi projetado de uma maneira ruim.

Esse problema impede o design abstrato limpo, por exemplo,

public interface Repository<ID, E extends Entity<ID>> {

    E getById(ID id) throws EntityNotFoundException<E, ID>;
}

O fato de uma cláusula catch falhar para genéricos não é reificado não é desculpa para isso. O compilador pode simplesmente proibir tipos genéricos concretos que estendem Throwable ou desautorizar genéricos dentro de cláusulas catch.

Michele Sollecito
fonte
+1. minha resposta - stackoverflow.com/questions/30759692/…
ZhongYu 11/11/2015
1
A única maneira que eles poderiam ter projetado melhor foi tornando ~ 10 anos de código dos clientes incompatíveis. Essa foi uma decisão comercial viável. O design estava correto ... dado o contexto .
Stephen C
1
Então, como você capturará essa exceção? A única maneira que funcionaria é capturar o tipo bruto EntityNotFoundException. Mas isso tornaria os genéricos inúteis.
Frans
4

Os genéricos são verificados no momento da compilação quanto à correção do tipo. As informações de tipo genérico são removidas em um processo chamado apagamento de tipo . Por exemplo, List<Integer>será convertido para o tipo não genérico List.

Por causa do apagamento do tipo , os parâmetros do tipo não podem ser determinados em tempo de execução.

Vamos supor que você tenha permissão para estender Throwableassim:

public class GenericException<T> extends Throwable

Agora vamos considerar o seguinte código:

try {
    throw new GenericException<Integer>();
}
catch(GenericException<Integer> e) {
    System.err.println("Integer");
}
catch(GenericException<String> e) {
    System.err.println("String");
}

Devido ao apagamento do tipo , o tempo de execução não saberá qual bloco de captura executar.

Portanto, é um erro em tempo de compilação se uma classe genérica for uma subclasse direta ou indireta de Throwable.

Fonte: Problemas com apagamento de tipo

outdev
fonte
Obrigado. Esta é a mesma resposta que a fornecida por Torsten .
Hosam Aly
Não, não é. A resposta de Torsten não me ajudou, porque não explicava que tipo de apagamento / reificação é.
Good Night Nerd Pride
2

Eu esperaria que seja porque não há como garantir a parametrização. Considere o seguinte código:

try
{
    doSomethingThatCanThrow();
}
catch (MyException<Foo> e)
{
    // handle it
}

Como você observa, a parametrização é apenas açúcar sintático. No entanto, o compilador tenta garantir que a parametrização permaneça consistente em todas as referências a um objeto no escopo da compilação. No caso de uma exceção, o compilador não tem como garantir que MyException seja lançada apenas de um escopo que está processando.

kdgregory
fonte
Sim, mas por que não é sinalizado como "inseguro", como acontece com os lançamentos, por exemplo?
eljenso
Porque com uma conversão, você está dizendo ao compilador "Eu sei que esse caminho de execução produz o resultado esperado". Com uma exceção, você não pode dizer (para todas as exceções possíveis) "Eu sei onde isso foi lançado". Mas, como eu disse acima, é um palpite; Eu não estava lá.
Kdgregory # 01/02
"Eu sei que esse caminho de execução produz o resultado esperado." Você não sabe, você espera que sim. É por isso que os genéricos e os downcasts são estaticamente inseguros, mas, no entanto, são permitidos. Votei na resposta de Torsten, porque vejo o problema. Aqui eu não.
eljenso
Se você não sabe que um objeto é de um tipo específico, não deve convertê-lo. A idéia de um elenco é que você tem mais conhecimento que o compilador e está tornando esse conhecimento explicitamente parte do código.
Kdgregory
Sim, e aqui você também pode ter mais conhecimento que o compilador, já que deseja fazer uma conversão desmarcada de MyException para MyException <Foo>. Talvez você "saiba" que será um MyException <Foo>.
eljenso