Java si vs. try / catch overhead

¿Hay alguna sobrecarga en Java para usar un bloque try / catch , en oposición a un bloque if (suponiendo que el código adjunto no lo solicita)?

Por ejemplo, tome las siguientes dos implementaciones simples de un método de “ajuste seguro” para cadenas:

public String tryTrim(String raw) { try { return raw.trim(); } catch (Exception e) { } return null; } public String ifTrim(String raw) { if (raw == null) { return null; } return raw.trim(); } 

Si la entrada en raw rara vez es null , ¿hay alguna diferencia de rendimiento entre los dos métodos?

Además, ¿ es un buen patrón de progtwigción utilizar el enfoque tryTrim() para simplificar el diseño del código, especialmente cuando se pueden evitar muchos bloques que comprueban las condiciones de error raro encerrando el código en un bloque try / catch?

Por ejemplo, es un caso común tener un método con N parameters , que usa M <= N de ellos cerca de su inicio, fallando rápidamente y de manera determinista si dicho parámetro es “inválido” (por ejemplo, una cadena vacía o nula), sin afectar el rest del código.

En tales casos, en lugar de tener que escribir k * M si los bloques (donde k es el número promedio de verificaciones por parámetro, p. Ej. k = 2 para cadenas vacías o nulas), un bloque try / catch acortaría significativamente el código y 1 -2 línea de comentario podría utilizarse para señalar explícitamente la lógica “no convencional”.

Tal patrón también aceleraría el método, especialmente si las condiciones de error ocurren raramente, y lo haría sin comprometer la seguridad del progtwig (asumiendo que las condiciones de error son “normales”, por ejemplo, como en un método de procesamiento de cadena donde valores nulos o vacíos son aceptables, aunque raramente en presencia).

Sé que estás preguntando sobre el rendimiento general, pero realmente no deberías usar try / catch y if intercambiable.

try / catch es para cosas que salen mal que están fuera de su control y no en el flujo normal del progtwig. Por ejemplo, ¿intenta escribir en un archivo y el sistema de archivos está lleno? Esa situación normalmente debería manejarse con try / catch .

if declaraciones deben ser flujo normal y verificación de errores ordinarios. Entonces, por ejemplo, ¿el usuario no puede llenar un campo de entrada requerido? Úselo para eso, no try / catch .

Me parece que su código de ejemplo sugiere fuertemente que el enfoque correcto es una statement if y no una try / catch .

Para responder a su pregunta, supongo que generalmente hay más sobrecarga en un try / catch que en un if . Para estar seguro, obtenga un generador de perfiles de Java y descubra el código específico que le interesa. Es posible que la respuesta pueda variar según la situación.

Esta pregunta casi ha sido “respondida hasta la muerte”, pero creo que hay algunos puntos más que podrían ser útiles:

  • El uso de try / catch para un flujo de control no excepcional es un mal estilo (en Java). (A menudo hay debate sobre lo que significa “no excepcional” … pero ese es un tema diferente).

  • Parte de la razón por la cual es un estilo malo es que try / catch es mucho más caro que una statement de flujo de control regular 1 . La diferencia real depende del progtwig y la plataforma, pero espero que sea 1000 o más veces más costoso. Entre otras cosas, la creación del objeto de excepción captura un seguimiento de stack, buscando y copiando información sobre cada cuadro en la stack. Cuanto más profunda es la stack, más necesita ser copiada.

  • Otra parte de la razón por la cual es un estilo malo es que el código es más difícil de leer.

1 – El JIT en versiones recientes de Java 7 puede optimizar el manejo de excepciones para reducir drásticamente los gastos generales, en algunos casos. Sin embargo, estas optimizaciones no están habilitadas por defecto.

También hay problemas con la forma en que ha escrito el ejemplo:

  • La Exception captura es una práctica muy mala, porque existe la posibilidad de que atrape otras excepciones no verificadas por accidente. Por ejemplo, si lo hiciera con una llamada a raw.substring(1) , también podría detectar posibles StringIndexOutOfBoundsException s … y ocultar errores.

  • Lo que intenta hacer tu ejemplo es (probablemente) el resultado de una práctica deficiente al tratar con cadenas null . Como principio general, debe intentar minimizar el uso de cadenas null e intentar limitar su propagación (intencional). Siempre que sea posible, use una cadena vacía en lugar de null para indicar “sin valor”. Y cuando tenga un caso donde necesite pasar o devolver una cadena vacía, documente claramente en su método javadocs. Si sus métodos se llaman con un null cuando no deberían … es un error. Deja que arroje una excepción. No intente compensar el error por (en este ejemplo) devolviendo null .


SEGUIR

Mi pregunta era más general, no solo para valores nulos.

… y la mayoría de los puntos en mi respuesta no son sobre valores nulos!

Pero tenga en cuenta que hay muchos casos en los que desea permitir el valor nulo ocasional, o cualquier otro valor que pueda producir una excepción, y simplemente ignórelos. Este es el caso, por ejemplo, al leer valores de clave / par de algún lugar y pasarlos a un método como el tryTrim () anterior.

Sí, hay situaciones en null que se esperan valores null , y debe lidiar con ellos.

Pero yo diría que lo que tryTrim() está haciendo es (típicamente) la forma incorrecta de lidiar con null . Compara estos tres bits de código:

 // Version 1 String param = httpRequest.getParameter("foo"); String trimmed = tryTrim(param); if (trimmed == null) { // deal with case of 'foo' parameter absent } else { // deal with case of 'foo' parameter present } // Version 2 String param = httpRequest.getParameter("foo"); if (param == null) { // deal with case of 'foo' parameter absent } else { String trimmed = param.trim(); // deal with case of 'foo' parameter present } // Version 3 String param = httpRequest.getParameter("foo"); if (param == null) { // treat missing and empty parameters the same param = ""; } String trimmed = param.trim(); 

En definitiva, tiene que tratar el null diferente a una cadena normal, y por lo general es una buena idea hacerlo lo antes posible. Cuanto más se permita que el null se propague desde su origen de punto, más probable es que el progtwigdor olvide que un valor null es una posibilidad, y escriba un código con errores que asum un valor no nulo. Y olvidar que un parámetro de solicitud HTTP podría faltar (es decir, param == null ) es un caso clásico en el que esto sucede.

No estoy diciendo que tryTrim() sea ​​intrínsecamente malo. Pero el hecho de que un progtwigdor sienta la necesidad de escribir métodos como este es probablemente indicativo de un manejo nulo menos que ideal.

Usa la segunda versión. Nunca use excepciones para el flujo de control cuando haya otras alternativas disponibles, ya que eso no es para lo que están ahí. Las excepciones son por circunstancias excepcionales.

Si bien sobre el tema, no atrape la Exception aquí, y especialmente no la trague. En su caso, esperaría una NullPointerException . Si capturaras algo, eso es lo que atraparías ( pero vuelve al párrafo uno, no hagas esto ). Cuando capturas (¡y tragas!) Exception , estás diciendo “no importa lo que vaya mal, puedo manejarlo. No me importa de qué se trata”. ¡Tu progtwig podría estar en un estado irrevocable! Solo capte aquello para lo que esté preparado, permita que todo lo demás se propague a una capa que pueda manejarlo, incluso si esa capa es la capa superior y todo lo que hace es registrar la excepción y luego presionar el interruptor de expulsión.

De lo contrario, las excepciones son rápidas de lanzar y atrapar (aunque un if es probablemente aún más rápido), pero lo lento está creando la traza de la stack de la excepción, porque necesita recorrer toda la stack actual. (En general, es malo usar excepciones para el flujo de control, pero cuando eso realmente se necesita y las excepciones deben ser rápidas, es posible omitir la creación del rastreo de stack anulando el método Throwable.fillInStackTrace() , o para guardar una instancia de excepción y lanzarlo repetidamente en lugar de crear siempre una nueva instancia de excepción).

En lo que respecta a los gastos generales, puede probar usted mismo:

 public class Overhead { public static void main(String[] args) { String testString = ""; long startTimeTry = System.nanoTime(); tryTrim(testString); long estimatedTimeTry = System.nanoTime() - startTimeTry; long startTimeIf = System.nanoTime(); ifTrim(testString); long estimatedTimeIf = System.nanoTime() - startTimeIf; System.out.println("Try time:" + estimatedTimeTry); System.out.println("If time:" + estimatedTimeIf); } public static String tryTrim(String raw) { try { return raw.trim(); } catch (Exception e) { } return null; } public static String ifTrim(String raw) { if (raw == null) { return null; } return raw.trim(); } 

}

Los números que obtuve son:

 Try time:8102 If time:1956 

En cuanto a qué estilo, es una pregunta por separado. La sentencia if parece bastante natural, pero la prueba parece realmente extraña por varias razones: – capturaste Exception aunque estés buscando valor NULL, ¿esperas que ocurra algo “excepcional” (de lo contrario, capturar NullException)? – Atrapaste esa Excepción, ¿vas a reportarla o tragar? etc. etc. etc.

Editar: vea mi comentario sobre por qué esta es una prueba inválida, pero realmente no quería dejar esto parado aquí. Simplemente intercambiando tryTrim y ifTrim, de repente obtenemos los siguientes resultados (en mi máquina):

 Try time:2043 If time:6810 

En lugar de comenzar a explicar todo esto, solo lea esto para el comienzo: Cliff también tiene algunas diapositivas geniales sobre todo el tema, pero no puedo encontrar el enlace en este momento.

Saber cómo funciona la excepción en Hotspot, estoy bastante seguro de que en una prueba correcta try / catch sin excepción) sería el rendimiento base (porque no hay gastos generales), pero el JIT puede jugar algunos trucos con las comprobaciones de Nullpointer seguidas por invocaciones de método (sin comprobación explícita, pero atrapa la excepción hw si el objeto es nulo) en cuyo caso obtendríamos el mismo resultado. Además, no lo olvide: ¡estamos hablando de la diferencia de uno fácilmente predecible si sería UN ciclo de CPU! La llamada de recorte costará un millón de veces eso.