Uma característica peculiar da inferência de tipo de exceção em Java 8

85

Enquanto escrevia o código para outra resposta neste site, me deparei com esta peculiaridade:

static void testSneaky() {
  final Exception e = new Exception();
  sneakyThrow(e);    //no problems here
  nonSneakyThrow(e); //ERRROR: Unhandled exception: java.lang.Exception
}

@SuppressWarnings("unchecked")
static <T extends Throwable> void sneakyThrow(Throwable t) throws T {
  throw (T) t;
}

static <T extends Throwable> void nonSneakyThrow(T t) throws T {
  throw t;
}

Em primeiro lugar, estou bastante confuso por que a sneakyThrowchamada é OK para o compilador. Que tipo possível ele inferiu Tquando não há menção em qualquer lugar de um tipo de exceção não verificado?

Em segundo lugar, aceitando que isso funciona, por que então o compilador reclama na nonSneakyThrowchamada? Eles parecem muito parecidos.

Marko Topolnik
fonte

Respostas:

66

O T de sneakyThrowé inferido ser RuntimeException. Isso pode ser seguido da especificação de idioma na inferência de tipo ( http://docs.oracle.com/javase/specs/jls/se8/html/jls-18.html )

Em primeiro lugar, há uma observação na seção 18.1.3:

Um limite do formulário throws αé puramente informativo: ele direciona a resolução para otimizar a instanciação de α de forma que, se possível, não seja um tipo de exceção verificado.

Isso não afeta nada, mas nos aponta para a seção Resolução (18.4), que tem mais informações sobre os tipos de exceção inferidos com um caso especial:

... Caso contrário, se o conjunto de limite contém throws αi, e os limites superiores adequados de αi são, no máximo, Exception, Throwable, e Object, em seguida, Ti = RuntimeException.

Este caso se aplica a sneakyThrow- o único limite superior é Throwable, portanto, Tinfere-se RuntimeExceptionque está de acordo com as especificações, portanto, ele compila. O corpo do método é imaterial - a conversão não verificada é bem-sucedida em tempo de execução porque não acontece de fato, deixando um método que pode derrotar o sistema de exceção verificado em tempo de compilação.

nonSneakyThrownão compila, pois o método Ttem um limite inferior de Exception(ou seja, Tdeve ser um supertipo de Exception, ou Exceptionele mesmo), que é uma exceção verificada, devido ao tipo com o qual está sendo chamado, de modo que Té inferido como Exception.

theecoop
fonte
1
@Maksym Você deve estar falando sério sneakyThrow. Os regulamentos especiais sobre a inferência de throws Tformulários não existiam na especificação do Java 7.
Marko Topolnik
1
Pequenos detalhes: In nonSneakyThrow, Tdeve ser Exception, não "um supertipo de" Exception, porque é exatamente o tipo do argumento declarado em tempo de compilação no site da chamada.
llogiq
1
@llogiq Se eu li a especificação corretamente, ela tem um limite inferior Exceptione um limite superior de Throwable, então o menor limite superior, que é o tipo inferido resultante, é Exception.
thecoop
1
@llogiq Observe que o tipo do argumento apenas define um limite de tipo inferior porque qualquer supertipo do argumento também é aceitável.
Marko Topolnik
2
a Exceptionfrase "ou ela mesma" pode ser útil para o leitor, mas geralmente, deve-se notar que a especificação sempre usa os termos "subtipo" e "supertipo" no sentido de "incluir a si mesma" ...
Holger
17

Se a inferência de tipo produz um único limite superior para uma variável de tipo, normalmente o limite superior é escolhido como a solução. Por exemplo, se T<<Number, a solução é T=Number. Embora Integer, Floatetc. também possam satisfazer a restrição, não há um bom motivo para escolhê-los Number.

Esse também foi o caso para throws Tem java 5-7: T<<Throwable => T=Throwable. (Todas as soluções de lançamento sorrateiro tinham <RuntimeException>argumentos de tipo explícitos , caso contrário, <Throwable>é inferido.)

Em java8, com a introdução de lambda, isso se torna problemático. Considere este caso

interface Action<T extends Throwable>
{
    void doIt() throws T;
}

<T extends Throwable> void invoke(Action<T> action) throws T
{
    action.doIt(); // throws T
}    

Se invocarmos com um lambda vazio, o que seria Tinferido como?

    invoke( ()->{} ); 

A única restrição Té um limite superior Throwable. No estágio anterior de java8, T=Throwableseria inferido. Veja este relatório que fiz.

Mas isso é muito bobo, inferir Throwable, uma exceção verificada, de um bloco vazio. Uma solução foi proposta no relatório (que é aparentemente adotada pela JLS) -

If E has not been inferred from previous steps, and E is in the throw clause, 
and E has an upper constraint E<<X,
    if X:>RuntimeException, infer E=RuntimeException
    otherwise, infer E=X. (X is an Error or a checked exception)

ou seja, se o limite superior for Exceptionou Throwable, escolha RuntimeExceptioncomo a solução. Neste caso, não é uma boa razão para escolher um subtipo particular do limite superior.

ZhongYu
fonte
Qual é o significado de X:>RuntimeExceptionem seu último trecho de exemplo?
marsouf de
1

Com sneakyThrow, o tipo Té uma variável de tipo genérico limitada sem um tipo específico (porque não há de onde o tipo poderia vir).

Com nonSneakyThrow, o tipo Té o mesmo que o argumento, portanto, em seu exemplo, o Tde nonSneakyThrow(e);é Exception. Como testSneaky()não declara um lançado Exception, é mostrado um erro.

Observe que esta é uma interferência conhecida de Genéricos com exceções verificadas.

llogiq
fonte
Então, porque sneakyThrownão é realmente inferido para nenhum tipo específico, e o "elenco" é para um tipo indefinido? Eu me pergunto o que realmente acontece com isso.
Marko Topolnik