Czy warto… złapać pętlę w środku lub na zewnątrz?


185

Mam pętlę, która wygląda mniej więcej tak:

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

Jest to główna treść metody, której jedynym celem jest zwrócenie tablicy liczb zmiennoprzecinkowych. Chcę zwrócić tę metodę, nulljeśli wystąpi błąd, więc umieszczam pętlę w try...catchbloku, tak jak to:

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

Ale potem pomyślałem również o umieszczeniu try...catchbloku w pętli, w ten sposób:

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

Czy jest jakiś powód, wydajność lub w inny sposób, aby preferować jedno od drugiego?


Edycja: Wydaje się, że konsensus polega na tym, że łatwiej jest umieścić pętlę wewnątrz try / catch, prawdopodobnie w ramach własnej metody. Nadal jednak trwa debata, która jest szybsza. Czy ktoś może to przetestować i wrócić z jednolitą odpowiedzią?


2
Nie wiem o wydajności, ale kod wygląda na bardziej przejrzysty, gdy try catch znajduje się poza pętlą for.
Jack B Nimble,

Odpowiedzi:


132

WYSTĘP:

Nie ma absolutnie żadnej różnicy wydajności w tym, gdzie umieszczone są struktury try / catch. Wewnętrznie są one implementowane jako tabela zakresu kodu w strukturze, która jest tworzona podczas wywoływania metody. Podczas wykonywania metody struktury try / catch są całkowicie poza obrazem, chyba że nastąpi rzut, a następnie lokalizacja błędu jest porównywana z tabelą.

Oto odniesienie: http://www.javaworld.com/javaworld/jw-01-1997/jw-01-hood.html

Tabela jest opisana mniej więcej w połowie.


Okej, więc mówisz, że nie ma różnicy oprócz estetyki. Czy możesz połączyć się ze źródłem?
Michael Myers

2
Dzięki. Przypuszczam, że wszystko mogło się zmienić od 1997 roku, ale raczej nie zmniejszyłyby wydajności.
Michael Myers

1
Absolutnie to trudne słowo. W niektórych przypadkach może to hamować optymalizacje kompilatora. Nie jest to koszt bezpośredni, ale może to być pośrednia kara za wyniki.
Tom Hawtin - tackline

2
To prawda, że ​​powinienem był trochę zapanować z entuzjazmem, ale dezinformacja działała mi na nerwy! W przypadku optymalizacji, ponieważ nie wiemy, w jaki sposób wpłynie to na wydajność, myślę, że wróciłem do wypróbowania i przetestowania (jak zawsze).
Jeffrey L Whitledge,

1
nie ma różnicy w wydajności, tylko jeśli kod działa bez wyjątków.
Onur Bıyık

72

Wydajność : jak powiedział Jeffrey w odpowiedzi, w Javie nie ma to większego znaczenia.

Zasadniczo , dla czytelności kodu, wybór miejsca, w którym można przechwycić wyjątek, zależy od tego, czy pętla ma być przetwarzana, czy nie.

W twoim przykładzie wróciłeś po złapaniu wyjątku. W takim przypadku umieściłem try / catch wokół pętli. Jeśli chcesz po prostu złapać złą wartość, ale kontynuować przetwarzanie, włóż ją do środka.

Trzeci sposób : zawsze możesz napisać własną statyczną metodę ParseFloat i obsługiwać wyjątki w tej metodzie zamiast w pętli. Uczynienie obsługi wyjątków izolowanym dla samej pętli!

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
Patrz bliżej Wychodzi z pierwszego wyjątku w obu przypadkach. Na początku też myślałem o tym samym.
kervin

2
Byłem świadomy, dlatego powiedziałem w jego przypadku, ponieważ wraca, gdy napotyka problem, wtedy skorzystam z zewnętrznego wychwytu. Dla jasności stosuję tę zasadę ...
Ray Hayes,

Niezły kod, ale niestety Java nie ma luksusu z naszymi parametrami.
Michael Myers

ByRef? Minęło tak dużo czasu, odkąd dotknąłem Javy!
Ray Hayes,

2
Ktoś inny zasugerował TryParse, który w C # /. Net pozwala na bezpieczne parsowanie bez zajmowania się wyjątkami, który jest teraz replikowany, a metodę można ponownie wykorzystać. Jeśli chodzi o pierwotne pytanie, wolałbym pętlę wewnątrz haczyka, po prostu wygląda czystiej, nawet jeśli nie ma problemu z wydajnością ..
Ray Hayes

46

W porządku, po tym jak Jeffrey L. Whitledge powiedział, że nie ma różnicy w wydajności (od 1997), poszedłem i przetestowałem to. Przeprowadziłem ten mały test porównawczy:

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

Sprawdziłem wynikowy kod bajtowy za pomocą javap, aby upewnić się, że nic nie zostało wstawione.

Wyniki pokazały, że przy niewielkich optymalizacjach JIT Jeffrey ma rację ; absolutnie nie ma różnicy w wydajności na Javie 6, VM klienta Sun (nie miałem dostępu do innych wersji). Łączna różnica czasu jest rzędu kilku milisekund w całym teście.

Dlatego jedynym czynnikiem, który wygląda najczystiej. Uważam, że drugi sposób jest brzydki, więc trzymam się albo pierwszego, albo Raya Hayesa .


Jeśli moduł obsługi wyjątków pobiera przepływ poza pętlę, to wydaje mi się, że najczystsze jest umieszczenie go poza pętlą - zamiast umieszczania klauzuli catch najwyraźniej w pętli, która jednak zwraca. Używam linii białych znaków wokół „złapanego” ciała takich metod „catch-all”, aby jaśniej oddzielić ciało od otaczającego bloku try .
Thomas W

16

Chociaż wydajność może być taka sama, a to, co „wygląda” lepiej, jest bardzo subiektywne, nadal istnieje dość duża różnica w funkcjonalności. Weź następujący przykład:

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

Pętla while znajduje się w bloku try catch, zmienna „j” jest zwiększana, aż osiągnie 40, drukowana, gdy j mod 4 wynosi zero, a wyjątek jest zgłaszany, gdy j osiągnie 20.

Przed szczegółami oto inny przykład:

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

Ta sama logika jak powyżej, z tą różnicą, że blok try / catch znajduje się teraz w pętli while.

Oto wyjście (podczas try / catch):

4
8
12 
16
in catch block

I inne wyjście (spróbuj / catch in while):

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

Tutaj masz dość znaczącą różnicę:

podczas try / catch wypada z pętli

spróbuj / złap w czasie, gdy pętla będzie aktywna


Więc owiń try{} catch() {}pętlę, jeśli pętla jest traktowana jako pojedyncza „jednostka”, a znalezienie problemu w tej jednostce powoduje, że cała „jednostka” (lub pętla w tym przypadku) idzie na południe. Umieść try{} catch() {}wewnątrz pętli, jeśli traktujesz pojedynczą iterację pętli lub pojedynczy element w tablicy jako całą „jednostkę”. Jeśli wystąpi wyjątek w tej „jednostce”, pomijamy ten element, a następnie przechodzimy do następnej iteracji pętli.
Galaxy

14

Zgadzam się ze wszystkimi postami dotyczącymi wydajności i czytelności. Są jednak przypadki, w których to naprawdę ma znaczenie. Kilka innych osób wspomniało o tym, ale może to być łatwiejsze z przykładami.

Rozważ ten nieco zmodyfikowany przykład:

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

Jeśli chcesz, aby metoda parseAll () zwróciła null, jeśli wystąpią jakiekolwiek błędy (jak w oryginalnym przykładzie), umieść try / catch na zewnątrz w następujący sposób:

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

W rzeczywistości powinieneś prawdopodobnie zwrócić tutaj błąd zamiast wartości zerowej i ogólnie nie lubię mieć wielu zwrotów, ale masz pomysł.

Z drugiej strony, jeśli chcesz po prostu zignorować problemy i przeanalizować dowolne ciągi znaków, umieść try / catch wewnątrz pętli w następujący sposób:

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
Dlatego powiedziałem: „Jeśli chcesz po prostu złapać złą wartość, ale kontynuować przetwarzanie, włóż ją do środka”.
Ray Hayes

5

Jak już wspomniano, wydajność jest taka sama. Jednak wrażenia użytkownika niekoniecznie są identyczne. W pierwszym przypadku szybko zawiedziesz (tj. Po pierwszym błędzie), jednak jeśli umieścisz blok try / catch w pętli, możesz przechwycić wszystkie błędy, które powstałyby dla danego wywołania metody Podczas analizowania tablicy wartości z ciągów, w których spodziewane są pewne błędy formatowania, zdecydowanie są przypadki, w których chciałbyś być w stanie przedstawić wszystkie błędy użytkownikowi, aby nie musiał próbować ich naprawiać jeden po drugim. .


To nie jest prawda w tym konkretnym przypadku (wracam w bloku catch), ale ogólnie warto o tym pamiętać.
Michael Myers

4

Jeśli nie powiedzie się wszystko, to pierwszy format ma sens. Jeśli chcesz móc przetwarzać / zwracać wszystkie elementy, które nie zawiodły, musisz użyć drugiego formularza. To byłyby moje podstawowe kryteria wyboru między metodami. Osobiście, jeśli to wszystko albo nic, nie użyłbym drugiej formy.


4

Dopóki masz świadomość tego, co musisz osiągnąć w pętli, możesz umieścić try catch poza pętlą. Ale ważne jest, aby zrozumieć, że pętla zakończy się natychmiast po wystąpieniu wyjątku i nie zawsze może być to, czego chcesz. Jest to w rzeczywistości bardzo częsty błąd w oprogramowaniu Java. Ludzie muszą przetwarzać wiele elementów, takich jak opróżnianie kolejki i fałszywie polegać na zewnętrznej instrukcji try / catch obsługującej wszystkie możliwe wyjątki. Mogą również obsługiwać tylko określony wyjątek w pętli i nie oczekiwać żadnego innego wyjątku. Następnie, jeśli wystąpi wyjątek, który nie jest obsługiwany wewnątrz pętli, wówczas pętla zostanie „wstępnie zaprogramowana”, kończy się prawdopodobnie przedwcześnie, a zewnętrzna instrukcja catch obsługuje wyjątek.

Gdyby pętla miała za zadanie opróżnić kolejkę, wówczas najprawdopodobniej pętla mogłaby się zakończyć, zanim ta kolejka zostanie naprawdę opróżniona. Bardzo częsta wina.


2

W twoich przykładach nie ma funkcjonalnej różnicy. Uważam, że twój pierwszy przykład jest bardziej czytelny.


2

Powinieneś preferować wersję zewnętrzną niż wewnętrzną. To tylko konkretna wersja reguły, przenieś wszystko poza pętlę, którą możesz przenieść poza pętlę. W zależności od kompilatora IL i kompilatora JIT dwie wersje mogą, ale nie muszą, mieć różne parametry wydajności.

Z drugiej strony powinieneś prawdopodobnie spojrzeć na float.TryParse lub Convert.ToFloat.


2

Jeśli umieścisz try / catch w pętli, będziesz zapętlać po wyjątku. Jeśli umieścisz go poza pętlą, zatrzymasz się, gdy tylko zostanie zgłoszony wyjątek.


Zwracam wartość NULL w wyjątku, więc w moim przypadku nie byłoby to kontynuowane.
Michael Myers

2

Moim zdaniem, bloki try / catch są niezbędne, aby zapewnić odpowiednią obsługę wyjątków, ale tworzenie takich bloków ma wpływ na wydajność. Ponieważ pętle zawierają intensywne powtarzalne obliczenia, nie zaleca się umieszczania bloków try / catch w pętlach. Ponadto wydaje się, że tam, gdzie występuje ten warunek, często jest wychwytywany „Wyjątek” lub „RuntimeException”. Należy unikać przechwycenia wyjątku RuntimeException w kodzie. Ponownie, jeśli pracujesz w dużej firmie, konieczne jest prawidłowe zarejestrowanie tego wyjątku lub zatrzymanie wyjątku środowiska wykonawczego. Cały punkt tego opisu jestPLEASE AVOID USING TRY-CATCH BLOCKS IN LOOPS


1

ustawienie specjalnej ramki stosu dla try / catch powoduje dodatkowe obciążenie, ale JVM może być w stanie wykryć fakt powrotu i zoptymalizować to.

w zależności od liczby iteracji różnica w wydajności będzie prawdopodobnie nieistotna.

Jednak zgadzam się z innymi, że umieszczenie go poza pętlą sprawia, że ​​korpus pętli wygląda na czystszy.

Jeśli istnieje szansa, że ​​kiedykolwiek będziesz chciał kontynuować przetwarzanie zamiast wyjść, jeśli jest niepoprawny numer, wtedy kod powinien znajdować się w pętli.


1

Jeśli jest w środku, zyskasz narzut struktury struktury try / catch N razy, a nie tylko raz na zewnątrz.


Za każdym razem, gdy wywoływana jest struktura Try / Catch, nakłada się to na wykonanie metody. Wystarczy odrobina pamięci i tików procesora potrzebnych do poradzenia sobie ze strukturą. Jeśli uruchamiasz pętlę 100 razy, i dla hipotetycznej przyczyny, powiedzmy, że koszt to 1 tyknięcie za próbę / złapanie połączenia, wtedy posiadanie Try / Catch w pętli kosztuje 100 tyknięć, w przeciwieństwie do tylko 1 tyknięcia, jeśli jest to poza pętlą.


Czy potrafisz trochę rozwinąć ogólne koszty?
Michael Myers

1
Okej, ale Jeffrey L. Whitledge mówi, że nie ma dodatkowych kosztów ogólnych. Czy możesz podać źródło, które mówi, że istnieje?
Michael Myers

Koszt jest niewielki, prawie nieistotny, ale tam jest - wszystko ma swój koszt. Oto artykuł na blogu z Nieba programisty ( tiny.cc/ZBX1b ) dotyczący platformy .NET, a także post grupy dyskusyjnej Reshat Sabiq dotyczący JAVA ( tiny.cc/BV2hS )
Stephen Wrighton

Rozumiem ... Więc teraz pytanie brzmi, czy koszt poniesiony przy wejściu do try / catch (w którym to przypadku powinien być poza pętlą), czy też jest stały przez czas trwania try / catch (w którym to przypadku powinien być w pętli)? Mówisz, że to nr 1. I wciąż nie jestem do końca przekonany, że jest jakiś koszt.
Michael Myers

1
Według tej książki Google Book ( tinyurl.com/3pp7qj ) „najnowsze maszyny wirtualne nie wykazują kary”.
Michael Myers

1

Chodzi przede wszystkim o to, by zachęcić do pierwszego stylu: umożliwienia skonsolidowania obsługi błędów i obsługi tylko raz, a nie natychmiast w każdym możliwym miejscu błędu.


Źle! Wyjątkiem jest określenie, jakie wyjątkowe zachowanie należy zastosować, gdy wystąpi „błąd”. Zatem wybór wewnętrznego lub zewnętrznego bloku try / catch naprawdę zależy od tego, jak chcesz obsłużyć leczenie pętli.
gizmo

Można to zrobić równie dobrze za pomocą błędów poprzedzających wyjątek, takich jak przekazywanie flag „wystąpił błąd”.
wnoise

1

włóż to do środka. Możesz kontynuować przetwarzanie (jeśli chcesz) lub możesz rzucić pomocny wyjątek, który informuje klienta o wartości myString i indeksie tablicy zawierającej złą wartość. Myślę, że NumberFormatException powie ci już złą wartość, ale zasadą jest umieszczenie wszystkich pomocnych danych w zgłaszanych wyjątkach. Pomyśl o tym, co byłoby dla ciebie interesujące w debuggerze w tym momencie programu.

Rozważać:

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

W razie potrzeby naprawdę docenisz taki wyjątek z jak największą ilością informacji.


Tak, jest to również ważny punkt. W moim przypadku czytam z pliku, więc wszystko, czego naprawdę potrzebuję, to ciąg, którego nie udało się przeanalizować. Więc zrobiłem System.out.println (ex) zamiast zgłosić inny wyjątek (chcę przeanalizować tyle plików, ile jest legalne).
Michael Myers

1

Chciałbym dodać własne 0.02co dwóch konkurencyjnych zagadnieniach, gdy patrzę na ogólny problem miejsca, w którym należy ustawić obsługę wyjątków:

  1. „Szersza” odpowiedzialność try-catchbloku (tj. Poza twoją pętlą w twoim przypadku) oznacza, że ​​zmieniając kod w pewnym późniejszym momencie, możesz przez pomyłkę dodać linię obsługiwaną przez istniejący catchblok; być może nieumyślnie. W twoim przypadku jest to mniej prawdopodobne, ponieważ wyraźnie łapieszNumberFormatException

  2. Im „węższa” odpowiedzialność try-catchbloku, tym trudniejsze staje się refaktoryzowanie. Szczególnie, gdy (jak w twoim przypadku) wykonujesz instrukcję „nielokalną” z poziomu catchbloku ( return nullinstrukcji).


1

To zależy od obsługi awarii. Jeśli chcesz tylko pominąć elementy błędu, spróbuj w środku:

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

W każdym innym przypadku wolałbym spróbować na zewnątrz. Kod jest bardziej czytelny, bardziej przejrzysty. Może lepiej byłoby zgłosić wyjątek IllegalArgumentException w przypadku błędu, jeśli zwraca wartość null.


1

Wrzucę moje 0,02 $. Czasami kończy się konieczność dodania „w końcu” później w kodzie (ponieważ kto kiedykolwiek pisze swój kod doskonale za pierwszym razem?). W takich przypadkach nagle sensowniej jest spróbować try / catch poza pętlą. Na przykład:

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

Ponieważ jeśli pojawi się błąd, czy nie, chcesz tylko raz zwolnić połączenie z bazą danych (lub wybrać ulubiony typ innego zasobu ...).


1

Innym aspektem nie wymienionym powyżej jest fakt, że każdy try-catch ma trochę wpływ na stos, co może mieć wpływ na metody rekurencyjne.

Jeśli metoda „outer ()” wywołuje metodę „inner ()” (która może wywoływać się rekurencyjnie), spróbuj zlokalizować try-catch w metodzie „outer ()”, jeśli to możliwe. Prosty przykład „awarii stosu”, którego używamy w klasie wydajności, kończy się niepowodzeniem przy około 6400 klatkach, gdy try-catch jest w metodzie wewnętrznej, i przy około 11,600, gdy jest w metodzie zewnętrznej.

W prawdziwym świecie może to stanowić problem, jeśli używasz wzoru złożonego i masz duże, złożone struktury zagnieżdżone.


0

Jeśli chcesz uchwycić wyjątek dla każdej iteracji lub sprawdzić, co to jest wyjątek generowany i złapać wszystkie wyjątki w itertaion, umieść try ... catch wewnątrz pętli. Nie spowoduje to przerwania pętli, jeśli wystąpi wyjątek i można przechwycić każdy wyjątek w każdej iteracji w całej pętli.

Jeśli chcesz przerwać pętlę i zbadać wyjątek za każdym razem, gdy zostanie zgłoszony, użyj try ... catch out of the loop. Spowoduje to przerwanie pętli i wykonanie instrukcji po złapaniu (jeśli występuje).

Wszystko zależy od twoich potrzeb. Wolę używać try ... catch wewnątrz pętli podczas wdrażania, ponieważ jeśli wystąpi wyjątek, wyniki nie są niejednoznaczne, a pętla nie ulegnie uszkodzeniu i nie wykona się całkowicie.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.