Generando clases Java con parámetros de valor en tiempo de compilación

10

Considere una situación en la que una clase implementa el mismo comportamiento básico, métodos, etc., pero podrían existir múltiples versiones diferentes de esa clase para diferentes usos. En mi caso particular, tengo un vector (un vector geométrico, no una lista) y ese vector podría aplicarse a cualquier espacio euclidiano N-dimensional (1 dimensional, 2 dimensional, ...). ¿Cómo se puede definir esta clase / tipo?

Esto sería fácil en C ++, donde las plantillas de clase pueden tener valores reales como parámetros, pero no tenemos ese lujo en Java.

Los dos enfoques que se me ocurren para resolver este problema son:

  1. Tener una implementación de cada caso posible en tiempo de compilación.

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

    Obviamente, esta solución consume mucho tiempo y es extremadamente tediosa para el código. En este ejemplo, no parece tan malo, pero en mi código real estoy tratando con vectores que tienen múltiples implementaciones cada uno, con hasta cuatro dimensiones (x, y, zyw). Actualmente tengo más de 2.000 líneas de código, aunque cada vector realmente solo necesita 500.

  2. Especificar parámetros en tiempo de ejecución.

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

    Desafortunadamente, esta solución perjudica el rendimiento del código, da como resultado un código más desordenado que la solución anterior y no es tan seguro en el momento de la compilación (no se puede garantizar en el momento de la compilación que el vector con el que está tratando sea realmente bidimensional, por ejemplo).

Actualmente estoy desarrollando en Xtend, por lo que si hay soluciones Xtend disponibles, también serían aceptables.

Parker Hoyes
fuente
Dado que está utilizando Xtend, ¿lo está haciendo en el contexto de un DSL Xtext?
Dan1701
2
Las DSL son excelentes para aplicaciones de código gen. En pocas palabras, crea una pequeña gramática del lenguaje, una instancia de ese lenguaje (que describe varios vectores, en este caso) y un código que se ejecuta cuando se guarda la instancia (generando su código Java). Hay muchos recursos y ejemplos en el sitio Xtext .
Dan1701
2
Hay una solución perfecta para este problema utilizando tipos dependientes (es más o menos para lo que fueron creados), pero desafortunadamente eso no está disponible en Java. Iría con la primera solución si solo tiene un número pequeño y fijo de clases (digamos que solo usa vectores de 1, 2 y 3 dimensiones), y la última solución para más que eso. Obviamente, no puedo decir con certeza sin ejecutar el código, pero no creo que no habrá impacto en el rendimiento que te preocupa
gardenhead
1
Esas dos clases no tienen la misma interfaz, no son polimórficas, pero está tratando de usarlas polimórficamente.
Martin Spamer
1
Si está escribiendo matemáticas de álgebra lineal y le preocupa el rendimiento, entonces por qué Java. No puedo ver nada más que problemas en eso.
Sopel

Respuestas:

1

En casos como este, uso la generación de código.

Escribo una aplicación Java que genera el código real. De esa manera, puede usar fácilmente un bucle for para generar un montón de versiones diferentes. Yo uso JavaPoet , lo que hace que sea bastante sencillo construir el código real. Luego puede integrar la ejecución de la generación de código en su sistema de compilación.

Winston Ewert
fuente
0

Tengo un modelo muy similar en mi aplicación y nuestra solución fue simplemente mantener un mapa de un tamaño dinámico, similar a su solución 2.

Simplemente no tendrá que preocuparse por el rendimiento con un primitivo java array como ese. Generamos matrices con tamaños de límite superior de 100 columnas (léase: 100 vectores dimensionales) por 10.000 filas, y hemos tenido un buen rendimiento con tipos de vectores mucho más complejos que su solución 2. Puede intentar sellar la clase o los métodos de marcado como final acelerarlo, pero creo que estás optimizando prematuramente.

Puede obtener algunos ahorros en el código (a costa del rendimiento) creando una clase base para compartir su código:

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

Entonces, por supuesto, si está en Java-8 +, puede usar interfaces predeterminadas para hacer esto aún más estricto:

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
}

En última instancia, más allá de eso, no tiene opciones con la JVM. Por supuesto, puede escribirlos en C ++ y usar algo como JNA para unirlos; esta es nuestra solución para algunas de las operaciones de matriz rápida, donde usamos fortran e MKL de inteligencia, pero esto solo ralentizará las cosas si simplemente escribe tu matriz en C ++ y llama a sus captadores / establecedores desde java.

Groostav
fuente
Mi principal preocupación no es el rendimiento, es la verificación en tiempo de compilación. Realmente me gustaría una solución donde el tamaño del vector y las operaciones que se pueden realizar en él se determinan en tiempo de compilación (como con las plantillas de C ++). Quizás su solución sea mejor si se trata de matrices que pueden tener un tamaño de hasta 1000 componentes, pero en este caso solo estoy tratando con vectores con un tamaño de 1 a 10.
Parker Hoyes
Si usa algo como la primera o segunda solución, puede crear esas subclases. Ahora también estoy leyendo en Xtend, y parece un poco como Kotlin. Con Kotlin, probablemente pueda usar los data classobjetos para crear fácilmente 10 subclases de vectores. Con Java, suponiendo que pueda incorporar toda su funcionalidad a la clase base, cada subclase tomará entre 1 y 10 líneas. ¿Por qué no crear una clase base?
Groostav
El ejemplo que proporcioné está simplificado en exceso, mi código real tiene muchos métodos definidos para Vector, como el producto de puntos vectoriales, la adición y multiplicación por componentes, etc. Aunque podría implementarlos usando una clase base y su asArraymétodo, esos diversos métodos no se verificarían en el momento de la compilación (podría realizar un producto punto entre un vector escalar y un vector cartesiano y se compilaría bien, pero fallará en el tiempo de ejecución) .
Parker Hoyes
0

Considere una enumeración con cada Vector nombrado que tenga un constructor que consista en una matriz (inicializada en la lista de parámetros con los nombres de dimensión o similar, o tal vez solo un número entero para el tamaño o una matriz de componentes vacía, su diseño) y una lambda para El método getMagnitude. Podría hacer que la enumeración también implemente una interfaz para setComponents / getComponent (s), y simplemente establezca qué componente era cuál en su uso, eliminando getX, et al. Debería inicializar cada objeto con sus valores de componentes reales antes de su uso, posiblemente comprobando que el tamaño de la matriz de entrada coincida con los nombres o el tamaño de la dimensión.

Luego, si extiende la solución a otra dimensión, simplemente modifica la enumeración y lambda.

Kloder
fuente
1
Por favor, proporcione un fragmento de código breve para ilustrar su solución.
Tulains Córdova
0

Según su opción 2, ¿por qué no simplemente hacer esto? Si desea evitar el uso de la base en bruto, puede hacerla abstracta:

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);
  }
}
JimmyJames
fuente
Esto es similar al "método 2" en mi pregunta. Sin embargo, su solución ofrece una forma de garantizar la seguridad de los tipos en tiempo de compilación, sin embargo, la sobrecarga de crear un double[]archivo no es deseable en comparación con una implementación que simplemente usa 2 primitivas double. En un ejemplo tan mínimo como este, parece una microoptimización, pero considere un caso mucho más complejo en el que intervienen muchos más metadatos y el tipo en cuestión tiene una vida útil corta.
Parker Hoyes
1
Correcto, como dice, esto se basa en el método 2. Basado en su discusión con Groostav con respecto a su respuesta, tuve la impresión de que su preocupación no era con el rendimiento. ¿Ha cuantificado esta sobrecarga, es decir, creando 2 objetos en lugar de 1? En cuanto a la vida útil corta, las JVM modernas están optimizadas para este caso y deberían tener un costo de GC más bajo (básicamente 0) que los objetos de vida más larga. No estoy seguro de cómo juegan los metadatos en esto. ¿Es este metadato escalar o dimensional?
JimmyJames
El proyecto real en el que estaba trabajando era un marco de geometría para ser utilizado en un renderizador hiperdimensional. Esto significa que estaba creando objetos mucho más complejos que vectores como elipsoides, ortótopos, etc. y las transformaciones generalmente involucraban matrices. La complejidad de trabajar con geometría de dimensiones superiores hizo que la seguridad de tipografía para la matriz y el tamaño del vector fuera deseable, mientras que todavía había un deseo significativo de evitar la creación de objetos tanto como fuera posible.
Parker Hoyes
Lo que creo que realmente estaba buscando era una solución más automatizada que produjera un código de bytes similar al método 1, que en realidad no es posible en Java o Xtend estándar. Cuando terminé, estaba usando el método 2, donde los parámetros de tamaño de estos objetos debían ser dinámicos en tiempo de ejecución, y creando tediosamente implementaciones más eficientes y especializadas para los casos en que estos parámetros eran estáticos. La implementación reemplazaría el supertipo "dinámico" Vectorcon una implementación más especializada (por ejemplo Vector3) si su vida útil fuera relativamente larga.
Parker Hoyes
0

Una idea:

  1. Un vector de clase base abstracto que proporciona implementaciones de dimensión variable basadas en un método getComponent (i).
  2. Subclases individuales Vector1, Vector2, Vector3, que cubren los casos típicos, anulando los métodos de Vector.
  3. Una subclase de DynVector para el caso general.
  4. Métodos de fábrica con listas de argumentos de longitud fija para los casos típicos, declarados para devolver Vector1, Vector2 o Vector3.
  5. Un método de fábrica var-args, declarado para devolver Vector, instanciando Vector1, Vector2, Vector3 o DynVector, dependiendo de la longitud del arglista.

Esto le brinda un buen rendimiento en casos típicos y algo de seguridad en tiempo de compilación (aún se puede mejorar) sin sacrificar el caso general.

Esqueleto de código:

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

}
Ralf Kleberhoff
fuente