Generieren von Java-Klassen mit Wertparametern zur Kompilierungszeit


10

Stellen Sie sich eine Situation vor, in der eine Klasse dasselbe grundlegende Verhalten, dieselben Methoden usw. implementiert, jedoch mehrere unterschiedliche Versionen dieser Klasse für unterschiedliche Verwendungszwecke vorhanden sein können. In meinem speziellen Fall habe ich einen Vektor (einen geometrischen Vektor, keine Liste) und dieser Vektor könnte auf jeden N-dimensionalen euklidischen Raum (1-dimensional, 2-dimensional, ...) angewendet werden. Wie kann diese Klasse / dieser Typ definiert werden?

Dies wäre in C ++ einfach, wo Klassenvorlagen tatsächliche Werte als Parameter haben können, aber wir haben diesen Luxus in Java nicht.

Die zwei Ansätze, die ich mir vorstellen kann, um dieses Problem zu lösen, sind:

  1. Implementierung jedes möglichen Falls zur Kompilierungszeit.

    public interface Vector {
        public double magnitude();
    }
    
    public class Vector1 implements Vector {
        public final double x;
        public Vector1(double x) {
            this.x = x;
        }
        @Override
        public double magnitude() {
            return x;
        }
        public double getX() {
            return x;
        }
    }
    
    public class Vector2 implements Vector {
        public final double x, y;
        public Vector2(double x, double y) {
            this.x = x;
            this.y = y;
        }
        @Override
        public double magnitude() {
            return Math.sqrt(x * x + y * y);
        }
        public double getX() {
            return x;
        }
        public double getY() {
            return y;
        }
    }

    Diese Lösung ist offensichtlich sehr zeitaufwändig und äußerst mühsam für den Code. In diesem Beispiel scheint es nicht schlecht zu sein, aber in meinem eigentlichen Code habe ich es mit Vektoren zu tun, die jeweils mehrere Implementierungen mit bis zu vier Dimensionen (x, y, z und w) haben. Ich habe derzeit über 2.000 Codezeilen, obwohl jeder Vektor wirklich nur 500 benötigt.

  2. Parameter zur Laufzeit angeben.

    public class Vector {
        private final double[] components;
        public Vector(double[] components) {
            this.components = components;
        }
        public int dimensions() {
            return components.length;
        }
        public double magnitude() {
            double sum = 0;
            for (double component : components) {
                sum += component * component;
            }
            return Math.sqrt(sum);
        }
        public double getComponent(int index) {
            return components[index];
        }
    }

    Leider beeinträchtigt diese Lösung die Codeleistung, führt zu unübersichtlichem Code als die vorherige Lösung und ist zur Kompilierungszeit nicht so sicher (es kann zur Kompilierungszeit nicht garantiert werden, dass der Vektor, mit dem Sie sich befassen, tatsächlich zweidimensional ist. zum Beispiel).

Ich entwickle derzeit in Xtend. Wenn also Xtend-Lösungen verfügbar sind, sind diese ebenfalls akzeptabel.


Tun Sie dies, da Sie Xtend verwenden, im Kontext eines Xtext-DSL?
Dan1701

2
DSLs eignen sich hervorragend für Code-Gen-Anwendungen. Kurz gesagt, Sie erstellen eine kleine Sprachgrammatik, eine Instanz dieser Sprache (in diesem Fall verschiedene Vektoren beschreibend) und einen Code, der ausgeführt wird, wenn die Instanz gespeichert wird (Generieren Ihres Java-Codes). Auf der Xtext-Site finden Sie zahlreiche Ressourcen und Beispiele .
Dan1701

2
Es gibt eine perfekte Lösung für dieses Problem mit abhängigen Typen (es ist mehr oder weniger das, wofür sie erstellt wurden), aber leider ist dies in Java nicht verfügbar. Ich würde mit der ersten Lösung gehen, wenn Sie nur eine kleine, feste Anzahl von Klassen haben (sagen wir, Sie verwenden nur 1-, 2- und 3-dimensionale Vektoren), und die letztere Lösung für mehr als das. Natürlich kann ich nicht sicher sagen, ohne Ihren Code auszuführen, aber ich glaube nicht, dass es die Auswirkungen auf die Leistung geben wird, über die Sie sich Sorgen machen
Gardenhead

1
Diese beiden Klassen haben nicht dieselbe Schnittstelle, sie sind nicht polymorph, aber Sie versuchen, sie polymorph zu verwenden.
Martin Spamer

1
Wenn Sie lineare Algebra-Mathematik schreiben und sich Gedanken über die Leistung machen, warum dann Java? Darin sehe ich nur Probleme.
Sopel

Antworten:


1

In solchen Fällen verwende ich die Codegenerierung.

Ich schreibe eine Java-Anwendung, die den eigentlichen Code generiert. Auf diese Weise können Sie einfach eine for-Schleife verwenden, um eine Reihe verschiedener Versionen zu generieren. Ich benutze JavaPoet , was es ziemlich einfach macht, den eigentlichen Code aufzubauen. Anschließend können Sie die Codegenerierung in Ihr Build-System integrieren.


0

Ich habe ein sehr ähnliches Modell in meiner Anwendung und unsere Lösung bestand darin, einfach eine Karte mit einer dynamischen Größe zu führen, ähnlich Ihrer Lösung 2.

Mit einem solchen Java-Array-Grundelement müssen Sie sich einfach keine Gedanken über die Leistung machen. Wir generieren Matrizen mit Obergrenzengrößen von 100 Spalten (gelesen: 100 dimensionale Vektoren) mit 10.000 Zeilen, und wir haben eine gute Leistung mit viel komplexeren Vektortypen als Ihre Lösung 2 erzielt. Sie könnten versuchen, die Klasse oder die Markierungsmethoden als endgültig zu versiegeln um es zu beschleunigen, aber ich denke, Sie optimieren vorzeitig.

Sie können Codeeinsparungen erzielen (auf Kosten der Leistung), indem Sie eine Basisklasse erstellen, um Ihren Code gemeinsam zu nutzen:

public interface Vector(){

    abstract class Abstract {           
        protected abstract double[] asArray();

        int dimensions(){ return asArray().length; }

        double magnitude(){ 
            double sum = 0;
            for (double component : asArray()) {
                sum += component * component;
            }
            return Math.sqrt(sum);
        }     

        //any additional behavior here   
    }
}

public class Scalar extends Vector.Abstract {
    private double x;

    public double getX(){
        return x;
    }

    @Override
    public double[] asArray(){
        return new double[]{x};
    }
}

public class Cartesian extends Vector.Abstract {

    public double x, y;

    public double getX(){ return x; }
    public double getY(){ return y; }

    @Override public double[] asArray(){ return new double[]{x, y}; }
}

Wenn Sie Java-8 + verwenden, können Sie natürlich standardmäßige Schnittstellen verwenden, um dies noch enger zu gestalten:

public interface Vector{

    default public double magnitude(){
        double sum = 0;
        for (double component : asArray()) {
            sum += component * component;
        }
        return Math.sqrt(sum);
    }

    default public int dimensions(){
        return asArray().length;
    }

    default double getComponent(int index){
        return asArray()[index];
    }

    double[] asArray();

    // giving up a little bit of static-safety in exchange for 
    // runtime exceptions, we can implement the getX(), getY() 
    // etc methods here, 
    // and simply have them throw if the dimensionality is too low 
    // (you can of course do this on the abstract-class strategy as well)

    //document or use checked-exceptions to indicate that these methods throw IndexOutOfBounds exceptions (or a wrapped version)

    default public getX(){
        return getComponent(0);
    }
    default public getY(){
        return getComponent(1);
    }
    //...


    }

    //as a general rule, defaulted interfaces should assume statelessness, 
    // so you want to avoid putting mutating operations 
    // as defaulted methods on an interface, since they'll only make your life harder
}

Darüber hinaus haben Sie mit der JVM keine Optionen mehr. Sie können sie natürlich in C ++ schreiben und sie mit JNA überbrücken - dies ist unsere Lösung für einige der schnellen Matrixoperationen, bei denen wir die MKL von fortran und Intel verwenden -, aber dies wird die Dinge nur verlangsamen, wenn Sie schreiben einfach Ihre Matrix in C ++ und rufen ihre Getter / Setter von Java aus auf.


Mein Hauptanliegen ist nicht die Leistung, sondern die Überprüfung der Kompilierungszeit. Ich hätte wirklich gerne eine Lösung, bei der die Größe des Vektors und die Operationen, die daran ausgeführt werden können, zur Kompilierungszeit bestimmt werden (wie bei C ++ - Vorlagen). Vielleicht ist Ihre Lösung am besten, wenn Sie mit Matrizen arbeiten, die bis zu 1000 Komponenten groß sein können, aber in diesem Fall habe ich es nur mit Vektoren mit einer Größe von 1 - 10 zu tun.
Parker Hoyes

Wenn Sie so etwas wie die erste oder zweite Lösung verwenden, können Sie diese Unterklassen erstellen. Jetzt lese ich auch nur auf Xtend und es scheint ein bisschen wie Kotlin. Mit Kotlin können Sie die Objekte wahrscheinlich verwenden data class, um auf einfache Weise 10 Vektorunterklassen zu erstellen. Unter der Annahme, dass Sie mit Java alle Ihre Funktionen in die Basisklasse ziehen können, benötigt jede Unterklasse 1-10 Zeilen. Warum nicht eine Basisklasse erstellen?
Groostav

Das Beispiel, das ich bereitgestellt habe, ist zu stark vereinfacht. In meinem eigentlichen Code sind viele Methoden für den Vektor definiert, z. B. das Vektorpunktprodukt, die komponentenweise Addition und Multiplikation usw. Obwohl ich diese mithilfe einer Basisklasse und Ihrer asArrayMethode implementieren könnte, würden diese verschiedenen Methoden zur Kompilierungszeit nicht überprüft (Sie könnten ein Punktprodukt zwischen einem Skalar und einem kartesischen Vektor ausführen und es würde gut kompilieren, aber zur Laufzeit fehlschlagen). .
Parker Hoyes

0

Stellen Sie sich eine Aufzählung vor, bei der jeder benannte Vektor einen Konstruktor hat, der aus einem Array (initialisiert in der Parameterliste mit den Dimensionsnamen oder ähnlichem oder vielleicht nur einer Ganzzahl für die Größe oder einem leeren Komponentenarray - Ihrem Design) und einem Lambda für besteht die getMagnitude-Methode. Sie könnten die Aufzählung auch eine Schnittstelle für setComponents / getComponent (s) implementieren lassen und einfach feststellen, welche Komponente welche in ihrer Verwendung war, wodurch getX et al. Sie müssen jedes Objekt vor der Verwendung mit seinen tatsächlichen Komponentenwerten initialisieren und möglicherweise überprüfen, ob die Größe des Eingabearrays mit den Dimensionsnamen oder der Größe übereinstimmt.

Wenn Sie die Lösung dann auf eine andere Dimension erweitern, ändern Sie einfach die Aufzählung und das Lambda.


1
Bitte geben Sie einen Kurzcode-Ausschnitt Ihrer Lösung an.
Tulains Córdova

0

Warum nicht einfach basierend auf Ihrer Option 2? Wenn Sie die Verwendung der Rohdatenbasis verhindern möchten, können Sie sie abstrakt gestalten:

class Vector2 extends Vector
{
  public Vector2(double x, double y) {
    super(new double[]{x,y});
  }

  public double getX() {
    return getComponent(0);
  }

  public double getY() {
    return getComponent(1);
  }
}

Dies ähnelt "Methode 2" in meiner Frage. Ihre Lösung bietet jedoch eine Möglichkeit, die Typensicherheit beim Kompilieren zu gewährleisten. Der Aufwand für das Erstellen eines double[]ist jedoch im Vergleich zu einer Implementierung, bei der lediglich zwei Grundelemente verwendet werden, unerwünscht double. In einem so minimalen Beispiel scheint dies eine Mikrooptimierung zu sein, aber betrachten Sie einen viel komplexeren Fall, in dem viel mehr Metadaten beteiligt sind und der betreffende Typ eine kurze Lebensdauer hat.
Parker Hoyes

1
Richtig, wie es heißt, basiert dies auf Methode 2. Aufgrund Ihrer Diskussion mit Groostav über seine Antwort hatte ich den Eindruck, dass Ihr Anliegen nicht die Leistung war. Haben Sie diesen Overhead quantifiziert, dh 2 Objekte anstelle von 1 erstellt? Für kurze Lebensdauern sind moderne JVMs für diesen Fall optimiert und sollten niedrigere GC-Kosten (im Grunde 0) haben als länger lebende Objekte. Ich bin mir nicht sicher, wie die Metadaten dazu beitragen. Sind diese Metadaten skalar oder dimensional?
JimmyJames

Das eigentliche Projekt, an dem ich arbeitete, war ein Geometrie-Framework, das in einem hyperdimensionalen Renderer verwendet werden sollte. Dies bedeutet, dass ich viel komplexere Objekte als Vektoren wie Ellipsoide, Orthotope usw. erstellt habe und Transformationen normalerweise Matrizen beinhalten. Die Komplexität der Arbeit mit höherdimensionaler Geometrie machte die Typensicherheit für die Matrix- und Vektorgröße wünschenswert, während immer noch ein erheblicher Wunsch bestand, die Erzeugung von Objekten so weit wie möglich zu vermeiden.
Parker Hoyes

Was ich wirklich gesucht habe, war eine automatisiertere Lösung, die Bytecode ähnlich Methode 1 erzeugt, was in Standard-Java oder Xtend nicht wirklich möglich ist. Am Ende habe ich Methode 2 verwendet, bei der die Größenparameter dieser Objekte zur Laufzeit dynamisch sein mussten, und mühsam effizientere, spezialisierte Implementierungen für Fälle erstellt, in denen diese Parameter statisch waren. Die Implementierung würde den "dynamischen" Supertyp Vectordurch eine spezialisiertere Implementierung ersetzen (z. B. Vector3), wenn seine Lebensdauer relativ lang sein sollte.
Parker Hoyes

0

Eine Idee:

  1. Ein abstrakter Basisklassenvektor, der Implementierungen mit variablen Dimensionen basierend auf einer getComponent (i) -Methode bereitstellt.
  2. Einzelne Unterklassen Vector1, Vector2, Vector3, die die typischen Fälle abdecken und die Vector-Methoden überschreiben.
  3. Eine DynVector-Unterklasse für den allgemeinen Fall.
  4. Factory-Methoden mit Argumentlisten fester Länge für die typischen Fälle, die als Vector1, Vector2 oder Vector3 zurückgegeben werden.
  5. Eine var-args-Factory-Methode, die deklariert ist, um Vector zurückzugeben, und Vector1, Vector2, Vector3 oder DynVector instanziiert, abhängig von der Länge der Arglist.

Dies bietet Ihnen in typischen Fällen eine gute Leistung und eine gewisse Sicherheit während der Kompilierung (kann noch verbessert werden), ohne den allgemeinen Fall zu beeinträchtigen.

Code-Skelett:

public abstract class Vector {
    protected abstract int dimension();
    protected abstract double getComponent(int i);
    protected abstract void setComponent(int i, double value);

    public double magnitude() {
        double sum = 0.0;
        for (int i=0; i<dimension(); i++) {
            sum += getComponent(i) * getComponent(i);
        }
        return Math.sqrt(sum);
    }

    public void add(Vector other) {
        for (int i=0; i<dimension(); i++) {
            setComponent(i, getComponent(i) + other.getComponent(i));
        }
    }

    public static Vector1 create(double x) {
        return new Vector1(x);
    }

    public static Vector create(double... values) {
        switch(values.length) {
        case 1:
            return new Vector1(values[0]);
        default:
            return new DynVector(values);
        }

    }
}

class Vector1 extends Vector {
    private double x;

    public Vector1(double x) {
        super();
        this.x = x;
    }

    @Override
    public double magnitude() {
        return Math.abs(x);
    }

    @Override
    protected int dimension() {
        return 1;
    }

    @Override
    protected double getComponent(int i) {
        return x;
    }

    @Override
    protected void setComponent(int i, double value) {
        x = value;
    }

    @Override
    public void add(Vector other) {
        x += ((Vector1) other).x;
    }

    public void add(Vector1 other) {
        x += other.x;
    }
}

class DynVector extends Vector {
    private double[] values;
    public DynVector(double[] values) {
        this.values = values;
    }

    @Override
    protected int dimension() {
        return values.length;
    }

    @Override
    protected double getComponent(int i) {
        return values[i];
    }

    @Override
    protected void setComponent(int i, double value) {
        values[i] = value;
    }

}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.