web-dev-qa-db-fra.com

Comment implémenter des méthodes statiques abstraites en Java?

De nombreuses questions se posent sur l'impossibilité d'inclure des méthodes Java abstraites statiques. Il existe également de nombreuses solutions de contournement (défaut de conception/résistance de la conception). Mais je n'en trouve pas pour le problème spécifique que je vais énoncer sous peu.

Il me semble que ceux qui ont créé Java, et bon nombre de ceux qui l'utilisent, ne pensent pas aux méthodes statiques comme moi et beaucoup d'autres, comme fonctions de classe ou comme méthodes appartenant à la classe et pas à n'importe quel objet. Alors, y a-t-il un autre moyen d'implémenter une fonction de classe?

Voici mon exemple: en mathématiques, un groupe est un ensemble d’objets qui peuvent être composés les uns avec les autres à l’aide d’une opération * judicieuse - par exemple, les nombres réels positifs forment un groupe sous une multiplication normale (x * y = x × y), et l'ensemble des entiers forme un groupe, où l'opération 'multiplication' est son addition (m * n = m + n).

Une façon naturelle de modéliser cela en Java consiste à définir une interface (ou une classe abstraite) pour les groupes:

public interface GroupElement
{
  /**
  /* Composes with a new group element.
  /* @param elementToComposeWith - the new group element to compose with.
  /* @return The composition of the two elements.
   */
  public GroupElement compose(GroupElement elementToComposeWith)
}

Nous pouvons implémenter cette interface pour les deux exemples que j'ai donnés ci-dessus:

public class PosReal implements GroupElement
{
  private double value;

  // getter and setter for this field

  public PosReal(double value)
  {
    setValue(value);
  }

  @Override
  public PosReal compose(PosReal multiplier)
  {
    return new PosReal(value * multiplier.getValue());
  }
}

et

public class GInteger implements GroupElement
{
  private int value;

  // getter and setter for this field

  public GInteger(double value)
  {
    setValue(value);
  }

  @Override
  public GInteger compose(GInteger addend)
  {
    return new GInteger(value + addend.getValue());
  }
}

Cependant, un groupe possède une autre propriété importante: chaque groupe a un élément identité - un élément e tel que x * e = x pour tous les x du groupe. Par exemple, l'élément d'identité pour les réels positifs sous multiplication est 1, et l'élément d'identité pour les entiers sous addition est . Dans ce cas, il est logique de disposer d'une méthode pour chaque classe d'implémentation, comme suit:

public PosReal getIdentity()
{
  return new PosReal(1);
}

public GInteger getIdentity()
{
  return new GInteger(0);
}

Mais ici nous rencontrons des problèmes - la méthode getIdentity ne dépend d'aucune instance de l'objet, et doit donc être déclarée static (en fait, nous pouvons souhaiter y faire référence à partir d'un contexte statique). Mais si nous mettons la méthode getIdentity dans l'interface, nous ne pouvons pas la déclarer static dans l'interface. Par conséquent, il ne peut s'agir de static dans aucune classe d'implémentation.

Existe-t-il un moyen d'implémenter cette méthode getIdentity qui:

  1. Force la cohérence sur toutes les implémentations de GroupElement, de sorte que chaque implémentation de GroupElement soit forcée d'inclure une fonction getIdentity.
  2. Se comporte de manière statique; En d'autres termes, nous pouvons obtenir l'élément identité pour une implémentation donnée de GroupElement sans instancier d'objet pour cette implémentation.

La condition (1) dit essentiellement «est abstrait» et la condition (2) dit «est statique», et je sais que static et abstract sont incompatibles en Java. Alors, y a-t-il des concepts connexes dans le langage qui peuvent être utilisés pour faire cela?

13
John Gowers

Ce que vous demandez essentiellement, c’est la possibilité d’imposer, au moment de la compilation, qu’une classe définisse une méthode statique donnée avec une signature spécifique.

Vous ne pouvez pas vraiment faire cela en Java, mais la question est: en avez-vous vraiment besoin?

Supposons donc que vous preniez l’option actuelle d’implémenter une getIdentity() statique dans chacune de vos sous-classes. Considérez que vous n’avez réellement besoin de cette méthode que si vous utilisez use it et, bien sûr, si vous essayez de l’utiliser mais qu’elle n’est pas définie, vous allez obtenez une erreur de compilation. vous rappelant de le définir.

Si vous le définissez mais que la signature n'est pas "correcte" et que vous tentez de l'utiliser autrement que vous l'avez définie, vous obtiendrez déjà une erreur du compilateur (à propos de l'appel avec des paramètres non valides, ou un problème de type de retour, etc.). ).

Comme vous ne pouvez pas appeler des méthodes statiques sous-classées via un type de base, vous devez always les appeler explicitement, par exemple. GInteger.getIdentity(). Et puisque le compilateur se plaindra déjà si vous appelez GInteger.getIdentity() lorsque getIdentity() n'est pas défini, ou si vous l'utilisez incorrectement, vous obtenez essentiellement un contrôle lors de la compilation. La seule chose qui vous manque, bien sûr, est la possibilité de faire en sorte que la méthode statique soit définie même si vous ne l'utilisez jamais dans votre code.

Donc, ce que vous avez déjà est assez proche.

Votre exemple est un bon exemple qui explique what vous voulez, mais je vous mets au défi de proposer un exemple dans lequel il est nécessaire de disposer d’un avertissement au moment de la compilation concernant une fonction statique manquante; La seule chose à laquelle je peux penser, c'est que si vous créez une bibliothèque destinée à être utilisée par d'autres et que vous voulez vous assurer de ne pas oublier d'implémenter une fonction statique particulière - mais de tester correctement les unités de toutes vos sous-classes. peut également le détecter lors de la compilation (vous ne pouvez pas tester une getIdentity() si elle n'était pas présente).

Remarque: Si vous demandez la possibilité de appeler une méthode statique à l'aide d'un Class<?>, vous ne pouvez pas, en soi (sans réflexion) - mais vous pouvez toujours obtenir la fonctionnalité que vous souhaitez. vouloir, comme décrit dans réponse de Giovanni Botta }; vous sacrifierez les contrôles au moment de la compilation pour les contrôles au moment de l'exécution, mais aurez la possibilité d'écrire des algorithmes génériques à l'aide d'identité. Donc, cela dépend vraiment de votre objectif final.

5
Jason C

Un groupe mathématique n'a qu'une seule opération caractéristique, mais une classe Java peut avoir un nombre quelconque d'opérations. Par conséquent, ces deux concepts ne correspondent pas.

Je peux imaginer quelque chose comme une classe Java Group consistant en une Set d'éléments et une opération spécifique, qui constituerait une interface en soi. Quelque chose comme

public interface Operation<E> {
   public E apply(E left, E right);
}

Avec cela, vous pouvez construire votre groupe:

public abstract class Group<E, O extends Operation<E>> {
    public abstract E getIdentityElement();
}

Je sais que ce n’est pas tout à fait ce que vous aviez à l’esprit, mais comme je l’ai dit plus haut, un groupe mathématique est un concept quelque peu différent de celui d’une classe.

3
Ray

Il y a peut-être un certain malentendu dans votre raisonnement. Vous voyez qu'un "groupe" mathématique est tel que vous le définissez (si je me souviens bien); mais ses éléments ne se caractérisent pas par le fait qu'ils appartiennent à ce groupe. Ce que je veux dire, c'est qu'un entier (ou réel) est une entité autonome, qui appartient également au groupe XXX (parmi ses autres propriétés).

Donc, dans le contexte de la programmation, je séparerais la définition (class) d'une forme de groupe de celle de ses membres, en utilisant probablement des génériques:

interface Group<T> {
    T getIdentity();
    T compose(T, T);
}

Une définition encore plus analytique serait:

/** T1: left operand type, T2: right ..., R: result type */
interface BinaryOperator<T1, T2, R> {
    R operate(T1 a, T2 b);
}

/** BinaryOperator<T,T,T> is a function TxT -> T */
interface Group<T, BinaryOperator<T,T,T>> {
    void setOperator(BinaryOperator<T,T,T> op);
    BinaryOperator<T,T,T> getOperator();
    T getIdentity();
    T compose(T, T); // uses the operator
}

Tout cela est une idée. Cela fait longtemps que je n’ai pas touché aux mathématiques, je peux donc me tromper énormément.

S'amuser!

3

Il n’existe pas de méthode Java pour ce faire (vous pourriez peut-être faire quelque chose comme ça dans Scala) et toutes les solutions de contournement que vous trouverez sont basées sur une convention de codage.

En Java, votre interface GroupElement déclare généralement deux méthodes statiques telles que celle-ci:

public static <T extends GroupElement> 
  T identity(Class<T> type){ /* implementation omitted */ }

static <T extends GroupElement> 
  void registerIdentity(Class<T> type, T identity){ /* implementation omitted */ }

Vous pouvez facilement implémenter ces méthodes en utilisant une classe pour mapper une instance ou une solution de votre choix. Le fait est que vous conservez une carte statique des éléments d’identité, une pour chaque implémentation GroupElement.

Et voici la nécessité d’une convention: chaque sous-classe de GroupElement devra déclarer de manière statique son propre élément d’identité, par exemple,

public class SomeGroupElement implements GroupElement{
  static{
    GroupElement.registerIdentity(SomeGroupElement.class, 
      /* create the identity here */);
  }
}

Dans la méthode identity, vous pouvez générer une RuntimeException si l'identité n'a jamais été enregistrée. Cela ne vous donnera pas une vérification statique mais au moins une vérification à l'exécution pour vos classes GroupElement.

L'alternative à cela est un peu plus détaillée et vous oblige à instancier vos classes GroupElement via une fabrique uniquement, qui se chargera également de renvoyer l'élément identité (et d'autres objets/fonctions similaires):

public interface GroupElementFactory<T extends GroupElement>{
  T instance();
  T identity();
}

Il s'agit d'un modèle généralement utilisé dans les applications d'entreprise lorsque l'usine est injectée via un framework d'injection de dépendance (Guice, Spring) dans l'application et qu'il est peut-être trop détaillé, plus difficile à gérer et peut-être trop pour vous.

EDIT: Après avoir lu certaines des réponses, je conviens que vous devriez modéliser au niveau du groupe, pas au niveau des éléments du groupe, car les types d’élément pourraient être partagés entre différents groupes. Néanmoins, les réponses ci-dessus fournissent un schéma général pour appliquer le comportement que vous décrivez.

EDIT 2 : Par "convention de codage" ci-dessus, j'entends avoir une méthode statique getIdentity dans chaque sous-classe de GroupElement, comme mentionné par certains. Cette approche a l'inconvénient de ne pas permettre l'écriture d'algorithmes génériques sur le groupe. Encore une fois, la meilleure solution est celle mentionnée dans la première édition.

2
Giovanni Botta

Si vous avez besoin de la capacité de générer une identité lorsque la classe n'est pas connue au moment de la compilation, la première question est la suivante: comment savoir, au moment de l'exécution, quelle classe vous voulez? Si la classe est basée sur un autre objet, je pense que le moyen le plus propre est de définir une méthode dans la superclasse qui signifie "obtenir une identité dont la classe est la même chose que" un autre objet.

public GroupElement getIdentitySameClass();

Cela devrait être remplacé dans chaque sous-classe. Le remplacement n'utiliserait probablement pas l'objet; l'objet ne serait utilisé que pour sélectionner la bonne variable getIdentity à appeler de manière polymorphe. Très probablement, vous voudriez aussi une getIdentity statique dans chaque classe (mais je ne sais pas comment le compilateur en forcera l'écriture), le code dans la sous-classe ressemblera probablement

public static GInteger getIdentity() { ... whatever }

@Override
public GInteger getIdentitySameClass() { return getIdentity(); }

D'un autre côté, si la classe dont vous avez besoin provient d'un objet Class<T>, vous devrez utiliser une réflexion commençant par getMethod . Ou voyez la réponse de Giovanni, que je pense être meilleure.

1
ajb

Nous sommes tous d’accord pour dire que si vous souhaitez implémenter des groupes, vous aurez besoin d’une interface de groupe et de classes.

public interface Group<MyGroupElement extends GroupElement>{
    public MyGroupElement getIdentity()
}

Nous implémentons les groupes en tant que singletons afin que nous puissions accéder à getIdentity de manière statique à travers instance.

public class GIntegerGroup implements Group<GInteger>{

    // singleton stuff
    public final static instance = new GIntgerGroup();
    private GIntgerGroup(){};

    public GInteger getIdentity(){
        return new GInteger(0);
    }
}

public class PosRealGroup implements Group<PosReal>{

    // singleton stuff
    public final static instance = new PosRealGroup();
    private PosRealGroup(){}        

    public PosReal getIdentity(){
        return new PosReal(1);
    }
}

si nous devons également pouvoir obtenir l'identité d'un élément de groupe, je mettrais à jour votre interface GroupElement avec:

public Group<GroupElement> getGroup();

et GInteger avec:

public GIntegerGroup getGroup(){
    return GIntegerGroup.getInstance(); 
}

et PosReal avec:

public PosRealGroup getGroup(){
    return PosRealGroup.getInstance(); 
}
1
Colin

"la méthode getIdentity ne dépend d'aucune instance de l'objet et doit donc être déclarée statique"

En fait, si cela ne dépend d'aucune instance, il peut simplement renvoyer une valeur constante, il n'est pas nécessaire que ce soit statique.

Ce n’est pas parce qu’une méthode statique ne dépend pas d’une instance que vous devez toujours l’utiliser pour ce type de situation.

0
Leo