Grokking Java culture: ¿por qué las cosas son tan pesadas? ¿Para qué se optimiza? [cerrado]

288

Solía ​​codificar mucho en Python. Ahora, por razones de trabajo, codifico en Java. Los proyectos que hago son bastante pequeños, y posiblemente Python funcionaría mejor, pero hay razones válidas que no son de ingeniería para usar Java (no puedo entrar en detalles).

La sintaxis de Java no es un problema; Es solo otro idioma. Pero aparte de la sintaxis, Java tiene una cultura, un conjunto de métodos de desarrollo y prácticas que se consideran "correctas". Y por ahora estoy fallando completamente en "asimilar" esa cultura. Así que realmente agradecería explicaciones o punteros en la dirección correcta.

Un ejemplo completo mínimo está disponible en una pregunta de desbordamiento de pila que comencé: https://stackoverflow.com/questions/43619566/returning-a-result-with-several-values-the-java-way/43620339

Tengo una tarea: analizar (desde una sola cadena) y manejar un conjunto de tres valores. En Python es una línea única (tupla), en Pascal o C un registro / estructura de 5 líneas.

Según las respuestas, el equivalente de una estructura está disponible en la sintaxis de Java y un triple está disponible en una biblioteca Apache ampliamente utilizada, aunque la forma "correcta" de hacerlo es en realidad creando una clase separada para el valor, completa con captadores y colocadores. Alguien fue muy amable al proporcionar un ejemplo completo. Tenía 47 líneas de código (bueno, algunas de estas líneas eran espacios en blanco).

Entiendo que una gran comunidad de desarrollo probablemente no esté "equivocada". Así que este es un problema con mi comprensión.

Las prácticas de Python optimizan la legibilidad (que, en esa filosofía, conduce a la capacidad de mantenimiento) y después de eso, la velocidad de desarrollo. Las prácticas de C se optimizan para el uso de recursos. ¿Para qué se optimizan las prácticas Java? Mi mejor suposición es la escalabilidad (todo debería estar en un estado listo para un proyecto de millones de LOC), pero es una suposición muy débil.

Mikhail Ramendik
fuente
1
no responderá a los aspectos técnicos de la pregunta, pero aún en contexto: softwareengineering.stackexchange.com/a/63145/13899
Newtopian
74
Puede disfrutar el ensayo de Steve Yegge Ejecución en el Reino de los sustantivos
Coronel Panic
3
Aquí está lo que creo que es la respuesta correcta. No estás asimilando Java porque piensas en programar como un administrador de sistemas, no como un programador. Para usted, el software es algo que utiliza para lograr una tarea particular de la manera más conveniente. He estado escribiendo código Java durante 20 años, y algunos de los proyectos en los que he trabajado han tenido un equipo de 20, 2 años para lograrlo. Java no es un reemplazo para Python ni viceversa. Ambos hacen su trabajo, pero sus trabajos son completamente diferentes.
Richard
1
La razón por la que Java es el estándar de facto es porque es simplemente correcto. Fue creado correctamente, la primera vez, por personas serias que tenían barbas antes de que las barbas estuvieran de moda. Si estás acostumbrado a activar scripts de shell para manipular archivos, Java parece intolerablemente hinchado. Pero si desea crear un clúster de servidores redundante doble capaz de servir a 50 millones de personas al día, respaldado por el almacenamiento en caché de redis en clúster, 3 sistemas de facturación y un clúster de base de datos Oracle de 20 millones de libras. No va a utilizar Python, incluso si accede a la base de datos en Python tiene 25 líneas menos de código.
Richard

Respuestas:

237

El lenguaje Java

Creo que todas estas respuestas están perdiendo el punto al intentar atribuir la intención a la forma en que funciona Java. La verbosidad de Java no se deriva de que esté orientada a objetos, ya que Python y muchos otros lenguajes todavía tienen una sintaxis terser. La verbosidad de Java tampoco proviene de su soporte de modificadores de acceso. En cambio, es simplemente cómo se diseñó y evolucionó Java.

Java se creó originalmente como una C ligeramente mejorada con OO. Como tal, Java tiene una sintaxis de la era de los 70. Además, Java es muy conservador acerca de agregar características para mantener la compatibilidad con versiones anteriores y permitirle resistir el paso del tiempo. Si Java hubiera agregado características modernas como los literales XML en 2005, cuando XML estaba de moda, el lenguaje habría estado repleto de características fantasmas que a nadie le importan y que limitan su evolución 10 años después. Por lo tanto, Java simplemente carece de mucha sintaxis moderna para expresar conceptos concisamente.

Sin embargo, no hay nada fundamental que evite que Java adopte esa sintaxis. Por ejemplo, Java 8 agregó lambdas y referencias de métodos, reduciendo en gran medida la verbosidad en muchas situaciones. Java podría agregar de manera similar soporte para declaraciones de tipo de datos compactas como las clases de caso de Scala. Pero Java simplemente no lo ha hecho. Tenga en cuenta que los tipos de valores personalizados están en el horizonte y esta característica puede introducir una nueva sintaxis para declararlos. Supongo que ya veremos.


La cultura de Java

La historia del desarrollo empresarial de Java nos ha llevado en gran medida a la cultura que vemos hoy. A finales de los 90 / principios de los 00, Java se convirtió en un lenguaje extremadamente popular para las aplicaciones comerciales del lado del servidor. En aquel entonces, esas aplicaciones se escribieron en gran medida ad-hoc e incorporaron muchas preocupaciones complejas, como las API HTTP, las bases de datos y el procesamiento de fuentes XML.

En los años 00 quedó claro que muchas de estas aplicaciones tenían mucho en común y los marcos para gestionar estas preocupaciones, como Hibernate ORM, el analizador Xerces XML, JSP y la API de servlet, y EJB, se hicieron populares. Sin embargo, si bien estos marcos redujeron el esfuerzo para trabajar en el dominio particular que configuraron para automatizar, requirieron configuración y coordinación. En ese momento, por cualquier razón, era popular escribir marcos para atender el caso de uso más complejo y, por lo tanto, estas bibliotecas eran complicadas de configurar e integrar. Y con el tiempo se volvieron cada vez más complejos a medida que acumulaban características. El desarrollo empresarial de Java gradualmente se convirtió cada vez más en unir bibliotecas de terceros y menos en escribir algoritmos.

Finalmente, la tediosa configuración y administración de las herramientas empresariales se volvió lo suficientemente dolorosa como para que los marcos, especialmente el marco de Spring, aparecieran para administrar la administración. Podría poner toda su configuración en un solo lugar, según la teoría, y la herramienta de configuración luego configuraría las piezas y las uniría. Desafortunadamente, estos "marcos de trabajo" agregaron más abstracción y complejidad a toda la bola de cera.

En los últimos años, las bibliotecas más livianas han crecido en popularidad. No obstante, toda una generación de programadores de Java alcanzó la mayoría de edad durante el crecimiento de los marcos empresariales pesados. Sus modelos a seguir, los que desarrollan los marcos, escribieron fábricas de fábrica y cargadores de beans de configuración proxy. Tenían que configurar e integrar estas monstruosidades día a día. Y como resultado, la cultura de la comunidad en su conjunto siguió el ejemplo de estos marcos y tendió a un exceso de ingeniería.

El secreto de Solomonoff
fuente
15
Lo curioso es que otros idiomas parecen estar aprendiendo algunos "java-ismos". Las características de PHP OO están fuertemente inspiradas en Java, y hoy en día JavaScript es criticado por su gran cantidad de marcos y lo difícil que es reunir todas las dependencias e iniciar una nueva aplicación.
marcus
14
¿@marcus tal vez porque algunas personas finalmente aprendieron lo de "no reinventar la rueda"? Las dependencias son el costo de no reinventar la rueda después de todo
Walfrat
99
"Terse" a menudo se traduce en arcano, "inteligente" , un código de golf.
Tulains Córdova
77
Respuesta reflexiva y bien escrita. Contexto histórico. Relativamente no opinado. Bien hecho.
Cheezmeister
10
@ TulainsCórdova Estoy de acuerdo en que deberíamos "evitar ... trucos ingeniosos como la peste" (Donald Knuth), pero eso no significa escribir un forbucle cuando mapsería más apropiado (y legible). Hay un medio feliz.
Brian McCutchon
73

Creo que tengo una respuesta para uno de los puntos que plantea que otros no han planteado.

Java se optimiza para la detección de errores de programador en tiempo de compilación al nunca hacer suposiciones

En general, Java tiende a inferir solo hechos sobre el código fuente después de que el programador ya ha expresado explícitamente su intención. El compilador de Java nunca hace suposiciones sobre el código y solo usará inferencia para reducir el código redundante.

La razón detrás de esta filosofía es que el programador es solo humano. Lo que escribimos no siempre es lo que realmente pretendemos que haga el programa. El lenguaje Java intenta mitigar algunos de esos problemas al obligar al desarrollador a declarar siempre explícitamente sus tipos. Esa es solo una forma de verificar que el código que se escribió realmente cumpla lo que se pretendía.

Algunos otros idiomas impulsan esa lógica aún más al verificar las condiciones previas, las condiciones posteriores y las invariantes (aunque no estoy seguro de que lo hagan en tiempo de compilación). Estas son formas aún más extremas para que el programador haga que el compilador verifique su propio trabajo.

En su caso, esto significa que para que el compilador garantice que realmente está devolviendo los tipos que cree que está devolviendo, debe proporcionar esa información al compilador.

En Java hay dos formas de hacerlo:

  1. Utilizo Triplet<A, B, C>como tipo de retorno (que en realidad debería estar en java.util, y realmente no puedo explicar por qué no lo es. Sobre todo porque JDK8 introducir Function, BiFunction, Consumer, BiConsumer, etc ... pero parece que Pairy Tripletal menos tendría sentido. Pero estoy divagando )

  2. Cree su propio tipo de valor para ese propósito, donde cada campo se nombra y se escribe correctamente.

En ambos casos, el compilador puede garantizar que su función devuelva los tipos que declaró, y que la persona que llama se dé cuenta de cuál es el tipo de cada campo devuelto y los use en consecuencia.

Algunos idiomas proporcionan verificación de tipo estático Y inferencia de tipo al mismo tiempo, pero eso deja la puerta abierta para una clase sutil de problemas de desajuste de tipo. Cuando el desarrollador tenía la intención de devolver un valor de cierto tipo, pero en realidad devuelve otro y el compilador TODAVÍA acepta el código porque sucede que, por coincidencia, tanto la función como la persona que llama solo usan métodos que se pueden aplicar a ambos y los tipos reales

Considere algo como esto en Typecript (o tipo de flujo) donde se usa la inferencia de tipos en lugar de la escritura explícita.

function parseDurationInMillis(csvLine){
    // here the developer intends to return a Number, 
    // but actually it's a String
    return csv.firstField();
}

// Compiler infers that parseDurationInMillis is String, so it does
// string concatenation and infers that plusTwoSeconds is String
// Developer actually intended Number
var plusTwoSeconds = 2000 + parseDurationInMillis(csvLine);

Este es un caso trivial tonto, por supuesto, pero puede ser mucho más sutil y conducir a problemas difíciles de depurar, porque el código parece correcto. Este tipo de problemas se evita por completo en Java, y en eso está diseñado todo el lenguaje.


Tenga en cuenta que siguiendo los principios orientados a objetos adecuados y el modelado basado en dominio, el caso de análisis de duración en la pregunta vinculada también podría devolverse como un java.time.Durationobjeto, que sería mucho más explícito que los dos casos anteriores.

LordOfThePigs
fuente
38
Afirmar que Java se optimiza para la corrección del código puede ser un punto válido, pero me parece (programación de muchos lenguajes, recientemente un poco de Java) que el lenguaje falla o es extremadamente ineficiente al hacerlo. De acuerdo, Java es viejo y muchas cosas no existían en ese momento, pero hay alternativas modernas que proporcionan una mejor verificación de corrección, como Haskell (¿y me atrevo a decir? Rust or Go). Toda la dificultad de Java es innecesaria para este objetivo. - Y además, esto no explica en absoluto la cultura, pero Solomonoff reveló que la cultura es BS de todos modos.
tomsmeding
8
Esa muestra no demuestra que la inferencia de tipos es el problema, solo que la decisión de JavaScript de permitir conversiones implícitas en un lenguaje dinámico es estúpida. Cualquier lenguaje dinámico sensato o lenguaje promocionado estáticamente con inferencia de tipos no permitiría esto. Como ejemplo para ambos tipos: python lanzaría un tiempo de ejecución, excepto que Haskell no compilaría.
Voo
39
@tomsmeding Vale la pena señalar que este argumento es especialmente tonto porque Haskell ha existido durante 5 años más que Java. Java puede tener un sistema de tipos, pero ni siquiera tenía una verificación de corrección de vanguardia cuando se lanzó.
Alexis King
21
"Java se optimiza para la detección de errores en tiempo de compilación" - No. Lo proporciona , pero, como otros han señalado, ciertamente no lo optimiza en ningún sentido de la palabra. Además, este argumento es completamente ajenos a la pregunta de OP porque otros idiomas que no realmente optimizan la detección de errores en tiempo de compilación tienen mucho más ligero sintaxis para escenarios similares. La verbosidad exagerada de Java no tiene nada que ver con su verificación en tiempo de compilación.
Konrad Rudolph
19
@KonradRudolph Sí, esta respuesta es completamente absurda, aunque supongo que es emocionalmente atractiva. No estoy seguro de por qué tiene tantos votos. Haskell (o quizás aún más significativamente, Idris) optimiza mucho más la corrección que Java, y tiene tuplas con una sintaxis ligera. Lo que es más, definir un tipo de datos es una línea, y obtienes todo lo que obtendría la versión Java. Esta respuesta es una excusa para un diseño de lenguaje pobre y una verbosidad innecesaria, pero suena bien si te gusta Java y no estás familiarizado con otros idiomas.
Alexis King
50

Java y Python son los dos idiomas que más uso, pero vengo de la otra dirección. Es decir, estaba inmerso en el mundo de Java antes de comenzar a usar Python, por lo que podría ayudarlo. Creo que la respuesta a la pregunta más amplia de "por qué las cosas son tan pesadas" se reduce a 2 cosas:

  • Los costos en torno al desarrollo entre los dos son como el aire en un globo animal de globo largo. Puede apretar una parte del globo y otra parte se hincha. Python tiende a exprimir la primera parte. Java aprieta la parte posterior.
  • Java todavía carece de algunas características que eliminen parte de este peso. Java 8 hizo una gran mella en esto, pero la cultura no ha digerido completamente los cambios. Java podría usar algunas cosas más como yield.

Java 'optimiza' para software de alto valor que será mantenido por muchos años por grandes equipos de personas. He tenido la experiencia de escribir cosas en Python y mirarlas un año después y estar desconcertado por mi propio código. En Java puedo ver pequeños fragmentos de código de otras personas e instantáneamente saber lo que hace. En Python realmente no puedes hacer eso. No es que uno sea mejor como parece darse cuenta, solo tienen diferentes costos.

En el caso específico que mencionas, no hay tuplas. La solución fácil es crear una clase con valores públicos. Cuando salió Java por primera vez, la gente hacía esto con bastante regularidad. El primer problema al hacerlo es que es un dolor de cabeza de mantenimiento. Si necesita agregar algo de lógica o seguridad de hilo o si desea usar polimorfismo, al menos necesitará tocar cada clase que interactúa con ese objeto 'tuple-esque'. En Python hay soluciones para esto, como __getattr__etc., por lo que no es tan grave.

Sin embargo, hay algunos malos hábitos (IMO) en torno a esto. En este caso, si quieres una tupla, me pregunto por qué lo convertirías en un objeto mutable. Solo debería necesitar getters (en una nota al margen, odio la convención get / set pero es lo que es). Creo que una clase básica (mutable o no) puede ser útil en un contexto privado o paquete privado en Java . Es decir, al limitar las referencias en el proyecto a la clase, puede refactorizar más tarde según sea necesario sin cambiar la interfaz pública de la clase. Aquí hay un ejemplo de cómo puede crear un objeto inmutable simple:

public class Blah 
{
  public static Blah blah(long number, boolean isInSeconds, boolean lessThanOneMillis)
  {
    return new Blah(number, isInSeconds, lessThanOneMillis);
  }

  private final long number;
  private final boolean isInSeconds;
  private final boolean lessThanOneMillis;

  public Blah(long number, boolean isInSeconds, boolean lessThanOneMillis)
  {
    this.number = number;
    this.isInSeconds = isInSeconds;
    this.lessThanOneMillis = lessThanOneMillis;
  }

  public long getNumber()
  {
    return number;
  }

  public boolean isInSeconds()
  {
    return isInSeconds;
  }

  public boolean isLessThanOneMillis()
  {
    return lessThanOneMillis;
  }
}

Este es un tipo de patrón que uso. Si no está utilizando un IDE, debe comenzar. Generará los captadores (y los setters si los necesita) para usted, por lo que esto no es tan doloroso.

Me sentiría negligente si no señalara que ya hay un tipo que parece satisfacer la mayoría de sus necesidades aquí . Dejando eso de lado, el enfoque que está utilizando no se adapta bien a Java, ya que juega con sus debilidades, no con sus fortalezas. Aquí hay una mejora simple:

public class Blah 
{
  public static Blah fromSeconds(long number)
  {
    return new Blah(number * 1000_000);
  }

  public static Blah fromMills(long number)
  {
    return new Blah(number * 1000);
  }

  public static Blah fromNanos(long number)
  {
    return new Blah(number);
  }

  private final long nanos;

  private Blah(long nanos)
  {
    this.nanos = nanos;
  }

  public long getNanos()
  {
    return nanos;
  }

  public long getMillis()
  {
    return getNanos() / 1000; // or round, whatever your logic is
  }

  public long getSeconds()
  {
    return getMillis() / 1000; // or round, whatever your logic is
  }

  /* I don't really know what this is about but I hope you get the idea */
  public boolean isLessThanOneMillis()
  {
    return getMillis() < 1;
  }
}
JimmyJames
fuente
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
maple_shaft
2
¿Puedo sugerirle que también mire Scala con respecto a las características de Java "más modernas" ...
MikeW
1
@MikeW De hecho, comencé con Scala en los primeros lanzamientos y estuve activo en los foros de Scala por un tiempo. Creo que es un gran logro en el diseño del lenguaje, pero llegué a la conclusión de que no era realmente lo que estaba buscando y que era un punto fijo en esa comunidad. Debería mirarlo nuevamente porque probablemente ha cambiado significativamente desde entonces.
JimmyJames
Esta respuesta no aborda el aspecto del valor aproximado planteado por el OP.
ErikE
@ErikE Las palabras "valor aproximado" no aparecen en el texto de la pregunta. Si puede señalarme la parte específica de la pregunta que no abordé, puedo intentar hacerlo.
JimmyJames
41

Antes de enfadarse demasiado con Java, lea mi respuesta en su otra publicación .

Una de sus quejas es la necesidad de crear una clase solo para devolver un conjunto de valores como respuesta. ¡Esta es una preocupación válida que creo que muestra que sus intuiciones de programación están en camino! Sin embargo, creo que otras respuestas están perdiendo la marca al apegarse al antipatrón de obsesión primitiva al que te has comprometido. Y Java no tiene la misma facilidad de trabajo con múltiples primitivas que Python, donde puede devolver múltiples valores de forma nativa y asignarlos a múltiples variables fácilmente.

Pero una vez que empiezas a pensar en lo que un ApproximateDurationtipo hace por ti, te das cuenta de que no tiene un alcance tan limitado como "solo una clase aparentemente innecesaria para devolver tres valores". El concepto representado por esta clase es en realidad uno de los conceptos de negocio de su dominio central: la necesidad de poder representar los tiempos de manera aproximada y compararlos. Esto debe ser parte del lenguaje ubicuo del núcleo de su aplicación, con un buen soporte de objetos y dominios para que pueda ser probado, modular, reutilizable y útil.

¿Su código que suma duraciones aproximadas juntas (o duraciones con un margen de error, sin embargo usted lo representa) es totalmente procesal o tiene algún objeto? Yo propondría que un buen diseño en torno a la suma de duraciones aproximadas dictaría hacerlo fuera de cualquier código de consumo, dentro de una clase que se pueda probar. Creo que usar este tipo de objeto de dominio tendrá un efecto dominó positivo en su código que lo ayudará a alejarse de los pasos del procedimiento línea por línea para lograr una sola tarea de alto nivel (aunque con muchas responsabilidades), hacia clases de responsabilidad única que están libres de los conflictos de diferentes preocupaciones.

Por ejemplo, supongamos que aprende más acerca de qué precisión o escala se requiere realmente para que la suma y la duración de su duración funcionen correctamente, y descubre que necesita un indicador intermedio para indicar "aproximadamente 32 milisegundos de error" (cerca del cuadrado raíz de 1000, así que a medio camino logarítmicamente entre 1 y 1000). Si se ha vinculado a un código que usa primitivas para representar esto, tendrá que encontrar cada lugar del código donde lo tenga is_in_seconds,is_under_1msy cambiarlo ais_in_seconds,is_about_32_ms,is_under_1ms. ¡Todo tendría que cambiar por todas partes! Hacer una clase cuya responsabilidad es registrar el margen de error para que pueda consumirse en otro lugar libera a sus consumidores de conocer detalles de qué márgenes de error importan o algo sobre cómo se combinan, y les permite especificar el margen de error que es relevante En el momento. (Es decir, ningún código de consumo cuyo margen de error es correcto se ve obligado a cambiar cuando agrega un nuevo nivel de margen de error en la clase, ya que todos los márgenes de error anteriores siguen siendo válidos).

Frase de cierre

La queja sobre la pesadez de Java parece desaparecer a medida que te acercas a los principios de SOLID y GRASP, y a una ingeniería de software más avanzada.

Apéndice

Agregaré de manera completamente gratuita e injusta que las propiedades automáticas de C # y la capacidad de asignar propiedades de solo obtención en los constructores ayudan a limpiar aún más el código algo desordenado que requerirá "la forma Java" (con campos de respaldo privados explícitos y funciones de captador / configurador) :

// Warning: C# code!
public sealed class ApproximateDuration {
   public ApproximateDuration(int lowMilliseconds, int highMilliseconds) {
      LowMilliseconds = lowMilliseconds;
      HighMilliseconds = highMilliseconds;
   }
   public int LowMilliseconds { get; }
   public int HighMilliseconds { get; }
}

Aquí hay una implementación de Java de lo anterior:

public final class ApproximateDuration {
  private final int lowMilliseconds;
  private final int highMilliseconds;

  public ApproximateDuration(int lowMilliseconds, int highMilliseconds) {
    this.lowMilliseconds = lowMilliseconds;
    this.highMilliseconds = highMilliseconds;
  }

  public int getLowMilliseconds() {
    return lowMilliseconds;
  }

  public int getHighMilliseconds() {
    return highMilliseconds;
  }
}

Ahora que está bastante limpio. Tenga en cuenta el uso muy importante e intencional de la inmutabilidad: esto parece crucial para este tipo particular de clase de valor.

Para el caso, esta clase también es un candidato decente para ser un struct, un tipo de valor. Algunas pruebas mostrarán si cambiar a una estructura tiene un beneficio de rendimiento en tiempo de ejecución (podría).

ErikE
fuente
99
Creo que esta es mi respuesta favorita. ¡Encuentro que cuando promuevo una clase de basura desechable como esta en algo adulto, se convierte en el hogar de todas las responsabilidades relacionadas, y zing! ¡todo se vuelve más limpio! Y generalmente aprendo algo interesante sobre el espacio del problema al mismo tiempo. Obviamente, hay una habilidad para evitar el exceso de ingeniería ... pero Gosling no fue estúpido cuando dejó de lado la sintaxis de tuplas, al igual que con los GOTO. Su declaración final es excelente. ¡Gracias!
SusanW
2
Si le gusta esta respuesta, lea sobre Diseño impulsado por dominio. Tiene mucho que decir sobre este tema de representar conceptos de dominio en código.
neontapir
@neontapir Sí, gracias! ¡Sabía que había un nombre para eso! :-) Aunque debo decir que lo prefiero cuando un concepto de dominio aparece orgánicamente a partir de un poco de refactorización inspirada (¡así!) ... es un poco como cuando puedes solucionar tu problema de gravedad del siglo XIX descubriendo Neptuno ... .
SusanW
@SusanW Estoy de acuerdo. ¡Refactorizar para eliminar la obsesión primitiva puede ser una excelente manera de descubrir conceptos de dominio!
neontapir
Agregué una versión Java del ejemplo como se discutió. No estoy al tanto de todo el azúcar sintáctico mágico en C #, así que si falta algo, avíseme.
JimmyJames
24

Tanto Python como Java están optimizados para la mantenibilidad de acuerdo con la filosofía de sus diseñadores, pero tienen ideas muy diferentes sobre cómo lograrlo.

Python es un lenguaje multi-paradigmático que optimiza la claridad y simplicidad del código (fácil de leer y escribir).

Java es (tradicionalmente) un lenguaje OO de clase de paradigma único que optimiza la explicidad y la coherencia , incluso a costa de un código más detallado.

Una tupla de Python es una estructura de datos con un número fijo de campos. La misma funcionalidad se puede lograr con una clase regular con campos declarados explícitamente. En Python es natural proporcionar tuplas como alternativa a las clases porque le permite simplificar enormemente el código, especialmente debido al soporte de sintaxis incorporado para las tuplas.

Pero esto realmente no coincide con la cultura Java para proporcionar estos accesos directos, ya que ya puede usar clases explícitas declaradas. No es necesario introducir un tipo diferente de estructura de datos solo para guardar algunas líneas de código y evitar algunas declaraciones.

Java prefiere un solo concepto (clases) aplicado consistentemente con un mínimo de azúcar sintáctico para casos especiales, mientras que Python proporciona múltiples herramientas y mucho azúcar sintáctico para permitirle elegir el más conveniente para cualquier propósito en particular.

JacquesB
fuente
+1, pero ¿cómo se combina esto con lenguajes tipados estáticamente con clases, como Scala, que sin embargo encontraron la necesidad de introducir tuplas? Creo que las tuplas son la solución más ordenada en algunos casos, incluso para idiomas con clases.
Andres F.
@AndresF .: ¿Qué quieres decir con malla? Scala es un lenguaje distinto de Java y tiene diferentes principios de diseño y modismos.
JacquesB
Lo dije en respuesta a su quinto párrafo, que comienza con "pero esto realmente no coincide con la cultura Java [...]". De hecho, estoy de acuerdo en que la verbosidad es parte de la cultura de Java, pero la falta de tuplas no puede ser porque "ya usan clases explícitamente declaradas"; después de todo, Scala también tiene clases explícitas (e introduce las clases de casos más concisas ), pero También permite tuplas! Al final, creo que es probable que Java no haya introducido tuplas no solo porque las clases pueden lograr lo mismo (con más desorden), sino también porque su sintaxis debe ser familiar para los programadores de C, y C no tenía tuplas.
Andres F.
@AndresF .: Scala no tiene aversión contra las múltiples formas de hacer las cosas, combina características tanto del paradigma funcional como de la OO clásica. Está más cerca de la filosofía de Python de esa manera.
JacquesB
@AndresF .: Sí, Java comenzó con una sintaxis cercana a C / C ++, pero simplificó mucho. C # comenzó como una copia casi exacta de Java, pero a lo largo de los años ha agregado muchas características, incluidas las tuplas. Por lo tanto, es realmente una diferencia en la filosofía del diseño del lenguaje. (Sin embargo, las versiones posteriores de Java parecen ser menos dogmáticas. La función de orden superior se rechazó inicialmente porque podría hacer lo mismo qué clases, pero ahora se han introducido. Así que tal vez estamos viendo un cambio en la filosofía.)
JacquesB
16

No busques prácticas; Por lo general, es una mala idea, como se dice en Mejores prácticas MALO, patrones ¿BUENO? . Sé que no estás pidiendo mejores prácticas, pero sigo pensando que encontrarás algunos elementos relevantes allí.

Buscar una solución a su problema es mejor que una práctica, y su problema no es una tupla para devolver tres valores en Java rápidamente:

  • Hay matrices
  • Puede devolver una matriz como una lista en una línea: Arrays.asList (...)
  • Si desea mantener el lado del objeto con la menor cantidad posible (y sin lombok):

class MyTuple {
    public final long value_in_milliseconds;
    public final boolean is_in_seconds;
    public final boolean is_under_1ms;
    public MyTuple(long value_in_milliseconds,....){
        ...
    }
 }

Aquí tiene un objeto inmutable que contiene solo sus datos y público, por lo que no hay necesidad de captadores. Sin embargo, tenga en cuenta que si usa algunas herramientas de serialización o una capa de persistencia como un ORM, comúnmente usan getter / setter (y pueden aceptar un parámetro para usar campos en lugar de getter / setter). Y es por eso que estas prácticas se usan mucho. Entonces, si desea conocer las prácticas, es mejor entender por qué están aquí para un mejor uso de ellas.

Finalmente: uso getters porque uso muchas herramientas de serialización, pero tampoco las escribo; Uso lombok: uso los accesos directos proporcionados por mi IDE.

Walfrat
fuente
99
Todavía hay valor en comprender expresiones idiomáticas comunes que se encuentran en varios idiomas, ya que se convierten en un estándar de facto más accesible. Esos modismos tienden a caer bajo el título de "mejores prácticas" por cualquier razón.
Berin Loritsch
3
Hola, una solución sensata al problema. Parece bastante razonable escribir una clase (/ struct) con unos pocos miembros públicos en lugar de una solución OOP sobredimensionada con ingeniería completa con [gs] etters.
tomsmeding
3
Esto lleva a la hinchazón porque, a diferencia de Python, no se recomienda la búsqueda o una solución más corta y elegante. Pero lo bueno es que la hinchazón será más o menos del mismo tipo para cualquier desarrollador de Java. Por lo tanto, existe un mayor grado de mantenimiento porque los desarrolladores son más intercambiables, y si dos proyectos se unen o dos equipos divergen involuntariamente, hay menos diferencia de estilo para luchar.
Mikhail Ramendik
2
@MikhailRamendik Es una compensación entre YAGNI y OOP. OOP ortodoxo dice que cada objeto debe ser reemplazable; Lo único que importa es la interfaz pública del objeto. Esto le permite escribir código que esté menos enfocado en objetos concretos y más en interfaces; y dado que los campos no pueden ser partes de una interfaz, nunca los exponga. Esto puede hacer que el código sea mucho más fácil de mantener a largo plazo. Además, le permite evitar estados no válidos. En su muestra original, es posible tener un objeto que sea "<ms" y "s", que son mutuamente excluyentes. Eso es malo.
Luaan
2
@PeterMortensen Es una biblioteca de Java que hace muchas cosas poco relacionadas. Aunque es muy bueno. Aquí están las características
Michael
11

Sobre modismos de Java en general:

Hay varias razones por las cuales Java tiene clases para todo. Hasta donde yo sé, la razón principal es:

Java debería ser fácil de aprender para principiantes. Cuanto más explícitas son las cosas, más difícil es perder detalles importantes. Ocurre menos magia que sería difícil de comprender para los principiantes.


En cuanto a su ejemplo específico: la línea de argumento para una clase separada es esta: si esas tres cosas están suficientemente relacionadas entre sí que se devuelven como un valor, vale la pena nombrar esa "cosa". Y la introducción de un nombre para un grupo de cosas que están estructuradas de una manera común significa definir una clase.

Puede reducir el repetitivo con herramientas como Lombok:

@Value
class MyTuple {
    long value_in_milliseconds;
    boolean is_in_seconds;
    boolean is_under_1ms;
}
marstato
fuente
55
También es el proyecto AutoValue de Google: github.com/google/auto/blob/master/value/userguide/index.md
Ivan
¡Esta es también la razón por la cual los programas Java pueden mantenerse con relativa facilidad !
Thorbjørn Ravn Andersen
7

Hay muchas cosas que se podrían decir sobre la cultura Java, pero creo que en el caso de que te enfrentes ahora, hay algunos aspectos importantes:

  1. El código de la biblioteca se escribe una vez, pero se usa con mucha más frecuencia. Si bien es bueno minimizar la sobrecarga de escribir la biblioteca, probablemente valga la pena a la larga escribir de una manera que minimice la sobrecarga de usar la biblioteca.
  2. Eso significa que los tipos de autodocumentación son geniales: los nombres de los métodos ayudan a aclarar lo que sucede y lo que se obtiene de un objeto.
  3. La escritura estática es una herramienta muy útil para eliminar ciertas clases de errores. Ciertamente no soluciona todo (a la gente le gusta bromear sobre Haskell que una vez que obtienes el sistema de tipos para aceptar tu código, probablemente sea correcto), pero hace que sea muy fácil hacer que ciertas cosas incorrectas sean imposibles.
  4. Escribir código de biblioteca se trata de especificar contratos. La definición de interfaces para sus argumentos y tipos de resultados hace que los límites de sus contratos estén más claramente definidos. Si algo acepta o produce una tupla, no hay forma de decir si es el tipo de tupla que realmente debería recibir o producir, y hay muy pocas restricciones en el tipo genérico (¿tiene incluso el número correcto de elementos? ellos del tipo que estabas esperando?).

Clases de "estructura" con campos

Como han mencionado otras respuestas, puede usar una clase con campos públicos. Si haces estos finales, obtienes una clase inmutable y los inicializas con el constructor:

   class ParseResult0 {
      public final long millis;
      public final boolean isSeconds;
      public final boolean isLessThanOneMilli;

      public ParseResult0(long millis, boolean isSeconds, boolean isLessThanOneMilli) {
         this.millis = millis;
         this.isSeconds = isSeconds;
         this.isLessThanOneMilli = isLessThanOneMilli;
      }
   }

Por supuesto, esto significa que está atado a una clase en particular, y cualquier cosa que necesite producir o consumir un resultado de análisis debe usar esta clase. Para algunas aplicaciones, está bien. Para otros, eso puede causar algo de dolor. Gran parte del código Java se trata de definir contratos, y eso generalmente lo llevará a las interfaces.

Otro escollo es que con un enfoque basado en clases, está exponiendo campos y todos esos campos deben tener valores. Por ejemplo, isSeconds y millis siempre tienen que tener algún valor, incluso si isLessThanOneMilli es verdadero. ¿Cuál debería ser la interpretación del valor del campo millis cuando isLessThanOneMilli es verdadero?

"Estructuras" como interfaces

Con los métodos estáticos permitidos en las interfaces, en realidad es relativamente fácil crear tipos inmutables sin una gran sobrecarga sintáctica. Por ejemplo, podría implementar el tipo de estructura de resultados del que estás hablando como algo así:

   interface ParseResult {
      long getMillis();

      boolean isSeconds();

      boolean isLessThanOneMilli();

      static ParseResult from(long millis, boolean isSeconds, boolean isLessThanOneMill) {
         return new ParseResult() {
            @Override
            public boolean isSeconds() {
               return isSeconds;
            }

            @Override
            public boolean isLessThanOneMilli() {
               return isLessThanOneMill;
            }

            @Override
            public long getMillis() {
               return millis;
            }
         };
      }
   }

Eso sigue siendo un montón de repeticiones, estoy totalmente de acuerdo, pero también hay un par de beneficios, y creo que esos comienzan a responder algunas de sus preguntas principales.

Con una estructura como este resultado de análisis, el contrato de su analizador está muy claramente definido. En Python, una tupla no es realmente distinta de otra tupla. En Java, la escritura estática está disponible, por lo que ya descartamos ciertas clases de errores. Por ejemplo, si devuelve una tupla en Python y desea devolver la tupla (millis, isSeconds, isLessThanOneMilli), puede hacer accidentalmente:

return (true, 500, false)

cuando quisiste decir:

return (500, true, false)

Con este tipo de interfaz Java, no puede compilar:

return ParseResult.from(true, 500, false);

en absoluto. Tu tienes que hacer:

return ParseResult.from(500, true, false);

Eso es un beneficio de los idiomas tipados estáticamente en general.

Este enfoque también comienza a darle la capacidad de restringir los valores que puede obtener. Por ejemplo, al llamar a getMillis (), puede verificar si isLessThanOneMilli () es verdadero y, si lo es, lanzar una IllegalStateException (por ejemplo), ya que no hay un valor significativo de millis en ese caso.

Haciendo difícil hacer lo incorrecto

Sin embargo, en el ejemplo de interfaz anterior, aún tiene el problema de que podría intercambiar accidentalmente los argumentos isSeconds e isLessThanOneMilli, ya que tienen el mismo tipo.

En la práctica, es posible que desee utilizar TimeUnit y la duración, para obtener un resultado como:

   interface Duration {
      TimeUnit getTimeUnit();

      long getDuration();

      static Duration from(TimeUnit unit, long duration) {
         return new Duration() {
            @Override
            public TimeUnit getTimeUnit() {
               return unit;
            }

            @Override
            public long getDuration() {
               return duration;
            }
         };
      }
   }

   interface ParseResult2 {

      boolean isLessThanOneMilli();

      Duration getDuration();

      static ParseResult2 from(TimeUnit unit, long duration) {
         Duration d = Duration.from(unit, duration);
         return new ParseResult2() {
            @Override
            public boolean isLessThanOneMilli() {
               return false;
            }

            @Override
            public Duration getDuration() {
               return d;
            }
         };
      }

      static ParseResult2 lessThanOneMilli() {
         return new ParseResult2() {
            @Override
            public boolean isLessThanOneMilli() {
               return true;
            }

            @Override
            public Duration getDuration() {
               throw new IllegalStateException();
            }
         };
      }
   }

Eso va a ser mucho más código, pero solo necesita escribirlo una vez, y (suponiendo que haya documentado correctamente las cosas), las personas que terminan usando su código no tienen que adivinar qué significa el resultado, y no puede hacer accidentalmente cosas como result[0]cuando quieren decir result[1]. Todavía puede crear instancias de manera bastante sucinta, y obtener datos de ellas tampoco es tan difícil:

  ParseResult2 x = ParseResult2.from(TimeUnit.MILLISECONDS, 32);
  ParseResult2 y = ParseResult2.lessThanOneMilli();

Tenga en cuenta que también podría hacer algo como esto con el enfoque basado en la clase. Simplemente especifique constructores para los diferentes casos. Sin embargo, todavía tiene el problema de qué inicializar los otros campos y no puede evitar el acceso a ellos.

Otra respuesta mencionó que la naturaleza de tipo empresarial de Java significa que la mayor parte del tiempo, está componiendo otras bibliotecas que ya existen o escribiendo bibliotecas para que otras personas las usen. Su API pública no debería requerir mucho tiempo consultando la documentación para descifrar los tipos de resultados si se puede evitar.

Solo escribe estas estructuras una vez, pero las crea muchas veces, por lo que aún desea esa creación concisa (que obtiene). La escritura estática asegura que los datos que está obteniendo de ellos sean lo que espera.

Ahora, dicho todo esto, todavía hay lugares donde las tuplas o listas simples pueden tener mucho sentido. Puede haber menos sobrecarga al devolver una matriz de algo, y si ese es el caso (y esa sobrecarga es significativa, lo que determinaría con la creación de perfiles), entonces usar una simple matriz de valores internamente puede tener mucho sentido. Su API pública aún debería tener tipos claramente definidos.

Joshua Taylor
fuente
He usado mi propia enumeración en lugar de TimeUnit al final, porque acabo de agregar UNDER1MS junto con las unidades de tiempo reales. Así que ahora solo tengo dos campos, no tres. (En teoría, podría hacer un mal uso de TimeUnit.NANOSECONDS, pero eso sería muy confuso.)
Mikhail Ramendik
No creé un captador, pero ahora veo cómo un captador me permitiría lanzar una excepción si, con originalTimeUnit=DurationTimeUnit.UNDER1MS, la persona que llama intenta leer el valor de milisegundos.
Mikhail Ramendik
Sé que lo has mencionado, pero recuerda que tu ejemplo con las tuplas de Python es realmente sobre tuplas dinámicas vs tuplas estáticamente escritas; en realidad no dice mucho sobre las tuplas mismas. Usted puede haber escrito estáticamente tuplas que dejar de compilar, como estoy seguro de que sabe :)
Andrés F.
1
@Veedrac No estoy seguro de lo que quieres decir. Mi punto fue que está bien escribir el código de la biblioteca de manera más detallada como esta (a pesar de que lleva más tiempo), ya que pasará más tiempo usando el código que escribiéndolo .
Joshua Taylor
1
@Veedrac Sí, eso es cierto. Pero yo sostengo que no es una distinción entre el código que será reutilizado de manera significativa, y el código que no lo hará. Por ejemplo, en un paquete Java, algunas de las clases son públicas y se utilizan desde otras ubicaciones. Esas API deberían estar bien pensadas. Internamente, hay clases que solo necesitan hablar entre ellas. Esos acoplamientos internos son los lugares donde las estructuras rápidas y sucias (por ejemplo, un Objeto [] que se espera que tenga tres elementos) podrían estar bien. Para la API pública, generalmente se debe preferir algo más significativo y explícito.
Joshua Taylor
7

El problema es que comparas manzanas con naranjas . Preguntó cómo simular la devolución de más de un valor único, dando un ejemplo de Python rápido y sucio con tupla sin tipo y, de hecho, recibió la respuesta prácticamente unica .

La respuesta aceptada proporciona una solución comercial correcta. No hay una solución temporal rápida que deba tirar e implementar correctamente la primera vez que necesite hacer algo práctico con el valor devuelto, sino una clase POJO que sea compatible con un gran conjunto de bibliotecas, incluida la persistencia, la serialización / deserialización, instrumentación y todo lo posible.

Esto tampoco es largo en absoluto. Lo único que necesita escribir son las definiciones de campo. Se pueden generar setters, getters, hashCode y equals. Entonces, su pregunta real debería ser, por qué los captadores y establecedores no se generan automáticamente, sino que es un problema de sintaxis (el problema del azúcar sintáctico, dirían algunos) y no el problema cultural.

Y finalmente, estás pensando demasiado tratando de acelerar algo que no es importante en absoluto. El tiempo dedicado a escribir clases DTO es insignificante en comparación con el tiempo dedicado a mantener y depurar el sistema. Por lo tanto, nadie optimiza para menos verbosidad.

Comunidad
fuente
2
"Nadie" es incorrecto de hecho: hay un montón de herramientas que se optimizan para menos verbosidad, las expresiones regulares son un ejemplo extremo. Pero es posible que haya querido decir "nadie en el mundo convencional de Java", y en esa lectura la respuesta tiene mucho sentido, gracias. Mi propia preocupación por la verbosidad no es el tiempo dedicado a escribir código sino el tiempo dedicado a leerlo; el IDE no ahorrará tiempo de lectura; pero parece que la idea es que el 99% de las veces solo se lee la definición de interfaz, ¿así que eso debería ser conciso?
Mikhail Ramendik
5

Hay tres factores diferentes que contribuyen a lo que estás observando.

Tuplas versus campos nombrados

Quizás el más trivial: en los otros idiomas usaste una tupla. Debatir si las tuplas es una buena idea no es realmente el punto, pero en Java usaste una estructura más pesada, por lo que es una comparación un poco injusta: podrías haber usado una variedad de objetos y algún tipo de conversión.

Sintaxis del lenguaje

¿Podría ser más fácil declarar la clase? No estoy hablando de hacer públicos los campos o usar un mapa, sino algo así como las clases de caso de Scala, que proporcionan todos los beneficios de la configuración que describiste, pero mucho más conciso:

case class Foo(duration: Int, unit: String, tooShort: Boolean)

Podríamos tener eso, pero hay un costo: la sintaxis se vuelve más complicada. Por supuesto, puede valer la pena para algunos casos, o incluso para la mayoría de los casos, o incluso para la mayoría de los casos durante los próximos 5 años, pero debe ser juzgado. Por cierto, esta es una de las cosas buenas con los lenguajes que puede modificar usted mismo (es decir, lisp), y observe cómo esto es posible debido a la simplicidad de la sintaxis. Incluso si en realidad no modifica el idioma, una sintaxis simple permite herramientas más potentes; por ejemplo, muchas veces extraño algunas opciones de refactorización disponibles para Java pero no para Scala.

Filosofía del lenguaje

Pero el factor más importante es que un idioma debe permitir una cierta forma de pensar. A veces puede parecer opresivo (a menudo he deseado soporte para una determinada función), pero eliminar funciones es tan importante como tenerlas. ¿Podrías apoyar todo? Claro, pero también podrías escribir un compilador que compila todos los idiomas. En otras palabras, no tendrá un idioma: tendrá un superconjunto de idiomas y cada proyecto básicamente adoptará un subconjunto.

Por supuesto, es posible escribir código que vaya en contra de la filosofía del lenguaje y, como observó, los resultados a menudo son feos. Tener una clase con solo un par de campos en Java es similar a usar un var en Scala, degenerar predicados de prólogo a funciones, hacer un inseguroPerformIO en haskell, etc. Las clases de Java no están destinadas a ser ligeras, no están ahí para pasar datos. . Cuando algo parece difícil, a menudo es fructífero retroceder y ver si hay otra forma. En tu ejemplo:

¿Por qué la duración se separa de las unidades? Hay muchas bibliotecas de tiempo que le permiten declarar una duración, algo como Duración (5, segundos) (la sintaxis variará), que luego le permitirá hacer lo que quiera de una manera mucho más sólida. Quizás desee convertirlo. ¿Por qué verificar si el resultado [1] (o [2]?) Es una 'hora' y se multiplica por 3600? Y para el tercer argumento: ¿cuál es su propósito? Supongo que en algún momento tendrá que imprimir "menos de 1 ms" o el tiempo real, esa es una lógica que naturalmente pertenece a los datos de tiempo. Es decir, deberías tener una clase como esta:

class TimeResult {
    public TimeResult(duration, unit, tooShort)
    public String getResult() {
        if tooShort:
           return "too short"
        else:
           return format(duration)
}

}

o lo que sea que realmente quieras hacer con los datos, por lo tanto, encapsulando la lógica.

Por supuesto, puede haber un caso en el que esta forma no funcione: ¡no estoy diciendo que este sea el algoritmo mágico para convertir los resultados de tuplas en un código Java idiomático! Y puede haber casos en los que es muy feo y malo, y quizás debiste haber usado un idioma diferente, ¡por eso hay tantos después de todo!

Pero mi punto de vista sobre por qué las clases son "estructuras pesadas" en Java es que no está destinado a usarlas como contenedores de datos sino como celdas lógicas autónomas.

Thanos Tintinidis
fuente
Lo que quiero hacer con las duraciones es en realidad hacer una suma de ellas y luego compararlas con otras. No hay salida involucrada. La comparación debe tener en cuenta el redondeo, por lo que necesito saber las unidades de tiempo originales en el momento de la comparación.
Mikhail Ramendik
Probablemente sería posible crear una forma de agregar y comparar instancias de mi clase, pero esto podría ser enormemente pesado. Especialmente porque también necesito dividir y multiplicar por flotantes (hay porcentajes para verificar en esa IU). Así que por ahora hice una clase inmutable con campos finales, que parece un buen compromiso.
Mikhail Ramendik
La parte más importante de esta pregunta en particular fue el lado más general de las cosas, y de todas las respuestas parece que se está uniendo una imagen.
Mikhail Ramendik
@MikhailRamendik tal vez algún tipo de acumulador sería una opción, pero conoce mejor el problema. De hecho, no se trata de resolver este problema en particular, lo siento si me desvié un poco: mi punto principal es que el lenguaje desalienta el uso de datos "simples". a veces, esto te ayuda a repensar tu enfoque - otras veces un int es solo un int
Thanos Tintinidis
@MikhailRamendik Lo bueno del modo repetitivo es que una vez que tienes una clase, puedes agregarle comportamiento. Y / o hágalo inmutable como lo hizo (esta es una buena idea la mayoría de las veces; siempre puede devolver un nuevo objeto como la suma). Al final, su clase "superflua" puede encapsular todo el comportamiento que necesita y luego el costo para los captadores se vuelve insignificante. Además, puede que los necesite o no. Considere usar lombok o autovalor .
maaartinus
5

A mi entender, las razones principales son

  1. Las interfaces son la forma básica de Java de abstraer las clases.
  2. Java solo puede devolver un único valor de un método: un objeto o una matriz o un valor nativo (int / long / double / float / boolean).
  3. Las interfaces no pueden contener campos, solo métodos. Si desea acceder a un campo, debe seguir un método, por lo tanto, getters y setters.
  4. Si un método devuelve una interfaz, debe tener una clase de implementación para realmente devolver.

Esto le da el "Debe escribir una clase para devolver cualquier resultado no trivial" que a su vez es bastante pesado. Si usó una clase en lugar de la interfaz, podría tener campos y usarlos directamente, pero eso lo vincula a una implementación específica.

Thorbjørn Ravn Andersen
fuente
Para añadir a esto: si usted solamente necesita esto en un pequeño contexto (por ejemplo, un método privado en una clase), que está bien usar un dado testimonio - lo ideal inmutable. Pero realmente realmente no debería filtrarse en el contrato público de la clase (o peor aún, ¡la biblioteca!). Puede decidir contra los registros simplemente para asegurarse de que esto nunca suceda con futuros cambios de código, por supuesto; A menudo es la solución más simple.
Luaan
Y estoy de acuerdo con la vista "Las tuplas no funcionan bien en Java". Si usa genéricos, esto terminará muy rápidamente desagradable.
Thorbjørn Ravn Andersen
@ ThorbjørnRavnAndersen Funcionan bastante bien en Scala, un lenguaje de tipo estático con genéricos y tuplas. Entonces, ¿tal vez es culpa de Java?
Andres F.
@AndresF. Tenga en cuenta la parte "Si usa genéricos". La forma en que Java hace esto no se presta a construcciones complejas en lugar de, por ejemplo, Haskell. No conozco a Scala lo suficientemente bien como para hacer una comparación, pero ¿es cierto que Scala permite múltiples valores de retorno? Eso podría ayudar bastante.
Thorbjørn Ravn Andersen
@ ThorbjørnRavnAndersen Las funciones de Scala tienen un solo valor de retorno que puede ser de tipo tupla (por ejemplo, def f(...): (Int, Int)es una función fque devuelve un valor que resulta ser una tupla de enteros). No estoy seguro de que los genéricos en Java sean el problema; tenga en cuenta que Haskell también borra los tipos, por ejemplo. No creo que haya una razón técnica por la que Java no tenga tuplas.
Andres F.
3

Estoy de acuerdo con JacquesB responde que

Java es (tradicionalmente) un lenguaje OO basado en clases de paradigma único que optimiza la explicidad y la coherencia

Pero la explicidad y la coherencia no son los objetivos finales para optimizar. Cuando dice 'Python está optimizado para facilitar la lectura', menciona de inmediato que el objetivo final es 'mantenibilidad' y 'velocidad de desarrollo'.

¿Qué logras cuando tienes la explicidad y la coherencia, hecho de manera Java? Mi opinión es que ha evolucionado como un lenguaje que pretende proporcionar una forma predecible, consistente y uniforme para resolver cualquier problema de software.

En otras palabras, la cultura Java está optimizada para hacer que los gerentes crean que entienden el desarrollo de software.

O, como dijo un sabio hace mucho tiempo ,

La mejor manera de juzgar un lenguaje es mirar el código escrito por sus proponentes. "Radix enim omnium malorum est cupiditas", y Java es claramente un ejemplo de programación orientada al dinero (MOP). Como el principal defensor de Java en SGI me dijo: "Alex, tienes que ir a donde está el dinero". Pero no quiero ir particularmente a donde está el dinero, por lo general no huele bien allí.

artem
fuente
3

(Esta respuesta no es una explicación para Java en particular, sino que aborda la pregunta general de "¿Para qué podrían optimizar las prácticas [pesadas]?")

Considere estos dos principios:

  1. Es bueno cuando su programa hace lo correcto. Deberíamos facilitar la escritura de programas que hagan lo correcto.
  2. Es malo cuando su programa hace lo incorrecto. Deberíamos hacer que sea más difícil escribir programas que hagan lo incorrecto.

Intentar optimizar uno de estos objetivos a veces puede interferir con el otro (es decir, hacer que sea más difícil hacer lo incorrecto también puede hacer que sea más difícil hacer lo correcto , o viceversa).

Las compensaciones que se realicen en un caso particular dependen de la aplicación, las decisiones de los programadores o del equipo en cuestión y la cultura (de la organización o comunidad lingüística).

Por ejemplo, si un error o una interrupción de algunas horas en su programa puede resultar en la pérdida de vidas (sistemas médicos, aeronáutica) o incluso simplemente dinero (como millones de dólares en los sistemas de anuncios de Google), usted haría diferentes compensaciones (no solo en su idioma pero también en otros aspectos de la cultura de ingeniería) de lo que lo haría para un guión único: es probable que se incline hacia el lado "pesado".

Otros ejemplos que tienden a hacer que su sistema sea más "pesado":

  • Cuando tienes una gran base de código trabajada por muchos equipos durante muchos años, una gran preocupación es que alguien pueda usar la API de otra persona incorrectamente. Una función que se llama con argumentos en el orden incorrecto, o se llama sin garantizar algunas condiciones previas / restricciones que espera, podría ser catastrófica.
  • Como un caso especial de esto, digamos que su equipo mantiene una API o biblioteca en particular, y le gustaría cambiarla o refactorizarla. Cuanto más "restringidos" estén sus usuarios en cómo podrían usar su código, más fácil será cambiarlo. (Tenga en cuenta que sería preferible aquí tener garantías reales aquí de que nadie podría estar usándolo de una manera inusual).
  • Si el desarrollo se divide entre varias personas o equipos, puede parecer una buena idea que una persona o equipo "especifique" la interfaz y que otros la implementen. Para que funcione, debe poder ganar un grado de confianza cuando se realiza la implementación, de que la implementación realmente coincide con la especificación.

Estos son solo algunos ejemplos, para darle una idea de los casos en los que hacer que las cosas sean "pesadas" (y dificultarle escribir un código rápidamente) puede ser realmente intencional. (¡Incluso se podría argumentar que si escribir código requiere mucho esfuerzo, puede llevarlo a pensar más cuidadosamente antes de escribir código! Por supuesto, esta línea de argumento rápidamente se vuelve ridícula).

Un ejemplo: el sistema Python interno de Google tiende a hacer las cosas "pesadas", de modo que no puede simplemente importar el código de otra persona, debe declarar la dependencia en un archivo BUILD , el equipo cuyo código desea importar debe tener su biblioteca declarada como visible para su código, etc.


Nota : Todo lo anterior es solo cuando las cosas tienden a ponerse "pesadas". Absolutamente no pretendo que Java o Python (ya sea los propios lenguajes o sus culturas) hagan compensaciones óptimas para un caso particular; eso es para que lo pienses. Dos enlaces relacionados en tales compensaciones:

ShreevatsaR
fuente
1
¿Qué hay de leer el código? Hay otra compensación allí. El código abrumado por encantamientos extraños y rituales profusos es más difícil de leer; con repetitivo y texto innecesario (¡y jerarquías de clase!) en su IDE, se hace más difícil ver qué está haciendo realmente el código. Puedo ver el argumento de que el código no necesariamente debe ser fácil de escribir (no estoy seguro si estoy de acuerdo con él, pero tiene algún mérito), pero definitivamente debería ser fácil de leer . Obviamente, el código complejo puede y ha sido construido con Java, sería una tontería afirmar lo contrario, pero ¿fue gracias a su verbosidad o a pesar de ello?
Andres F.
@AndresF. Edité la respuesta para aclarar que no se trata específicamente de Java (de lo cual no soy un gran admirador). Pero sí, compensa al leer el código: en un extremo, desea poder leer fácilmente "lo que el código está haciendo realmente" como usted dijo. En el otro extremo, desea poder ver fácilmente respuestas a preguntas como: ¿cómo se relaciona este código con otro código? ¿Qué es lo peor que podría hacer este código? ¿Cuál puede ser su impacto en otro estado, cuáles son sus efectos secundarios? ¿De qué factores externos podría depender este código? (Por supuesto, idealmente queremos respuestas rápidas a ambos grupos de preguntas)
ShreevatsaR
2

La cultura de Java ha evolucionado con el tiempo con fuertes influencias tanto de código abierto como de fondos de software empresarial, lo cual es una mezcla extraña si realmente lo piensas. Las soluciones empresariales exigen herramientas pesadas, y el código abierto exige simplicidad. El resultado final es que Java está en algún lugar en el medio.

Parte de lo que influye en la recomendación es que lo que se considera legible y mantenible en Python y Java es muy diferente.

  • En Python, la tupla es una característica del lenguaje .
  • Tanto en Java como en C #, la tupla es (o sería) una función de biblioteca .

Solo menciono C # porque la biblioteca estándar tiene un conjunto de clases Tuple <A, B, C, .. n>, y es un ejemplo perfecto de cuán difíciles de manejar son las tuplas si el lenguaje no las admite directamente. En casi todas las instancias, su código se vuelve más legible y mantenible si tiene clases bien elegidas para manejar el problema. En el ejemplo específico en su pregunta de desbordamiento de pila vinculada, los otros valores se expresarían fácilmente como captadores calculados en el objeto devuelto.

Una solución interesante que hizo la plataforma C # que proporciona un término medio feliz es la idea de objetos anónimos (publicados en C # 3.0 ) que rascan esta picazón bastante bien. Desafortunadamente, Java aún no tiene un equivalente.

Hasta que se modifiquen las características del lenguaje Java, la solución más fácil de leer y mantener es tener un objeto dedicado. Esto se debe a las restricciones en el lenguaje que se remontan a sus inicios en 1995. Los autores originales tenían muchas más características de lenguaje planificadas que nunca lo hicieron, y la compatibilidad con versiones anteriores es una de las principales limitaciones que rodean la evolución de Java a lo largo del tiempo.

Berin Loritsch
fuente
44
En la última versión de c #, las nuevas tuplas son ciudadanos de primera clase en lugar de clases de biblioteca. Se necesitaron demasiadas versiones para llegar allí.
Igor Soloydenko
Los últimos 3 párrafos se pueden resumir como "El lenguaje X tiene la característica que busca que Java no tiene", y no veo lo que agregan a la respuesta. ¿Por qué mencionar C # en un tema de Python a Java? No, simplemente no veo el punto. Un poco extraño de un tipo de representante de 20k +.
Olivier Grégoire
1
Como dije, "Solo menciono C # porque las tuplas son una función de biblioteca" similar a cómo funcionaría en Java si tuviera tuplas en la biblioteca principal. Es muy difícil de usar, y la mejor respuesta es casi siempre tener una clase dedicada. Sin embargo, los objetos anónimos rascan una picazón similar a la diseñada para las tuplas. Sin soporte de idioma para algo mejor, esencialmente tienes que trabajar con lo que es más fácil de mantener.
Berin Loritsch
@IgorSoloydenko, ¿tal vez hay esperanza para Java 9? Esta es solo una de esas características que son el siguiente paso lógico para lambdas.
Berin Loritsch
1
@IgorSoloydenko No estoy seguro de que sea cierto (ciudadanos de primera clase): parecen ser extensiones de sintaxis para crear y desenvolver instancias de clases de la biblioteca, en lugar de tener soporte de primera clase en el CLR como class, struct, enum do.
Pete Kirkham el
0

Creo que una de las cosas centrales sobre el uso de una clase en este caso es que lo que se combina debe mantenerse unido.

He tenido esta discusión al revés, sobre los argumentos del método: considere un método simple que calcula el IMC:

CalculateBMI(weight,height)
{
  System.out.println("BMI: " + (( weight / height ) x 703));
}

En este caso, argumentaría en contra de este estilo porque el peso y la altura están relacionados. El método "comunica" esos son dos valores separados cuando no lo son. ¿Cuándo calcularías un IMC con el peso de una persona y la altura de otra? No tendría sentido.

CalculateBMI(Person)
{
  System.out.println("BMI: " + (( Person.weight / Person.height ) x 703));
}

Tiene mucho más sentido porque ahora comunicas claramente que la altura y el peso provienen de la misma fuente.

Lo mismo vale para devolver múltiples valores. Si están claramente conectados, devuelva un pequeño paquete ordenado y use un objeto, si no devuelven múltiples valores.

Pieter B
fuente
44
Ok, pero supongamos que tiene una tarea (hipotética) para mostrar un gráfico: ¿cómo depende el IMC del peso para una altura fija particular? Entonces, no puede hacer eso sin crear una Persona para cada punto de datos. Y, llevándolo al extremo, no puede crear una instancia de clase Persona sin fecha de nacimiento válida y número de pasaporte. ¿Ahora que? No downvote por cierto, no son razones válidas para la agrupación de propiedades entre sí de alguna clase.
artem
1
@artem es el siguiente paso donde entran en juego las interfaces. Entonces, una interfaz de peso de persona podría ser algo así. Te estás quedando atrapado en el ejemplo que di para aclarar que lo que va de la mano debería estarlo.
Pieter B
0

Para ser sincero, la cultura es que los programadores de Java originalmente tendían a salir de las universidades donde se enseñaban principios orientados a objetos y principios de diseño de software sostenible.

Como ErikE dice en más palabras en su respuesta, parece que no estás escribiendo código sostenible. Lo que veo de su ejemplo es que hay un enredo muy incómodo de preocupaciones.

En la cultura Java, tenderá a conocer qué bibliotecas están disponibles, y eso le permitirá lograr mucho más que su programación fuera de lo común. Por lo tanto, estaría intercambiando sus idiosincrasias por los patrones y estilos de diseño que habían sido probados en entornos industriales duros.

Pero como usted dice, esto no tiene inconvenientes: hoy, después de haber usado Java durante más de 10 años, tiendo a usar Node / Javascript o Go para nuevos proyectos, porque ambos permiten un desarrollo más rápido, y con arquitecturas de estilo de microservicio estas son a menudo es suficiente A juzgar por el hecho de que Google primero estaba usando mucho Java, pero fue el creador de Go, creo que podrían estar haciendo lo mismo. Pero a pesar de que ahora uso Go y Javascript, sigo usando muchas de las habilidades de diseño que obtuve de años de uso y comprensión de Java.

Tom
fuente
1
Cabe destacar que la herramienta de reclutamiento de foobar que utiliza Google permite utilizar dos idiomas para las soluciones: java y python. Me parece interesante que Go no sea una opción. Me encontré con esta presentación en Go y tiene algunos puntos interesantes sobre Java y Python (así como otros lenguajes). Por ejemplo, hay un punto que destaca: "Quizás como resultado, los programadores no expertos han confundido" la facilidad de use "con interpretación y tipeo dinámico". Vale la pena leerlo.
JimmyJames
@JimmyJames acaba de llegar a la diapositiva diciendo "en manos de expertos, son geniales" - buen resumen del problema en la pregunta
Tom