Java, если vs. try/catch

Есть ли накладные расходы в Java для использования блока try/catch, а не if block (если предположить, что вложенный код в противном случае не запрашивает)?

Например, возьмите следующие две простые реализации метода "безопасной обрезки" для строк:

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

Если вход raw встречается редко null, есть ли разница в производительности между двумя методами?

Кроме того, является хорошим шаблоном программирования для использования подхода tryTrim() для упрощения компоновки кода, особенно когда многие if blocks проверяют редкие условия ошибки избежать, заключая код в один блок try/catch?

Например, обычный метод имеет метод с N parameters, который использует M <= N из них рядом с его началом, быстро и детерминированно, если какой-либо такой параметр является "недопустимым" (например, нулевой или пустой строка), не затрагивая остальную часть кода.

В таких случаях вместо того, чтобы писать k * M , если блоки (где k - среднее количество проверок на один параметр, например k = 2 для пустых или пустых строк), Блок try/catch значительно сократит код, и комментарий 1-2 строки может быть использован для явного указания "нетрадиционной" логики.

Такой шаблон также ускорит этот метод, особенно если условия ошибки происходят редко, и это сделало бы это без ущерба для безопасности программы (при условии, что условия ошибки являются "нормальными", например, как в методе обработки строк, где null или пустые значения приемлемы, хотя и редко присутствуют).

Ответ 1

Я знаю, что вы спрашиваете о накладных расходах, но вы действительно не должны использовать try/catch и if взаимозаменяемые.

try/catch - это то, что идет не так, как вне вашего контроля, а не в нормальном потоке программы. Например, попытка записи в файл и файловая система заполнены? Обычно эту ситуацию следует обрабатывать с помощью try/catch.

Операторы

if должны быть нормальным потоком и обычной проверкой ошибок. Так, например, пользователь не заполняет требуемое поле ввода? Используйте для этого if, а не try/catch.

Мне кажется, что ваш примерный код настоятельно указывает, что правильный подход есть оператор if, а не try/catch.

Чтобы ответить на ваш вопрос, я бы предположил, что в try/catch, как правило, больше накладных расходов, чем if. Чтобы точно знать, получите профилировщик Java и узнайте, какой именно код вам нужен. Возможно, ответ может варьироваться в зависимости от ситуации.

Ответ 2

Этот вопрос почти был "отвечен до смерти", но я думаю, что есть еще несколько моментов, которые можно было бы с пользой сделать:

  • Использование try / catch для не исключительного потока управления - это плохой стиль (в Java). (Часто говорят о том, что означает "не исключительные"... но это другая тема.)

  • Отчасти это плохой стиль: try / catch на порядок дороже обычного оператора потока управления 1. Фактическая разница зависит от программы и платформы, но я ожидаю, что она будет в 1000 или более раз дороже. Помимо прочего, создание объекта исключения захватывает трассировку стека, просматривая и копируя информацию о каждом кадре в стеке. Чем глубже стек, тем больше нужно скопировать.

  • Другая причина, по которой это плохой стиль, - это то, что код труднее читать.

1 - JIT в последних версиях Java 7 может оптимизировать обработку исключений, чтобы резко сократить накладные расходы, в некоторых случаях. Однако по умолчанию эти оптимизации не включены.

Также есть проблемы с тем, как вы написали пример:

  • Захват Exception - очень плохая практика, потому что есть вероятность, что вы случайно поймаете другие неконтролируемые исключения. Например, если вы сделали это во время вызова raw.substring(1), вы также поймали потенциальный StringIndexOutOfBoundsException... и спрятали ошибки.

  • То, что ваш пример пытается сделать (вероятно), является результатом плохой практики работы с строками null. Как общий принцип, вы должны попытаться свести к минимуму использование строк null и попытаться ограничить их (преднамеренное) распространение. По возможности используйте пустую строку вместо null для обозначения "no value". И когда у вас есть случай, когда вам нужно передать или вернуть строку null, четко пропишите ее в своем методе javadocs. Если ваши методы вызываются с помощью null, когда они не должны... это ошибка. Пусть это порождает исключение. Не пытайтесь компенсировать ошибку (в этом примере), возвращая null.


Followup

Мой вопрос был более общим, а не только для нулевых значений.

... и большинство точек в моем ответе не связаны с нулевыми значениями!

Но помните, что есть много случаев, когда вы хотите разрешить случайное значение null или любое другое значение, которое может вызвать исключение, и просто игнорировать их. Это имеет место, например, при чтении значений ключей/пар откуда-то и передаче их методу, например tryTrim().

Да, есть ситуации, когда ожидаются значения null, и вам нужно иметь дело с ними.

Но я бы сказал, что то, что делает tryTrim() (обычно), является неправильным способом работы с null. Сравните эти три бита кода:

// 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();

В конечном счете вам приходится иметь дело с null по-другому от обычной строки, и обычно это хорошая идея сделать это как можно скорее. Чем дальше null разрешено распространяться от его начала координат, тем больше вероятность того, что программист забудет, что значение null является возможностью, и напишите багги-код, который принимает ненулевое значение. И забыть, что параметр запроса HTTP может отсутствовать (т.е. param == null), является классическим случаем, когда это происходит.

Я не говорю, что tryTrim() по своей сути плохой. Но тот факт, что программист считает, что такие методы написания информации, как это, вероятно, указывает на то, что обработка с нулевым нулевым значением не идеальна.

Ответ 3

Используйте вторую версию. Никогда не используйте исключения для потока управления, когда доступны другие альтернативы, поскольку это не то, для чего они предназначены. Исключения для исключительных обстоятельств.

В то время как на тему, не ловите Exception здесь, и особенно не проглатывайте ее. В вашем случае вы ожидаете NullPointerException. Если вы что-то поймаете, вы поймаете (, но вернитесь к пункту 1, не делайте этого). Когда вы ловите (и проглатываете!) Exception, вы говорите: "что бы ни случилось, я могу справиться с этим. Мне все равно, что это". Ваша программа может быть в безотзывном состоянии! Только поймите, с чем вы готовы справиться, пусть все остальное проталкивается к слою, который может справиться с ним, даже если этот слой является верхним слоем, и все, что он делает, записывает исключение и затем удаляет переключатель извлечения.

Ответ 4

В противном случае исключения бывают быстрыми, чтобы бросать и ловить (хотя if, вероятно, еще быстрее), но медленная вещь создает трассировку стека исключений, потому что ей нужно пройти весь текущий стек. (В общем случае плохо использовать исключения для потока управления, но когда это действительно необходимо и исключения должны быть быстрыми, можно пропустить построение трассировки стека, переопределив метод Throwable.fillInStackTrace() или сохранить один экземпляр исключения и выбросить его многократно вместо того, чтобы всегда создавать новый экземпляр исключения.)

Ответ 5

Что касается накладных расходов, вы можете проверить себя:

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

}

Число, которое я получил:

Try time:8102
If time:1956

Что касается стиля - это целый отдельный вопрос. Оператор if выглядит довольно естественно, но попытка выглядит странно по нескольким причинам: - вы поймали исключение, даже если вы проверяете значение NULL, ожидаете ли вы чего-то "исключительного" (иначе поймите NullException)? - ты поймал это Исключение, ты собираешься сообщить об этом или усвоить? и т.д. и т.д.

Изменить: см. мой комментарий, почему это недопустимый тест, но я действительно не хотел оставлять это место здесь. Просто заменив tryTrim и ifTrim, мы неожиданно получаем следующие результаты (на моей машине):

Try time:2043
If time:6810

Вместо того, чтобы все объяснять, просто прочитайте этот для начала - у Клиффа также есть отличные слайды по всей теме, но я не могу найти ссылка на данный момент.

Зная, как работает обработка исключений в Hotspot, я уверен, что в правильном тесте try/catch без исключения) будет базовая производительность (потому что нет никаких накладных расходов), но JIT может сыграть некоторые трюки с Nullpointer проверки, за которыми следуют вызовы методов (нет явной проверки, но поймать исключение hw, если объект имеет значение null), и в этом случае мы получим тот же результат. Также не забывайте: мы говорим о различии одного легко предсказуемого, если бы это был ОДИН процессорный цикл! Вызов обрезки будет в миллион раз больше.