Нужно попробовать... поймать внутрь или снаружи петли?

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

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, возможно, внутри его собственного метода. Тем не менее, по-прежнему продолжаются дебаты по этому вопросу. Может ли кто-нибудь проверить это и вернуться с единым ответом?

Ответ 1

Хорошо, после Jeffrey L Whitledge сказал, что не было разницы в производительности (с 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 client VM (у меня не было доступа к другим версиям). Общая разница во времени составляет порядка нескольких миллисекунд по всему тесту.

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

Ответ 2

ИСПОЛНЕНИЯ:

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

Здесь ссылка: http://www.javaworld.com/javaworld/jw-01-1997/jw-01-hood.html

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

Ответ 3

Производительность: как Джеффри сказал в своем ответе, в 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;
    }
}

Ответ 4

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

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

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

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

С другой стороны, если вы хотите, чтобы он просто игнорировал проблемы и анализировал все строки, которые он может, вы поставили 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;
}

Ответ 5

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

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 находится внутри блока catch try, переменная "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

И другой вывод (try/catch in while):

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

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

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

try/catch in, пока активный цикл не работает

Ответ 6

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

Ответ 7

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

Ответ 8

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

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

Ответ 9

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

Ответ 10

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

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

Ответ 11

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


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

Ответ 12

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

Ответ 13

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

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

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

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

Ответ 14

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

Ответ 15

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

Рассмотрим:

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

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

Ответ 16

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

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

  • "Более узкая" ответственность блока try-catch, тем сложнее рефакторинг. В частности, когда (как в вашем случае) вы выполняете "нелокальную" команду из блока catch (оператор return null).

Ответ 17

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

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

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

Ответ 18

Я поставлю свои $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.
}

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

Ответ 19

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

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

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