it-swarm-es.com

Grokking Java culture - ¿por qué las cosas son tan pesadas? ¿Para qué se optimiza?

Solía ​​codificar en Python mucho. Ahora, por razones de trabajo, codifico en Java. Los proyectos que hago son bastante pequeños, y posiblemente Python lo haría funciona 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 no logro "asimilar" esa cultura. 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- valores-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.

De acuerdo con las respuestas, el equivalente de una estructura está disponible en Java sintaxis y un triple está disponible en una biblioteca Apache muy utilizada, pero la forma "correcta" de hacerlo es en realidad creando una clase separada para el valor, completa con getters y setters. Alguien fue muy amable al proporcionar un ejemplo completo. Era 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 (lo que, en esa filosofía, conduce a la mantenibilidad) 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 conjetura 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.

291
Mikhail Ramendik

El Java Idioma

Creo que todas estas respuestas están perdiendo el punto al tratar de atribuir la intención a la forma en que Java funciona. La verbosidad de Java no se deriva de que esté orientada a objetos, ya que Python y muchos otros lenguajes también tienen 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 la prueba 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 de manera clara.

Sin embargo, no hay nada fundamental que impida a Java adoptar 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 compactas de tipos de datos 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 Java Cultura

La historia del desarrollo de la empresa 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 API HTTP, bases de datos y procesamiento de feeds 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 el 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. Java el desarrollo empresarial 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, vinieran a 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 marco" añadieron más abstracción y complejidad encima de 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 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.

237
Reinstate Monica

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 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 sus tipos de forma explícita. Esa es solo una forma de verificar que el código que se escribió realmente cumple lo que se pretendía.

Algunos otros lenguajes Llevan esa lógica aún más al verificar las precondiciones, postcondiciones e 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. Utilizar Triplet<A, B, C> como tipo de retorno (que realmente debería estar en Java.util, y realmente no puedo explicar por qué no lo es. Especialmente desde que JDK8 introduce Function, BiFunction, Consumer, BiConsumer, etc ... Parece que Pair y Triplet al menos tendría sentido. Pero yo divago)

  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 pretendía 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 utilizan métodos que pueden aplicarse tanto a la intención. y los tipos reales.

Considere algo como esto en TypeScript (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 se ve 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 adecuados orientados a objetos 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.Duration object, que sería mucho más explícito que los dos casos anteriores.

74
LordOfThePigs

Java y Python son los dos lenguajes que más uso pero vengo de la otra dirección. Es decir, estaba en el mundo Java antes Comencé a usar Python para poder ayudar. 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 exprime 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 se '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 mirarlo un año después y quedarme desconcertado con mi propio código. En Java puedo mirar pequeños fragmentos de código de otras personas e instantáneamente saben lo que hace. En Python realmente no puedes hacer eso. No es que uno sea mejor como pareces darte cuenta, simplemente 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 Java salió por primera vez, la gente hizo 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 quiere usar polimorfismo, al menos necesitará tocar todas las clases que interactúan con ese objeto 'Tuple-esque'. En Python hay soluciones para esto como __getattr__ etc. así 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 es adecuado para 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;
  }
}
50
JimmyJames

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 tiene, 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 tipo ApproximateDuration 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 sus conceptos de negocio de 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 juntas 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 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 sobre 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 "error de aproximadamente 32 milisegundos" (cerca del cuadrado raíz de 1000, así que a medio camino logarítmicamente entre 1 y 1000). Si te has limitado a un código que usa primitivas para representar esto, tendrás que encontrar todos los lugares del código donde tienes is_in_seconds,is_under_1ms y cámbielo a is_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).

Declaración de cierre

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

Anexo

Agregaré de manera completamente gratuita e injustamente 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 manera Java" (con campos de respaldo privados y funciones getter/setter):

// 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 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).

41
ErikE

Ambos Python y 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 lograr esto.

Python es un lenguaje de paradigmas múltiples que se optimiza para claridad y simplicidad de código (fácil de leer y escribir).

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

A Python Tuple es una estructura de datos con un número fijo de campos. Una clase normal con campos explícitamente declarados puede lograr la misma funcionalidad. En Python it 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 tales accesos directos, ya que ya puede usar clases declaradas explícitas. 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 particular propósito.

24
JacquesB

No busques prácticas; generalmente es una mala idea, como se dice en Mejores prácticas MAL, patrones BUENOS?. 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 que Tuple devuelva tres valores en Java rápido:

  • 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.

16
Walfrat

Acerca de Java modismos 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 la siguiente: 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;
}
11
marstato

El problema es que compara manzanas con naranjas . Preguntó cómo simular la devolución de más de un valor simple dando un ejemplo rápido y sucio python con Tuple sin tipo y realmente recibió el prácticamente respuesta de una línea .

La respuesta aceptada proporciona una solución comercial correcta. No hay una solución temporal rápida que tendría que tirar e implementar correctamente la primera vez que necesitaría 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.

7
user253752

Hay muchas cosas que se podrían decir sobre la cultura Java, pero creo que en el caso en el que te enfrentas en este momento, 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 está sucediendo 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 los haces 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. Mucho 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 absolutamente 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, todavía 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();
            }
         };
      }
   }

Esto se está convirtiendo en un lote más código, pero solo necesita escribirlo una vez, y (suponiendo que haya documentado correctamente las cosas), las personas que termina usando su código no tiene que adivinar lo que 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, aún 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 la 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, así que todavía quieres esa creación concisa (que obtienes). 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 matriz simple de valores internamente puede tener mucho sentido. Su API pública aún debería tener tipos claramente definidos.

7
Joshua Taylor

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, esto es una de las cosas agradables con idiomas que puede modificar usted mismo (es decir, LISP), y observe cómo esto es posible debido a la simplicidad de la sintaxis. Incluso si 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 Perfemio inseguro en haskell, etc. Java las clases no están destinadas para ser ligeros, no están allí 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 Tuple a código idiomático Java! Y puede haber casos en los que es muy feo y malo, y tal vez debería 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 debes usarlas como contenedores de datos sino como celdas lógicas autónomas.

5
Thanos Tintinidis

A mi entender, las razones principales son

  1. Las interfaces son la manera básica Java para 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 que realmente regrese.

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.

(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, lo que hace 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 la 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 llamada con argumentos en el orden incorrecto, o llamada 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 ser capaz de 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 interno Python) 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 de construcción , 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 afirmo que Java o Python (ya sea los propios idiomas) , o sus culturas) hacen compensaciones óptimas para cualquier caso en particular; eso es para que usted piense. Dos enlaces relacionados en tales compensaciones:

3
ShreevatsaR

Estoy de acuerdo con JacquesB responde que

Java es (tradicionalmente) un lenguaje de paradigma único basado en clases OO 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 explicidad y consistencia, hecho Java manera? 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, Java culture está optimizado para hacer que los gerentes crean que entienden el desarrollo de software.

O, como n sabio lo expresó 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 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í.

3
artem

La cultura Java) ha evolucionado con el tiempo con fuertes influencias tanto de código abierto como de software empresarial, lo cual es una mezcla extraña si realmente lo piensas. Las soluciones empresariales exigen herramientas pesadas y 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 lo que se considera legible y mantenible en Python y Java es muy diferente.

  • En Python, la Tupla es una característica del lenguaje .
  • En ambos Java y 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 lo difíciles que son las tuplas si el lenguaje no las admite directamente. En casi todos los casos, su código se vuelve más fácil de leer y mantener 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 (lanzado en C # 3. ) que rasca esta picazón bastante bien. Desafortunadamente, Java todavía 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 limitaciones 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.

2
Berin Loritsch

Para ser sincero, la cultura es que Java originalmente tendían a salir de 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ás a estar al tanto de las bibliotecas disponibles, y eso te permitirá lograr mucho más que tu programación original. Por lo tanto, estarías intercambiando tus idiosincrasias para los patrones y estilos de diseño que se han probado y probado en entornos industriales de núcleo duro.

Pero como usted dice, esto no tiene inconvenientes: hoy, después de haber usado Java por 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 microservicio, a menudo son suficientes. A juzgar por el hecho de que Google primero estaba usando Java en gran medida, pero fue el creador de Go, supongo que podrían estar haciendo lo mismo. Pero aunque ahora uso Go y Javascript, Todavía uso muchas de las habilidades de diseño que obtuve de años de uso y comprensión de Java.

0
Tom

Creo que una de las cosas centrales sobre el uso de una clase en este caso es que lo que se combina debe permanecer 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.

0
Pieter B