Por que Optional.get () sem chamar isPresent () é ruim, mas não iterator.next ()?


23

Ao usar a nova API Java de fluxos Java8 para encontrar um elemento específico em uma coleção, escrevo um código como este:

    String theFirstString = myCollection.stream()
            .findFirst()
            .get();

Aqui, o IntelliJ avisa que get () é chamado sem verificar primeiro isPresent ().

No entanto, este código:

    String theFirstString = myCollection.iterator().next();

... não dá aviso.

Existe alguma diferença profunda entre as duas técnicas, que torna a abordagem de streaming mais "perigosa" de alguma forma, que é crucial nunca chamar get () sem chamar isPresent () primeiro? Pesquisando no Google, encontro artigos falando sobre como os programadores são descuidados com o Opcional <> e presumo que eles possam chamar get () sempre.

O problema é que eu gosto de receber uma exceção o mais próximo possível do local onde meu código está com bugs, que nesse caso seria o local para onde estou chamando get () quando não há nenhum elemento a ser encontrado. Não quero escrever ensaios inúteis como:

    Optional<String> firstStream = myCollection.stream()
            .findFirst();
    if (!firstStream.isPresent()) {
        throw new IllegalStateException("There is no first string even though I totally expected there to be! <sarcasm>This exception is much more useful than NoSuchElementException</sarcasm>");
    }
    String theFirstString = firstStream.get();

... a menos que haja algum perigo em provocar exceções de get () das quais eu não conheço?


Depois de ler a resposta de Karl Bielefeldt e um comentário de Hulk, percebi que meu código de exceção acima era um pouco desajeitado, aqui está algo melhor:

    String errorString = "There is no first string even though I totally expected there to be! <sarcasm>This exception is much more useful than NoSuchElementException</sarcasm>";
    String firstString = myCollection.stream()
            .findFirst()
            .orElseThrow(() -> new IllegalStateException(errorString));

Isso parece mais útil e provavelmente virá natural em muitos lugares. Ainda sinto que desejo escolher um elemento de uma lista sem ter que lidar com tudo isso, mas pode ser que eu precise me acostumar com esses tipos de construções.


7
A parte seguinte da exceção do seu último exemplo pode ser muito mais concisa se você usar ouElseThrow - e ficará claro para todos os leitores que você pretende que a exceção seja lançada.
Hulk

Respostas:


12

Antes de tudo, considere que a verificação é complexa e provavelmente é relativamente demorada. Requer alguma análise estática e pode haver um pouco de código entre as duas chamadas.

Segundo, o uso idiomático das duas construções é muito diferente. Eu programo em tempo integral no Scala, que usa Optionsmuito mais frequentemente do que Java. Não me lembro de uma única instância em que precisei usar um .get()em um Option. Você quase sempre deve retornar o Optionalde sua função ou usar em seu orElse()lugar. Essas são maneiras muito superiores de lidar com valores ausentes do que lançar exceções.

Como .get()raramente deve ser chamado em uso normal, faz sentido que um IDE inicie uma verificação complexa quando vê um .get()para ver se foi usado corretamente. Por outro lado, iterator.next()é o uso adequado e onipresente de um iterador.

Em outras palavras, não se trata de um ser ruim e o outro não, é um caso comum que é fundamental para sua operação e é altamente provável que crie uma exceção durante os testes do desenvolvedor, e o outro seja um raramente usado caso que pode não ser exercido o suficiente para gerar uma exceção durante o teste do desenvolvedor. Esses são os tipos de casos que você deseja que os IDEs o avisem.


Pode ser que eu precise aprender mais sobre esse negócio opcional - vi o material java8 como uma boa maneira de fazer o mapeamento de listas de objetos que fazemos muito em nosso aplicativo da web. Então, você deve enviar <> s opcionais para todos os lugares? Isso vai levar algum tempo para se acostumar, mas isso é outro post do SE! :)
Frank da TV

@ Frank da TV É menos que eles devam estar em todos os lugares, e mais que eles permitem que você escreva funções que transformam um valor do tipo contido, e você as liga com mapou flatMap. Optionalsão uma ótima maneira de evitar ter que colocar tudo em nullcheques.
19416 Morgen

15

A classe Opcional deve ser usada quando não se sabe se o item que ela contém está presente ou não. O aviso existe para notificar os programadores de que existe um caminho de código possível adicional que eles podem manipular normalmente. A idéia é evitar que exceções sejam lançadas. O caso de uso que você apresenta não é para o qual foi projetado e, portanto, o aviso é irrelevante para você - se você realmente deseja que uma exceção seja lançada, vá em frente e desative-a (embora apenas para esta linha, e não globalmente), você deve tomar a decisão de tratar ou não o caso não presente em uma instância por instância, e o aviso o lembrará de fazer isso.

Indiscutivelmente, um aviso semelhante deve ser gerado para os iteradores. No entanto, isso nunca foi feito, e adicionar um agora provavelmente faria com que muitos avisos nos projetos existentes fossem uma boa idéia.

Observe também que lançar sua própria exceção pode ser mais útil do que apenas uma exceção padrão, pois incluir uma descrição de qual elemento deveria existir, mas não pode ser muito útil na depuração posteriormente.


6

Concordo que não há nenhuma diferença inerente a elas, no entanto, gostaria de apontar uma falha maior.

Nos dois casos, você tem um tipo, que fornece um método que não é garantido que funcione e que você deve chamar outro método antes disso. Se o seu IDE / compilador / etc avisa sobre um caso ou outro, depende apenas se ele suporta esse caso e / ou se você está configurado para isso.

Dito isto, os dois pedaços de código são cheiros de código. Essas expressões sugerem falhas de design subjacentes e produzem código quebradiço.

Você está certo em querer fracassar rapidamente, é claro, mas a solução, pelo menos para mim, não pode ser simplesmente encolher os ombros e jogar as mãos no ar (ou uma exceção na pilha).

Sabe-se que sua coleção não está vazia por enquanto, mas tudo em que você pode confiar é no seu tipo: Collection<T>- e isso não garante que você não seja vazio. Mesmo sabendo que agora não está vazio, um requisito alterado pode levar à alteração antecipada do código e, de repente, seu código é atingido por uma coleção vazia.

Agora você é livre para recuar e dizer aos outros que deveria ter uma lista não vazia. Você pode ficar feliz por encontrar esse 'erro' rapidamente. No entanto, você tem um software quebrado e precisa alterar seu código.

Compare isso com uma abordagem mais pragmática: como você obtém uma coleção e ela pode estar vazia, você se força a lidar com esse caso usando o # map opcional, #orElse ou qualquer outro desses métodos, exceto #get.

O compilador faz o máximo de trabalho possível para você, de modo que você pode confiar, tanto quanto possível, no contrato estático de obter a Collection<T>. Quando seu código nunca viola esse contrato (como por meio de uma dessas duas cadeias de chamadas fedorentas), seu código fica muito menos fragilizado com a alteração das condições ambientais.

No cenário acima, uma alteração que produza uma coleção de entradas vazia não terá nenhuma importância para o seu código. É apenas mais uma entrada válida e, portanto, ainda é tratada corretamente.

O custo disso é que você deve investir tempo em projetos melhores, em vez de a) arriscar código quebradiço ou b) complicar desnecessariamente as coisas lançando exceções.

Em uma nota final: como nem todos os IDEs / compiladores alertam sobre as chamadas iterator (). Next () ou optional.get () sem as verificações anteriores, esse código geralmente é sinalizado nas revisões. Pelo que vale, eu não permitiria que esse código fosse aprovado em uma revisão e exigisse uma solução limpa.


Acho que posso concordar um pouco sobre o cheiro do código - fiz o exemplo acima para fazer um pequeno post. Outro caso mais válido pode ser extrair um elemento obrigatório de uma lista, o que não é uma coisa estranha para aparecer em um aplicativo IMO.
Frank da TV

Spot on. "Escolha o elemento com a propriedade p da lista" -> list.stream (). Filter (p) .findFirst () .. e mais uma vez somos forçados a fazer a pergunta "e se ele não estiver lá?" .. pão e manteiga diariamente. Se é mandatoray e "just there there" - por que você o escondeu em várias outras coisas?
19416 Frank

4
Porque talvez seja uma lista carregada de um banco de dados. Talvez a lista seja uma coleção estatística reunida em alguma outra função: sabemos que temos os objetos A, B e C, quero encontrar a quantidade de qualquer coisa para B. Muitos casos válidos (e na minha aplicação, comuns).
Frank da TV

Não conheço seu banco de dados, mas o meu não garante pelo menos um resultado por consulta. O mesmo para coleções estatísticas - quem pode dizer que elas sempre estarão vazias? Concordo que é simples argumentar que deve haver esse ou aquele elemento, mas prefiro digitar estática adequada sobre a documentação ou, pior ainda, algumas dicas de uma discussão.
Frank
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.