Потрібно спробувати… ловити всередині або зовні петлю?


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

Чи є якась причина, ефективність чи інше, щоб віддавати перевагу одній іншій?


Редагувати: Здається, що консенсус полягає в тому, що чіткіше вставити цикл усередити спробу / лову, можливо, всередині власного методу. Однак все ще триває дискусія щодо того, що швидше. Чи може хтось перевірити це і повернутися з єдиною відповіддю?


2
Я не знаю про продуктивність, але код виглядає більш чистим із спробою вилову поза циклом for.
Джек Б Німбл,

Відповіді:


132

ВИКОНАННЯ:

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

Ось посилання: http://www.javaworld.com/javaworld/jw-01-1997/jw-01-hood.html

Таблиця описана приблизно на півдорозі вниз.


Гаразд, тож ти кажеш, що немає різниці, крім естетики. Чи можете ви пов’язати джерело?
Майкл Майерс

2
Дякую. Я думаю, що все може змінитися з 1997 року, але навряд чи це зробить його менш ефективним.
Майкл Майерс

1
Абсолютно жорстке слово. У деяких випадках це може гальмувати оптимізацію компілятора. Це не пряма вартість, але це може бути покарання за непряме виконання.
Том Хотін - таклін

2
Щоправда, я гадаю, я мав би трохи панувати в моєму ентузіазмі, але дезінформація набралася мені на нерви! Що стосується оптимізацій, оскільки ми не можемо знати, на який спосіб буде впливати продуктивність, я, мабуть, повернувся до тестування (як завжди).
Джефрі Л Вітлідж

1
різниці в продуктивності немає, лише якщо код працює без винятку.
Onur Bıyık

72

Продуктивність : як сказав Джефрі у своїй відповіді, у Java це не має великої різниці.

Як правило , для читабельності коду вибір вашого місця вилучення залежить від того, хочете ви, щоб цикл зберігав обробку чи ні.

У вашому прикладі ви повернулися після вибору винятку. У такому випадку я б поставив спробу / зловити навколо циклу. Якщо ви просто хочете отримати непогане значення, але продовжуєте обробляти, покладіть його всередину.

Третій спосіб : Ви завжди можете написати власний статичний метод 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? Так давно я торкнувся Яви!
Рей Хейс

2
Хтось ще запропонував TryParse, який в мережі C # /. Дозволяє зробити безпечний аналіз без роботи з винятками, це тепер реплікується, і метод повторно використовується. Що стосується оригінального питання, я б вважав за краще цикл всередині улову, він просто виглядає більш чистим, навіть якщо не виникає проблем з продуктивністю ..
Рей Хейс,

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 абсолютно немає різниці в продуктивності Java V, клієнт Sun (я не мав доступу до інших версій). Загальна різниця в часі становить приблизно кілька мілісекунд протягом усього тесту.

Тому єдине врахування - це те, що виглядає найчистішим. Я вважаю, що другий спосіб некрасивий, тому я буду дотримуватися або першого шляху, або Рея Хейса .


Якщо обробник винятків приймає потік за межами циклу, я відчуваю, що найясніше розмістити його поза циклом - Замість того, щоб розміщувати заперечний пункт, очевидно, всередині циклу, який все-таки повертається. Я використовую лінію пробілів навколо "спійманого" тіла таких методів "catch-all", щоб більш чітко відокремити тіло від всезахисного блоку спробу .
Thomas 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");
    }

Поки цикл знаходиться всередині блоку спробу вловлювання, змінна '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.

Ось вихід (під час пробування / лову):

4
8
12 
16
in catch block

І інший вихід (спробуйте / уловлюйте час):

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

Там у вас досить значна різниця:

в той час як у спробі / лові пробивається з циклу

спробувати / вхопити, зберігаючи цикл активним


Тож покладіть try{} catch() {}навколо циклу, якщо цикл розглядається як єдиний "одиниця", і пошук проблеми в цьому блоці змушує весь "блок" (або цикл у цьому випадку) піти на південь. Покладіть try{} catch() {}всередину циклу, якщо ви обробляєте одну ітерацію циклу або окремий елемент у масиві як цілий "блок". Якщо в цьому "блоці" відбувається виняток, ми просто пропускаємо цей елемент, а потім переходимо до наступної ітерації циклу.
Галактика

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, якщо є якісь помилки (наприклад, оригінальний приклад), ви поставите спробу / 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;
}

Насправді вам, мабуть, слід повернути помилку замість нульової, і взагалі мені не подобається мати кілька повернень, але ви розумієте.

З іншого боку, якщо ви хочете, щоб він просто ігнорував проблеми, і розбирав усі струни, які він може, ви поставите спробувати / 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" всередині циклу, ви зможете зафіксувати всі помилки, які були б створені для даного виклику методу. Під час розбору масиву значень з рядків, де ви очікуєте певних помилок форматування, напевно є випадки, коли ви хочете мати можливість представити всі помилки користувачеві, щоб їм не потрібно було намагатися виправляти їх по черзі .


Це не стосується конкретного випадку (я повертаюсь у блок вилову), але це добре враховувати загалом.
Майкл Майерс

4

Якщо його все або нічого не виходить, то перший формат має сенс. Якщо ви хочете обробити / повернути всі невдалі елементи, вам потрібно скористатися другою формою. Це були б мої основні критерії вибору між методами. Особисто, якщо це все або нічого, я б не використовував другу форму.


4

Поки ви знаєте, що вам потрібно зробити в циклі, ви можете поставити спробу лову поза циклом. Але важливо розуміти, що цикл закінчиться, як тільки станеться виняток, і це не завжди може бути тим, що ви хочете. Це насправді дуже поширена помилка програмного забезпечення на базі Java. Людям потрібно обробити ряд елементів, наприклад спорожнення черги, і помилково покластися на зовнішню заяву про те, що обробляє всі можливі винятки. Вони також можуть обробляти лише певний виняток всередині циклу і не очікувати, що виникне інший виняток. Тоді, якщо трапляється виняток, який не обробляється всередині циклу, то цикл буде "попередньо висунутий", він закінчується, можливо, передчасно, і зовнішній випадок вилучення обробляє виняток.

Якщо цикл мав би свою роль у житті для випорожнення черги, то ця петля, швидше за все, може закінчитися до того, як цю чергу справді спорожнить. Дуже поширена вина.


2

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


2

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

На іншій ноті, ви, мабуть, слід поглянути на float.TryParse або Convert.ToFloat.


2

Якщо ви помістите спробу / лов у цикл, ви продовжуватимете циклічно після виключення. Якщо поставити його поза циклом, ви зупинитесь, як тільки буде викинуто виняток.


Ну, я повертаю нуль за винятком, тому це не продовжуватиме обробку в моєму випадку.
Майкл Майерс

2

Моя точка зору - блоки спробу / лову необхідні для забезпечення правильної обробки винятків, але створення таких блоків має наслідки для продуктивності. Оскільки Loops містять інтенсивні повторювані обчислення, не рекомендується ставити блоки try / catch всередині циклів. Крім того, схоже, де виникає ця умова, часто потрапляє "Виняток" або "RuntimeException". RuntimeException, що потрапляє в код, слід уникати. Знову ж таки, якщо ви працюєте у великій компанії, важливо правильно зареєструвати цей виняток або зупинити його виняток. Цілий пункт цього описуPLEASE AVOID USING TRY-CATCH BLOCKS IN LOOPS


1

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

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

Однак я погоджуюся з іншими, що наявність зовнішньої петлі робить корпус петлі більш чистим.

Якщо є шанс, що ви коли-небудь захочете продовжувати обробку, а не виходити, якщо є недійсне число, тоді ви хочете, щоб код знаходився всередині циклу.


1

Якщо вона всередині, то ви отримаєте накладні витрати структури спробу / лову N разів, на відміну від лише одного разу зовні.


Кожен раз, коли викликається структура Try / Catch, вона додає накладних витрат на виконання методу. Тільки небагато пам'яті та процесорних кліщів, необхідних для вирішення структури. Якщо ви запускаєте цикл у 100 разів, а для гіпотетичного ради, скажімо, що вартість становить 1 галочку за виклик спробу / лову, тоді наявність кнопки Try / Catch всередині циклу коштує вам 100 кліщів, на відміну від лише 1 галочки, якщо це поза петлі.


Ви можете трохи детальніше розказати над головою?
Майкл Майерс

1
Гаразд, але Джеффрі Л Уітлідж каже, що зайвих накладних витрат немає . Чи можете ви надати джерело, яке каже, що воно є?
Майкл Майерс

Вартість невелика, майже незначна, але вона є - на все є вартість. Ось стаття в блозі з небес програміста ( tiny.cc/ZBX1b ), що стосується .NET, а ось публікація групи новин від Решата Сабіка щодо JAVA ( tiny.cc/BV2hS )
Стівен Врігтон,

Я бачу ... Отже, тепер питання полягає в тому, чи є витрати, понесені при введенні спроби / лову (в такому випадку це повинно бути поза циклом), чи це постійне протягом тривалості спроби / лову (у такому випадку це має бути всередині петлі)? Ви кажете, що це №1. І я до сих пір не повністю переконаний , що це вартість.
Майкл Майерс

1
Згідно з цією книгою Google ( tinyurl.com/3pp7qj ), "останні VM не показують жодного штрафу".
Майкл Майерс

1

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


Неправильно! Суть винятків полягає у визначенні того, яку виняткову поведінку слід прийняти, коли виникає "помилка". Таким чином, вибір внутрішнього або зовнішнього блоку пробування / захоплення дійсно залежить від того, як ви хочете обробити обробку циклу.
gizmo

Це може бути зроблено однаково добре з обробкою помилок перед виключенням, такими як передача прапорів "сталася помилка".
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оператора).


1

Це залежить від несправності. Якщо ви просто хочете пропустити елементи помилки, спробуйте всередині:

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

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


1

Я покладу $ 0,02 дюйма. Іноді ви закінчуєте додавати "нарешті" згодом у свій код (адже хто колись ідеально пише їх код?). У цих випадках раптом має сенс мати спробу / ловити поза циклом. Наприклад:

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

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


1

Ще один аспект, який не згадується у вищезгаданому, - це той факт, що кожен пробний вилов має певний вплив на стек, що може мати наслідки для рекурсивних методів.

Якщо метод "external ()" називає метод "Internal ()" (який може називати себе рекурсивно), спробуйте знайти, якщо можливо, спроба улов у методі "external ()". Простий приклад "краху стека", який ми використовуємо в класі продуктивності, виходить з ладу приблизно в 6 400 кадрів, коли пробний вхід знаходиться у внутрішньому методі, і приблизно в 11 600, коли він знаходиться у зовнішньому методі.

У реальному світі це може бути проблемою, якщо ви використовуєте композитний візерунок і маєте великі складні вкладені структури.


0

Якщо ви хочете отримати виняток для кожної ітерації, або перевірте, яку ітерацію викидає "Виняток", і вкажіть усі винятки в ітерації, поставте спробу ... вловити всередині циклу. Це не порушить цикл, якщо станеться виняток, і ви зможете зловити кожен виняток у кожній ітерації протягом циклу.

Якщо ви хочете розірвати цикл і вивчити Виняток кожного разу, коли його викинули, використовуйте спробу ... вийміть з циклу. Це порушить цикл і виконає оператори після вилову (якщо вони є).

Все залежить від вашої потреби. Я вважаю за краще використовувати спробувати ... ловити всередині циклу під час розгортання, оскільки, якщо виняток відбувається, результати не неоднозначні, і цикл не розірветься та не виконається повністю.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.