12 horas con Java, 15 años después

No recuerdo cuándo fue la última vez que utilicé Java de verdad. Tal vez en 2002 o 2003. Desde entonces no había vuelto a escribir código Java más allá de alguna prueba rápida con Android. Durante este tiempo sí que he estado (relativamente) pendiente de la evolución del lenguaje, sobre todo porque la JVM es una plataforma que me parece más que interesante a la que sigo planteándome migrar.

Recientemente he tenido la oportunidad de trabajar con código Java de una aplicación real, y aunque no han sido más que unas horas, me ha permitido hacerme una (muy ligera) idea de lo que podría ser trabajar con él.

Éste no deja de ser un post muy personal con la experiencia de alguien acostumbrado a programar en C#, Javascript y TypeScript que vuelve a tocar Java 15 años después, pero tal vez a alguien más le resulte interesante estas primeras impresiones tras tanto tiempo desconectado.

El entorno

Lo primero que me ha sorprendido gratamente es la agilidad del entorno en general. Sonará raro, seguro, pero viniendo de Visual Studio que es cada vez más mastodóntico y lento, y recordando el Eclipse de hace 15 años, la sensación al utilizar IntelliJ es que todo funciona bastante más rápido de lo que me esperaba. También es cierto que no he estado trabajando con un proyecto grande y seguramente eso se note mucho.

Me hubiera gustado utilizar emacs, pero un lenguaje tan verbose como Java parece más apropiado para un IDE y toda la parafernalia añadida (intellisense, refactoring automático), y las soluciones que he encontrado para emacs son un poco exóticas (como tener que ejecutar Eclipse en segundo plano para usar eclim).

Utilizar IntelliJ también contribuye a que la experiencia me resulte mucho más familiar. Como usuario habitual de Resharper (y con los atajos de teclado de IntelliJ IDEA), la mayoría de las acciones salen de forma natural. Además, tiene algunos detalles que me han gustado especialmente, como mostrar inline el nombre del parámetro de un método cuando recibe valores constantes. Este detalle, que puede parecer una tontería, ayuda cuando no tienes parámetros con nombre y utilizas métodos con muchos parámetros para construir datos de test:

IntelliJ

La gestión de ficheros de código basada en carpetas en lugar de en soluciones me gusta. O eso creo. En parte coincido con Eduard Tomàs en que la solución de Visual Studio no soluciona nada, y en ese sentido me gusta la simplicidad de las carpetas. Por otra parte, eso obliga a crear un montón de carpetas para gestionar los espacios de nombres paquetes, y eso me gusta menos. Al hilo de eso, tampoco me gusta no poder tener varias clases (públicas) en un mismo fichero, o partir una clase en varios ficheros cuando trabajas con clases grandes como las que a veces me gusta usar.

Por la compilación a través de maven he podido pasar de puntillas. Afortunadamente es un proyecto que ya me han dado montado con unos scripts de compilación estupendos y no he tenido que pegarme con ella. Viendo el pom.xml no parece demasiado terrible para alguien que ha disfrutado de las maravillas de MSBuild.

El código

En cuanto al código como tal, algunas cosas que recordaba como incómodas me lo siguen pareciendo.

El lenguaje es verbose. Mucho, para los tiempos que corren.

No tener propiedades, ni inicializadores de tipo, ni parámetros opcionales, no poder utilizar expresiones para el cuerpo de los métodos, no poder declarar las variables como var/let/const y dejar que el compilador infiera el tipo… Son cosas que hacen que haya que escribir y leer mucho código. En la parte de escribir ayuda el IDE, que hace casi todo sólo, pero al leer hay mucho ruido.

Incluso en APIs más modernas, como los streams, sigue siendo necesario escribir de más (o yo no sé usarlas bien, que es también es probable). Basta comparar estos dos ejemplos en Java y C#:

public List<string> idsToString(Person ...persons) {
	return Arrays.asList(persons).stream()
              .map(x -> Integer.toString(x.id()))
              .collect(Collectors.toList());
}
public IEnumerable<string> IdsToString(params Person[] persons) {
	return persons.Select(x => x.Id.ToString());
}

Actualización: Comobien indica Javier Neira, este código se puede escribir de forma más corta y concisa.

En C# me ahorro el tener que convertir los parámetros de la función variádica a nada, puedo usar las extensiones de IEnumerable directamente sobre el array. En el caso de Java, necesito envolver el array con un Arrays.asList y luego utilizar stream(). En Java además necesito el método estático Integer.toString() y utilizar luego un Collector para obtener el resultado, mientras que en C# me evito ambas cosas.

Parte de estos inconvenientes vienen de lo que, para mi, es otra de las cosas incómodas de Java: la separación entre tipos primitivos y objetos.

En C# el sistema de tipos está unificado, y mediante la distinción entre clases y estructuras se crea una jerarquía de tipos completa que incluye tanto tipos básicos (int, long, double) como objetos, por lo que la forma de manejar unos y otros es más homogénea. En Java, la separación es rígida e implica más restricciones sobre cómo podemos manejarlos. Una ventaja de enfoque de Java es que evita que hagamos boxing/unboxing inadvertidamente, algo que podría conllevar problemas de rendimiento pero que, sinceramente, no es algo que te pase todos los días.

Esta separación de tipos básicos y objetos tiene otra implicación, y es que obliga a clonar APIs para simplificar su uso. Por ejemplo, al trabajar con streams encontramos un map para convertir de un objeto a otro, pero también un mapToInt, mapToLong y mapToDouble para trabajar fácilmente con esos tipos básicos sin tener que envolverlos en Integer, Long o Double. Quizá también se haga para poder aplicar alguna optimización necesaria por no tener los tipos genéricos reificados, pero eso ya se me escapa.

Hay ocasiones en las que echo de menos poder redefinir operadores. Al menos para String. El ==, concretamente. Era uno de los fallos que más cometía al programar en Java años atrás, y me temo que lo sigo haciendo: comparar Strings por referencia en lugar de por valor. La verdad es que no se me ocurre en qué escenario querría comparar Strings por referencia y no por valor.

Pero sin duda, lo que más incómodo me resulta son las checked exceptions. Si ya en C# me gusta poco utilizar async/await o restricciones sobre genéricos por la viralidad que tienen, lo de las checked exceptions me parece infernal.

Aquí estoy convencido de que tengo parte de culpa, porque a priori la idea de que las excepciones formen parte de la signatura del método tiene cierto sentido. A fin de cuentas, ayuda a documentar y a comprobar estáticamente que los errores se gestionan. Pero en la práctica, hay muchas ocasiones en las que la única salida que veo es capturar una excepción y relanzarla como RuntimeException para satisfacer al compilador y dejar que todo explote en tiempo de ejecución (que es lo mejor que puedo hacer la mayoría de las veces).

A la hora de navegar por el código, una cosa que me resulta confusa es que no se siga la convención de llamar a los interfaces ICosa. Entiendo los argumentos a favor, como que así puedes convertir un interfaz a una clase sin necesidad de renombrar, o que al cliente debería darle igual si lo que recibe es un interfaz o una clase, pero al estar acostumbrado a C#, se me hace raro. Cuestión de habituarse, supongo.

El ecosistema

Con respecto a las librerías con las que me he topado, tampoco he profundizado en ellas lo suficiente como para tener una opinión clara.

jUnit en los tests me resulta muy familiar (llevo 10 años usando NUnit). sql2o es un Micro ORM fácil de manejar que te evita el horrible API de JDBC (que es tan poco amigable como ADO.NET, pero encima con las checked exceptions).

Me ha gustado lo poco que he visto de Spring Boot. Recordaba el Spring original, con sus toneladas de XML, y el port para .NET, y aunque nunca llegué a utilizarlos, siempre me parecieron algo de lo que huir. Sin embargo Spring Boot tiene un aspecto más manejable. Entre lo más atractivo, la posibilidad de generar un jar autocontenido con el servidor embebido, y poder meter dentro un servidor web, un servidor ftp o lo que necesites, sin tener que depender de servicios externos. Imagino que habrá escenarios en los que sea mejor desplegar sobre un servidor externo, pero esta opción es cómoda y sencilla.

Conclusión

Soy plenamente consciente de que esta comparación es totalmente injusta y superficial. Conozco mucho mejor .NET que Java, y eso hace que esté más acostumbrado y todo me resulte más sencillo y familiar en esa plataforma. Además, seguro que muchas de las cosas que me parecen incómodas de Java se podrían resolver limpiamente conociendo mejor las herramientas y las posibilidades que ofrece el lenguaje (y algunas, como la inferencia de tipos, creo que están planificadas para Java 9).

Pese a lo que pueda parecer, la experiencia ha sido agradable. Si tuviera que vivir programando en Java, creo que sobreviviría sin sufrir demasiado. Las herramientas son potentes, las librerías están bien y la plataforma es, posiblemente, la más estable que existe hoy en día.

Si acabara dando el salto a la JVM, ¿usaría Java? Intentaría evitarlo. Trataría de buscar otro lenguaje un poco menos verbose y con más opciones para salirte de la programación orientada a objetos «pura» (pura de Java/C#, claro). ¿Scala? Tal vez, aunque lo que he oído del rendimiento del compilador me tira un poco para atrás. ¿Clojure? ¿Frege? Muy divertidos, pero problemáticos a la hora de formar un equipo. ¿Kotlin? Quizá sería una opción. No es un lenguaje que me atraiga especialmente, al igual que no me atraía TypeScript, pero podría ser una alternativa práctica.

13 comentarios en “12 horas con Java, 15 años después

  1. Mmm java sí que hace autoboxing (con sus eventuales desagradables sorpresas) pero solo en los parametros de entrada o en la salida de los metodos.
    El ejemplo puede quedar un poco mas corto, usando alguna utilidad de las clases Stream y Collector e imports estaticos:
    https://gist.github.com/jneira/bf75d507be0a3005dc37f24052cdba68

    Pero la idea es devolver streams «all way down» hasta el consumidor de los datos (que hara un foreach o lo convertira en una lista en memoria) y separar claramente las operaciones que consumen los datos de aquellas que añaden transformaciones sobre los mismos. ¿Es buena idea? Bueno, como todo, depende del contexto

  2. ¡Gracias! No sabía lo del autoboxing y usando los métodos de utilidad queda más corto, tienes razón.

    Lo de devolver Streams está bien, pero en ese caso concreto necesitaba materializar a un List porque es lo que me pedía el API.

    Estoy de acuerdo es que lo de pasar Streams por todas partes puede ser buena o mala idea dependiendo del caso (en C# pasa igual con IEnumerable).


  3. Estoy de acuerdo es que lo de pasar Streams por todas partes puede ser buena o mala idea dependiendo del caso (en C# pasa igual con IEnumerable).

    No conozco demasiado el core de c# pero un IEnumerable puede ser una colleccion lazy o eager (como ArrayList), ¿no? Y por el tipo devuelto no sabes en un momento dado si la colleccion esta en memoria o no (tienes que mirar el codigo).
    En java han separado la colleccion lazy (Stream) de las previas y no las han unificado en una interfaz comun, con lo que es explicito en todo momento que tienes entre manos.

  4. Cierto, separarlo ayuda por lo menos a saber qué esperar (aunque entiendo que podrías llegar a implementar un stream eager).

    Yo lo decía también por la parte de side-effects. Nada te garantiza que en el map no estés haciendo algo que mute estado global o que requiera IO, y recorrer dos veces un stream puede ser muy emocionante.

  5. IntelliJ, de lo mejor, la versión free es más que suficiente.

    Maven, bastante bien, notablemente mejor (por fácil de usar) que msbuild.

    «var/let/const», la inferencia en Java y C# las veo similares, aquí C# te deja poner «var» pero no puede inferir el tipo a la derecha, en Java sí. Para unas cosas es mejor lo primero y para otras lo segundo. Pocos lenguajes disfrutan de la inferencia de Haskell :)

    Verbosity, tiene cosas mejorables pero en general no está mal, la importación de estáticos y referencias a métodos ayuda bastante. Un poco más corto aún es: `Stream.of(persons).map(x -> Integer.toString(x.id())).collect(toList())` pero se echa en falta la composición de funciones y algunas inferencias (ej. Integer::toString debería ser válido y no lo es). Aun así, efectivamente extender los tipos como en C# molaría.

    «checked exceptions» puedes crear tus propias unchecked para que no sean explícitas y «dejar que todo explote en tiempo de ejecución», para las checked prefiero usar mónadas (ej. `Optional` aunque prefiero `Either`).

    «Junit» bueno. «Spring» para proyecto bueno, para producto malo.

    Hay otras cosillas como la cagada de no poder definir qué es público y privado a nivel de módulo y tener que tener todo en el mismo namespace (carpeta). Veremos como lo dejan en Java 9 que no lo tengo claro.

    @jneira, «Pero la idea es devolver streams», el problema de C# de que ya esté materializado se producirá cuando se estén efectuando acciones laterales (no puras) que no se volverán a hacer, en Java eso no te pasará pero sí algo similar si no tienes cuidado en el orden de las ejecuciones con similares acciones impuras (obteniendo efectos inesperados). No tengo claro que esa pequeña ventaja de Java compense tanto `mapToObj`.

    Por otro lado, creo que hay una ruptura notable entre Java =8 (si bien en Java 7 ya se veía venir). Muchos programadores cómodos en Java =8, a mi sin duda me gusta ese giro funcional, pero a la gente le echa para atrás.

    ¿Se puede reemplazar C#/.Net por Java? sin duda y más visto la carnicería que están haciendo.

  6. («creo que hay una ruptura notable entre Java menor o igual que 7 y Java mayor o igual que 8»)

  7. GreenEyed dijo:

    Java sí hace autoboxing y NO SOLO en los parámetros de entrada y salida de los métodos.
    //// Totalmente valido en medio de un método
    Integer object = new Integer(7);
    int primitive = object;

    Aunque no sea una solución del lenguaje, Lombok ayuda bastante en cuanto a «verborreicidad».
    En cuanto a las excepciones… bueno yo sigo pensando que en el propio JDK se usan mal en muchos sitios, admitido por ellos mismos, y que tampoco hay para tanto y prefiero hacerlo conscientemente. Aunque sí es cierto que los dos factores son una mala combinación.

    Con todo lo que dices… yo ahora mismo le estoy echando un ojo a Kotlin, parametros por nombre, unchecked exceptions, control de valores null, inferencia de tipos… y el soporte del IDE es bueno, aunque mejorable. Los creadores son los mismos que del IntelliJ así que…

    Ahora mismo estoy convirtiendo una aplicación real en Spring Boot a Kotlin y el 90% ha sido cambiar sintaxis. Todavía le queda pero parece que hay plan.

  8. Siempre prefiero JPA , creo que nunca utilice JDBC a pelo, es algo demaciado muy al fondo en el core y siempre estube bien arropado por Hibernate, mi ORM preferido.

    En algun momento comence a aprender MEAN, pienso que javascript es el futuro, pero la capa de persistencia con Java es muy comoda y repetitiva, en nodejs el ORM que veo que va agarrando ritmo es waterline (https://github.com/balderdashy/waterline) pero aun esta años luz de hibernate y JPA, espero se mueva mas rapido y hagan su «JPA» algo como NPA (Nodejs Persistence API).

  9. Ojala migre a la jvm y adopte Scala como lenguaje.
    Seria un gusto escuchar sus comentarios e impresiones con este lenguaje. Muchas veces me he planteado aprenderlo, pero me gustaría escuchar el concepto de alguien que emite tanta mesura y sensatez en sus buenos aportes, como lo hace usted en cada post.
    Se le ha criticado a Scala que es demasiado ruidoso al intentar integrar muchas de las buenas abstracciones funcionales dentro de un modelo orientado a objetos. Sera esto cierto ?

  10. Gracias Edwin. Lo que comentas es otra de las cosas de Scala que no me acaba de gustar. Parece como si hubieran intentado meter demasiadas cosas en el lenguaje y queda todo un poco inconexo.

  11. GreenEyed dijo:

    La última del propio creador del lenguaje es añadir los espacios significativos para sustituir las llaves… pero que sea opcional.

    Da la impresión que en vez de hacer un buen martillo, están intentando hacer una mega-navaja-suiza con desde un martillo neumático a un microscopio y que le sirve y guste a todo el mundo.

  12. No es por conocer mas c# que java, es que c# es mejor que java en muchos aspectos.

    Desde mi punto de vista (ojo, muy personal), con .net core no veo necesario cambiar a java, y si se quiere programación funcional, f# cubre muy bien ese aspecto.

Comentarios cerrados.