Haruskah coba ... tangkap masuk ke dalam atau di luar lingkaran?


185

Saya memiliki lingkaran yang terlihat seperti ini:

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

Ini adalah konten utama dari metode yang tujuan utamanya adalah mengembalikan array float. Saya ingin metode ini kembali nulljika ada kesalahan, jadi saya meletakkan loop di dalam try...catchblok, seperti ini:

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

Tapi kemudian saya juga berpikir untuk meletakkan try...catchblok di dalam loop, seperti ini:

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

Apakah ada alasan, kinerja atau sebaliknya, untuk lebih memilih satu daripada yang lain?


Sunting: Konsensus tampaknya lebih bersih untuk meletakkan loop di dalam try / catch, mungkin di dalam metodenya sendiri. Namun, masih ada perdebatan tentang mana yang lebih cepat. Bisakah seseorang menguji ini dan kembali dengan jawaban yang seragam?


2
Saya tidak tahu tentang kinerjanya, tetapi kodenya terlihat lebih bersih dengan try catch di luar for for.
Jack B Nimble

Jawaban:


132

KINERJA:

Sama sekali tidak ada perbedaan kinerja di mana struktur coba / tangkap ditempatkan. Secara internal, mereka diimplementasikan sebagai tabel rentang kode dalam struktur yang dibuat ketika metode dipanggil. Ketika metode ini dieksekusi, struktur try / catch benar-benar keluar dari gambar kecuali terjadi lemparan, kemudian lokasi kesalahan dibandingkan dengan tabel.

Berikut referensi: http://www.javaworld.com/javaworld/jw-01-1997/jw-01-hood.html

Tabel dijelaskan sekitar setengah jalan.


Oke, jadi maksudmu tidak ada perbedaan kecuali estetika. Bisakah Anda menautkan ke sumber?
Michael Myers

2
Terima kasih. Saya kira hal-hal mungkin telah berubah sejak 1997, tetapi mereka hampir tidak membuatnya kurang efisien.
Michael Myers

1
Benar-benar kata yang sulit. Dalam beberapa kasus dapat menghambat optimisasi kompiler. Bukan biaya langsung, tetapi mungkin merupakan penalti kinerja tidak langsung.
Tom Hawtin - tackline

2
Benar, saya kira saya seharusnya sedikit berkuasa dalam antusiasme saya, tetapi informasi yang salah itu membuat saya gelisah! Dalam hal optimasi, karena kita tidak dapat mengetahui ke arah mana kinerja akan terpengaruh, saya kira kembali untuk mencoba dan menguji (seperti biasa).
Jeffrey L Whitledge

1
tidak ada perbedaan kinerja, hanya jika kode berjalan tanpa pengecualian.
Onur Bıyık

72

Kinerja : seperti yang dikatakan Jeffrey dalam jawabannya, di Jawa tidak ada banyak perbedaan.

Secara umum , untuk keterbacaan kode, pilihan Anda di mana untuk menangkap pengecualian tergantung pada apakah Anda ingin loop tetap diproses atau tidak.

Dalam contoh Anda, Anda kembali setelah menangkap pengecualian. Dalam hal ini, saya akan mencoba / menangkap loop. Jika Anda hanya ingin menangkap nilai buruk tetapi terus memproses, masukkan ke dalam.

Cara ketiga : Anda selalu bisa menulis metode ParseFloat statis Anda sendiri dan memiliki penanganan pengecualian yang ditangani dalam metode itu daripada loop Anda. Membuat penanganan pengecualian terisolasi ke loop itu sendiri!

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
Lihat lebih dekat. Dia keluar dengan pengecualian pertama di keduanya. Saya juga memikirkan hal yang sama pada awalnya.
kervin

2
Saya sadar, itu sebabnya saya katakan dalam kasusnya, karena dia kembali ketika dia mengenai masalah maka saya akan menggunakan tangkapan luar. Untuk lebih jelasnya, itulah aturan yang akan saya gunakan ...
Ray Hayes

Kode yang bagus, tapi sayangnya Java tidak memiliki parameter parameter mewah.
Michael Myers

ByRef? Sudah begitu lama sejak saya menyentuh Jawa!
Ray Hayes

2
Orang lain menyarankan TryParse yang di C # /. Net memungkinkan Anda melakukan parse aman tanpa berurusan dengan pengecualian, yang sekarang direplikasi dan metode ini dapat digunakan kembali. Adapun pertanyaan asli, saya lebih suka loop di dalam tangkapan, itu hanya terlihat lebih bersih bahkan jika tidak ada masalah kinerja ..
Ray Hayes

46

Baiklah, setelah Jeffrey L Whitledge mengatakan bahwa tidak ada perbedaan kinerja (pada tahun 1997), saya pergi dan mengujinya. Saya menjalankan patokan kecil ini:

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

Saya memeriksa bytecode yang dihasilkan menggunakan javap untuk memastikan bahwa tidak ada yang diuraikan.

Hasil penelitian menunjukkan bahwa, dengan asumsi optimasi JIT tidak signifikan, Jeffrey benar ; sama sekali tidak ada perbedaan kinerja pada Java 6, Sun client VM (Saya tidak memiliki akses ke versi lain). Perbedaan waktu total adalah pada urutan beberapa milidetik selama seluruh tes.

Karena itu, satu-satunya pertimbangan adalah apa yang terlihat paling bersih. Saya menemukan bahwa cara kedua jelek, jadi saya akan tetap pada cara pertama atau cara Ray Hayes .


Jika exception-handler mengambil aliran di luar loop, maka saya merasa paling jelas untuk menempatkannya di luar loop - Daripada menempatkan klausa tangkapan tampaknya dalam loop, yang tetap kembali. Saya menggunakan garis spasi putih di sekitar tubuh "tertangkap" dari metode "tangkap semua" itu, untuk lebih jelas memisahkan tubuh dari blok percobaan yang semuanya tertutup .
Thomas W

16

Meskipun kinerjanya mungkin sama dan apa yang "terlihat" lebih baik sangat subyektif, masih ada perbedaan fungsionalitas yang cukup besar. Ambil contoh berikut:

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

Loop sementara berada di dalam blok coba tangkap, variabel 'j' bertambah hingga mencapai 40, dicetak ketika j mod 4 bernilai nol dan pengecualian dilemparkan ketika j mencapai 20.

Sebelum ada detail, berikut ini contoh lainnya:

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

Logika yang sama seperti di atas, satu-satunya perbedaan adalah bahwa blok try / catch sekarang berada di dalam loop while.

Di sinilah output (saat dalam coba / tangkap):

4
8
12 
16
in catch block

Dan output lainnya (coba / tangkap sementara):

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

Di sana Anda memiliki perbedaan yang cukup signifikan:

saat di coba / tangkap keluar dari loop

coba / tangkap sementara loop tetap aktif


Jadi letakkan di try{} catch() {}sekitar loop jika loop diperlakukan sebagai satu "unit" dan menemukan masalah dalam unit yang membuat seluruh "unit" (atau loop dalam kasus ini) pergi ke selatan. Masukkan try{} catch() {}di dalam loop jika Anda memperlakukan iterasi tunggal dari loop, atau elemen tunggal dalam array sebagai "unit" keseluruhan. Jika pengecualian dalam "unit" itu terjadi, kita lewati saja elemen itu dan kemudian kita lanjutkan ke iterasi loop berikutnya.
Galaxy

14

Saya setuju dengan semua posting kinerja dan keterbacaan. Namun, ada kasus di mana itu benar-benar penting. Beberapa orang lain menyebutkan ini, tetapi mungkin lebih mudah untuk melihat dengan contoh.

Pertimbangkan contoh yang sedikit dimodifikasi ini:

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

Jika Anda ingin metode parseAll () mengembalikan null jika ada kesalahan (seperti contoh asli), Anda akan meletakkan try / catch di luar seperti ini:

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

Pada kenyataannya, Anda mungkin harus mengembalikan kesalahan di sini alih-alih nol, dan umumnya saya tidak suka memiliki banyak pengembalian, tetapi Anda mendapatkan idenya.

Di sisi lain, jika Anda hanya ingin mengabaikan masalahnya, dan mengurai Strings apa pun yang bisa, Anda akan meletakkan try / catch di bagian dalam loop seperti ini:

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
Itu sebabnya saya berkata, "Jika Anda hanya ingin menangkap nilai buruk tetapi teruskan pemrosesan, letakkan di dalam."
Ray Hayes

5

Seperti yang sudah disebutkan, kinerjanya sama. Namun, pengalaman pengguna tidak harus sama. Dalam kasus pertama, Anda akan gagal cepat (yaitu setelah kesalahan pertama), namun jika Anda meletakkan blok coba / tangkap di dalam loop, Anda bisa menangkap semua kesalahan yang akan dibuat untuk panggilan yang diberikan ke metode. Saat mem-parsing array nilai dari string tempat Anda mengharapkan beberapa kesalahan pemformatan, pasti ada kasus di mana Anda ingin dapat menyajikan semua kesalahan kepada pengguna sehingga mereka tidak perlu mencoba dan memperbaikinya satu per satu .


Ini tidak benar untuk kasus khusus ini (saya akan kembali di blok tangkapan), tetapi itu hal yang baik untuk diingat secara umum.
Michael Myers

4

Jika semuanya gagal, maka format pertama masuk akal. Jika Anda ingin dapat memproses / mengembalikan semua elemen yang tidak gagal, Anda perlu menggunakan formulir kedua. Itu akan menjadi kriteria dasar saya untuk memilih antara metode. Secara pribadi, jika semuanya atau tidak sama sekali, saya tidak akan menggunakan bentuk kedua.


4

Selama Anda menyadari apa yang harus Anda capai dalam loop, Anda bisa meletakkan try catch di luar loop. Tetapi penting untuk memahami bahwa perulangan akan berakhir segera setelah pengecualian terjadi dan itu mungkin tidak selalu seperti yang Anda inginkan. Ini sebenarnya kesalahan yang sangat umum dalam perangkat lunak berbasis Java. Orang perlu memproses sejumlah item, seperti mengosongkan antrian, dan secara salah mengandalkan pernyataan coba-coba luar yang menangani semua kemungkinan pengecualian. Mereka juga bisa menangani hanya pengecualian khusus di dalam loop dan tidak mengharapkan pengecualian lain terjadi. Kemudian jika pengecualian terjadi yang tidak ditangani di dalam loop maka loop akan "dipraih", itu mungkin berakhir sebelum waktunya dan pernyataan tangkapan luar menangani pengecualian.

Jika loop memiliki peran dalam kehidupan untuk mengosongkan antrian maka loop itu sangat mungkin bisa berakhir sebelum antrian itu benar-benar dikosongkan. Kesalahan yang sangat umum.


2

Dalam contoh Anda tidak ada perbedaan fungsional. Saya menemukan contoh pertama Anda lebih mudah dibaca.


2

Anda harus memilih versi luar daripada versi dalam. Ini hanya versi spesifik dari aturan, pindahkan apa pun di luar loop yang dapat Anda pindahkan di luar loop. Bergantung pada kompiler IL dan kompiler JIT, dua versi Anda mungkin atau mungkin tidak berakhir dengan karakteristik kinerja yang berbeda.

Pada catatan lain Anda mungkin harus melihat float.TryParse atau Convert.ToFloat.


2

Jika Anda memasukkan try / catch ke dalam loop, Anda akan terus mengulang setelah pengecualian. Jika Anda meletakkannya di luar loop, Anda akan berhenti segera setelah pengecualian dilemparkan.


Yah, saya mengembalikan null dengan pengecualian, jadi tidak akan terus memproses dalam kasus saya.
Michael Myers

2

Perspektif saya akan mencoba / menangkap blok diperlukan untuk memastikan penanganan pengecualian yang tepat, tetapi membuat blok tersebut memiliki implikasi kinerja. Karena, Loop berisi perhitungan berulang yang intensif, tidak disarankan untuk menempatkan blok percobaan / tangkap di dalam loop. Selain itu, tampaknya di mana kondisi ini terjadi, sering kali "Pengecualian" atau "RuntimeException" yang tertangkap. RuntimeException tertangkap dalam kode harus dihindari. Sekali lagi, jika jika Anda bekerja di perusahaan besar, penting untuk mencatat pengecualian itu dengan benar, atau menghentikan runtime exception untuk terjadi. Inti dari deskripsi ini adalahPLEASE AVOID USING TRY-CATCH BLOCKS IN LOOPS


1

mengatur bingkai stack khusus untuk try / catch menambahkan overhead tambahan, tetapi JVM mungkin dapat mendeteksi fakta bahwa Anda kembali dan mengoptimalkannya.

tergantung pada jumlah iterasi, perbedaan kinerja kemungkinan akan diabaikan.

Namun saya setuju dengan yang lain yang memilikinya di luar loop membuat loop body terlihat lebih bersih.

Jika ada kemungkinan Anda ingin melanjutkan dengan proses alih-alih keluar jika ada nomor yang tidak valid, maka Anda ingin kode berada di dalam loop.


1

Jika ada di dalam, maka Anda akan mendapatkan overhead dari struktur try / catch N kali, bukan hanya sekali di luar.


Setiap kali struktur Coba / Tangkap dipanggil, ia menambah biaya overhead untuk pelaksanaan metode. Hanya sedikit kutu memori & prosesor yang diperlukan untuk menangani struktur. Jika Anda menjalankan loop 100 kali, dan demi hipotetis, katakanlah biayanya 1 tick per try / catch call, maka memiliki Try / Catch di dalam loop berharga 100 ticks, dibandingkan dengan hanya 1 tick jika itu di luar loop.


Bisakah Anda menguraikan sedikit di atas kepala?
Michael Myers

1
Oke, tapi Jeffrey L Whitledge mengatakan tidak ada overhead tambahan. Bisakah Anda memberikan sumber yang mengatakan ada?
Michael Myers

Biayanya kecil, hampir dapat diabaikan, tetapi ada di sana - semuanya memiliki biaya. Berikut ini adalah artikel blog dari Programmer's Heaven ( tiny.cc/ZBX1b ) tentang .NET dan di sini ada posting newsgroup dari Reshat Sabiq mengenai JAVA ( tiny.cc/BV2hS )
Stephen Wrighton

Saya mengerti ... Jadi sekarang pertanyaannya adalah, apakah biaya yang dikeluarkan untuk memasukkan try / catch (dalam hal ini harus di luar loop), atau apakah itu konstan selama durasi try / catch (dalam hal ini harus berada di dalam loop)? Anda mengatakan itu # 1. Dan aku masih belum sepenuhnya yakin ada adalah biaya.
Michael Myers

1
Menurut Google Book ini ( tinyurl.com/3pp7qj ), "VM terbaru tidak menunjukkan penalti".
Michael Myers

1

Inti dari pengecualian adalah untuk mendorong gaya pertama: membiarkan penanganan kesalahan dikonsolidasikan dan ditangani satu kali, tidak segera di setiap situs kesalahan yang mungkin.


Salah! Inti dari pengecualian adalah untuk menentukan perilaku luar biasa apa yang harus diadopsi ketika "kesalahan" terjadi. Jadi memilih blok percobaan / penangkapan dalam atau luar sangat tergantung pada bagaimana Anda ingin menangani perawatan loop.
alat

Itu dapat dilakukan sama baiknya dengan perawatan kesalahan pra-pengecualian, seperti melewati bendera "terjadi kesalahan" di sekitar.
Berkumandang

1

taruh di dalam. Anda dapat terus memproses (jika Anda mau) atau Anda dapat membuang pengecualian yang membantu yang memberi tahu klien nilai myString dan indeks array yang berisi nilai buruk. Saya pikir NumberFormatException sudah akan memberi tahu Anda nilai buruk tetapi prinsipnya adalah untuk menempatkan semua data yang bermanfaat dalam pengecualian yang Anda buang. Pikirkan tentang apa yang akan menarik bagi Anda di debugger pada titik ini dalam program.

Mempertimbangkan:

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

Pada saat dibutuhkan Anda akan sangat menghargai pengecualian seperti ini dengan sebanyak mungkin informasi di dalamnya.


Ya, ini juga poin yang valid. Dalam kasus saya, saya membaca dari file, jadi semua yang saya butuhkan adalah string yang gagal diurai. Jadi saya melakukan System.out.println (ex) alih-alih melempar pengecualian lain (saya ingin mem-parsing sebanyak file yang legal).
Michael Myers

1

Saya ingin menambahkan pendapat saya sendiri 0.02ctentang dua pertimbangan yang bersaing ketika melihat masalah umum di mana harus menempatkan penanganan pengecualian:

  1. "Lebih luas" tanggung jawab try-catchblok (yaitu di luar loop dalam kasus Anda) berarti bahwa ketika mengubah kode di beberapa titik kemudian, Anda mungkin keliru menambahkan garis yang ditangani oleh catchblok Anda yang ada ; mungkin tidak sengaja. Dalam kasus Anda, ini lebih kecil kemungkinannya karena Anda secara eksplisit menangkap aNumberFormatException

  2. Semakin "sempit" tanggung jawab try-catchblok, semakin sulit menjadi refactoring. Terutama ketika (seperti dalam kasus Anda) Anda mengeksekusi instruksi "non-lokal" dari dalam catchblok ( return nullpernyataan).


1

Itu tergantung pada penanganan kegagalan. Jika Anda hanya ingin melewatkan elemen kesalahan, coba di dalam:

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

Dalam kasus lain saya lebih suka mencoba di luar. Kode lebih mudah dibaca, lebih bersih. Mungkin akan lebih baik untuk melempar IllegalArgumentException dalam kasus kesalahan daripada mengembalikan null.


1

Saya akan memasukkan $ 0,02 saya. Kadang-kadang Anda akhirnya perlu menambahkan "akhirnya" nanti dalam kode Anda (karena siapa yang pernah menulis kode mereka dengan sempurna pertama kali?). Dalam kasus tersebut, tiba-tiba lebih masuk akal untuk mencoba / menangkap di luar loop. Sebagai contoh:

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

Karena jika Anda mendapatkan kesalahan, atau tidak, Anda hanya ingin melepaskan koneksi basis data Anda (atau memilih jenis sumber daya favorit lainnya ...) satu kali.


1

Aspek lain yang tidak disebutkan di atas adalah kenyataan bahwa setiap try-catch memiliki beberapa dampak pada stack, yang dapat memiliki implikasi untuk metode rekursif.

Jika metode "outer ()" memanggil metode "inner ()" (yang dapat menyebut dirinya secara rekursif), cobalah untuk menemukan try-catch dalam metode "outer ()" jika memungkinkan. Contoh "stack crash" sederhana yang kami gunakan di kelas kinerja gagal pada sekitar 6.400 frame ketika try-catch berada di metode dalam, dan sekitar 11.600 saat berada di metode luar.

Di dunia nyata, ini bisa menjadi masalah jika Anda menggunakan pola Komposit dan memiliki struktur bersarang yang besar dan kompleks.


0

Jika Anda ingin menangkap Pengecualian untuk setiap iterasi, atau memeriksa di mana Pengecualian iterasi dilemparkan dan menangkap setiap Pengecualian dalam iterasi, tempat coba ... menangkap di dalam loop. Ini tidak akan memutus loop jika Pengecualian terjadi dan Anda dapat menangkap setiap Pengecualian di setiap iterasi sepanjang loop.

Jika Anda ingin memutus perulangan dan memeriksa Pengecualian kapan pun dilemparkan, gunakan coba ... catch out of the loop. Ini akan memutus loop dan menjalankan pernyataan setelah catch (jika ada).

Itu semua tergantung kebutuhan Anda. Saya lebih suka menggunakan try ... tangkap di dalam loop saat menggunakan sebagai, jika Pengecualian terjadi, hasilnya tidak ambigu dan loop tidak akan rusak dan dieksekusi sepenuhnya.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.