web-dev-qa-db-fra.com

Puis-je définir l'interface négatable en Java?

En posant cette question pour clarifier ma compréhension des classes de types et des types de type supérieur, je ne cherche pas de solutions de contournement en Java.


En Haskell, je pourrais écrire quelque chose comme

class Negatable t where
    negate :: t -> t

normalize :: (Negatable t) => t -> t
normalize x = negate (negate x)

Ensuite, en supposant que Bool ait une instance de Negatable,

v :: Bool
v = normalize True

Et tout fonctionne bien.


En Java, il ne semble pas possible de déclarer une interface Negatable correcte. Nous pourrions écrire:

interface Negatable {
    Negatable negate();
}

Negatable normalize(Negatable a) {
    a.negate().negate();
}

Mais alors, contrairement à Haskell, ce qui suit ne serait pas compilé sans conversion (supposons que MyBoolean implémente Negatable):

MyBoolean val = normalize(new MyBoolean()); // does not compile; val is a Negatable, not a MyBoolean

Existe-t-il un moyen de faire référence au type d'implémentation dans une interface Java, ou s'agit-il d'une limitation fondamentale du système de type Java?? S'il s'agit d'une limitation, s'agit-il d'un type de support plus important? Je ne le pense pas: cela ressemble à une autre limitation. Si oui, a-t-il un nom?

Merci, et s'il vous plaît laissez-moi savoir si la question n'est pas claire!

52
zale

En général non.

Vous pouvez utilisez des astuces (comme suggéré dans les autres réponses) pour que cela fonctionne, mais elles ne fournissent pas toutes les garanties que la classe de types Haskell fait. Plus précisément, en Haskell, je pourrais définir une fonction comme celle-ci:

doublyNegate :: Negatable t => t -> t
doublyNegate v = negate (negate v)

On sait maintenant que l'argument et la valeur de retour de doublyNegate sont tous deux t. Mais l’équivalent Java:

public <T extends Negatable<T>> T doublyNegate (Negatable<T> v)
{
    return v.negate().negate();
}

non, car Negatable<T> pourrait être implémenté par un autre type:

public class X implements Negatable<SomeNegatableClass> {
    public SomeNegatableClass negate () { return new SomeNegatableClass(); }
    public static void main (String[] args) { 
       new X().negate().negate();   // results in a SomeNegatableClass, not an X
}

Cela n’est pas particulièrement grave pour cette application, mais pose des problèmes pour d’autres classes de types Haskell, par exemple. Equatable. Il n’existe aucun moyen de mettre en oeuvre une classe Java Equatable sans utiliser d’objet supplémentaire et d’envoyer une instance de cet objet partout où nous envoyons des valeurs nécessitant une comparaison, par exemple:

public interface Equatable<T> {
    boolean equal (T a, T b);
}
public class MyClass
{
    String str;

    public static class MyClassEquatable implements Equatable<MyClass> 
    { 
         public boolean equal (MyClass a, MyClass b) { 
             return a.str.equals(b.str);
         } 
    }
}
...
public <T> methodThatNeedsToEquateThings (T a, T b, Equatable<T> eq)
{
    if (eq.equal (a, b)) { System.out.println ("they're equal!"); }
}  

(En fait, c'est exactement la façon dont Haskell implémente les classes de types, mais il masque le paramètre qui vous est transmis, vous n'avez donc pas besoin de savoir quelle implémentation envoyer où.)

Essayer de le faire avec des interfaces simples Java) conduit à des résultats contre-intuitifs:

public interface Equatable<T extends Equatable<T>>
{
    boolean equalTo (T other);
}
public MyClass implements Equatable<MyClass>
{
    String str;
    public boolean equalTo (MyClass other) 
    {
        return str.equals(other.str);
    }
}
public Another implements Equatable<MyClass>
{
    public boolean equalTo (MyClass other)
    {
        return true;
    }
}

....
MyClass a = ....;
Another b = ....;

if (b.equalTo(a))
    assertTrue (a.equalTo(b));
....

Vous vous attendez, en raison du fait que equalTo doit vraiment être défini symétriquement, que si l'instruction if compilée, l'assertion compilerait également, mais ce n'est pas le cas, car MyClass ne correspond pas à Another même si l'inverse est vrai. Mais avec une classe de type Haskell Equatable, nous savons que si areEqual a b Fonctionne, alors areEqual b a Est également valide. [1]

Une autre limitation des interfaces par rapport aux classes de types est qu’une classe de types peut fournir un moyen de créer une valeur qui implémente la classe de types sans avoir de valeur existante (par exemple, l’opérateur return de Monad), pour une interface, vous devez déjà avoir un objet du type pour pouvoir appeler ses méthodes.

Vous demandez s'il existe un nom pour cette limitation, mais je ne le connais pas. C'est simplement parce que les classes de types sont en réalité différentes des interfaces orientées objet, malgré leurs similitudes, parce qu'elles sont implémentées de cette manière fondamentalement différente: un objet est un sous-type de son interface, transportant ainsi directement une copie des méthodes de l'interface sans modifier leur modification. définition, alors qu’une classe de type est une liste séparée de fonctions personnalisées chacune par substitution de variables de type. Il n'y a pas de relation de sous-type entre un type et une classe de type ayant une instance pour le type (un Haskell Integer n'est pas un sous-type de Comparable, par exemple: il existe simplement un Comparable instance pouvant être transmise chaque fois qu'une fonction doit pouvoir comparer ses paramètres et que ces paramètres sont des entiers).

[1]: L'opérateur Haskell == Est en réalité implémenté à l'aide d'une classe de type, Eq ... je ne l'ai pas utilisée car la surcharge d'opérateurs dans Haskell peut être déroutante pour les personnes non familiarisées avec la lecture. Code Haskell.

12
Jules

En fait, oui. Pas directement, mais vous pouvez le faire. Incluez simplement un paramètre générique puis dérivez du type générique.

public interface Negatable<T> {
    T negate();
}

public static <T extends Negatable<T>> T normalize(T a) {
    return a.negate().negate();
}

Vous souhaitez implémenter cette interface comme si

public static class MyBoolean implements Negatable<MyBoolean> {
    public boolean a;

    public MyBoolean(boolean a) {
        this.a = a;
    }

    @Override
    public MyBoolean negate() {
        return new MyBoolean(!this.a);
    }

}

En fait, la bibliothèque standard Java utilise cette astuce exacte pour implémenter Comparable.

public interface Comparable<T> {
    int compareTo(T o);
}
63
Silvio Mayolo

J'interprète la question comme

Comment pouvons-nous implémenter un polymorphisme ad hoc en utilisant des classes de types en Java?

Vous pouvez faire quelque chose de très similaire en Java, mais sans les garanties de sécurité de type de Haskell - la solution présentée ci-dessous peut générer des erreurs lors de l'exécution.

Voici comment vous pouvez le faire:

  1. Définir une interface qui représente la classe de types

    interface Negatable<T> {
      T negate(T t);
    }
    
  2. Implémentez un mécanisme qui vous permet de enregistrer des instances de la classe de types pour différents types. Ici, un HashMap statique fera:

    static HashMap<Class<?>, Negatable<?>> instances = new HashMap<>();
    static <T> void registerInstance(Class<T> clazz, Negatable<T> inst) {
      instances.put(clazz, inst);
    }
    @SuppressWarnings("unchecked")
    static <T> Negatable<T> getInstance(Class<?> clazz) {
      return (Negatable<T>)instances.get(clazz);
    }
    
  3. Définissez la méthode normalize qui utilise le mécanisme ci-dessus pour obtenir l'instance appropriée en fonction de la classe d'exécution de l'objet transmis:

      public static <T> T normalize(T t) {
        Negatable<T> inst = Negatable.<T>getInstance(t.getClass());
        return inst.negate(inst.negate(t));
      }
    
  4. Enregistrez des instances réelles pour différentes classes:

    Negatable.registerInstance(Boolean.class, new Negatable<Boolean>() {
      public Boolean negate(Boolean b) {
        return !b;
      }
    });
    
    Negatable.registerInstance(Integer.class, new Negatable<Integer>() {
      public Integer negate(Integer i) {
        return -i;
      }
    });
    
  5. Utilise le!

    System.out.println(normalize(false)); // Boolean `false`
    System.out.println(normalize(42));    // Integer `42`
    

L'inconvénient principal est que, comme nous l'avons déjà mentionné, la recherche d'instance de classe de classe peut échouer à l'exécution, pas à la compilation (comme dans Haskell). L'utilisation d'une mappe de hachage statique est également sous-optimale, car elle soulève tous les problèmes d'une variable globale partagée, ce qui pourrait être atténué par des mécanismes d'injection de dépendance plus sophistiqués. Générer automatiquement des instances de classe de classe à partir d’autres instances de classe nécessiterait encore plus d’infrastructures (pourrait être fait dans une bibliothèque). Mais en principe, il implémente un polymorphisme ad hoc en utilisant des classes de classes en Java.

Code complet:

import Java.util.HashMap;

class TypeclassInJava {

  static interface Negatable<T> {
    T negate(T t);

    static HashMap<Class<?>, Negatable<?>> instances = new HashMap<>();
    static <T> void registerInstance(Class<T> clazz, Negatable<T> inst) {
      instances.put(clazz, inst);
    }
    @SuppressWarnings("unchecked")
    static <T> Negatable<T> getInstance(Class<?> clazz) {
      return (Negatable<T>)instances.get(clazz);
    }
  }

  public static <T> T normalize(T t) {
    Negatable<T> inst = Negatable.<T>getInstance(t.getClass());
    return inst.negate(inst.negate(t));
  }

  static {
    Negatable.registerInstance(Boolean.class, new Negatable<Boolean>() {
      public Boolean negate(Boolean b) {
        return !b;
      }
    });

    Negatable.registerInstance(Integer.class, new Negatable<Integer>() {
      public Integer negate(Integer i) {
        return -i;
      }
    });
  }

  public static void main(String[] args) {
    System.out.println(normalize(false));
    System.out.println(normalize(42));
  }
}
7
Andrey Tyukin

Vous recherchez des génériques, en plus de l'auto-saisie. L'auto-typage est la notion d'espace générique qui correspond à la classe de l'instance.

Cependant, l'auto-typage n'existe pas en Java.

Cela peut être résolu avec des génériques cependant.

public interface Negatable<T> {
    public T negate();
}

Ensuite

public class MyBoolean implements Negatable<MyBoolean>{

    @Override
    public MyBoolean negate() {
        //your impl
    }

}

Quelques implications pour les développeurs:

  • Ils doivent spécifier eux-mêmes le moment où ils implémentent l'interface, par exemple. MyBoolean implements Negatable<MyBoolean>
  • Pour étendre MyBoolean, il serait nécessaire de redéfinir la méthode negate.
7
Taylor