Стоит ли пытаться ... поймать выход внутри или снаружи петли?

185

У меня есть цикл, который выглядит примерно так:

for (int i = 0; i < max; i++) {
    String myString = ...;
    float myNum = Float.parseFloat(myString);
    myFloats[i] = myNum;
}

Это основное содержание метода, единственной целью которого является возвращение массива с плавающей точкой. Я хочу, чтобы этот метод возвращал, nullесли есть ошибка, поэтому я помещаю цикл в try...catchблок, например так:

try {
    for (int i = 0; i < max; i++) {
        String myString = ...;
        float myNum = Float.parseFloat(myString);
        myFloats[i] = myNum;
    }
} catch (NumberFormatException ex) {
    return null;
}

Но потом я также подумал о том, чтобы поместить try...catchблок внутрь цикла, например так:

for (int i = 0; i < max; i++) {
    String myString = ...;
    try {
        float myNum = Float.parseFloat(myString);
    } catch (NumberFormatException ex) {
        return null;
    }
    myFloats[i] = myNum;
}

Есть ли какая-либо причина, производительность или иное, чтобы отдать предпочтение одному над другим?


Редактировать: консенсус, кажется, заключается в том, что было бы чётче поместить цикл внутрь try / catch, возможно, внутри собственного метода. Тем не менее, есть еще споры о том, что быстрее. Может кто-нибудь проверить это и вернуться с единым ответом?

Майкл Майерс
источник
2
Я не знаю о производительности, но код выглядит чище с помощью функции catch вне цикла for.
Джек Б. Нимбл

Ответы:

132

ПРОИЗВОДИТЕЛЬНОСТЬ:

В том, где размещены структуры try / catch, нет абсолютно никакой разницы в производительности. Внутренне они реализованы в виде таблицы диапазона кода в структуре, которая создается при вызове метода. Пока метод выполняется, структуры try / catch полностью не видны, если только не произойдет выброс, тогда место ошибки сравнивается с таблицей.

Вот ссылка: http://www.javaworld.com/javaworld/jw-01-1997/jw-01-hood.html

Таблица описана примерно на полпути вниз.

Джеффри Л Уитледж
источник
Итак, вы говорите, что нет никакой разницы, кроме эстетики. Можете ли вы дать ссылку на источник?
Майкл Майерс
2
Спасибо. Я полагаю, что с 1997 года все могло измениться, но вряд ли это сделает его менее эффективным.
Майкл Майерс
1
Абсолютно жесткое слово. В некоторых случаях это может помешать оптимизации компилятора. Не прямые затраты, но это может быть косвенное снижение производительности.
Том Хотин - tackline
2
Правда, я полагаю, что должен был немного царить в моем энтузиазме, но дезинформация действовала мне на нервы! В случае оптимизаций, поскольку мы не можем знать, каким образом это повлияет на производительность, я думаю, вернулись к пробному тестированию (как всегда).
Джеффри Л Уитледж
1
разницы в производительности нет, только если код работает без исключений.
Онур Бийык,
72

Производительность : как сказал Джеффри в своем ответе, в Java это не имеет большого значения.

Как правило , для удобочитаемости кода выбор того, где перехватить исключение, зависит от того, хотите ли вы, чтобы цикл продолжал обработку или нет.

В вашем примере вы вернулись после ловли исключения. В этом случае я бы поставил try / catch вокруг цикла. Если вы просто хотите поймать плохое значение, но продолжите обработку, поместите его внутрь.

Третий способ : вы всегда можете написать свой собственный статический метод ParseFloat и иметь дело с обработкой исключений в этом методе, а не в цикле. Делаем обработку исключений изолированной от самого цикла!

class Parsing
{
    public static Float MyParseFloat(string inputValue)
    {
        try
        {
            return Float.parseFloat(inputValue);
        }
        catch ( NumberFormatException e )
        {
            return null;
        }
    }

    // ....  your code
    for(int i = 0; i < max; i++) 
    {
        String myString = ...;
        Float myNum = Parsing.MyParseFloat(myString);
        if ( myNum == null ) return;
        myFloats[i] = (float) myNum;
    }
}
Рэй Хейс
источник
1
Присмотрись. Он выходит на первое исключение в обоих. Сначала я тоже так думал.
Кервин
2
Я знал, вот почему я сказал в его случае, так как он возвращается, когда он сталкивается с проблемой, тогда я использую внешнюю съемку. Для ясности, это правило я бы использовал ...
Рэй Хейс
Хороший код, но, к сожалению, в Java нет такой роскоши, как параметры.
Майкл Майерс
ByRef? Давно я не касался Java!
Рэй Хейс
2
Кто-то еще предложил TryParse, который в C # /. Net позволяет вам выполнять безопасный анализ, не обращаясь к исключениям, который теперь реплицируется, и метод можно использовать повторно. Что касается исходного вопроса, я бы предпочел цикл внутри улова, он выглядит чище, даже если нет проблем с производительностью ..
Рэй Хейс
46

Хорошо, после того, как Джеффри Л. Уитледж сказал, что не было никакой разницы в производительности (по состоянию на 1997 год), я пошел и проверил ее. Я провел этот небольшой тест:

public class Main {

    private static final int NUM_TESTS = 100;
    private static int ITERATIONS = 1000000;
    // time counters
    private static long inTime = 0L;
    private static long aroundTime = 0L;

    public static void main(String[] args) {
        for (int i = 0; i < NUM_TESTS; i++) {
            test();
            ITERATIONS += 1; // so the tests don't always return the same number
        }
        System.out.println("Inside loop: " + (inTime/1000000.0) + " ms.");
        System.out.println("Around loop: " + (aroundTime/1000000.0) + " ms.");
    }
    public static void test() {
        aroundTime += testAround();
        inTime += testIn();
    }
    public static long testIn() {
        long start = System.nanoTime();
        Integer i = tryInLoop();
        long ret = System.nanoTime() - start;
        System.out.println(i); // don't optimize it away
        return ret;
    }
    public static long testAround() {
        long start = System.nanoTime();
        Integer i = tryAroundLoop();
        long ret = System.nanoTime() - start;
        System.out.println(i); // don't optimize it away
        return ret;
    }
    public static Integer tryInLoop() {
        int count = 0;
        for (int i = 0; i < ITERATIONS; i++) {
            try {
                count = Integer.parseInt(Integer.toString(count)) + 1;
            } catch (NumberFormatException ex) {
                return null;
            }
        }
        return count;
    }
    public static Integer tryAroundLoop() {
        int count = 0;
        try {
            for (int i = 0; i < ITERATIONS; i++) {
                count = Integer.parseInt(Integer.toString(count)) + 1;
            }
            return count;
        } catch (NumberFormatException ex) {
            return null;
        }
    }
}

Я проверил полученный байт-код, используя javap, чтобы убедиться, что ничего не вставлено.

Результаты показали, что, предполагая незначительную оптимизацию JIT, Джеффри прав ; нет абсолютно никакой разницы в производительности на Java 6, клиентской виртуальной машине Sun (у меня не было доступа к другим версиям). Общая разница во времени составляет порядка нескольких миллисекунд за весь тест.

Поэтому единственным соображением является то, что выглядит наиболее чистым. Я считаю, что второй путь уродлив, поэтому я буду придерживаться либо первого, либо пути Рэя Хейса .

Майкл Майерс
источник
Если обработчик исключений принимает поток за пределами цикла, то я чувствую, что было бы лучше поместить его за пределы цикла - вместо того, чтобы помещать предложение catch, по-видимому, внутри цикла, которое, тем не менее, возвращает. Я использую линию пробела вокруг «пойманного» тела таких «универсальных» методов, чтобы более четко отделить тело от охватывающего блока try .
Томас W
16

Хотя производительность может быть одинаковой, а то, что «выглядит» лучше, очень субъективно, функциональность по-прежнему довольно велика. Возьмите следующий пример:

Integer j = 0;
    try {
        while (true) {
            ++j;

            if (j == 20) { throw new Exception(); }
            if (j%4 == 0) { System.out.println(j); }
            if (j == 40) { break; }
        }
    } catch (Exception e) {
        System.out.println("in catch block");
    }

Цикл while находится внутри блока try catch, переменная 'j' увеличивается до 40, выводится, когда j mod 4 равно нулю, и выдается исключение, когда j достигает 20.

Перед любыми подробностями, вот другой пример:

Integer i = 0;
    while (true) {
        try {
            ++i;

            if (i == 20) { throw new Exception(); }
            if (i%4 == 0) { System.out.println(i); }
            if (i == 40) { break; }

        } catch (Exception e) { System.out.println("in catch block"); }
    }

Та же логика, что и выше, с той лишь разницей, что блок try / catch теперь находится внутри цикла while.

Здесь выводится (в то время как в try / catch):

4
8
12 
16
in catch block

И другой вывод (попробуйте / поймайте в то время как):

4
8
12
16
in catch block
24
28
32
36
40

Там у вас есть довольно существенная разница:

в то время как в try / catch вырывается из цикла

попробуй / поймай пока держит цикл активным

seBaka28
источник
Поэтому обведите try{} catch() {}цикл, если цикл рассматривается как один «блок», и обнаружение проблемы в этом блоке заставляет весь «блок» (или цикл в данном случае) идти на юг. Поместите try{} catch() {}в цикл, если вы рассматриваете одну итерацию цикла или один элемент в массиве как целую «единицу». Если возникает исключение в этом «модуле», мы просто пропускаем этот элемент и затем переходим к следующей итерации цикла.
Galaxy
14

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

Рассмотрим этот слегка измененный пример:

public static void main(String[] args) {
    String[] myNumberStrings = new String[] {"1.2345", "asdf", "2.3456"};
    ArrayList asNumbers = parseAll(myNumberStrings);
}

public static ArrayList parseAll(String[] numberStrings){
    ArrayList myFloats = new ArrayList();

    for(int i = 0; i < numberStrings.length; i++){
        myFloats.add(new Float(numberStrings[i]));
    }
    return myFloats;
}

Если вы хотите, чтобы метод parseAll () возвращал значение null, если есть какие-либо ошибки (как в оригинальном примере), вы бы поместили try / catch снаружи следующим образом:

public static ArrayList parseAll1(String[] numberStrings){
    ArrayList myFloats = new ArrayList();
    try{
        for(int i = 0; i < numberStrings.length; i++){
            myFloats.add(new Float(numberStrings[i]));
        }
    } catch (NumberFormatException nfe){
        //fail on any error
        return null;
    }
    return myFloats;
}

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

С другой стороны, если вы хотите, чтобы он просто игнорировал проблемы и разбирал все возможные строки, вы бы поместили try / catch внутри цикла следующим образом:

public static ArrayList parseAll2(String[] numberStrings){
    ArrayList myFloats = new ArrayList();

    for(int i = 0; i < numberStrings.length; i++){
        try{
            myFloats.add(new Float(numberStrings[i]));
        } catch (NumberFormatException nfe){
            //don't add just this one
        }
    }

    return myFloats;
}
Мэтт Н
источник
2
Вот почему я сказал: «Если вы просто хотите поймать плохое значение, но продолжите обработку, поместите его внутрь».
Рэй Хейс
5

Как уже упоминалось, производительность одинакова. Однако пользовательский опыт не обязательно идентичен. В первом случае вы быстро потерпите неудачу (то есть после первой ошибки), однако, если вы поместите блок try / catch в цикл, вы можете зафиксировать все ошибки, которые будут созданы для данного вызова метода. При разборе массива значений из строк, где вы ожидаете ошибок форматирования, определенно есть случаи, когда вы хотели бы иметь возможность представить все ошибки пользователю, чтобы им не нужно было пытаться исправить их по одному ,

user19810
источник
Это не верно для этого конкретного случая (я возвращаюсь в блоке catch), но это хорошая вещь, чтобы иметь в виду в целом.
Майкл Майерс
4

Если это не все или ничего, то первый формат имеет смысл. Если вы хотите иметь возможность обрабатывать / возвращать все исправные элементы, вам нужно использовать вторую форму. Это были бы мои основные критерии для выбора между методами. Лично, если бы это было все или ничего, я бы не использовал вторую форму.

Джо Скора
источник
4

Пока вы знаете, что вам нужно сделать в цикле, вы можете поместить try try вне цикла. Но важно понимать, что цикл закончится, как только произойдет исключение, и это не всегда может быть тем, что вы хотите. На самом деле это очень распространенная ошибка в программном обеспечении на основе Java. Людям необходимо обработать несколько элементов, таких как очистка очереди, и ложно полагаться на внешний оператор try / catch, обрабатывающий все возможные исключения. Они также могут обрабатывать только определенное исключение внутри цикла и не ожидать появления какого-либо другого исключения. Затем, если возникает исключение, которое не обрабатывается внутри цикла, тогда цикл будет «предопределенным», он может закончиться преждевременно, и внешний оператор catch обработает исключение.

Если в жизни цикл выполнял свою роль по освобождению очереди, то этот цикл, скорее всего, мог бы закончиться до того, как эта очередь была действительно очищена. Очень распространенная ошибка.

Гуннар Форсгрен - Mobimation
источник
2

В ваших примерах нет функциональной разницы. Я считаю ваш первый пример более читабельным.

Джейми
источник
2

Вы должны предпочесть внешнюю версию над внутренней версией. Это всего лишь конкретная версия правила, переместите все, что находится за пределами цикла, и вы можете выйти за пределы цикла. В зависимости от компилятора IL и компилятора JIT две ваши версии могут иметь или не иметь разные характеристики производительности.

С другой стороны, вам, вероятно, стоит взглянуть на float.TryParse или Convert.ToFloat.

Орион Адриан
источник
2

Если вы поместите try / catch внутри цикла, вы будете продолжать цикл после исключения. Если вы поместите его вне цикла, вы остановитесь, как только будет сгенерировано исключение.

Джоэл Коухорн
источник
Ну, я возвращаю null в исключительных случаях, поэтому в моем случае обработка не будет продолжаться.
Майкл Майерс
2

С моей точки зрения, блоки try / catch необходимы для обеспечения правильной обработки исключений, но создание таких блоков влияет на производительность. Поскольку циклы содержат интенсивные повторяющиеся вычисления, не рекомендуется помещать блоки try / catch внутри циклов. Кроме того, кажется, что, где возникает это условие, часто это «Исключение» или «RuntimeException», который перехватывается. Следует избегать попадания RuntimeException в код. Опять же, если вы работаете в большой компании, важно правильно зарегистрировать это исключение или прекратить возникновение исключения во время выполнения. Весь смысл этого описанияPLEASE AVOID USING TRY-CATCH BLOCKS IN LOOPS

surendrapanday
источник
1

установка специального фрейма стека для try / catch добавляет дополнительные издержки, но JVM может обнаружить факт возврата и оптимизировать его.

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

Однако я согласен с остальными, что наличие петли вне ее делает корпус петли более чистым.

Если есть вероятность, что вы когда-нибудь захотите продолжить обработку, а не завершить работу, если найдется недопустимый номер, тогда вы бы хотели, чтобы код был внутри цикла.

Matt
источник
1

Если он внутри, то вы получите накладные расходы структуры try / catch N раз, а не один раз снаружи.


Каждый раз, когда вызывается структура Try / Catch, это увеличивает накладные расходы на выполнение метода. Просто немного памяти и тиков процессора, необходимых для работы со структурой. Если вы выполняете цикл 100 раз и, предположительно, скажем, стоимость составляет 1 тик за вызов try / catch, то наличие Try / Catch внутри цикла обойдется вам в 100 тиков, в отличие от 1 тика, если это вне петли.

Стивен Райтон
источник
Можете ли вы рассказать немного о накладных расходах?
Майкл Майерс
1
Хорошо, но Джеффри Л. Уитледж говорит, что нет лишних накладных расходов. Можете ли вы предоставить источник, который говорит, что есть?
Майкл Майерс
Стоимость небольшая, почти незначительная, но она есть - все имеет стоимость. Вот статья в блоге программиста Heaven ( tiny.cc/ZBX1b ), касающаяся .NET, и статья в новостной группе Reshat Sabiq, касающаяся JAVA ( tiny.cc/BV2hS )
Стивен Райтон,
Понятно ... Итак, теперь вопрос в том, является ли стоимость, понесенная при входе в try / catch (в этом случае она должна быть вне цикла), или она постоянна на протяжении попытки / catch (в этом случае она должен быть внутри цикла)? Вы говорите, что это # ​​1. И я до сих пор не полностью убежден , что это стоимость.
Майкл Майерс
1
Согласно этой книге Google ( tinyurl.com/3pp7qj ), «последние виртуальные машины не наказывают».
Майкл Майерс
1

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

wnoise
источник
Неправильно! Точка исключений состоит в том, чтобы определить, какое исключительное поведение следует применять при возникновении «ошибки». Таким образом, выбор внутреннего или внешнего блока try / catch действительно зависит от того, как вы хотите обработать цикл.
штуковина
Это можно сделать одинаково хорошо с обработкой ошибок до исключения, такой как передача флагов «ошибка произошла».
wnoise
1

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

Рассматривать:

try {
   // parse
} catch (NumberFormatException nfe){
   throw new RuntimeException("Could not parse as a Float: [" + myString + 
                              "] found at index: " + i, nfe);
} 

В трудное время вы по достоинству оцените подобное исключение, содержащее как можно больше информации.

Кайл Дайер
источник
Да, это верный момент также. В моем случае я читаю из файла, так что все, что мне действительно нужно, - это строка, которую не удалось проанализировать. Поэтому я сделал System.out.println (ex) вместо того, чтобы выдавать другое исключение (я хочу проанализировать столько файлов, сколько допустимо).
Майкл Майерс
1

Я хотел бы добавить свои собственные 0.02cсоображения о двух конкурирующих соображениях, когда смотрю на общую проблему того, где расположить обработку исключений:

  1. Чем «шире» ответственность try-catchблока (т. Е. Вне цикла в вашем случае) означает, что при изменении кода на более позднем этапе вы можете по ошибке добавить строку, которая обрабатывается вашим существующим catchблоком; возможно непреднамеренно. В вашем случае это менее вероятно, потому что вы явно ловитеNumberFormatException

  2. Чем «меньше» ответственность try-catchблока, тем сложнее становится рефакторинг. Особенно когда (как в вашем случае) вы выполняете «нелокальную» инструкцию из catchблока ( return nullоператора).

oxbow_lakes
источник
1

Это зависит от обработки ошибок. Если вы просто хотите пропустить элементы ошибки, попробуйте внутри:

for(int i = 0; i < max; i++) {
    String myString = ...;
    try {
        float myNum = Float.parseFloat(myString);
        myFloats[i] = myNum;
    } catch (NumberFormatException ex) {
        --i;
    }
}

В любом другом случае я бы предпочел попробовать снаружи. Код более читабелен, он более чистый. Может быть, было бы лучше, если бы в случае ошибки было возвращено исключение IllegalArgumentException в случае ошибки.

Арне Бурмейстер
источник
1

Я положу свои 0,02 доллара. Иногда вам приходится добавлять «наконец» позже в ваш код (потому что, кто когда-либо отлично пишет свой код в первый раз?). В этих случаях внезапно становится более целесообразным использовать try / catch вне цикла. Например:

try {
    for(int i = 0; i < max; i++) {
        String myString = ...;
        float myNum = Float.parseFloat(myString);
        dbConnection.update("MY_FLOATS","INDEX",i,"VALUE",myNum);
    }
} catch (NumberFormatException ex) {
    return null;
} finally {
    dbConnection.release();  // Always release DB connection, even if transaction fails.
}

Потому что, если вы получили ошибку или нет, вы хотите разорвать соединение с базой данных (или выбрать свой любимый тип другого ресурса ...) один раз.

Огрский псалом33
источник
1

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

Если метод "external ()" вызывает метод "inner ()" (который может вызывать себя рекурсивно), попробуйте найти try-catch в методе "outer ()", если это возможно. Простой пример «сбоя стека», который мы используем в классе производительности, дает сбой примерно в 6400 кадрах, когда try-catch находится во внутреннем методе, и примерно в 11 600, когда он находится во внешнем методе.

В реальном мире это может быть проблемой, если вы используете шаблон Composite и имеете большие сложные вложенные структуры.

Джефф Хилл
источник
0

Если вы хотите перехватывать исключения для каждой итерации или проверять, на какой итерации выдается исключение, и перехватывать все исключения в итерациях, поместите try ... catch внутри цикла. Это не разорвет цикл, если возникнет исключение, и вы можете перехватить каждое исключение в каждой итерации на протяжении всего цикла.

Если вы хотите разорвать цикл и проверять Исключение при каждом вызове, используйте try ... catch вне цикла. Это нарушит цикл и выполнит операторы после catch (если есть).

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

Джунед хан Момин
источник