Génération de classes Java avec des paramètres de valeur au moment de la compilation

10

Considérez une situation où une classe implémente le même comportement de base, les mêmes méthodes, et cetera, mais plusieurs versions différentes de cette classe peuvent exister pour différentes utilisations. Dans mon cas particulier, j'ai un vecteur (un vecteur géométrique, pas une liste) et ce vecteur pourrait s'appliquer à n'importe quel espace euclidien à N dimensions (1 dimension, 2 dimensions, ...). Comment définir cette classe / type?

Ce serait facile en C ++ où les modèles de classe peuvent avoir des valeurs réelles comme paramètres, mais nous n'avons pas ce luxe en Java.

Les deux approches auxquelles je peux penser qui pourraient être adoptées pour résoudre ce problème sont les suivantes:

  1. Avoir une implémentation de chaque cas possible au moment de la compilation.

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

    Cette solution est évidemment très longue et extrêmement fastidieuse à coder. Dans cet exemple, cela ne semble pas trop mauvais, mais dans mon code actuel, je traite avec des vecteurs qui ont plusieurs implémentations chacun, avec jusqu'à quatre dimensions (x, y, z et w). J'ai actuellement plus de 2000 lignes de code, même si chaque vecteur n'a vraiment besoin que de 500.

  2. Spécification des paramètres lors de l'exécution.

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

    Malheureusement, cette solution nuit aux performances du code, entraîne un code plus désordonné que l'ancienne solution et n'est pas aussi sûre au moment de la compilation (il ne peut pas être garanti au moment de la compilation que le vecteur avec lequel vous traitez est en réalité bidimensionnel, par exemple).

Je suis actuellement en train de développer dans Xtend, donc si des solutions Xtend sont disponibles, elles seraient également acceptables.

Parker Hoyes
la source
Puisque vous utilisez Xtend, faites-vous cela dans le contexte d'une DSL Xtext?
Dan1701
2
Les DSL sont parfaits pour les applications de code-gen. En un mot, vous créez une petite grammaire de langage, une instance de ce langage (décrivant divers vecteurs, dans ce cas), et du code qui s'exécute lorsque l'instance est enregistrée (générant votre code Java). Il y a beaucoup de ressources et d'exemples sur le site Xtext .
Dan1701
2
Il existe une solution parfaite à ce problème en utilisant des types dépendants (c'est plus ou moins pour cela qu'ils ont été créés), mais hélas, ce n'est pas disponible en Java. J'irais avec la première solution si vous n'avez qu'un petit nombre fixe de classes (disons que vous n'utilisez que des vecteurs à 1, 2 et 3 dimensions), et la dernière solution pour plus que cela. Évidemment, je ne peux pas dire avec certitude sans exécuter votre code, mais je ne pense pas qu'il y aura un impact sur les performances qui vous inquiète
gardenhead
1
Ces deux classes n'ont pas la même interface, elles ne sont pas polymorphes mais vous essayez de les utiliser polymorphiquement.
Martin Spamer
1
Si vous écrivez des mathématiques d'algèbre linéaire et que vous êtes préoccupé par les performances, alors pourquoi java. Je ne vois rien d'autre que des problèmes là-dedans.
Sopel

Réponses:

1

Dans des cas comme celui-ci, j'utilise la génération de code.

J'écris une application java qui génère le code réel. De cette façon, vous pouvez facilement utiliser une boucle for pour générer un tas de versions différentes. J'utilise JavaPoet , ce qui facilite la création du code réel. Ensuite, vous pouvez intégrer l'exécution de la génération de code dans votre système de génération.

Winston Ewert
la source
0

J'ai un modèle très similaire sur mon application et notre solution était de simplement conserver une carte de taille dynamique, similaire à votre solution 2.

Vous n'aurez tout simplement pas à vous soucier des performances avec une primitive de tableau java comme celle-ci. Nous générons des matrices avec des tailles de limite supérieure de 100 colonnes (lire: 100 vecteurs dimensionnels) par 10 000 lignes, et nous avons eu de bonnes performances avec des types de vecteurs beaucoup plus complexes que votre solution 2. Vous pouvez essayer de sceller la classe ou de marquer les méthodes comme finales pour l'accélérer, mais je pense que vous optimisez prématurément.

Vous pouvez obtenir des économies de code (au détriment des performances) en créant une classe de base pour partager votre code:

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

Alors bien sûr, si vous êtes sur Java-8 +, vous pouvez utiliser des interfaces par défaut pour rendre cela encore plus serré:

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
}

Au-delà de cela, vous n'avez plus d'options avec la JVM. Vous pouvez bien sûr les écrire en C ++ et utiliser quelque chose comme JNA pour les relier - c'est notre solution pour certaines des opérations matricielles rapides, où nous utilisons fortran et MKL d'Intel - mais cela ne fera que ralentir les choses si vous écrivez simplement votre matrice en C ++ et appelez ses getters / setters depuis java.

Groostav
la source
Ma principale préoccupation n'est pas la performance, c'est la vérification à la compilation. J'aimerais vraiment une solution où la taille du vecteur et les opérations qui peuvent y être effectuées sont déterminées au moment de la compilation (comme avec les modèles C ++). Peut-être que votre solution est la meilleure si vous avez affaire à des matrices pouvant avoir jusqu'à 1000 composants, mais dans ce cas, je ne traite que des vecteurs d'une taille de 1 à 10.
Parker Hoyes
Si vous utilisez quelque chose comme la première ou la deuxième solution, vous pouvez créer ces sous-classes. Maintenant, je lis aussi sur Xtend, et cela ressemble un peu à Kotlin. Avec Kotlin, vous pouvez probablement utiliser les data classobjets pour créer facilement 10 sous-classes vectorielles. Avec java, en supposant que vous puissiez extraire toutes vos fonctionnalités dans la classe de base, chaque sous-classe prendra 1 à 10 lignes. Pourquoi ne pas créer une classe de base?
Groostav
L'exemple que j'ai donné est trop simplifié, mon code actuel a beaucoup de méthodes définies pour Vector telles que le produit vectoriel vectoriel, l'addition et la multiplication par composant, et cetera. Bien que je puisse les implémenter à l'aide d'une classe de base et de votre asArrayméthode, ces différentes méthodes ne seraient pas vérifiées au moment de la compilation (vous pourriez effectuer un produit scalaire entre un scalaire et un vecteur cartésien et cela se compilerait correctement, mais échouerait au moment de l'exécution) .
Parker Hoyes
0

Considérez une énumération avec chaque vecteur nommé ayant un constructeur composé d'un tableau (initialisé dans la liste des paramètres avec les noms de dimension ou similaire, ou peut-être juste un entier pour la taille ou un tableau de composants vide - votre conception), et un lambda pour la méthode getMagnitude. Vous pourriez demander à l'énumération d'implémenter une interface pour setComponents / getComponent (s), et simplement déterminer quel composant était lequel dans son utilisation, en éliminant getX, et al. Vous devrez initialiser chaque objet avec ses valeurs de composant réelles avant utilisation, en vérifiant éventuellement que la taille du tableau d'entrée correspond aux noms ou à la taille de dimension.

Ensuite, si vous étendez la solution à une autre dimension, vous modifiez simplement l'énumération et le lambda.

Kloder
la source
1
Veuillez fournir un court extrait de code illustrant votre solution.
Tulains Córdova
0

Sur la base de votre option 2, pourquoi ne pas simplement le faire? Si vous voulez empêcher l'utilisation de la base brute, vous pouvez la rendre abstraite:

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
la source
Ceci est similaire à la "méthode 2" dans ma question. Cependant, votre solution donne un moyen de garantir la sécurité des types au moment de la compilation, mais la surcharge de création d'un double[]est indésirable par rapport à une implémentation qui utilise simplement 2 primitives double. Dans un exemple aussi minimal que cela, cela ressemble à une microoptimisation, mais considérons un cas beaucoup plus complexe où beaucoup plus de métadonnées sont impliquées et le type en question a une courte durée de vie.
Parker Hoyes
1
Bon, comme il est dit, cela est basé sur la méthode 2. Sur la base de votre discussion avec Groostav en ce qui concerne sa réponse, j'ai eu l'impression que votre préoccupation n'était pas liée aux performances. Avez-vous quantifié cette surcharge, c'est-à-dire en créant 2 objets au lieu de 1? En ce qui concerne les courtes durées de vie, les machines virtuelles Java modernes sont optimisées pour ce cas et devraient avoir un coût GC inférieur (essentiellement 0) aux objets à durée de vie plus longue. Je ne sais pas comment les métadonnées entrent en jeu. Ces métadonnées sont-elles scalaires ou dimensionnelles?
JimmyJames
Le projet réel sur lequel je travaillais était un cadre géométrique à utiliser dans un rendu hyperdimensionnel. Cela signifie que je créais des objets beaucoup plus complexes que des vecteurs tels que les ellipsoïdes, les orthotopes et cetera et les transformations impliquaient généralement des matrices. La complexité du travail avec une géométrie dimensionnelle plus élevée rendait la sécurité de type pour la matrice et la taille du vecteur souhaitable alors qu'il existait toujours un désir important d'éviter autant que possible la création d'objets.
Parker Hoyes
Ce que je pense que je cherchais vraiment, c'était une solution plus automatisée qui produisait un bytecode similaire à la méthode 1, ce qui n'est pas vraiment possible en Java standard ou Xtend. Lorsque j'ai fini par le faire, j'utilisais la méthode 2, où les paramètres de taille de ces objets devaient être dynamiques au moment de l'exécution, et en créant fastidieusement des implémentations spécialisées plus efficaces pour les cas où ces paramètres étaient statiques. L'implémentation remplacerait le sur-type "dynamique" Vectorpar une implémentation plus spécialisée (par exemple Vector3) si sa durée de vie devait être relativement longue.
Parker Hoyes
0

Une idée:

  1. Un vecteur de classe de base abstraite fournissant des implémentations de dimension variable basées sur une méthode getComponent (i).
  2. Sous-classes individuelles Vector1, Vector2, Vector3, couvrant les cas typiques, remplaçant les méthodes Vector.
  3. Une sous-classe DynVector pour le cas général.
  4. Méthodes d'usine avec des listes d'arguments de longueur fixe pour les cas typiques, déclarées renvoyer Vector1, Vector2 ou Vector3.
  5. Une méthode d'usine var-args, déclarée renvoyer Vector, instanciant Vector1, Vector2, Vector3 ou DynVector, selon la longueur de l'arglist.

Cela vous donne de bonnes performances dans les cas typiques et une certaine sécurité à la compilation (peut encore être améliorée) sans sacrifier le cas général.

Squelette de code:

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
la source