1 puntos por GN⁺ 2024-08-04 | 1 comentarios | Compartir por WhatsApp
  • Java se prepara para introducir una función preliminar del lenguaje que añade marcadores de nulidad a los tipos: Foo! rechaza null y Foo? lo permite de forma intencional
  • Foo sin marcador deja la nulidad como no especificada para mantener compatibilidad con el código existente, y las conversiones entre tipos con distinta nulidad pueden generar advertencias o comprobaciones en tiempo de ejecución
  • Los campos y arreglos que rechazan null no pueden usar null como valor predeterminado inicial, por lo que los campos de instancia deben quedar asignados de forma definitiva antes de la llamada a super(...) y los arreglos necesitan un valor inicial para sus componentes
  • Si null se estrecha a Foo!, se produce una NullPointerException, y en las rutas de almacenamiento en arreglos y campos se evita la contaminación con ArrayStoreException y FieldStoreException
  • Optimizaciones como el aplanamiento de value class de Valhalla dependen de poder confiar en la exclusión de null, y por ahora no se busca aplicar esto a la biblioteca estándar ni reinterpretar automáticamente el código existente

Función preliminar para explicitar la nulidad en los tipos de Java

  • Java expresará si null forma parte del conjunto de valores de un tipo agregando marcadores de nulidad a los tipos
  • Foo! es un tipo null-restricted, que excluye null de su conjunto de valores
  • Foo? es un tipo nullable, que incluye intencionalmente null en su conjunto de valores
  • Foo sin marcador tiene nulidad unspecified; puede aparecer null, pero no se sabe si su presencia es intencional
  • Esta es una función preliminar que se activa con las banderas de compilación y ejecución --enable-preview

Objetivos y no objetivos

  • Expresar si los tipos por referencia de Java esperan null y ofrecer advertencias y comprobaciones para conversiones entre tipos con distinta nulidad
  • Debe ser compatible con el código Java existente y poder introducirse gradualmente sin generar incompatibilidades de código fuente ni binarias
  • Las variables de tipos que rechazan null deben inicializarse antes de ser leídas por primera vez, y el rechazo de null también debe aplicarse en tiempo de ejecución incluso entre clases compiladas por separado
  • Las optimizaciones de ejecución, como el aplanamiento de value class de Valhalla, deben contar con metadatos y garantías de integridad para poder confiar en los tipos con restricción de nulidad
  • Lo siguiente no es objetivo por ahora
    • No reinterpretar automáticamente el código existente
    • No convertir todos los posibles null en errores de compilación
    • No agregar formas anulables a tipos primitivos como int
    • No aplicar todavía esta extensión del lenguaje a la biblioteca estándar

Por qué hace falta

  • Una variable String en Java puede contener una referencia a un objeto String o null, pero el lenguaje no ofrece una forma de expresar cuál de las dos opciones era la intención
  • Muchos programas asumen que no habrá null, pero para imponerlo de forma consistente entre la especificación en Javadoc y el código de implementación hace falta trabajo adicional
  • Cuando esa expectativa se rompe, un valor null puede propagarse por el código y terminar provocando una excepción lejos del punto donde se originó el bug
  • Si el desarrollador puede expresar como parte del tipo su intención de rechazar o permitir null, la retroalimentación en compilación y las comprobaciones en ejecución ayudan a encontrar antes los null inesperados
  • En Valhalla, una variable de tipo value class puede optimizarse con una representación aplanada del valor, pero si hace falta un bit adicional para codificar null, eso puede aumentar el uso de memoria o impedir optimizaciones de almacenamiento
  • En Amber, la nulidad de los candidatos de pattern matching puede afectar la exhaustividad de switch, y la nulidad de un patrón de tipo puede determinar si coincide o no con null

Sintaxis de marcadores de nulidad y estructura de tipos

  • La nulidad se trata como una parte intrínseca del tipo, y Foo? y Foo son tipos distintos porque tienen nulidad diferente
  • Tanto el tipo del arreglo como el tipo de sus componentes pueden llevar marcador de nulidad
    • Foo?[]! es un tipo donde el arreglo en sí es null-restricted y los componentes son Foo nullable
    • En arreglos multidimensionales, se puede poner un marcador después de cada par de corchetes, y por convención se interpreta de izquierda a derecha, de afuera hacia adentro
  • Los tipos parametrizados y los argumentos de tipo también pueden llevar marcadores de nulidad
    • Predicate!<Foo?> es un Predicate null-restricted cuyo argumento de tipo es Foo nullable
  • Para expresar un tipo con restricción o con permiso de null, ! o ? debe aparecer explícitamente en el código fuente
  • A futuro podría evaluarse interpretar por defecto todos los tipos de una clase o unidad de compilación como null-restricted y usar ? solo como excepción, pero eso se trataría por separado

Reglas de inicialización para campos y arreglos

  • En Java actual, el valor predeterminado de los campos por referencia y de los componentes de arreglos es null, pero eso no sirve como valor inicial para campos o componentes null-restricted
  • Los campos y arreglos null-restricted siempre deben ser inicializados por el programa antes de ser leídos
  • Si un campo de instancia null-restricted no tiene inicializador, debe quedar asignado de forma definitiva antes de la llamada explícita o implícita a super(...) en cada constructor
    • Flexible Constructor Bodies JEP permite escribir al inicio del constructor el código de inicialización necesario
    • En ese contexto de construcción temprana no se permiten operaciones que hagan referencia a this o que puedan leer campos aún no inicializados
  • Si un campo de instancia null-restricted sí tiene inicializador, este se ejecuta al inicio de cada constructor antes de la llamada a super(...)
    • Un constructor que llama a this(...) sigue siendo un caso especial y no ejecuta el inicializador, como en las reglas actuales
  • Los campos estáticos null-restricted deben quedar asignados de forma definitiva cuando terminen todos los inicializadores y bloques de inicialización estáticos de la clase
    • Si otra clase intenta leer ese campo durante la inicialización de la clase, una comprobación en tiempo de ejecución detectará la lectura prematura y lanzará una excepción
  • Un arreglo con tipo de componente null-restricted debe proporcionar un valor inicial para cada componente en la expresión de creación del arreglo
    • Se pueden enumerar todos los valores con un inicializador de arreglo
    • También podría existir una nueva sintaxis abreviada, aunque por ahora sigue TBD

Nulidad de expresiones y conversiones de nulidad

  • El compilador de Java determina la nulidad de cada expresión durante la verificación de tipos
  • La nulidad de una referencia de variable proviene de la declaración de la variable, y la nulidad de una llamada a método proviene del tipo de retorno del método referenciado
  • El literal null es nullable
  • La mayoría de las demás expresiones de tipo por referencia son null-restricted
    • Incluye literales, concatenación de cadenas, this, creación de instancias de clase, creación de arreglos, referencias a métodos y expresiones lambda
  • Las conversiones de nulidad se permiten en contextos de asignación, invocación y cast
  • La conversión de nulidad de ampliación incluye
    • Foo!Foo?
    • Foo!Foo no especificado
    • Foo?Foo no especificado
    • Foo no especificado → Foo?
  • La conversión de nulidad de estrechamiento incluye
    • Foo?Foo!
    • Foo no especificado → Foo!
  • Al igual que el unboxing, el compilador realiza automáticamente la conversión de nulidad de estrechamiento, pero en ejecución hace una comprobación dinámica que puede lanzar NullPointerException
  • Intentar convertir directamente el literal null a un tipo null-restricted es un error de compilación

Comprobaciones en tiempo de ejecución y excepciones

  • Si en tiempo de ejecución un valor null se convierte por estrechamiento a un tipo null-restricted, se produce una NullPointerException
  • También pueden ocurrir durante la ejecución conversiones de nulidad de estrechamiento que no son visibles de forma explícita en el código fuente
  • Un arreglo cuyo tipo de componente fue asignado como null-restricted seguirá rechazando valores null mediante la comprobación normal de almacenamiento en arreglos, incluso si en el código fuente se trata con un tipo menos específico
    • Ese fallo de conversión provoca ArrayStoreException
  • Si un campo que no era null-restricted al momento de compilar luego pasa a serlo mediante compilación separada, una nueva comprobación de almacenamiento en campo rechazará guardar null
    • Ese fallo de conversión provoca FieldStoreException
  • En llamadas a métodos dentro de una relación de override, puede ocurrir primero una conversión al tipo de invocación del parámetro en el método superior y luego otra al tipo de parámetro del método que sobrescribe
  • El valor de retorno de un método también puede convertirse primero al tipo de retorno declarado del método y luego al tipo de retorno esperado en el punto de llamada

Genéricos, argumentos de tipo y override

  • Los usos de variables de tipo también pueden llevar marcadores de nulidad: T! es un tipo null-restricted y T? es un tipo nullable
  • Los tipos de variables null-restricted y nullable permiten afirmar una nulidad específica dentro del código genérico
  • Los tipos usados como argumentos de tipo también pueden expresar nulidad, y un marcador de nulidad colocado sobre un tipo de variable de tipo sobrescribe la nulidad afirmada por el argumento de tipo
  • Dentro de la implementación erased de una API genérica no se puede imponer la restricción de nulidad
    • Aun así, los casts implícitos habituales en el límite de una API genérica sí hacen cumplir en tiempo de ejecución los argumentos de tipo null-restricted
  • Por interoperabilidad, la nulidad dentro de los argumentos de tipo no se aplica con fuerza total
    • Predicate<String!> puede convertirse a Predicate<String> o Predicate<String?>
    • Esa conversión de nulidad sin comprobación puede generar advertencias
  • Cambiar la nulidad del tipo de componente de un arreglo también se permite como una conversión de nulidad sin comprobación, y sigue TBD en qué condiciones habrá verificaciones en tiempo de ejecución
  • Al determinar la igualdad de firmas de métodos, la nulidad se ignora
    • Un método puede sobrescribir a otro aunque no coincidan las nulidades de los parámetros o del tipo de retorno
    • Si distintas APIs introducen marcadores de nulidad de manera independiente, estas discrepancias pueden ser comunes
  • La nulidad no afecta la aplicabilidad de métodos ni puede hacer fallar la inferencia de argumentos de tipo, aunque sí puede influir en la nulidad inferida para el tipo de retorno de métodos genéricos
    • Los detalles del algoritmo de inferencia siguen TBD

Advertencias y errores del compilador

  • Convertir un tipo en null-restricted puede introducir nuevos errores de compilación
    • Cuando campos o arreglos de ese tipo no están inicializados
    • Cuando se intenta convertir el literal null a ese tipo
    • Comparar el literal null con una expresión de tipo null-restricted también podría ser error de compilación
  • Fuera de eso, el análisis de nulidad es complementario y no genera errores de compilación
  • javac ofrecerá advertencias para ayudar a evitar errores en tiempo de ejecución, y se recomienda que los IDE y otras herramientas de análisis sigan la misma dirección
  • Posibles causas de advertencia
    • Conversiones de nulidad de estrechamiento, incluidas las que provienen de tipos ?
    • Usar una expresión de tipo ? para acceder a miembros u otras operaciones hostiles a null
    • Casos donde la nulidad de un argumento de tipo no es consistente con su bound
    • Casos donde la nulidad de parámetros o retorno de un método no coincide con la del método sobrescrito
    • Conversiones sin comprobación que cambian la nulidad de un tipo

Archivos de clase, reflexión y cambios auxiliares

  • La mayoría de los usos de marcadores de nulidad se eliminan en el archivo class, y las conversiones de ejecución asociadas se representan directamente en el bytecode
  • La gramática del atributo Signature se actualizará para permitir ! y ? dentro de los tipos
  • La nulidad no se codifica en los descriptor de métodos y campos
  • Para evitar contaminación de campos, un nuevo atributo NullRestricted indicará que el campo no acepta valores null
    • Ese campo también deberá marcarse con ACC_STRICT y tendrá que inicializarse de forma estricta
    • El verificador comprobará que todos los campos de instancia con inicialización estricta estén asignados cuando el constructor llegue al punto de llamar a super(...)
    • Todo intento de escribir en el campo comprobará si el valor es null y, si lo es, lanzará FieldStoreException
  • La creación de arreglos null-restricted no será compatible con la instrucción anewarray y deberá hacerse mediante llamadas a la API de reflexión
  • No existirán literales Foo!.class ni Foo?.class, ni instancias correspondientes de java.lang.Class
  • Una nueva RuntimeType API describirá los conjuntos de tipos que se hacen cumplir en tiempo de ejecución en las comprobaciones de almacenamiento en arreglos y campos, incluidas las variantes null-restricted de todos los tipos de clase e interfaz
  • La API Field permitirá consultar el RuntimeType de un campo, y ese valor podrá diferir del resultado de getType
  • La API Array admitirá una variante de newInstance que exprese el tipo de componente con RuntimeType
    • Esa variante también permitirá proporcionar valores iniciales para los componentes del arreglo
    • Rechazará intentos de crear arreglos null-restricted sin valores iniciales
  • La deserialización tradicional no es compatible con campos y arreglos null-restricted, y un JEP aparte ofrecerá un mecanismo de serialización que no exponga campos ni arreglos null-restricted sin inicializar
  • La documentación generada por javadoc incluirá los marcadores de nulidad
  • Las APIs java.lang.reflect.Type y javax.lang.model codificarán la nulidad en la representación de tipos

Alternativas y dependencias

  • Varias herramientas del ecosistema Java ya implementan su propio seguimiento de null en compilación, pero como no cambian el lenguaje Java, su sintaxis suele limitarse a anotaciones y su impacto queda restringido a comprobaciones en compilación
  • Otros lenguajes rastrean la nulidad en el sistema de tipos, y muchos de ellos son null-restricted por defecto y consideran error asignar a un tipo null-restricted sin una comprobación explícita de null
  • En Java, esta función debe ser opcional y utilizable de manera gradual sin exigir una migración masiva única
  • La imposición de nulidad en tiempo de ejecución también puede implementarse con comprobaciones explícitas o llamadas a Objects.requireNonNull
    • Pero aplicarlo de forma consistente es engorroso, requiere documentación adicional y perjudica la legibilidad del programa
    • Tampoco hay una manera de aplicarlo directamente a almacenamientos de variables como campos y arreglos
  • El prerrequisito es Flexible Constructor Bodies (Second Preview)
    • Permite que un constructor ejecute sentencias y asigne campos de instancia antes de la llamada a super(...), lo que hace posible la exigencia de inicialización para campos null-restricted
  • Entre los trabajos futuros están
  • Otras posibles mejoras futuras incluyen aplicar marcadores de nulidad a partes de la API estándar, una representación más compacta de null check en bytecode, una imposición de bajo nivel más fuerte para parámetros de métodos null-restricted y mecanismos del lenguaje para afirmar implícitamente que todos los tipos en ciertos contextos son null-restricted

1 comentarios

 
GN⁺ 2024-08-04
Opiniones de Hacker News
  • Es interesante en qué se diferencia este enfoque del que introdujo C# hace algunos años. En C#, si activas la nulabilidad en un proyecto, todas las variables se declaran como no nulas a menos que las marques explícitamente como nullable; en esta propuesta, las variables existentes pasan a ser, en la práctica, una de tres opciones: nullable, explícitamente nullable o explícitamente non-nullable.
    Kotlin también es un lenguaje de la JVM, pero como C#, considera que algo es non-null si no se indica lo contrario, aunque Kotlin no tiene la carga de la compatibilidad hacia atrás. También ofrece una vía de escape con lateinit var, que permite dejar vacío un tipo non-nullable hasta inicializarlo en otro método, y lanza una excepción específica si se accede antes de la inicialización.
    Me pregunto por qué pusieron tres opciones. ¿No bastaría con mantener como nullable las variables sin anotación y hacer explícitamente non-null solo las variables anotadas? Si, aunque no pongas nada, automáticamente es nullable, no se me ocurre por qué alguien querría declarar algo como nullable.
    Me gusta más el enfoque de C#, pero este tiene la ventaja de poder usarse en una base de código legacy sin resolver todos los problemas de nulabilidad. En cambio, C# expone de inmediato los problemas de nulabilidad, mientras que esta propuesta permite mantenerlos ocultos como hasta ahora.
    Además, me parece rara la parte de que “un método puede sobrescribir otro aunque no coincida la nulabilidad de sus parámetros y valores de retorno”. Parece una trampa peligrosa al sobrescribir/implementar callbacks: el método original especifica un retorno non-null, pero se termina devolviendo null.

    • En mi empresa actual estamos migrando una base de código C# legacy a tipos de referencia nullable, y de verdad me encantaría tener una marca explícita de non-nullable, porque así se podría ver de inmediato qué partes ya fueron revisadas y anotadas.
      Por falta de tiempo, por ahora elegimos solo activar las anotaciones salvo en algunos puntos clave, pero seguimos manteniendo como marcas los NotNullAttribute y CanBeNullAttribute de JetBrains para ver de inmediato dónde se tomó una decisión consciente. El segundo podría eliminarse porque nullable ya tiene una marca explícita, pero el primero choca en nombre con la funcionalidad propia de C#.
      En ese sentido, las tres opciones son bastante deseables. Cuando el código tiene cientos de miles de líneas, no es fácil migrarlo rápido.
      En otros proyectos internos vamos ampliando gradualmente el alcance de la nulabilidad poniendo #nullable enable alrededor del código que tocamos. También tenemos la condición de que el código nuevo debe estar en un contexto nullable. Esto también sirve para explicitar las partes ya anotadas, pero es un enfoque viable con una base de código y un equipo mucho más chicos.
    • En Kotlin también existen tipos cuya nulabilidad no está especificada, aunque no se pueden definir directamente. Kotlin los llama tipos de plataforma (platform types) y, en errores y diagnósticos, los muestra con un signo de exclamación, como String!.
      El término “tipo de plataforma” y el símbolo de exclamación son confusos, pero fuera de eso el enfoque de Kotlin funciona bastante bien. Como Kotlin tuvo nulabilidad desde el principio, los programadores no pueden especificar tipos de plataforma directamente, pero siguen siendo necesarios para interoperar con plataformas base donde la nulabilidad es ambigua, como la JVM o JavaScript.
      En este enfoque, el valor predeterminado sigue siendo razonable. Por defecto, siempre debería ser non-nullable, y quien piense distinto no aprendió nada de Tony Hoare. Al mismo tiempo, también se mantiene la compatibilidad hacia atrás. Para Kotlin fue relativamente fácil; Java y C# también deben preservar la compatibilidad con el código fuente existente.
      Ni el enfoque de Java ni el de C# son ideales. El de C# cambia bastante el comportamiento del código según un flag del compilador, y el de Java convierte la peor opción en el valor predeterminado.
      Aun así, me inclino más por C#. Si haces que “probablemente puede ser null” sea la opción más fácil, la mayoría de los programadores la elegirá por defecto. Sobre todo en un lenguaje tan orientado a empresas como Java, probablemente será así. Los linters y las advertencias del compilador ayudarán a largo plazo, pero creo que pasarán muchos años antes de que la mayoría del código Java anote correctamente la nulabilidad. Los usuarios de C# sufrirán más en el corto plazo, pero es mucho más probable que lleguen antes al objetivo de una nulabilidad clara.
      https://kotlinlang.org/docs/java-interop.html#null-safety-an...
      https://www.infoq.com/presentations/Null-References-The-Bill...
    • Coincido en que es raro. El enfoque que esperaba era que, a nivel de código fuente, T? significara nullable, T! significara non-nullable, y que para el T normal en código fuente recién compilado hubiera alguna declaración parecida a un pragma que fijara el valor predeterminado por archivo fuente, por archivo package-info o globalmente mediante un switch del compilador.
      Luego, en una versión LTS posterior de Java, podrían cambiar el valor predeterminado global. Eso facilitaría la transición de proyectos, permitiría insertar/actualizar pragmas automáticamente y, si hiciera falta, mantener también el valor predeterminado anterior.
      Además, si se hubieran aprovechado las anotaciones existentes @Nonnull de JSR-305 como forma de indicar apariciones de tipos non-nullable en archivos de clase, también se podría haber ofrecido compatibilidad bidireccional con JDK antiguos.
    • Hacia el final hay una sección que dice: “proporcionar un mecanismo para que el lenguaje afirme que todos los tipos de cierto contexto están implícitamente restringidos a no ser null, sin que el programador tenga que usar el símbolo explícito !”.
      Creo que bastaría con algo como un flag del compilador o una etiqueta de módulo.
    • Creo que la razón es la compatibilidad hacia atrás del código, algo que Java valora mucho más que otros lenguajes.
      Aun así, supongo que aparecerá un flag del compilador que, si no se especifica otra cosa, asuma automáticamente non-nullable.
  • Se ve bien. Por fin hay una forma, a nivel del lenguaje, de eliminar miles de excepciones innecesarias y verificaciones de null. Pero la conversión automática que acota si algo es null se siente mal
    En los ejemplos de la propuesta, String? id(String! arg) { return arg; }, String s = null;, Object! o1 = s; // NPE, Object o2 = id(s); // NPE, Object o3 = (String!) s; // NPE, me parece que al menos los dos primeros casos deberían ser errores de compilación
    El último es ambiguo porque es explícito, pero preferiría que, dentro de if (s != null), el compilador entienda que el tipo efectivo es String! y permita String! ss = s;. Así no habría posibilidad de error

    • Desde la perspectiva de alguien que ha usado Java en la industria, es mejor que las verificaciones dinámicas ocurran solo cuando se solicitan explícitamente y que todo lo demás se maneje de forma estática. Bean Validation funciona bien porque, aunque un objeto pueda estar temporalmente en un estado inválido, se vuelve válido en el momento en que se valida explícitamente, o cuando el framework lo valida antes de entrar a mi código
      De hecho, prefiero usar Objects.requireNonNull(s) antes que el cast del último caso, porque es más explícito. Eso sí, estaría bueno tener algo como Objects.unsafeForceNonNull(s), que omita la verificación explícita y solo excluya los casos bloqueados por optimización. Con un método unsafe, se podría implementar requireNonNull directamente sin agregar análisis estático complejo
    • Dice que “no es un objetivo exigir que el programa maneje explícitamente todos los valores null que puedan ocurrir; los valores null no manejados pueden generar advertencias en tiempo de compilación, pero no errores”
      Lamentablemente, esto solo se comprobará en tiempo de ejecución
    • Si el primer o segundo ejemplo fueran errores de compilación, eso significaría que habría que anotar todos los puntos de uso de todas las bibliotecas. Hasta que esa biblioteca migre a tipos null, o si nunca lo hace, casi todas las líneas terminarían llenas de casts. No tiene sentido
      Por ejemplo, la biblioteca estándar indica explícitamente que, al menos por ahora, no migrará a tipos null
    • Si la esencia del problema es introducir null en la API, no estoy seguro de que eso sea un error del lenguaje. Entiendo que es verboso y que hace falta para ordenar las cosas, pero al final el problema está en los desarrolladores que escriben el código así, ¿no?
    • Si el segundo no es un error de compilación, el código legacy podrá llamar a funciones que reciben argumentos non-null, pero no sé si eso necesariamente es bueno. Es difícil juzgarlo antes de usarlo en la práctica
  • Parece imprescindible tener una forma de marcar todas las variables como non-null por defecto a nivel de paquete o, al menos, de archivo. De lo contrario, por seguridad se va a imponer la idea de usar la sintaxis T! en casi todas las variables, y eso solo generará mucho ruido

    • En “posibles mejoras futuras” aparece “proporcionar un mecanismo para que el lenguaje afirme que todos los tipos en ciertos contextos están implícitamente restringidos a null, sin que el programador tenga que escribir el símbolo explícito !
    • No parece tan malo. En nuestro proyecto ya tenemos el estándar de que todas las variables del código Java que poseemos no son null. Si se necesita una variable nullable, hay que anotarla con @Null
      El problema solo aparece en los límites del código que interactúa con bibliotecas. Esta nueva sintaxis probablemente sea parecida
    • Un linter agresivo podría tratar los tipos sin marca como non-null y exigir anotaciones de nulabilidad solo donde haga falta
  • Es una lástima la parte que dice “por ahora, no es un objetivo aplicar la mejora del lenguaje a la biblioteca estándar”
    Por experiencia de haber tenido que usar PHP, es molesto tener que quitar o volver a agregar propiedades ya garantizadas de los datos cada vez que uno interactúa con una biblioteca estándar enorme
    Java también debería incorporar esta expresividad de forma más activa en la biblioteca estándar y convertirla en ciudadana de primera clase

    • Uno de los mayores problemas de Java es que, al mejorar de forma incremental, el código recién mejorado vuelve a convertirse en código legacy con la siguiente funcionalidad incremental. El código legacy que usa Optional se vuelve una carga frente a la propuesta de nullable/non-nullable explícito. Los tipos record también podrían haber sido non-nullable por defecto
    • Parece que estás leyendo “por ahora” como “nunca”, pero en realidad esa transición en sí es un trabajo enorme y lo más probable es que quieran tratarla por separado
      Además, dividirlo en dos etapas permite lanzar esta funcionalidad como preview con más facilidad y, tras recibir feedback, incorporarla al diseño de forma definitiva. Si intentan hacerlo todo de una vez, casi no quedará margen para iterar sobre la funcionalidad en la práctica
    • La expresión clave es “por ahora”. Creo que, cuando la funcionalidad se estabilice, se convertirá en un objetivo
    • ¿“Tener que usar” PHP? El PHP moderno es bastante cómodo
  • Me gustaría que esta funcionalidad llegara a Java. La posibilidad de elección explícita a nivel del lenguaje, como T?, mejoró mucho la calidad de vida de los desarrolladores en Kotlin y TypeScript. En Java existen herramientas como NullAway, pero son engorrosas
    Creo que el soporte a nivel del lenguaje es mucho mejor que Optional/Maybe, porque permite concentrarse en la lógica real en vez de poner el código sobre rieles de map/flatMap
    https://github.com/uber/NullAway

    • ¿Proponen romper más de 30 años de compatibilidad de código fuente? ¿Por qué habría que hacer eso?
      Mejor usar otro lenguaje de la JVM, ¿no?
  • Como el sitio está caído ahora mismo, dejo un enlace archivado: https://web.archive.org/web/20240802081039/https://bugs.open...

    • No sorprende que lo hayan cambiado al “jira” que todos aman
  • “Exigir que el programa maneje explícitamente todos los valores null que puedan ocurrir no es el objetivo, y los valores null no manejados pueden generar advertencias en tiempo de compilación, pero no errores” es una mala decisión.
    Java es, en su mayor parte, un lenguaje de tipado estático; no entiendo por qué meter otro comportamiento dinámico. Espero que haya una forma sencilla de elevar esas advertencias a errores.

    • No se puede hacer eso. Rompería todos los programas Java existentes. Si se impusiera, habría que reescribir todos los programas para manejar respuestas null en todas partes. Con esta nueva funcionalidad, solo se puede aplicar el manejo obligatorio de null a las operaciones relacionadas con los nuevos tipos, y por eso no se rompe nada.
      Los lenguajes cuyo sistema de tipos obliga a manejar null/nil en todos los casos son mucho mejores. Pero Java, hoy, no es uno de esos lenguajes. Aun así, esto será una gran mejora.
  • Es una lástima que estemos aprendiendo estas lecciones tan tarde. Por defecto debería ser no anulable, inmutable por defecto y con el alcance más estrecho por defecto.
    En los diseños nuevos, con demasiada frecuencia se elige la comodidad inmediata en lugar de “hacer que el camino seguro sea el predeterminado”. Los valores predeterminados seguros requieren un diseño y una experiencia de usuario mucho más cuidadosos, pero el resultado es que casi todos los lenguajes, plataformas y tecnologías terminan llenos de trampas para dispararse en el pie. La ingeniería civil y la eléctrica tienen normativas, mientras que en software volvemos a aprender las mismas lecciones en nuevos lenguajes y tecnologías cada 30 años más o menos.

    • Lamentablemente, parece que Java todavía no aprendió la regla de no anulable por defecto. Esta propuesta introduce dos tipos nuevos, pero si uno pregunta si el tipo básico sin anotaciones es anulable, la respuesta sigue siendo “quién sabe”.
    • Lo lamentable es que Java esté aprendiendo esto tarde. “Nosotros” y “Java” no son lo mismo.
      Java es como una aldea del tercer mundo que aprende que para matar gérmenes hay que hervir el agua.
  • La mayor parte de lo que hice en Facebook fue usar Hack. La nulabilidad es un elemento central del sistema de tipos de Hack y elimina muchísimos errores innecesarios.
    Claro que eso no significa que nunca recibas un null inesperado. Como esta funcionalidad también se agregó al lenguaje más tarde, todavía había muchos tipos mixed heredados de sus raíces en PHP, lo que en la práctica significaba que podía ser cualquier cosa.
    Primero, me pregunto qué pasa con los arreglos anulables. El ejemplo String![] muestra el caso en que el objeto puede ser null, pero ¿qué pasa con el arreglo en sí? En Java, String labels[] = null; es totalmente legal. Entonces, ¿habría que declararlo como String![]! labels;?
    En Hack, vec $foo significa que ni foo ni sus elementos son null, y ?vec $foo significa que los elementos son no anulables pero foo puede ser null. En la práctica casi nunca hay una razón para usar un arreglo null, así que el valor predeterminado debería ser no anulable. El problema es que Java tiene todo el código heredado asumiendo la posibilidad de null.
    En los ejemplos de la propuesta Object! o1 = s, Object o2 = id(s), Object o3 = (String!) s, me parece que el 2 y el 3 deberían ser errores de compilación.
    Por último, me gusta más el operador de coerción as de Hack que el casteo de Java. Por ejemplo, foo($b) es un error de compilación, $b as A da un error en tiempo de ejecución si es null, y $a as ?B lo castea si es B; si no, devuelve null.
    Al final, la pregunta es si esto se puede aplicar por encima al SDK de Java y cómo se verá en código heredado.
    https://docs.hhvm.com/hack/types/nullable-types

    • Probablemente sea simplemente String![]? labels = null;.
  • Parece que todo lo bueno de Kotlin ahora está llegando a Java.
    Aun así, prefiero seguir trabajando en Kotlin, donde no tengo que lidiar con cosas como Lombok. Los records de Java sí están buenos.

    • De todos modos, los programadores Java seguirán usando los modismos de siempre y estarán atados a bases de código antiguas. Estas cosas tardarán mucho en usarse ampliamente, así que los programadores de Kotlin no tienen de qué preocuparse.
    • Java tiene una larga historia de inspirarse en otros lenguajes. Es bueno ver que sigue evolucionando a un ritmo bastante decente, manteniendo en gran medida la compatibilidad con versiones anteriores.