¿Por qué Java no permite subclases genéricas de Throwable?

146

De acuerdo con Java Language Sepecification , 3ra edición:

Es un error en tiempo de compilación si una clase genérica es una subclase directa o indirecta de Throwable.

Deseo entender por qué se ha tomado esta decisión. ¿Qué hay de malo con las excepciones genéricas?

(Hasta donde yo sé, los genéricos son simplemente azúcar sintáctica en tiempo de compilación, y se traducirán de Objecttodos modos en los .classarchivos, por lo que declarar efectivamente una clase genérica es como si todo en ella fuera un Object. Corríjame si me equivoco .)

Hosam Aly
fuente
1
Los argumentos de tipo genérico se reemplazan por el límite superior, que por defecto es Objeto. Si tienes algo como List <? extiende A>, luego se usa A en los archivos de clase.
Torsten Marek
Gracias @Torsten. No pensé en ese caso antes.
Hosam Aly
2
Es una buena pregunta de entrevista, esta.
skaffman el
@TorstenMarek: Si uno llama myList.get(i), obviamente gettodavía devuelve un Object. ¿El compilador inserta una conversión Apara capturar parte de la restricción en tiempo de ejecución? Si no, el OP tiene razón en que al final se reduce a Objects en tiempo de ejecución. (El archivo de clase ciertamente contiene metadatos sobre A, pero solo son metadatos AFAIK.)
Mihai Danila

Respuestas:

155

Como dijo Mark, los tipos no son reificables, lo cual es un problema en el siguiente caso:

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

Ambos SomeException<Integer>y SomeException<String>se borran al mismo tipo, no hay forma de que la JVM distinga las instancias de excepción y, por lo tanto, no hay forma de saber qué catchbloque debe ejecutarse.

Torsten Marek
fuente
3
pero ¿qué significa "reifiable"?
aberrant80
61
Por lo tanto, la regla no debe ser "los tipos genéricos no pueden subclasificar Throwable", sino que "las cláusulas catch siempre deben usar tipos sin formato".
Archie
3
Podrían no permitir el uso de dos bloques de captura con el mismo tipo juntos. De modo que usar SomeExc <Integer> solo sería legal, solo usar SomeExc <Integer> y SomeExc <String> juntos sería ilegal. Eso no supondría ningún problema, ¿o sí?
Viliam Búr
3
Oh, ahora lo entiendo. Mi solución causaría problemas con RuntimeExceptions, que no tienen que declararse. Entonces, si SomeExc es una subclase de RuntimeException, podría lanzar y capturar explícitamente SomeExc <Integer>, pero tal vez alguna otra función esté arrojando silenciosamente SomeExc <String> y mi bloque catch para SomeExc <Integer> también lo detectaría accidentalmente.
Viliam Búr
44
@ SuperJedi224 - No. Los hace bien, dada la restricción de que los genéricos tenían que ser compatibles con versiones anteriores.
Stephen C
14

Aquí hay un ejemplo simple de cómo usar la excepción:

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

El cuerpo de la instrucción TRy arroja la excepción con un valor dado, que es captado por la cláusula catch.

Por el contrario, la siguiente definición de una nueva excepción está prohibida, ya que crea un 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; }
}

Un intento de compilar lo anterior informa un error:

% 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

Esta restricción es sensata porque casi cualquier intento de detectar dicha excepción debe fallar, porque el tipo no es reificable. Uno podría esperar que un uso típico de la excepción sea algo como lo siguiente:

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;
    }
  }
}

Esto no está permitido, porque el tipo en la cláusula catch no es reificable. Al momento de escribir este artículo, el compilador de Sun informa una cascada de errores de sintaxis en tal caso:

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

Como las excepciones no pueden ser paramétricas, la sintaxis está restringida, por lo que el tipo debe escribirse como un identificador, sin el siguiente parámetro.

Adaptador IA
fuente
2
¿Qué quieres decir cuando dices "reifiable"? 'reificable' no es una palabra.
ForYourOwnGood 01 de
1
Yo mismo no sabía la palabra, pero una búsqueda rápida en Google me consiguió esto: java.sun.com/docs/books/jls/third_edition/html/…
Hosam Aly
13

Es esencialmente porque fue diseñado de mala manera.

Este problema impide un diseño abstracto limpio, por ejemplo,

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

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

El hecho de que una cláusula catch no funcione para los genéricos no está justificado no es excusa para ello. El compilador podría simplemente no permitir tipos genéricos concretos que extiendan Throwable o no permitir los genéricos dentro de las cláusulas catch.

Michele Sollecito
fuente
+1. mi respuesta - stackoverflow.com/questions/30759692/…
ZhongYu
1
La única forma en que podrían haberlo diseñado mejor fue haciendo incompatibles ~ 10 años de código de clientes. Esa fue una decisión comercial viable. El diseño era correcto ... dado el contexto .
Stephen C
1
Entonces, ¿cómo atraparás esta excepción? La única forma en que funcionaría es capturar el tipo sin formato EntityNotFoundException. Pero eso haría que los genéricos sean inútiles.
Frans
4

Los genéricos se verifican en tiempo de compilación para la corrección de tipo. La información de tipo genérico se elimina luego en un proceso llamado borrado de tipo . Por ejemplo, List<Integer>se convertirá al tipo no genérico List.

Debido a la eliminación de tipo , los parámetros de tipo no se pueden determinar en tiempo de ejecución.

Supongamos que tiene permiso para extenderse Throwableasí:

public class GenericException<T> extends Throwable

Ahora consideremos el siguiente código:

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

Debido a la eliminación de tipo , el tiempo de ejecución no sabrá qué bloque de captura ejecutar.

Por lo tanto, es un error en tiempo de compilación si una clase genérica es una subclase directa o indirecta de Throwable.

Fuente: problemas con el borrado de tipo

outdev
fuente
Gracias. Esta es la misma respuesta que la proporcionada por Torsten .
Hosam Aly
No, no es. La respuesta de Torsten no me ayudó, porque no explicaba qué tipo de borrado / reificación es.
Buenas noches Orgullo Nerd
2

Esperaría que sea porque no hay forma de garantizar la parametrización. Considere el siguiente código:

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

Como notará, la parametrización es solo azúcar sintáctico. Sin embargo, el compilador intenta asegurarse de que la parametrización permanezca consistente en todas las referencias a un objeto en el ámbito de compilación. En el caso de una excepción, el compilador no tiene forma de garantizar que MyException solo se arroje desde un ámbito que está procesando.

kdgregory
fuente
Sí, pero ¿por qué no se marca como "inseguro", como ocurre con los modelos por ejemplo?
eljenso 01 de
Porque con un reparto, le está diciendo al compilador "Sé que esta ruta de ejecución produce el resultado esperado". Con una excepción, no puede decir (para todas las excepciones posibles) "Sé dónde se arrojó esto". Pero, como digo más arriba, es una suposición; Yo no estaba ahi.
kdgregory
"Sé que esta ruta de ejecución produce el resultado esperado". No sabes, eso esperas. Es por eso que los genéricos y los downcasts son estáticamente inseguros, pero de todos modos están permitidos. Voté la respuesta de Torsten porque allí veo el problema. Aquí no lo hago.
eljenso 01 de
Si no sabe que un objeto es de un tipo particular, no debería lanzarlo. La idea general de un reparto es que tienes más conocimiento que el compilador y estás haciendo que ese conocimiento forme parte explícita del código.
kdgregory
Sí, y aquí también puede tener más conocimiento que el compilador, ya que desea hacer una conversión sin control de MyException a MyException <Foo>. Tal vez usted "sepa" que será una MyException <Foo>.
eljenso 01 de