Generazione di classi Java con parametri valore Compile-time


10

Considera una situazione in cui una classe implementa lo stesso comportamento di base, metodi, eccetera, ma potrebbero esistere più versioni diverse di quella classe per usi diversi. Nel mio caso particolare, ho un vettore (un vettore geometrico, non un elenco) e quel vettore potrebbe applicarsi a qualsiasi spazio euclideo N-dimensionale (1 dimensionale, 2 dimensionale, ...). Come può essere definita questa classe / tipo?

Ciò sarebbe facile in C ++ in cui i modelli di classe possono avere valori reali come parametri, ma non abbiamo quel lusso in Java.

I due approcci che posso pensare che potrebbero essere adottati per risolvere questo problema sono:

  1. Avere un'implementazione di ogni possibile caso al momento della compilazione.

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

    Questa soluzione richiede ovviamente molto tempo ed è estremamente noiosa da programmare. In questo esempio non sembra troppo male, ma nel mio codice attuale ho a che fare con vettori che hanno più implementazioni ciascuno, con un massimo di quattro dimensioni (x, y, z e w). Al momento ho oltre 2.000 righe di codice, anche se ogni vettore ne ha solo 500.

  2. Specifica dei parametri in fase di esecuzione.

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

    Sfortunatamente questa soluzione danneggia le prestazioni del codice, si traduce in un codice più disordinato rispetto alla soluzione precedente e non è così sicura in fase di compilazione (non è possibile garantire in fase di compilazione che il vettore con cui si ha a che fare sia effettivamente bidimensionale, per esempio).

Attualmente sto sviluppando in Xtend, quindi se sono disponibili soluzioni Xtend, sarebbero anche accettabili.


Dato che stai usando Xtend, lo stai facendo nel contesto di un DSL Xtext?
Dan1701,

2
I DSL sono ottimi per le applicazioni code-gen. In breve, si crea una piccola grammatica linguistica, un'istanza di quella lingua (che descrive vari vettori, in questo caso) e un codice che viene eseguito quando l'istanza viene salvata (generando il proprio codice Java). Ci sono molte risorse ed esempi sul sito Xtext .
Dan1701,

2
C'è una soluzione perfetta a questo problema usando tipi dipendenti (è più o meno quello per cui sono stati creati), ma purtroppo non è disponibile in Java. Andrei con la prima soluzione se avessi solo un piccolo numero fisso di classi (diciamo che usi solo vettori 1-, 2- e tridimensionali), e quest'ultima soluzione per di più. Ovviamente non posso dirlo con certezza senza eseguire il codice, ma non credo che ci sarà l'impatto sulle prestazioni di cui sei preoccupato
gardenhead,

1
Queste due classi non hanno la stessa interfaccia, non sono polimorfiche ma stai cercando di usarle polimorficamente.
Martin Spamer,

1
Se stai scrivendo matematica di algebra lineare e sei preoccupato per le prestazioni, allora perché Java. Non riesco a vedere altro che problemi in questo.
Sopel,

Risposte:


1

In casi come questo, utilizzo la generazione di codice.

Scrivo un'applicazione Java che genera il codice effettivo. In questo modo puoi facilmente usare un ciclo for per generare un mucchio di versioni diverse. Uso JavaPoet , il che rende abbastanza semplice costruire il codice attuale. Quindi è possibile integrare l'esecuzione della generazione del codice nel proprio sistema di generazione.


0

Ho un modello molto simile sulla mia applicazione e la nostra soluzione era semplicemente quella di mantenere una mappa di dimensioni dinamiche, simile alla tua soluzione 2.

Semplicemente non dovrai preoccuparti delle prestazioni con una primitiva di array Java come quella. Generiamo matrici con dimensioni del limite superiore di 100 colonne (leggi: 100 vettori dimensionali) per 10.000 righe e abbiamo avuto buone prestazioni con tipi di vettore molto più complessi della tua soluzione 2. Potresti provare a sigillare la classe o i metodi di marcatura come finali per accelerarlo, ma penso che tu stia ottimizzando prematuramente.

Puoi ottenere alcuni risparmi di codice (a scapito delle prestazioni) creando una classe base per condividere il tuo codice:

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

Quindi, naturalmente, se utilizzi Java-8 +, puoi utilizzare interfacce predefinite per rendere ancora più stretto:

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
}

In definitiva, con JVM non hai più opzioni. Puoi ovviamente scriverli in C ++ e usare qualcosa come JNA per collegarli in - questa è la nostra soluzione per alcune delle operazioni a matrice veloce, in cui usiamo fortran e MKL di Intel - ma questo rallenterà le cose solo se scrivi semplicemente la tua matrice in C ++ e chiami i suoi getter / setter da java.


La mia preoccupazione principale non sono le prestazioni, è il controllo in fase di compilazione. Vorrei davvero una soluzione in cui le dimensioni del vettore e le operazioni che possono essere eseguite su di esso sono determinate in fase di compilazione (come con i modelli C ++). Forse la tua soluzione è la migliore se hai a che fare con matrici che potrebbero avere dimensioni fino a 1000 componenti, ma in questo caso ho a che fare solo con vettori con dimensioni da 1 a 10.
Parker Hoyes

Se usi qualcosa come la prima o la seconda soluzione, puoi creare quelle sottoclassi. Adesso sto solo leggendo su Xtend, e sembra un po 'come Kotlin. Con Kotlin, puoi probabilmente usare gli data classoggetti per creare facilmente 10 sottoclassi vettoriali. Con java, supponendo che tu possa inserire tutte le tue funzionalità nella classe base, ogni sottoclasse prenderà 1-10 righe. Perché non creare una classe base?
Groostav,

L'esempio che ho fornito è semplificato, il mio codice attuale ha molti metodi definiti per Vector come il prodotto dot vector, l'aggiunta e la moltiplicazione dei componenti, eccetera. Anche se potrei implementarli usando una classe di base e il tuo asArraymetodo, quei vari metodi non verrebbero controllati in fase di compilazione (potresti eseguire un prodotto punto tra un vettore scalare e un vettore cartesiano e si comporterebbe bene, ma fallire in fase di esecuzione) .
Parker Hoyes,

0

Prendi in considerazione un enum con ogni vettore denominato con un costruttore costituito da un array (inizializzato nell'elenco dei parametri con i nomi delle dimensioni o simili, o forse solo un numero intero per la dimensione o un array di componenti vuoto - il tuo progetto) e un lambda per il metodo getMagnitude. Potresti avere l'enum anche implementare un'interfaccia per setComponents / getComponent (s), e stabilire semplicemente quale componente era quale nel suo utilizzo, eliminando getX, et al. Prima dell'uso è necessario inizializzare ciascun oggetto con i valori effettivi dei componenti, verificando eventualmente che la dimensione dell'array di input corrisponda ai nomi o alle dimensioni della dimensione.

Quindi se estendi la soluzione ad un'altra dimensione, modifichi semplicemente enum e lambda.


1
Fornisci un frammento di codice breve illustrazione della tua soluzione.
Tulains Córdova,

0

In base alla tua opzione 2, perché non farlo semplicemente? Se vuoi impedire l'uso della base grezza puoi renderlo astratto:

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

Questo è simile al "metodo 2" nella mia domanda. Tuttavia, la soluzione offre un modo per garantire la sicurezza dei tipi in fase di compilazione, tuttavia l'overhead della creazione di a double[]è indesiderabile rispetto a un'implementazione che utilizza semplicemente 2 primitive double. In un esempio minimale come questo sembra una microottimizzazione, ma considera un caso molto più complesso in cui sono coinvolti molti più metadati e il tipo in questione ha una durata breve.
Parker Hoyes,

1
Giusto, come si dice, questo si basa sul metodo 2. Sulla base della tua discussione con Groostav in merito alla sua risposta, ho avuto l'impressione che la tua preoccupazione non fosse per le prestazioni. Hai quantificato questo sovraccarico, ovvero creando 2 oggetti anziché 1? Per quanto riguarda le brevi durate, le moderne JVM sono ottimizzate per questo caso e dovrebbero avere un costo GC inferiore (sostanzialmente 0) rispetto agli oggetti più longevi. Non sono sicuro di come giocano i metadati in questo. Questi metadati sono scalari o dimensionali?
JimmyJames,

Il vero progetto a cui stavo lavorando era una struttura geometrica da utilizzare in un renderizzatore iperdimensionale. Ciò significa che stavo creando oggetti molto più complessi rispetto a vettori come ellissoidi, ortotopi eccetera e le trasformazioni di solito coinvolgono matrici. La complessità di lavorare con geometria dimensionale superiore ha reso desiderabile la sicurezza del tipo per matrice e dimensione del vettore, mentre c'era ancora un desiderio significativo di evitare la creazione di oggetti il ​​più possibile.
Parker Hoyes,

Quello che penso di essere veramente alla ricerca era una soluzione più automatizzata che producesse bytecode simile al metodo 1, che non è realmente possibile in Java o Xtend standard. Quando ho finito, ho usato il metodo 2 in cui i parametri di dimensione di questi oggetti dovevano essere dinamici in fase di esecuzione e creavano noiosamente implementazioni più efficienti e specializzate per i casi in cui questi parametri erano statici. L'implementazione sostituirà il supertipo "dinamico" Vectorcon un'implementazione più specializzata (ad esempio Vector3) se la sua durata dovesse essere relativamente lunga.
Parker Hoyes,

0

Un'idea:

  1. Un vettore di classe base astratto che fornisce implementazioni a dimensione variabile basate su un metodo getComponent (i).
  2. Sottoclassi individuali Vector1, Vector2, Vector3, che coprono i casi tipici, ignorando i metodi Vector.
  3. Una sottoclasse DynVector per il caso generale.
  4. Metodi di fabbrica con elenchi di argomenti a lunghezza fissa per i casi tipici, dichiarati per restituire Vector1, Vector2 o Vector3.
  5. Un metodo factory var-args, dichiarato per restituire Vector, istanziando Vector1, Vector2, Vector3 o DynVector, a seconda della lunghezza dell'arglist.

Ciò offre buone prestazioni in casi tipici e un po 'di sicurezza in fase di compilazione (può ancora essere migliorata) senza sacrificare il caso generale.

Scheletro di codice:

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

}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.