Deve tentar ... pegar ir dentro ou fora de um loop?

185

Eu tenho um loop que se parece com isso:

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

Esse é o conteúdo principal de um método cujo único objetivo é retornar a matriz de flutuadores. Quero que esse método retorne nullse houver um erro, então coloquei o loop dentro de um try...catchbloco, assim:

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

Mas também pensei em colocar o try...catchbloco dentro do loop, assim:

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

Existe algum motivo, desempenho ou não, para preferir um ao outro?


Edit: O consenso parece ser que é mais limpo colocar o loop dentro do try / catch, possivelmente dentro de seu próprio método. No entanto, ainda há um debate sobre o que é mais rápido. Alguém pode testar isso e voltar com uma resposta unificada?

Michael Myers
fonte
2
Não sei sobre o desempenho, mas o código parece mais limpo com a tentativa de capturar fora do loop for.
Jack B Nimble

Respostas:

132

DESEMPENHO:

Não há absolutamente nenhuma diferença de desempenho no local em que as estruturas try / catch são colocadas. Internamente, eles são implementados como uma tabela de intervalo de código em uma estrutura criada quando o método é chamado. Enquanto o método está em execução, as estruturas try / catch ficam completamente fora de cena, a menos que ocorra um lançamento, o local do erro é comparado com a tabela.

Aqui está uma referência: http://www.javaworld.com/javaworld/jw-01-1997/jw-01-hood.html

A tabela é descrita na metade do caminho.

Jeffrey L Whitledge
fonte
Ok, então você está dizendo que não há diferença, exceto estética. Você pode vincular a uma fonte?
Michael Myers
2
Obrigado. Suponho que as coisas possam ter mudado desde 1997, mas dificilmente a tornariam menos eficiente.
Michael Myers
1
Absolutamente é uma palavra difícil. Em alguns casos, pode inibir as otimizações do compilador. Não é um custo direto, mas pode ser uma penalidade indireta de desempenho.
Tom Hawtin - tackline
2
É verdade que acho que deveria ter reinado um pouco meu entusiasmo, mas as informações erradas estavam me dando nos nervos! No caso de otimizações, como não podemos saber de que maneira o desempenho seria afetado, acho que voltamos a experimentar e testar (como sempre).
Jeffrey L Whitledge
1
não há diferença de desempenho, apenas se o código for executado sem exceções.
Onur Bíyık
72

Desempenho : como Jeffrey disse em sua resposta, em Java não faz muita diferença.

Geralmente , para facilitar a leitura do código, sua escolha de onde capturar a exceção depende se você deseja que o loop continue processando ou não.

No seu exemplo, você retornou ao capturar uma exceção. Nesse caso, eu colocaria o try / catch em volta do loop. Se você simplesmente deseja pegar um valor ruim, mas continuar o processamento, coloque-o dentro.

A terceira maneira : você sempre pode escrever seu próprio método estático ParseFloat e ter o tratamento de exceções tratado nesse método, e não no seu loop. Tornando a manipulação de exceção isolada para o próprio loop!

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;
    }
}
Ray Hayes
fonte
1
Olhar mais de perto. Ele está saindo na primeira exceção em ambos. Eu pensei a mesma coisa também no começo.
kervin
2
Eu estava ciente, foi por isso que disse no caso dele, já que ele está retornando quando enfrenta um problema, então eu usaria uma captura externa. Para maior clareza, essa é a regra que eu usaria ...
Ray Hayes
Código legal, mas infelizmente o Java não tem o luxo de parâmetros externos.
Michael Myers
ByRef? Faz tanto tempo desde que toquei em Java!
Ray Hayes
2
Alguém sugeriu o TryParse, que em C # / .net permite fazer uma análise segura sem lidar com exceções, que agora é replicada e o método é reutilizável. Quanto à questão original, eu prefiro o loop dentro da captura, ele só parece mais limpo, mesmo se não houver nenhum problema de desempenho ..
Ray Hayes
46

Tudo bem, depois que Jeffrey L Whitledge disse que não havia diferença de desempenho (em 1997), eu fui e testei. Eu executei este pequeno benchmark:

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

Eu verifiquei o bytecode resultante usando javap para garantir que nada fique embutido.

Os resultados mostraram que, assumindo otimizações insignificantes no JIT, Jeffrey está correto ; não há absolutamente nenhuma diferença de desempenho no Java 6, VM cliente Sun (não tive acesso a outras versões). A diferença de tempo total é da ordem de alguns milissegundos durante todo o teste.

Portanto, a única consideração é o que parece mais limpo. Acho que a segunda maneira é feia, então vou me ater à primeira ou à maneira de Ray Hayes .

Michael Myers
fonte
Se o manipulador de exceções obtiver fluxo fora do loop, acho mais claro colocá-lo fora do loop - em vez de colocar uma cláusula catch aparentemente dentro do loop, que, no entanto, retorna. Eu uso uma linha de espaço em branco ao redor do corpo "capturado" de tais métodos "catch-all", para separar mais claramente o corpo do bloco try abrangente .
Thomas W
16

Embora o desempenho possa ser o mesmo e o que "pareça" melhor seja muito subjetivo, ainda há uma grande diferença na funcionalidade. Veja o seguinte exemplo:

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

O loop while está dentro do bloco try catch, a variável 'j' é incrementada até atingir 40, impressa quando j mod 4 é zero e uma exceção é lançada quando j atinge 20.

Antes de qualquer detalhe, aqui está o outro exemplo:

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

A mesma lógica acima, a única diferença é que o bloco try / catch está agora dentro do loop while.

Aí vem a saída (enquanto em try / catch):

4
8
12 
16
in catch block

E a outra saída (try / catch in while):

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

Lá você tem uma diferença bastante significativa:

enquanto em try / catch sai do loop

try / catch enquanto mantém o loop ativo

seBaka28
fonte
Então, rode try{} catch() {}o loop se o loop for tratado como uma única "unidade" e encontrar um problema nessa unidade faz com que toda a "unidade" (ou loop neste caso) vá para o sul. Coloque try{} catch() {}dentro do loop se estiver tratando uma única iteração do loop ou um único elemento em uma matriz como uma "unidade" inteira. Se uma exceção nessa "unidade" ocorrer, simplesmente pularemos esse elemento e continuaremos para a próxima iteração do loop.
Galaxy
14

Concordo com todas as postagens de desempenho e legibilidade. No entanto, existem casos em que isso realmente importa. Algumas outras pessoas mencionaram isso, mas pode ser mais fácil ver com exemplos.

Considere este exemplo ligeiramente modificado:

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

Se você deseja que o método parseAll () retorne nulo se houver algum erro (como o exemplo original), coloque o try / catch do lado de fora da seguinte maneira:

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

Na realidade, você provavelmente deve retornar um erro aqui em vez de nulo, e geralmente não gosto de ter vários retornos, mas você entendeu.

Por outro lado, se você quiser que ele ignore os problemas e analise o que for possível, poderá colocar o try / catch dentro do loop da seguinte maneira:

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;
}
Matt N
fonte
2
Foi por isso que eu disse: "Se você simplesmente deseja obter um valor ruim, mas continuar o processamento, coloque-o dentro".
Ray Hayes
5

Como já mencionado, o desempenho é o mesmo. No entanto, a experiência do usuário não é necessariamente idêntica. No primeiro caso, você falhará rapidamente (ou seja, após o primeiro erro); no entanto, se você colocar o bloco try / catch dentro do loop, poderá capturar todos os erros que seriam criados para uma determinada chamada ao método. Ao analisar uma matriz de valores de cadeias em que você espera alguns erros de formatação, há definitivamente casos em que você gostaria de apresentar todos os erros ao usuário para que eles não precisem tentar corrigi-los um a um .

user19810
fonte
Isso não é verdade para este caso em particular (estou retornando no bloco catch), mas é bom ter em mente em geral.
Michael Myers
4

Se falhar tudo ou nada, o primeiro formato faz sentido. Se você quiser processar / retornar todos os elementos que não falham, precisará usar o segundo formulário. Esses seriam meus critérios básicos para escolher entre os métodos. Pessoalmente, se for tudo ou nada, eu não usaria o segundo formulário.

Joe Skora
fonte
4

Contanto que você esteja ciente do que precisa realizar no loop, poderá tentar capturar fora do loop. Mas é importante entender que o loop terminará assim que a exceção ocorrer e que nem sempre é o que você deseja. Este é realmente um erro muito comum em software baseado em Java. As pessoas precisam processar vários itens, como esvaziar uma fila, e confiar falsamente em uma instrução try / catch externa que lida com todas as exceções possíveis. Eles também podem manipular apenas uma exceção específica dentro do loop e não esperam que ocorra qualquer outra exceção. Então, se ocorrer uma exceção que não é tratada dentro do loop, o loop será "preemted", finalizando possivelmente prematuramente e a instrução catch externa manipulará a exceção.

Se o loop tivesse como função na vida esvaziar uma fila, é provável que ele terminasse antes que a fila fosse realmente esvaziada. Falha muito comum.

Gunnar Forsgren - Mobimation
fonte
2

Nos seus exemplos, não há diferença funcional. Acho o seu primeiro exemplo mais legível.

Jamie
fonte
2

Você deve preferir a versão externa à versão interna. Esta é apenas uma versão específica da regra, mova qualquer coisa fora do loop que você possa mover para fora do loop. Dependendo do compilador IL e JIT, suas duas versões podem ou não ter características de desempenho diferentes.

Em outra nota, você provavelmente deve examinar float.TryParse ou Convert.ToFloat.

Orion Adrian
fonte
2

Se você colocar o try / catch dentro do loop, continuará fazendo o loop após uma exceção. Se você colocá-lo fora do loop, parará assim que uma exceção for lançada.

Joel Coehoorn
fonte
Bem, estou retornando nulo com exceção, para que não continue processando no meu caso.
Michael Myers
2

Minha perspectiva seria que os blocos try / catch são necessários para garantir o tratamento adequado das exceções, mas a criação desses blocos tem implicações no desempenho. Como os loops contêm cálculos repetitivos intensivos, não é recomendável colocar blocos try / catch dentro dos loops. Além disso, parece que onde essa condição ocorre, geralmente é "Exception" ou "RuntimeException" capturada. RuntimeException capturada no código deve ser evitada. Novamente, se você trabalha em uma grande empresa, é essencial registrar essa exceção corretamente ou impedir que a exceção de tempo de execução aconteça. O ponto inteiro desta descrição éPLEASE AVOID USING TRY-CATCH BLOCKS IN LOOPS

surendrapanday
fonte
1

a configuração de um quadro de pilha especial para try / catch adiciona uma sobrecarga adicional, mas a JVM pode detectar o fato de que você está retornando e otimizar isso.

dependendo do número de iterações, a diferença de desempenho provavelmente será insignificante.

No entanto, eu concordo com os outros que tê-lo fora do loop faz com que o corpo do loop pareça mais limpo.

Se houver uma chance de que você deseje continuar com o processamento, em vez de sair se houver um número inválido, convém que o código esteja dentro do loop.

Matt
fonte
1

Se estiver dentro, você ganhará a sobrecarga da estrutura try / catch N vezes, em vez de apenas uma vez do lado de fora.


Toda vez que uma estrutura Try / Catch é chamada, ela adiciona sobrecarga à execução do método. Apenas um pouco da memória e do processador necessários para lidar com a estrutura. Se você estiver executando um loop 100 vezes, e por uma questão hipotética, digamos que o custo seja de 1 tick por chamada try / catch, então ter o Try / Catch dentro do loop custa 100 ticks, em oposição a apenas 1 tick se for fora do loop.

Stephen Wrighton
fonte
Você pode elaborar um pouco sobre as despesas gerais?
Michael Myers
1
Ok, mas Jeffrey L Whitledge diz que não despesas extras. Você pode fornecer uma fonte que diz que existe?
Michael Myers
O custo é pequeno, quase insignificante, mas está lá - tudo tem um custo. Aqui está um artigo de blog a partir do programador Heaven ( tiny.cc/ZBX1b ) sobre .NET e aqui está um post newsgroup de Reshat Sabiq sobre Java ( tiny.cc/BV2hS )
Stephen Wrighton
Entendo ... Agora, agora a questão é: o custo incorrido ao inserir a tentativa / captura (nesse caso, deve estar fora do loop) ou é constante pela duração da tentativa / captura (nesse caso, deve estar dentro do loop)? Você diz que é o número 1. E eu estou ainda não inteiramente convencido lá é um custo.
Michael Myers
1
De acordo com este livro do Google ( tinyurl.com/3pp7qj ), "as últimas VMs não mostram penalidade".
Michael Myers
1

O ponto principal das exceções é incentivar o primeiro estilo: permitir que o tratamento de erros seja consolidado e tratado uma vez, e não imediatamente em todos os locais de erro possíveis.

wnoise
fonte
Errado! O ponto das exceções é determinar qual comportamento excepcional adotar quando ocorrer um "erro". Assim, a escolha do bloco try / catch interno ou externo depende realmente de como você deseja lidar com o tratamento do loop.
gizmo
Isso pode ser feito igualmente bem com tratamentos de erro anteriores a exceção, como passar sinalizadores de "erro ocorrido".
wnoise 28/09/08
1

coloque isso dentro. Você pode continuar processando (se desejar) ou pode lançar uma exceção útil que informa ao cliente o valor de myString e o índice da matriz que contém o valor inválido. Eu acho que NumberFormatException já vai lhe dizer o valor ruim, mas o princípio é colocar todos os dados úteis nas exceções que você lança. Pense no que seria interessante para você no depurador neste momento do programa.

Considerar:

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

Na hora da necessidade, você realmente apreciará uma exceção como essa, com o máximo de informações possível.

Kyle Dyer
fonte
Sim, este também é um ponto válido. No meu caso, estou lendo de um arquivo, então tudo o que realmente preciso é da sequência que falhou ao analisar. Então, eu fiz System.out.println (ex) em vez de lançar outra exceção (quero analisar o máximo de arquivo possível).
Michael Myers
1

Gostaria de acrescentar minhas 0.02cduas considerações concorrentes ao analisar o problema geral de onde posicionar o tratamento de exceções:

  1. A responsabilidade "mais ampla" do try-catchbloco (ou seja, fora do loop no seu caso) significa que, ao alterar o código em algum momento posterior, você pode adicionar por engano uma linha que é tratada pelo catchbloco existente ; possivelmente sem querer. No seu caso, isso é menos provável porque você está capturando explicitamente umNumberFormatException

  2. Quanto mais "restrita" a responsabilidade do try-catchbloco, mais difícil se torna a refatoração. Particularmente quando (como no seu caso) você está executando uma instrução "não local" de dentro do catchbloco (a return nullinstrução).

oxbow_lakes
fonte
1

Isso depende do tratamento de falhas. Se você apenas deseja pular os elementos de erro, tente dentro de:

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

Em qualquer outro caso, eu preferiria tentar fora. O código é mais legível, é mais limpo. Talvez seja melhor lançar uma IllegalArgumentException no caso de erro, se retornar nulo.

Arne Burmeister
fonte
1

Vou colocar meus US $ 0,02. Às vezes, você acaba adicionando um "finalmente" mais tarde no seu código (porque quem nunca escreveu o código perfeitamente na primeira vez?). Nesses casos, de repente faz mais sentido tentar / capturar fora do loop. Por exemplo:

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

Porque se você receber um erro ou não, você só deseja liberar sua conexão com o banco de dados (ou escolher seu tipo favorito de outro recurso ...) uma vez.

Salmo Ogro33
fonte
1

Outro aspecto não mencionado acima é o fato de que cada tentativa de captura tem algum impacto na pilha, o que pode ter implicações para métodos recursivos.

Se o método "outer ()" chamar o método "inner ()" (que pode se chamar recursivamente), tente localizar o try-catch no método "outer ()", se possível. Um exemplo simples de "falha na pilha" que usamos em uma classe de desempenho falha em cerca de 6.400 quadros quando o try-catch está no método interno e em cerca de 11.600 quando está no método externo.

No mundo real, isso pode ser um problema se você estiver usando o padrão Composite e tiver estruturas aninhadas grandes e complexas.

Jeff Hill
fonte
0

Se você deseja capturar Exception para cada iteração ou verificar em que iteração Exception é lançada e capturar todas as Exceções em uma iteração, coloque try ... catch dentro do loop. Isso não interromperá o loop se ocorrer uma exceção e você poderá capturar todas as exceções em cada iteração ao longo do loop.

Se você deseja interromper o loop e examinar a exceção sempre que lançada, use try ... catch fora do loop. Isso interromperá o loop e executará instruções após a captura (se houver).

Tudo depende da sua necessidade. Prefiro usar try ... catch dentro do loop durante a implantação, pois, se a exceção ocorrer, os resultados não serão ambíguos e o loop não será interrompido e executado completamente.

Juned Khan Momin
fonte