it-swarm.com.de

Wie kann ich abstrakte statische Methoden in Java implementieren?

Es gibt zahlreiche Fragen zur Unmöglichkeit, statische abstrakte Java-Methoden einzubeziehen. Hierfür gibt es auch einige Problemumgehungen (Konstruktionsfehler/Konstruktionsstärke). Aber ich kann keine für das spezifische Problem finden, das ich in Kürze darlegen werde.

Es scheint mir, dass die Leute, die Java gemacht haben, und ziemlich viele Leute, die es benutzen, statische Methoden nicht so sehen, wie ich und viele andere - als Klassenfunktionen oder Methoden, die zur Klasse gehören und nicht an irgendeinen Gegenstand. Gibt es also eine andere Möglichkeit, eine Klassenfunktion zu implementieren?

Hier ist mein Beispiel: In der Mathematik ist eine Gruppe eine Menge von Objekten, die mit einer Operation * auf sinnvolle Weise miteinander zusammengesetzt werden können - zum Beispiel bilden die positiven reellen Zahlen unter normaler Multiplikation eine Gruppe ( x * y = x × y ) und die Menge der ganzen Zahlen bilden eine Gruppe, in der die 'Multiplikations'-Operation addiert wird ( m * n = m + n ).

Eine natürliche Möglichkeit, dies in Java zu modellieren, besteht darin, eine Schnittstelle (oder eine abstrakte Klasse) für Gruppen zu definieren:

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

Wir können diese Schnittstelle für die beiden Beispiele implementieren, die ich oben angegeben habe:

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

und

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

Es gibt jedoch eine weitere wichtige Eigenschaft, die eine Gruppe hat: Jede Gruppe hat ein Identitätselement - ein Element e , sodass x * e = x für alle x in der Gruppe. Zum Beispiel ist das Identitätselement für positive reelle Zahlen unter Multiplikation 1 und das Identitätselement für ganze Zahlen unter Addition 0 . In diesem Fall ist es sinnvoll, für jede implementierende Klasse eine Methode wie die folgende zu haben:

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

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

Aber hier treten Probleme auf - die Methode getIdentity hängt nicht von einer Instanz des Objekts ab und sollte daher als static deklariert werden (in der Tat möchten wir möglicherweise aus einem statischen Kontext darauf verweisen). Wenn wir jedoch die Methode getIdentity in die Schnittstelle einfügen, können wir sie in der Schnittstelle nicht als static deklarieren, sodass sie in keiner implementierenden Klasse static sein kann.

Gibt es eine Möglichkeit, diese getIdentity-Methode zu implementieren, die:

  1. Erzwingt Konsistenz über alle Implementierungen von GroupElement, sodass jede Implementierung von GroupElement eine getIdentity -Funktion enthalten muss.
  2. Verhält sich statisch; d.h. wir können das Identitätselement für eine gegebene Implementierung von GroupElement abrufen, ohne ein Objekt für diese Implementierung zu instanziieren.

Bedingung (1) besagt im Wesentlichen "ist abstrakt" und Bedingung (2) besagt "ist statisch", und ich weiß, dass static und abstract in Java nicht kompatibel sind. Gibt es also verwandte Konzepte in der Sprache, die dazu verwendet werden können?

13
John Gowers

Im Wesentlichen fragen Sie nach der Möglichkeit, zur Kompilierzeit zu erzwingen, dass eine Klasse eine bestimmte statische Methode mit einer bestimmten Signatur definiert.

In Java kann man das nicht wirklich machen, aber die Frage ist: Muss man das wirklich?

Nehmen wir also an, Sie setzen Ihre derzeitige Option für die Implementierung einer statischen getIdentity() in jeder Ihrer Unterklassen ein. Bedenken Sie, dass Sie diese Methode erst benötigen, wenn Sie use it verwenden. Wenn Sie versuchen, sie zu verwenden, aber nicht definiert ist, erhalten Sie will einen Compiler-Fehler Sie daran erinnern, es zu definieren.

Wenn Sie sie definieren, die Signatur jedoch nicht "korrekt" ist und Sie versuchen, sie anders als von Ihnen definiert zu verwenden, erhalten Sie auch bereits einen Compiler-Fehler (etwa den Aufruf mit ungültigen Parametern oder ein Problem mit dem Rückgabetyp usw.). ).

Da Sie nicht über einen Basistyp untergeordnete statische Methoden aufrufen können, müssen Sie always explizit aufrufen, z. GInteger.getIdentity(). Und da der Compiler sich bereits beschwert, wenn Sie versuchen, GInteger.getIdentity() aufzurufen, wenn getIdentity() nicht definiert ist, oder wenn Sie ihn falsch verwenden, erhalten Sie im Wesentlichen eine Überprüfung der Kompilierungszeit. Das einzige, was Ihnen fehlt, ist natürlich die Möglichkeit, die Definition der statischen Methode zu erzwingen, selbst wenn Sie sie niemals in Ihrem Code verwenden.

Also, was du schon hast, ist ziemlich nahe.

Ihr Beispiel ist ein gutes Beispiel, das what Sie will, aber ich möchte Sie auffordern, ein Beispiel zu nennen, bei dem eine Kompilierzeitwarnung über eine fehlende statische Funktion erforderlich ist. Das Einzige, was mir in den Sinn kommt, ist, wenn Sie eine Bibliothek für andere Benutzer erstellen und sicherstellen möchten, dass Sie nicht vergessen, eine bestimmte statische Funktion zu implementieren, sondern das ordnungsgemäße Testen von Einheiten aller Ihrer Unterklassen kann dies auch während der Kompilierzeit abfangen (Sie könnten eine getIdentity() nicht testen, wenn sie nicht vorhanden ist).

Anmerkung: Betrachten Sie Ihren neuen Fragekommentar: Wenn Sie nach der Möglichkeit fragen, eine statische Methode mit einem Class<?> call aufzurufen, können Sie diese nicht (ohne Nachdenken) - aber Sie können die Funktionalität trotzdem erhalten wollen, wie in Giovanni Bottas Antwort beschrieben ; Sie werden Kompilierungsprüfungen für Laufzeitprüfungen opfern, können jedoch generische Algorithmen unter Verwendung von Identität schreiben. Es hängt also wirklich von Ihrem Endziel ab.

5
Jason C

Eine mathematische Gruppe hat nur eine charakteristische Operation, eine Java-Klasse kann jedoch eine beliebige Anzahl von Operationen haben. Daher stimmen diese beiden Konzepte nicht überein.

Ich kann mir so etwas wie eine Java-Klasse Group vorstellen, die aus einer Set von Elementen und einer bestimmten Operation besteht, die selbst eine Schnittstelle wäre. So etwas wie

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

Damit können Sie Ihre Gruppe aufbauen:

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

Ich weiß, das ist nicht ganz das, was Sie vorhatten, aber wie ich bereits gesagt habe, ist eine mathematische Gruppe ein etwas anderes Konzept als eine Klasse.

3
Ray

In Ihrer Argumentation kann es zu Missverständnissen kommen. Sie sehen eine mathematische "Gruppe", wie Sie sie definieren (wenn ich mich gut erinnern kann); Ihre Elemente sind jedoch nicht dadurch gekennzeichnet, dass sie zu dieser Gruppe gehören. Was ich damit meine, ist, dass eine ganze Zahl (oder reelle Zahl) eine eigenständige Einheit ist, die auch zur Gruppe XXX gehört (unter ihren anderen Eigenschaften).

Im Rahmen der Programmierung würde ich also die Definition (class) einer Gruppe von der ihrer Mitglieder trennen, wahrscheinlich mit Generika:

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

Noch mehr analytische Definition wäre:

/** 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
}

Das alles ist eine Idee. Ich habe Mathe schon seit geraumer Zeit nicht mehr angerührt, daher könnte ich mich irren.

Habe Spaß!

Es gibt keine Java-Methode, dies zu tun (Sie könnten in Scala möglicherweise so etwas tun), und alle Problemumgehungen basieren auf einigen Codierungskonventionen.

Die typische Art und Weise, wie dies in Java geschieht, besteht darin, dass Ihre Schnittstelle GroupElement zwei statische Methoden wie die folgende deklariert:

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 */ }

Sie können diese Methoden auf einfache Weise implementieren, indem Sie eine Klasse zur Instanzzuordnung oder eine selbst entwickelte Lösung Ihrer Wahl verwenden. Der Punkt ist, dass Sie eine statische Karte von Identitätselementen haben, eines für jede GroupElement-Implementierung.

Und hier kommt die Notwendigkeit einer Konvention: Jede Unterklasse von GroupElement muss ihr eigenes Identitätselement statisch deklarieren, z.

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

In der identity-Methode können Sie eine RuntimeException werfen, wenn die Identität nie registriert wurde. Dadurch erhalten Sie keine statische Überprüfung, aber zumindest die Laufzeitprüfung für Ihre GroupElement-Klassen.

Die Alternative dazu ist etwas ausführlicher und erfordert, dass Sie Ihre GroupElement-Klassen nur durch eine Factory instanziieren, die sich auch um die Rückgabe des Identitätselements (und anderer ähnlicher Objekte/Funktionen) kümmert:

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

Dies ist ein Muster, das normalerweise in Unternehmensanwendungen verwendet wird, wenn die Factory durch ein Abhängigkeitsinjektions-Framework (Guice, Spring) in der Anwendung injiziert wird. Es kann zu ausführlich sein, ist schwieriger zu warten und möglicherweise zu viel für Sie.

EDIT: Nachdem ich einige der anderen Antworten gelesen habe, stimme ich zu, dass Sie auf Gruppenebene modellieren sollten, nicht auf Gruppenelementebene, da Elementtypen von verschiedenen Gruppen gemeinsam genutzt werden könnten. Trotzdem bieten die obigen Antworten ein allgemeines Muster, um das von Ihnen beschriebene Verhalten durchzusetzen.

EDIT 2 : Unter "Codierungskonvention" verstehe ich die statische Methode getIdentity in jeder Unterklasse von GroupElement, wie von einigen bereits erwähnt. Dieser Ansatz hat den Nachteil, dass generische Algorithmen nicht gegen die Gruppe geschrieben werden können. Die beste Lösung dafür ist die im ersten Schnitt erwähnte.

2
Giovanni Botta

Wenn Sie die Fähigkeit benötigen, eine Identität zu generieren, deren Klasse zum Zeitpunkt des Kompilierens nicht bekannt ist, lautet die erste Frage: Woher wissen Sie zur Laufzeit, welche Klasse Sie möchten? Wenn die Klasse auf einem anderen Objekt basiert, denke ich, dass die sauberste Methode darin besteht, eine Methode in der Superklasse zu definieren, die "eine Identität erhalten, deren Klasse mit einem anderen Objekt identisch ist" bedeutet.

public GroupElement getIdentitySameClass();

Das müsste in jeder Unterklasse überschrieben werden. Die Überschreibung würde das Objekt wahrscheinlich nicht verwenden. Das Objekt würde nur verwendet, um den korrekten getIdentityfür den polymorphen Aufruf auszuwählen. Wahrscheinlich möchten Sie in jeder Klasse auch einen statischen getIdentity(aber der Compiler kann den Compiler nicht zwingen, einen zu schreiben), so dass der Code in der Unterklasse wahrscheinlich so aussieht

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

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

Wenn die von Ihnen benötigte Klasse jedoch aus einem Class<T>-Objekt stammt, müssen Sie die Reflektion verwenden, die mit getMethodNAME_ beginnt. Oder sehen Sie die Antwort von Giovanni, die meiner Meinung nach besser ist.

1
ajb

Wir sind uns alle einig, wenn Sie Gruppen implementieren möchten, benötigen Sie eine Gruppenschnittstelle und Klassen.

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

Wir implementieren die Gruppen als Singletons , so dass wir getIdentity statisch über instance zugreifen können.

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

wenn wir auch die Identität eines Gruppenelements abrufen müssen, würde ich Ihre GroupElement-Schnittstelle mit folgendem aktualisieren:

public Group<GroupElement> getGroup();

und GInteger mit:

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

und PosReal mit:

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

"Die Methode getIdentity hängt nicht von jeder Instanz des Objekts ab und sollte daher als statisch deklariert werden."

Wenn es nicht von irgendeiner Instanz abhängt, kann es nur einen konstanten Wert zurückgeben, es muss nicht statisch sein.

Nur weil eine statische Methode nicht von einer Instanz abhängt, heißt das nicht, dass Sie sie immer für diese Art von Situation verwenden sollten.

0
Leo