¿Por qué es Optional.get () sin llamar a isPresent () malo, pero no iterator.next ()?


23

Cuando uso la nueva API de secuencias Java8 para encontrar un elemento específico en una colección, escribo código como este:

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

Aquí IntelliJ advierte que se llama a get () sin verificar primero isPresent ().

Sin embargo, este código:

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

... no da ninguna advertencia.

¿Hay alguna diferencia profunda entre las dos técnicas, que hace que el enfoque de transmisión sea más "peligroso" de alguna manera, que es crucial nunca llamar a get () sin llamar primero a isPresent ()? Buscando en Google, encuentro artículos que hablan sobre cómo los programadores son descuidados con Opcional <> y supongo que pueden llamar a get () siempre que sea necesario.

La cosa es que me gusta que me arrojen una excepción lo más cerca posible del lugar donde mi código tiene errores, que en este caso sería donde estoy llamando a get () cuando no se encuentra ningún elemento. No quiero tener que escribir ensayos inútiles 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 exista algún peligro en provocar excepciones de get () que desconozco?


Después de leer la respuesta de Karl Bielefeldt y un comentario de Hulk, me di cuenta de que mi código de excepción anterior era un poco torpe, aquí hay algo mejor:

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

Esto parece más útil y probablemente será natural en muchos lugares. Todavía siento que querré poder elegir un elemento de una lista sin tener que lidiar con todo esto, pero eso podría ser que necesito acostumbrarme a este tipo de construcciones.


77
La parte de exclusión de excepciones de su último ejemplo podría escribirse mucho más conciso si usa o ElseThrow , y sería claro para todos los lectores que tiene la intención de lanzar la excepción.
Hulk

Respuestas:


12

En primer lugar, tenga en cuenta que la verificación es compleja y que probablemente sea relativamente lenta. Requiere un análisis estático, y puede haber bastante código entre las dos llamadas.

Segundo, el uso idiomático de las dos construcciones es muy diferente. Programo a tiempo completo en Scala, que usa con Optionsmucha más frecuencia que Java. No puedo recordar una sola instancia en la que necesitaba usar un .get()en un Option. Casi siempre debe devolver el Optionalde su función, o utilizarlo en su orElse()lugar. Estas son formas muy superiores de manejar los valores perdidos que lanzar excepciones.

Debido a .get()que rara vez se debe llamar en uso normal, tiene sentido que un IDE comience una comprobación compleja cuando vea un .get()para ver si se usó correctamente. En contraste, iterator.next()es el uso apropiado y ubicuo de un iterador.

En otras palabras, no se trata de que uno sea malo y el otro no, se trata de que uno sea un caso común que es fundamental para su funcionamiento y es muy probable que arroje una excepción durante las pruebas del desarrollador, y el otro sea un uso poco frecuente. caso que puede no ejercerse lo suficiente como para lanzar una excepción durante las pruebas de desarrollador. Esos son los tipos de casos sobre los que desea que los IDE le avisen.


Podría ser que necesito aprender más sobre este negocio opcional: en su mayoría he visto cosas de Java8 como una buena manera de hacer el mapeo de listas de objetos que hacemos mucho en nuestra aplicación web. ¿Entonces se supone que debes enviar <> s opcionales a todas partes? Eso llevará un tiempo acostumbrarse, ¡pero esa es otra publicación de SE! :)
Frank de televisión

@ TV's Frank Es menos que se supone que estén en todas partes, y más que te permitan escribir funciones que transforman un valor del tipo contenido, y las encadenas con mapo flatMap. OptionalEn su mayoría, son una excelente manera de evitar tener que poner todo entre corchetes null.
Morgen

15

La clase Opcional está diseñada para usarse cuando no se sabe si el elemento que contiene está presente o no. La advertencia existe para notificar a los programadores que hay una posible ruta de código adicional que pueden manejar con gracia. La idea es evitar que se produzcan excepciones. El caso de uso que presenta no es para lo que está diseñado y, por lo tanto, la advertencia es irrelevante para usted: si realmente desea que se lance una excepción, continúe y desactívela (aunque solo para esta línea, no globalmente), debería ser tomar la decisión de manejar o no el caso no presente caso por caso, y la advertencia le recordará que debe hacerlo.

Podría decirse que se debe generar una advertencia similar para los iteradores. Sin embargo, simplemente nunca se ha hecho, y agregar una ahora probablemente causaría demasiadas advertencias en los proyectos existentes como una buena idea.

También tenga en cuenta que lanzar su propia excepción puede ser más útil que solo una excepción predeterminada, ya que incluye una descripción de qué elemento se esperaba que existiera, pero no puede ser muy útil para la depuración en un momento posterior.


6

Estoy de acuerdo en que no hay una diferencia inherente en estos, sin embargo, me gustaría señalar un defecto más grande.

En ambos casos, tiene un tipo, que proporciona un método que no se garantiza que funcione y se supone que debe llamar a otro método antes de eso. Si su IDE / compilador / etc. le advierte sobre un caso u otro solo depende de si es compatible con este caso y / o lo tiene configurado para hacerlo.

Dicho esto, sin embargo, ambos fragmentos de código son olores de código. Estas expresiones sugieren fallas de diseño subyacentes y producen código frágil.

Tienes razón al querer fallar rápido, por supuesto, pero la solución, al menos para mí, no puede ser simplemente ignorarlo y tirar las manos al aire (o una excepción en la pila).

Puede que se sepa que su colección no está vacía por ahora, pero en lo único en lo que puede confiar es en su tipo: Collection<T>- y eso no garantiza su falta de vacío. Aunque ahora sepa que no está vacío, un requisito modificado puede hacer que se cambie el código por adelantado y, de repente, su código se vea afectado por una colección vacía.

Ahora puede retroceder y decirles a los demás que se suponía que debía obtener una lista no vacía. Puede estar feliz de encontrar ese "error" rápidamente. Sin embargo, tiene una pieza de software rota y necesita cambiar su código.

Compare esto con un enfoque más pragmático: dado que obtiene una colección y posiblemente puede estar vacía, se obliga a lidiar con ese caso mediante el uso de # map, #orElse o cualquier otro de estos métodos, excepto #get.

El compilador hace todo el trabajo posible para usted, de modo que puede confiar lo más posible en el contrato de tipo estático para obtener un Collection<T>. Cuando su código nunca viola este contrato (como a través de cualquiera de estas dos cadenas de llamadas malolientes), su código es mucho menos frágil a las condiciones ambientales cambiantes.

En el escenario anterior, un cambio que produzca una colección de entrada vacía no tendría ninguna consecuencia para su código. Es solo otra entrada válida y, por lo tanto, aún se maneja correctamente.

El costo de esto es que tiene que invertir tiempo en mejores diseños, en lugar de a) arriesgar código frágil ob) complicar innecesariamente las cosas lanzando excepciones.

En una nota final: dado que no todos los IDE / compiladores advierten sobre las llamadas iterator (). Next () u optional.get () sin las verificaciones previas, dicho código generalmente se marca en las revisiones. Por lo que vale, no permitiría que dicho código permitiera pasar una revisión y exigir una solución limpia.


Creo que puedo estar un poco de acuerdo con el código de olor: hice el ejemplo anterior para hacer una breve publicación. Otro caso más válido podría ser extraer un elemento obligatorio de una lista, lo que no es extraño que aparezca en una aplicación IMO.
TV Frank

Spot on. "Elija el elemento con la propiedad p de la lista" -> list.stream (). Filter (p) .findFirst () .. y nuevamente nos vemos obligados a hacer la pregunta "¿y si no está allí?" .. pan y mantequilla todos los días. Si es mandatoray y "solo ahí", ¿por qué lo escondiste en un montón de otras cosas en primer lugar?
Frank

44
Porque tal vez es una lista que se carga desde una base de datos. Tal vez la lista es una recopilación estadística que se recopiló en alguna otra función: sabemos que tenemos los objetos A, B y C, quiero encontrar la cantidad de lo que sea para B. Muchos casos válidos (y en mi aplicación, comunes).
TV Frank

No sé acerca de su base de datos, pero la mía no garantiza al menos un resultado por consulta. Lo mismo para las colecciones estadísticas: ¿quién puede decir que siempre estarán vacías? Estoy de acuerdo en que es sencillo razonar que debería haber este o aquel elemento, pero prefiero una escritura estática adecuada sobre la documentación o, peor aún, alguna información de una discusión.
Frank
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.