web-dev-qa-db-fra.com

Grokking Java culture - pourquoi les choses sont-elles si lourdes? Pour quoi l'optimiser?

J'avais l'habitude de coder en Python beaucoup. Maintenant, pour des raisons professionnelles, je code en Java. Les projets que je fais sont assez petits, et peut-être Python serait fonctionne mieux, mais il y a des raisons non techniques valables d'utiliser Java (je ne peux pas entrer dans les détails).

La syntaxe Java n'est pas un problème; c'est juste une autre langue. Mais en dehors de la syntaxe, Java a une culture, un ensemble de méthodes de développement et des pratiques qui sont considérées comme "correctes". Et pour l'instant, je ne parviens pas à "grogner" cette culture. Donc J'apprécierais vraiment des explications ou des indications dans la bonne direction.

Un exemple complet minimal est disponible dans une question Stack Overflow que j'ai commencée: https://stackoverflow.com/questions/43619566/returning-a-result-with-several- values-the-Java-way/43620339

J'ai une tâche - analyser (à partir d'une seule chaîne) et gérer un ensemble de trois valeurs. En Python c'est un one-liner (Tuple), en Pascal ou C un enregistrement/struct en 5-liner.

Selon les réponses, l'équivalent d'une structure est disponible en Java et un triple est disponible dans une bibliothèque Apache largement utilisée - mais la manière "correcte" de le faire est en fait de créer une classe distincte pour la valeur, avec des getters et des setters. Quelqu'un a été très gentil de fournir un exemple complet. Il s'agissait de 47 lignes de code (enfin, certaines de ces lignes étaient vides).

Je comprends qu'une énorme communauté de développement n'est probablement pas "fausse". C'est donc un problème avec ma compréhension.

Les pratiques Python optimisent la lisibilité (ce qui, dans cette philosophie, conduit à la maintenabilité) et ensuite la vitesse de développement. Les pratiques C optimisent l'utilisation des ressources. À quoi servent Java optimisent? Ma meilleure supposition est l'évolutivité (tout devrait être dans un état prêt pour un projet de millions de LOC), mais c'est une supposition très faible.

291
Mikhail Ramendik

La langue Java

Je crois que toutes ces réponses manquent de raison en essayant d'attribuer l'intention à la façon dont Java fonctionne. La verbosité de Java ne vient pas du fait qu'il soit orienté objet, car Python et de nombreux autres langages ont encore une syntaxe terser. La verbosité de Java ne vient pas non plus de sa prise en charge des modificateurs d'accès. Au lieu de cela, c'est simplement la façon dont Java a été conçu et a évolué.

Java a été initialement créé comme un C légèrement amélioré avec OO. En tant que tel, Java a une syntaxe des années 70. De plus, Java est très conservateur quant à l'ajout de fonctionnalités afin de conserver la compatibilité descendante et de lui permettre de résister à l'épreuve du temps. Si Java avait ajouté des fonctionnalités à la mode comme les littéraux XML en 2005, lorsque XML était à la mode, le langage aurait été rempli de fonctionnalités fantômes dont personne ne se soucie et qui limitent son évolution 10 ans plus tard. Par conséquent, Java manque simplement de syntaxe moderne pour exprimer les concepts de manière laconique.

Cependant, rien de fondamental n'empêche Java d'adopter cette syntaxe. Par exemple, Java 8 a ajouté des lambdas et des références de méthode, réduisant considérablement la verbosité dans de nombreuses situations. Java pourrait également ajouter la prise en charge des déclarations de type de données compactes telles que les classes de cas de Scala. Mais Java ne l'a tout simplement pas fait. Notez que des types de valeurs personnalisés sont à l'horizon et cette fonctionnalité peut introduire une nouvelle syntaxe pour les déclarer. Je suppose que nous verrons.


La Java Culture

L'histoire du développement de l'entreprise Java nous a largement conduits à la culture que nous connaissons aujourd'hui. À la fin des années 90/début des années 00, Java est devenu un langage extrêmement populaire pour les applications d'entreprise côté serveur. À l'époque, ces applications étaient largement écrites de manière ad hoc et intégraient de nombreuses préoccupations complexes, telles que les API HTTP, les bases de données et le traitement des flux XML.

Dans les années 2000, il est devenu clair que bon nombre de ces applications avaient beaucoup en commun et les cadres pour gérer ces problèmes, comme l'ORM Hibernate, l'analyseur XML Xerces, les JSP et l'API de servlet, et les EJB, sont devenus populaires. Cependant, bien que ces frameworks aient réduit l'effort de travailler dans le domaine particulier qu'ils ont défini pour automatiser, ils ont nécessité une configuration et une coordination. À l'époque, pour une raison quelconque, il était courant d'écrire des cadres pour répondre au cas d'utilisation le plus complexe et, par conséquent, ces bibliothèques étaient compliquées à configurer et à intégrer. Et au fil du temps, ils sont devenus de plus en plus complexes à mesure qu'ils accumulaient des fonctionnalités. Java le développement des entreprises est devenu progressivement de plus en plus un moyen de connecter des bibliothèques tierces et moins d'écrire des algorithmes.

Finalement, la configuration et la gestion fastidieuses des outils d'entreprise sont devenues suffisamment pénibles pour que les frameworks, notamment Spring, soient venus gérer la gestion. Vous pourriez mettre toute votre configuration en un seul endroit, selon la théorie, et l'outil de configuration configurerait ensuite les pièces et les relierait ensemble. Malheureusement, ces "framework frameworks" ajoutent plus d'abstraction et de complexité au-dessus de toute la boule de cire.

Au cours des dernières années, les bibliothèques plus légères ont gagné en popularité. Néanmoins, une génération entière de programmeurs Java est arrivée à maturité lors de la croissance des cadres d'entreprise lourds. Leurs modèles, ceux qui développent les frameworks, ont écrit des usines d'usine et des chargeurs de bean de configuration proxy. Ils devaient configurer et intégrer ces monstruosités au quotidien. Et en conséquence, la culture de la communauté dans son ensemble a suivi l'exemple de ces cadres et a eu tendance à trop sur-concevoir.

237
Reinstate Monica

Je pense avoir une réponse pour l'un des points que vous soulevez et qui n'ont pas été soulevés par d'autres.

Java optimise la détection des erreurs de programmation au moment de la compilation en ne faisant aucune hypothèse

En général, Java a tendance à déduire uniquement des faits sur le code source après que le programmeur a déjà exprimé explicitement son intention. Le compilateur Java ne fait jamais d'hypothèses sur la et n'utilisera l'inférence que pour réduire le code redondant.

La raison derrière cette philosophie est que le programmeur n'est qu'humain. Ce que nous écrivons n'est pas toujours ce que nous voulons réellement que le programme fasse. Le langage Java essaie d'atténuer certains de ces problèmes en forçant le développeur à toujours déclarer explicitement leurs types. C'est juste une façon de vérifier que le code qui a été écrit fait réellement ce qui était prévu.

Quelques autres langues Poussez cette logique encore plus loin en vérifiant les pré-conditions, les post-conditions et les invariants (bien que je ne sois pas sûr qu'ils le fassent au moment de la compilation). Ce sont des façons encore plus extrêmes pour le programmeur de demander au compilateur de vérifier son propre travail.

Dans votre cas, cela signifie que pour que le compilateur garantisse que vous renvoyez réellement les types que vous pensez renvoyer, vous devez fournir ces informations au compilateur.

Dans Java il y a deux façons de le faire:

  1. Utilisation Triplet<A, B, C> comme type de retour (qui devrait vraiment être dans Java.util, et je ne peux pas vraiment expliquer pourquoi ce n'est pas le cas. D'autant plus que JDK8 introduit Function, BiFunction, Consumer, BiConsumer, etc ... Il semble juste que Pair et Triplet au moins aurait du sens. Mais je m'égare)

  2. Créez votre propre type de valeur à cet effet, où chaque champ est nommé et tapé correctement.

Dans ces deux cas, le compilateur peut garantir que votre fonction renvoie les types déclarés et que l'appelant se rend compte du type de chaque champ renvoyé et les utilise en conséquence.

Certaines langues fournissent une vérification de type statique ET une inférence de type en même temps, mais cela laisse la porte ouverte à une classe subtile de problèmes de non-correspondance de type. Lorsque le développeur avait l'intention de renvoyer une valeur d'un certain type, mais en retourne en fait une autre et que le compilateur accepte TOUJOURS le code car il arrive que, par co-incidence, la fonction et l'appelant utilisent uniquement des méthodes qui peuvent être appliquées à la fois à la destination et les types réels.

Considérez quelque chose comme ceci dans TypeScript (ou type de flux) où l'inférence de type est utilisée à la place du typage explicite.

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

C'est un cas trivial stupide, bien sûr, mais il peut être beaucoup plus subtil et conduire à des problèmes difficiles à déboguer, car le code semble correct. Ce genre de problèmes est entièrement évité en Java, et c'est autour de cela que tout le langage est conçu.


Notez que suivant les principes orientés objet et la modélisation basée sur le domaine appropriés, le cas d'analyse de durée dans la question liée peut également être renvoyé sous la forme d'un Java.time.Duration objet, qui serait beaucoup plus explicite que les deux cas ci-dessus.

74
LordOfThePigs

Java et Python sont les deux langages que j'utilise le plus mais je viens de l'autre côté. C'est-à-dire que j'étais profondément dans le monde Java avant J'ai commencé à utiliser Python afin que je puisse être en mesure d'aider. Je pense que la réponse à la question plus large de "pourquoi les choses sont-elles si lourdes" se résume à 2 choses:

  • Les coûts de développement entre les deux sont comme l'air dans un long ballon animal ballon. Vous pouvez presser une partie du ballon et une autre partie gonfle. Python a tendance à comprimer la première partie. Java serre la dernière partie).
  • Java manque encore de fonctionnalités qui supprimeraient une partie de ce poids. Java 8 a fait une énorme entorse à cela, mais la culture n'a pas complètement digéré les changements. Java pourrait utiliser quelques autres choses comme yield.

Java "optimise" les logiciels de grande valeur qui seront maintenus pendant de nombreuses années par de grandes équipes de personnes. J'ai eu l'expérience d'écrire des trucs en Python et de le regarder un an plus tard et d'être dérouté par mon propre code. En Java je peux regarder de minuscules extraits de code d'autres personnes et savent instantanément ce qu'il fait. En Python vous ne pouvez pas vraiment faire ça. Ce n'est pas que l'un est meilleur comme vous semblez le réaliser, ils ont juste des coûts différents.

Dans le cas spécifique que vous mentionnez, il n'y a pas de tuples. La solution simple consiste à créer une classe avec des valeurs publiques. Lorsque Java est sorti pour la première fois, les gens l'ont fait assez régulièrement. Le premier problème avec cela est que c'est un casse-tête de maintenance. Si vous avez besoin d'ajouter une logique ou une sécurité de thread ou si vous souhaitez utiliser le polymorphisme, vous aurez au moins besoin de toucher chaque classe qui interagit avec cet objet "Tuple-esque". Dans Python il y a des solutions pour cela comme __getattr__ etc. donc ce n'est pas si terrible.

Il y a cependant de mauvaises habitudes (OMI). Dans ce cas, si vous voulez un tuple, je me demande pourquoi vous en feriez un objet mutable. Vous ne devriez avoir besoin que de getters (sur une note, je déteste la convention get/set mais c'est ce qu'elle est.) Je pense qu'une classe nue (mutable ou non) peut être utile dans un contexte privé ou package-privé en Java . En d'autres termes, en limitant les références dans le projet à la classe, vous pouvez refactoriser ultérieurement si nécessaire sans modifier l'interface publique de la classe. Voici un exemple de la façon dont vous pouvez créer un simple objet immuable:

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

C'est une sorte de modèle que j'utilise. Si vous n'utilisez pas d'IDE, vous devriez commencer. Il générera les getters (et les setters si vous en avez besoin) pour vous, donc ce n'est pas si douloureux.

Je me sentirais négligent si je ne soulignais pas qu'il existe déjà un type qui semble répondre à la plupart de vos besoins ici . Mis à part cela, l'approche que vous utilisez n'est pas bien adaptée à Java car elle joue avec ses faiblesses, pas ses forces. Voici une amélioration 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

Avant de devenir trop fou de Java, veuillez lire ma réponse dans votre autre article .

L'une de vos plaintes est la nécessité de créer une classe juste pour renvoyer un ensemble de valeurs comme réponse. C'est une préoccupation valable qui, je pense, montre que vos intuitions de programmation sont sur la bonne voie! Cependant, je pense que d'autres réponses manquent la cible en restant fidèles à l'anti-modèle d'obsession primitive auquel vous vous êtes engagé. Et Java n'a pas la même facilité de travail avec plusieurs primitives que Python a, où vous pouvez retourner plusieurs valeurs nativement et les affecter facilement à plusieurs variables) .

Mais une fois que vous commencez à penser à ce qu'un type ApproximateDuration fait pour vous, vous vous rendez compte qu'il n'est pas limité à "juste une classe apparemment inutile afin de renvoyer trois valeurs". Le concept représenté par cette classe est en fait l'un de vos principaux concepts commerciaux du domaine - la nécessité de pouvoir représenter les temps de manière approximative et de les comparer . Cela doit faire partie du langage omniprésent du cœur de votre application, avec une bonne prise en charge des objets et des domaines pour qu'il puisse être testé, modulaire, réutilisable et utile.

Votre code qui résume les durées approximatives ensemble (ou les durées avec une marge d'erreur, quelle que soit la façon dont vous le représentez) est-il entièrement procédural ou y a-t-il une quelconque objectivité? Je proposerais qu'une bonne conception autour de la somme des durées approximatives ensemble dicterait de le faire en dehors de tout code consommateur, au sein d'une classe qui peut elle-même être testée. Je pense que l'utilisation de ce type d'objet de domaine aura un effet d'entraînement positif dans votre code qui vous aidera à vous éloigner des étapes procédurales ligne par ligne pour accomplir une seule tâche de haut niveau (mais avec de nombreuses responsabilités), vers des classes à responsabilité unique qui sont exempts de conflits de préoccupations différentes.

Par exemple, disons que vous en savez plus sur la précision ou l'échelle qui est réellement requise pour que la somme et la comparaison de la durée fonctionnent correctement, et que vous découvrez que vous avez besoin d'un indicateur intermédiaire pour indiquer "une erreur d'environ 32 millisecondes" (près du carré) racine de 1000, donc à mi-chemin logarithmiquement entre 1 et 1000). Si vous vous êtes lié au code qui utilise des primitives pour représenter cela, vous devrez trouver chaque endroit dans le code où vous avez is_in_seconds,is_under_1ms et changez-le en is_in_seconds,is_about_32_ms,is_under_1ms. Tout devrait changer partout! Faire une classe dont la responsabilité est d'enregistrer la marge d'erreur afin qu'elle puisse être consommée ailleurs libère vos consommateurs de connaître les détails des marges d'erreur importantes ou quoi que ce soit sur la façon dont elles se combinent, et leur permet de spécifier simplement la marge d'erreur qui est pertinente en ce moment. (Autrement dit, aucun code consommateur dont la marge d'erreur est correcte n'est forcé de changer lorsque vous ajoutez un nouveau niveau de marge d'erreur dans la classe, car toutes les anciennes marges d'erreur sont toujours valides).

Déclaration de clôture

La plainte concernant la lourdeur de Java semble alors disparaître à mesure que vous vous rapprochez des principes de SOLID et GRASP, et de l'ingénierie logicielle plus avancée.

Addendum

J'ajouterai complètement gratuitement et injustement que les propriétés automatiques de C # et la capacité d'attribuer des propriétés get-only dans les constructeurs aident à nettoyer encore plus le code quelque peu désordonné dont "la Java façon" aura besoin (avec explicite champs de support privés et fonctions 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; }
}

Voici une implémentation Java de ce qui précède:

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

Maintenant, c'est sacrément propre. Notez l'utilisation très importante et intentionnelle de l'immuabilité - cela semble crucial pour ce type particulier de classe porteuse de valeur.

Pour cette question, cette classe est également un candidat décent pour être un struct, un type de valeur. Certains tests montreront si le passage à une structure présente un avantage en termes de performances d'exécution (il pourrait).

41
ErikE

Les deux Python et Java sont optimisés pour la maintenabilité selon la philosophie de leurs concepteurs, mais ils ont des idées très différentes sur la façon d'y parvenir.

Python est un langage multi-paradigme qui optimise pour clarté et simplicité de code (facile à lire et à écrire).

Java est (traditionnellement) un langage basé sur une classe à paradigme unique OO langage qui optimise pour explicitness et cohérence - même au prix de plus code détaillé.

A Python Tuple est une structure de données avec un nombre fixe de champs. La même fonctionnalité peut être obtenue par une classe régulière avec des champs explicitement déclarés. In Python it est naturel de fournir des tuples comme alternative aux classes car il vous permet de simplifier considérablement le code, notamment en raison de la prise en charge de la syntaxe intégrée pour les tuples.

Mais cela ne correspond pas vraiment à la culture Java pour fournir de tels raccourcis, car vous pouvez déjà utiliser des classes déclarées explicites. Pas besoin d'introduire un autre type de structure de données juste pour enregistrer quelques lignes de code et éviter certaines déclarations.

Java préfère un seul concept (classes) appliqué de manière cohérente avec un minimum de sucre syntaxique dans un cas spécial, tandis que Python fournit plusieurs outils et beaucoup de sucre syntaxique pour vous permettre de choisir le plus pratique pour tout particulier) objectif.

24
JacquesB

Ne cherchez pas de pratiques; c'est généralement une mauvaise idée, comme dit dans Meilleures pratiques MAUVAISES, modèles BON?. Je sais que vous ne demandez pas de meilleures pratiques, mais je pense toujours que vous y trouverez des éléments pertinents.

La recherche d'une solution à votre problème est meilleure qu'une pratique, et votre problème n'est pas un tuple pour renvoyer trois valeurs dans Java fast:

  • Il y a des tableaux
  • Vous pouvez renvoyer un tableau sous forme de liste dans une ligne: Arrays.asList (...)
  • Si vous voulez garder le côté objet avec le moins de passe-partout possible (et pas de 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,....){
        ...
    }
 }

Ici, vous avez un objet immuable contenant uniquement vos données, et public donc il n'y a pas besoin de getters. Notez cependant que si vous utilisez des outils de sérialisation ou une couche de persistance comme un ORM, ils utilisent généralement getter/setter (et peuvent accepter un paramètre pour utiliser des champs au lieu de getter/setter). Et c'est pourquoi ces pratiques sont très utilisées. Donc, si vous voulez en savoir plus sur les pratiques, il vaut mieux comprendre pourquoi elles sont là pour une meilleure utilisation de celles-ci.

Enfin: j'utilise des getters car j'utilise beaucoup d'outils de sérialisation, mais je ne les écris pas non plus; J'utilise lombok: j'utilise les raccourcis fournis par mon IDE.

16
Walfrat

À propos de Java idiomes en général:

Il y a plusieurs raisons pour lesquelles Java a des classes pour tout. Pour autant que je sache, la raison principale est:

Java devrait être facile à apprendre pour les débutants. Plus les choses sont explicites, plus il est difficile de manquer des détails importants. Il se produit moins de magie qui serait difficile à saisir pour les débutants.


Quant à votre exemple spécifique: la ligne d'argument pour une classe séparée est la suivante: si ces trois choses sont suffisamment liées les unes aux autres pour qu'elles soient renvoyées en une seule valeur, il vaut la peine de nommer cette "chose". Et introduire un nom pour un groupe de choses qui sont structurées d'une manière commune signifie définir une classe.

Vous pouvez réduire le passe-partout avec des outils comme Lombok:

@Value
class MyTuple {
    long value_in_milliseconds;
    boolean is_in_seconds;
    boolean is_under_1ms;
}
11
marstato

Le problème est que vous comparez des pommes à des oranges . Vous avez demandé comment simuler le retour de plus d'une valeur unique en donnant un exemple rapide et sale python avec un tuple non typé et vous avez en fait reçu pratiquement réponse à une ligne .

La réponse acceptée fournit une solution commerciale correcte. Aucune solution de contournement temporaire rapide que vous auriez à jeter et à implémenter correctement la première fois que vous auriez besoin de faire quoi que ce soit de pratique avec la valeur renvoyée, mais une classe POJO qui est compatible avec un grand ensemble de bibliothèques, y compris la persistance, la sérialisation/désérialisation, instrumentation et tout ce qui est possible.

Ce n'est pas long non plus. La seule chose que vous devez écrire est la définition des champs. Setters, getters, hashCode et equals peuvent être générés. Donc, votre vraie question devrait être, pourquoi les getters et setters ne sont pas générés automatiquement mais c'est un problème de syntaxe (problème de sucre syntaxique, diraient certains) et non le problème culturel.

Et enfin, vous pensez trop à essayer d'accélérer quelque chose qui n'est pas du tout important. Le temps passé à écrire des classes DTO est insignifiant par rapport au temps consacré à la maintenance et au débogage du système. Par conséquent, personne n'optimise pour moins de verbosité.

7
user253752

Il y a beaucoup de choses qui pourraient être dites à propos de la culture Java, mais je pense que dans le cas auquel vous êtes confronté en ce moment, il y a quelques aspects importants:

  1. Le code de bibliothèque est écrit une fois mais est utilisé beaucoup plus souvent. Bien qu'il soit agréable de minimiser les frais généraux d'écriture de la bibliothèque, il est probablement plus utile à long terme d'écrire d'une manière qui minimise les frais généraux d'utilisation de la bibliothèque.
  2. Cela signifie que les types d'auto-documentation sont excellents: les noms de méthode aident à clarifier ce qui se passe et ce que vous retirez d'un objet.
  3. Le typage statique est un outil très utile pour éliminer certaines classes d'erreurs. Cela ne résout certainement pas tout (les gens aiment plaisanter sur Haskell qu'une fois que le système de type accepte votre code, c'est probablement correct), mais il est très facile de rendre impossible certains types de mauvaises choses.
  4. L'écriture du code de bibliothèque consiste à spécifier des contrats. La définition d'interfaces pour vos types d'arguments et de résultats rend les limites de vos contrats plus clairement définies. Si quelque chose accepte ou produit un Tuple, rien ne dit si c'est le type de Tuple que vous devriez réellement recevoir ou produire, et il y a très peu de contraintes sur un type aussi générique (a-t-il même le bon nombre d'éléments? ils du type que vous attendiez?).

Classes "Struct" avec champs

Comme d'autres réponses l'ont mentionné, vous pouvez simplement utiliser une classe avec des champs publics. Si vous faites ces derniers, vous obtenez une classe immuable et vous les initialisez avec le constructeur:

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

Bien sûr, cela signifie que vous êtes lié à une classe particulière et que tout ce qui a besoin de produire ou de consommer un résultat d'analyse doit utiliser cette classe. Pour certaines applications, c'est très bien. Pour d'autres, cela peut causer de la douleur. Beaucoup Java consiste à définir des contrats, et cela vous emmènera généralement dans des interfaces.

Un autre écueil est qu'avec une approche basée sur les classes, vous exposez des champs et tous ces champs must ont des valeurs. Par exemple, isSeconds et millis doivent toujours avoir une certaine valeur, même si isLessThanOneMilli est vrai. Quelle devrait être l'interprétation de la valeur du champ millis lorsque isLessThanOneMilli est vrai?

"Structures" comme interfaces

Avec les méthodes statiques autorisées dans les interfaces, il est en fait relativement facile de créer des types immuables sans beaucoup de surcharge syntaxique. Par exemple, je pourrais implémenter le type de structure de résultat dont vous parlez comme quelque chose comme ceci:

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

C'est encore beaucoup de choses, je suis absolument d'accord, mais il y a aussi quelques avantages, et je pense que ceux-ci commencent à répondre à certaines de vos principales questions.

Avec une structure comme ce résultat d'analyse, le contrat de votre analyseur est très clairement défini. En Python, un Tuple n'est pas vraiment différent d'un autre Tuple. En Java, le typage statique est disponible, nous excluons donc déjà certaines classes d'erreurs. Par exemple, si vous retournez un tuple en Python et que vous souhaitez retourner le tuple (millis, isSeconds, isLessThanOneMilli), vous pouvez accidentellement faire:

return (true, 500, false)

quand tu voulais dire:

return (500, true, false)

Avec ce type d'interface Java, vous ne pouvez pas compiler:

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

du tout. Tu dois faire:

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

C'est un avantage des langages typés statiquement en général.

Cette approche commence également à vous donner la possibilité de restreindre les valeurs que vous pouvez obtenir. Par exemple, lorsque vous appelez getMillis (), vous pouvez vérifier si isLessThanOneMilli () est vrai, et si c'est le cas, lever une IllegalStateException (par exemple), car il n'y a pas de valeur significative de millis dans ce cas.

Rendre difficile de faire la mauvaise chose

Dans l'exemple d'interface ci-dessus, vous avez toujours le problème que vous pourriez échanger accidentellement les arguments isSeconds et isLessThanOneMilli, car ils ont le même type.

Dans la pratique, vous voudrez peut-être vraiment utiliser TimeUnit et la durée, afin d'obtenir un résultat comme:

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

Cela devient un beaucoup plus de code, mais vous n'avez besoin de l'écrire qu'une seule fois, et (en supposant que vous avez correctement documenté les choses), les personnes qui se retrouvent en utilisant votre le code n'a pas à deviner ce que signifie le résultat et ne peut pas faire accidentellement des choses comme result[0] quand ils veulent dire result[1]. Vous obtenez toujours créer des instances assez succinctement, et en extraire des données n'est pas si difficile non plus:

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

Notez que vous pouvez également faire quelque chose comme ça avec l'approche basée sur la classe. Spécifiez simplement des constructeurs pour les différents cas. Cependant, vous ne savez toujours pas à quoi initialiser les autres champs et vous ne pouvez pas empêcher leur accès.

Une autre réponse mentionne que la nature de type entreprise de Java signifie que la plupart du temps, vous composez d'autres bibliothèques qui existent déjà, ou écrivez des bibliothèques pour que d'autres personnes les utilisent. Votre API publique ne devrait pas nécessiter beaucoup de temps pour consulter la documentation pour déchiffrer les types de résultats si cela peut être évité.

Vous écrivez ces structures une seule fois, mais vous créez plusieurs fois, donc vous voulez toujours cette création concise (que vous obtenez). La saisie statique garantit que les données que vous en retirez correspondent à ce que vous attendez.

Maintenant, tout cela dit, il existe encore des endroits où de simples tuples ou listes peuvent avoir beaucoup de sens. Il peut y avoir moins de surcharge pour renvoyer un tableau de quelque chose, et si c'est le cas (et que cette surcharge est importante, ce que vous détermineriez avec le profilage), alors l'utilisation d'un simple tableau de valeurs en interne peut faire beaucoup de sens. Votre API publique devrait probablement avoir des types clairement définis.

7
Joshua Taylor

Ce sont trois facteurs différents qui contribuent à ce que vous observez.

Tuples versus champs nommés

Peut-être le plus trivial - dans les autres langues, vous avez utilisé un Tuple. La question de savoir si les tuples est une bonne idée n'est pas vraiment la question - mais dans Java vous avez utilisé une structure plus lourde, donc c'est une comparaison légèrement injuste: vous auriez pu utiliser un tableau d'objets et un transtypage de type.

Syntaxe du langage

Serait-il plus facile de déclarer la classe? Je ne parle pas de rendre les champs publics ou d'utiliser une carte, mais quelque chose comme les classes de cas de Scala, qui fournissent tous les avantages de la configuration que vous avez décrite mais beaucoup plus concis:

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

Nous pourrions avoir cela - mais il y a un coût: la syntaxe devient plus compliquée. Bien sûr, cela pourrait valoir la peine pour certains cas, ou même pour la plupart des cas, ou même pour la plupart des cas pour les 5 prochaines années - mais il doit être jugé. Soit dit en passant, c'est l'une des belles choses avec les langues que vous pouvez modifier vous-même (c'est-à-dire LISP) - et notez comment cela devient possible en raison de la simplicité de la syntaxe. Même si vous ne modifiez pas réellement le langage, une syntaxe simple active des outils plus puissants; par exemple, beaucoup de fois je manque certaines options de refactoring disponibles pour Java mais pas pour Scala.

Philosophie du langage

Mais le facteur le plus important est qu'une langue doit permettre une certaine façon de penser. Parfois, cela peut sembler oppressant (j'ai souvent souhaité la prise en charge d'une certaine fonctionnalité), mais supprimer des fonctionnalités est tout aussi important que de les avoir. Pourriez-vous tout soutenir? Bien sûr, mais alors vous pourriez aussi bien écrire un compilateur qui compile chaque langue. En d'autres termes, vous n'aurez pas de langue - vous aurez un surensemble de langues et chaque projet adopterait essentiellement un sous-ensemble.

Il est bien sûr possible d'écrire du code qui va à l'encontre de la philosophie du langage et, comme vous l'avez observé, les résultats sont souvent moches. Avoir une classe avec seulement quelques champs dans Java revient à utiliser un var dans Scala, dégénérer des prédicats de prologue en fonctions, faire un unsafePerformIO dans haskell etc. Java les classes ne sont pas destinées être léger - ils ne sont pas là pour transmettre des données. Lorsque quelque chose semble difficile, il est souvent utile de prendre du recul et de voir s'il existe un autre moyen. Dans votre exemple:

Pourquoi la durée est-elle distincte des unités? Il existe de nombreuses bibliothèques de temps qui vous permettent de déclarer une durée - quelque chose comme Durée (5, secondes) (la syntaxe variera), qui vous permettra ensuite de faire ce que vous voulez d'une manière beaucoup plus robuste. Peut-être que vous voulez le convertir - pourquoi vérifier si le résultat [1] (ou [2]?) Est une "heure" et multiplier par 3600? Et pour le troisième argument - quel est son but? Je suppose qu'à un moment donné, vous devrez imprimer "moins de 1 ms" ou l'heure réelle - c'est une logique qui appartient naturellement aux données temporelles. C'est à dire. vous devriez avoir une classe comme celle-ci:

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

}

ou tout ce que vous voulez réellement faire avec les données, d'où l'encapsulation de la logique.

Bien sûr, il peut y avoir un cas où cette méthode ne fonctionnera pas - je ne dis pas qu'il s'agit de l'algorithme magique pour convertir les résultats de Tuple en code idiomatique Java! Et il peut y avoir des cas où c'est très moche et mauvais et peut-être que vous auriez dû utiliser un langage différent - c'est pourquoi il y en a tellement après tout!

Mais mon point de vue sur la raison pour laquelle les classes sont des "structures lourdes" dans Java est que vous n'êtes pas censé les utiliser comme conteneurs de données mais comme des cellules logiques autonomes.

5
Thanos Tintinidis

À ma connaissance, les principales raisons sont

  1. Les interfaces sont la manière de base Java d'abstraction des classes absentes).
  2. Java ne peut renvoyer qu'une seule valeur à partir d'une méthode - un objet ou un tableau ou une valeur native (int/long/double/float/boolean).
  3. Les interfaces ne peuvent pas contenir de champs, uniquement des méthodes. Si vous voulez accéder à un champ, vous devez passer par une méthode - donc les getters et les setters.
  4. Si une méthode retourne une interface, vous devez avoir une classe d'implémentation pour retourner réellement.

Cela vous donne le "Vous devez écrire une classe pour retourner pour tout résultat non trivial" qui à son tour est plutôt lourd. Si vous avez utilisé une classe au lieu de l'interface, vous pouvez simplement avoir des champs et les utiliser directement, mais cela vous lie à une implémentation spécifique.

(Cette réponse n'est pas une explication pour Java en particulier, mais répond plutôt à la question générale de "Pour quoi les pratiques [lourdes] peuvent-elles optimiser?")

Considérez ces deux principes:

  1. C'est bien quand votre programme fait la bonne chose. Nous devons faciliter l'écriture de programmes qui font la bonne chose.
  2. C'est mauvais quand votre programme fait la mauvaise chose. Nous devrions rendre plus difficile l'écriture de programmes qui font le mauvais choix.

Essayer d'optimiser l'un de ces objectifs peut parfois entraver l'autre (c.-à-d. ce qui rend plus difficile de faire la mauvaise chose peut également rendre plus difficile de faire la bonne chose , ou vice-versa).

Les compromis à effectuer dans un cas particulier dépendent de l'application, des décisions des programmeurs ou de l'équipe en question et de la culture (de l'organisation ou de la communauté linguistique).

Par exemple, si un bug ou une interruption de quelques heures dans votre programme pouvait entraîner des pertes de vie (systèmes médicaux, aéronautique) ou même simplement de l'argent (comme des millions de dollars dans les systèmes de publicité de Google, par exemple), vous feriez des compromis différents (pas juste dans votre langue mais aussi dans d'autres aspects de la culture d'ingénierie) que vous le feriez pour un script unique: il est probablement penché vers le côté "lourd".

Autres exemples qui ont tendance à rendre votre système plus "lourd":

  • Lorsque vous avez une grande base de code sur laquelle de nombreuses équipes ont travaillé pendant de nombreuses années, une grande préoccupation est que quelqu'un utilise incorrectement l'API de quelqu'un d'autre. Une fonction appelée avec des arguments dans le mauvais ordre, ou appelée sans garantir certaines conditions préalables/contraintes qu'elle attend, pourrait être catastrophique.
  • Par exemple, supposons que votre équipe gère une API ou une bibliothèque particulière et souhaite la modifier ou la refactoriser. Plus vos utilisateurs sont "contraints" dans la façon dont ils pourraient utiliser votre code, plus il est facile de le modifier. (Notez qu'il serait préférable ici d'avoir des garanties réelles que personne ne pourrait l'utiliser de manière inhabituelle.)
  • Si le développement est réparti entre plusieurs personnes ou équipes, il peut sembler judicieux qu'une seule personne ou équipe "spécifie" l'interface et que d'autres la mettent réellement en œuvre. Pour que cela fonctionne, vous devez être en mesure de gagner une certaine confiance lorsque l'implémentation est terminée, que l'implémentation correspond réellement à la spécification.

Ce ne sont que quelques exemples, pour vous donner une idée des cas où rendre les choses "lourdes" (et rendre plus difficile pour vous d'écrire du code rapidement) peut être véritablement intentionnel. (On pourrait même affirmer que si l'écriture de code nécessite beaucoup d'efforts, cela peut vous amener à réfléchir plus attentivement avant d'écrire du code! Bien sûr, cette argumentation devient rapidement ridicule.)

Un exemple: le système interne de Google Python a tendance à alourdir les choses de sorte que vous ne pouvez pas simplement importer le code de quelqu'un d'autre, vous devez déclarer la dépendance dans un fichier BUILD , l'équipe dont vous souhaitez importer le code doit voir sa bibliothèque déclarée visible par votre code, etc.


Remarque : Tout ce qui précède concerne à peu près les choses qui ont tendance à devenir "lourdes". Je ne prétends absolument pas que Java ou Python (soit les langues elles-mêmes) , ou leurs cultures) font des compromis optimaux pour un cas particulier, c'est à vous de réfléchir. Deux liens connexes sur ces compromis:

3
ShreevatsaR

Je suis d'accord avec la réponse de JacquesB

Java est (traditionnellement) un langage basé sur une classe à paradigme unique OO langage qui optimise la clarté et la cohérence

Mais la justesse et la cohérence ne sont pas les objectifs finaux à optimiser. Lorsque vous dites que "python est optimisé pour la lisibilité", vous mentionnez immédiatement que l'objectif final est la "maintenabilité" et la "vitesse de développement".

Qu'est-ce que vous réalisez lorsque vous avez une explication et une cohérence, faites Java? Mon avis est qu'il a évolué comme un langage qui prétend fournir un moyen prévisible, cohérent et uniforme pour résoudre tout problème logiciel.

En d'autres termes, la culture Java Java est optimisée pour faire croire aux managers qu'ils comprennent le développement logiciel.

Ou, comme n type sage l'a dit il y a très longtemps ,

La meilleure façon de juger une langue est de regarder le code écrit par ses partisans. "Radix enim omnium malorum est cupiditas" - et Java est clairement un exemple de programmation orientée argent (MOP). En tant que principal promoteur de Java chez SGI m'a dit: "Alex, tu dois aller où est l'argent." Mais je ne veux pas particulièrement aller où l'argent est - ça ne sent généralement pas Nice là-bas.

3
artem

La culture Java Java a évolué au fil du temps avec de fortes influences issues à la fois des logiciels open source et des logiciels d'entreprise - ce qui est un mélange étrange si vous y pensez vraiment. Les solutions d'entreprise nécessitent des outils lourds et open source demande de la simplicité. Le résultat final est que Java est quelque part au milieu.

Une partie de ce qui influence la recommandation est ce qui est considéré comme lisible et maintenable dans Python et Java est très différent.

  • En Python, le Tuple est une fonctionnalité language.
  • Dans les deux Java et C #, le Tuple est (ou serait) une fonctionnalité bibliothèque.

Je ne mentionne que C # parce que la bibliothèque standard a un ensemble de classes Tuple <A, B, C, .. n>, et c'est un parfait exemple de la façon dont les tuples sont difficiles à manier si le langage ne les prend pas directement en charge. Dans presque tous les cas, votre code devient plus lisible et maintenable si vous avez bien choisi les classes pour gérer le problème. Dans l'exemple spécifique de votre question Stack Overflow liée, les autres valeurs seraient facilement exprimées sous forme de getters calculés sur l'objet de retour.

L'idée de objets anonymes (publiée dans C # 3. ) qui égratigne assez bien cette démangeaison est une solution intéressante que la plate-forme C # a proposée. Malheureusement, Java n'a pas encore d'équivalent.

Jusqu'à ce que les fonctionnalités du langage Java soient modifiées, la solution la plus lisible et maintenable est d'avoir un objet dédié. Cela est dû à des contraintes dans le langage qui remontent à ses débuts en 1995. Les auteurs originaux avaient prévu beaucoup plus de fonctionnalités de langage qui ne l'ont jamais fait, et la compatibilité descendante est l'une des principales contraintes entourant l'évolution de Java au fil du temps.

2
Berin Loritsch

Pour être franc, la culture est que Java avaient tendance à sortir des universités où les principes orientés objet et les principes de conception de logiciels durables étaient enseignés.

Comme ErikE le dit plus en termes dans sa réponse, vous ne semblez pas écrire du code durable. Ce que je vois dans votre exemple, c'est qu'il y a un enchevêtrement très délicat de préoccupations.

Dans la culture Java Java, vous aurez tendance à être conscient des bibliothèques disponibles, et cela vous permettra de faire bien plus que votre programmation initiale. Ainsi, vous échangeriez vos idiosyncrasies pour les modèles et les styles de conception qui avaient été essayés et testés dans des environnements industriels de noyau dur.

Mais comme vous le dites, ce n'est pas sans inconvénients: aujourd'hui, après avoir utilisé Java pendant plus de 10 ans, j'ai tendance à utiliser Node/Javascript ou Go pour de nouveaux projets, car les deux permettent un développement plus rapide , et avec des architectures de type microservice, cela suffit souvent. À en juger par le fait que Google utilisait d'abord Java, mais a été à l'origine de Go, je suppose qu'ils pourraient faire de même. Mais même si j'utilise maintenant Go et Javascript, J'utilise encore bon nombre des compétences en conception que j'ai acquises au fil des années d'utilisation et de compréhension de Java.

0
Tom

Je pense que l'une des choses essentielles à propos de l'utilisation d'une classe dans ce cas est que ce qui va ensemble doit rester ensemble.

J'ai eu cette discussion dans l'autre sens, sur les arguments de méthode: considérons une méthode simple qui calcule l'IMC:

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

Dans ce cas, je m'oppose à ce style car le poids et la taille sont liés. La méthode "communique" ce sont deux valeurs distinctes quand elles ne le sont pas. Quand calculeriez-vous un IMC avec le poids d'une personne et la taille d'une autre? Cela n'aurait aucun sens.

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

Cela a beaucoup plus de sens car maintenant vous communiquez clairement que la taille et le poids proviennent de la même source.

Il en va de même pour le retour de plusieurs valeurs. S'ils sont clairement connectés, renvoyez un petit paquet soigné et utilisez un objet, s'ils ne renvoient pas plusieurs valeurs.

0
Pieter B