Генерування Java-класів за допомогою параметрів значення компіляційного часу


10

Розглянемо ситуацію, коли клас реалізує одне і те ж основне поведінку, методи тощо, але може існувати кілька різних версій цього класу для різних цілей. У моєму конкретному випадку у мене є вектор (геометричний вектор, а не список), і цей вектор може застосовуватися до будь-якого N-мірного евклідового простору (1 мірний, 2 мірний, ...). Як можна визначити цей клас / тип?

Це легко в C ++, де шаблони класів можуть мати фактичні значення в якості параметрів, але у нас немає такої розкоші в Java.

Для вирішення цієї проблеми я можу скористатися двома підходами:

  1. Здійснення реалізації кожного можливого випадку під час компіляції.

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

    Це рішення, очевидно, дуже трудомістке і надзвичайно виснажливе кодування. У цьому прикладі це не здається занадто поганим, але в моєму фактичному коді я маю справу з векторами, які мають кілька реалізацій у кожному, що мають до чотирьох вимірів (x, y, z та w). Наразі у мене понад 2000 рядків коду, хоча кожному вектору дійсно потрібно 500.

  2. Визначення параметрів під час виконання.

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

    На жаль, це рішення шкодить продуктивності коду, приводить до коду messier, ніж попереднє рішення, і не є таким безпечним під час компіляції (не можна гарантувати під час компіляції, що вектор, з яким ви маєте справу, є двовимірним, наприклад).

Наразі я фактично розвиваюся в Xtend, тому, якщо є якісь рішення Xtend, вони також були б прийнятними.


Оскільки ви використовуєте Xtend, чи робите ви це в контексті Xtext DSL?
Dan1701

2
DSL-програми чудово підходять для програм кодового типу. У двох словах, ви створюєте невелику граматику мови, екземпляр цієї мови (описуючи в цьому випадку різні вектори) та деякий код, який виконується при збереженні екземпляра (генеруючи ваш код Java). На сайті Xtext є багато ресурсів та прикладів .
Dan1701

2
Існує ідеальне рішення цієї проблеми з використанням залежних типів (це більш-менш те, для чого вони були створені), але, на жаль, це не доступно на Java. Я б пішов з першим рішенням, якщо у вас є лише невелика, фіксована кількість класів (скажімо, ви використовуєте лише 1-, 2- і 3-мірні вектори), а останнє рішення - більше. Очевидно, я не можу сказати напевно, не запустивши свій код, але я не думаю, що буде вплив на продуктивність, який ви хвилюєте
gardenhead

1
Ці два класи не мають однакового інтерфейсу, вони не є поліморфними, але ви намагаєтесь їх використовувати поліморфно.
Мартін Спамер

1
Якщо ви пишете лінійну математику з алгебри і переймаєтесь ефективністю, то чому ява. Я не бачу в цьому нічого, крім проблем.
Сопель

Відповіді:


1

У таких випадках я використовую генерацію коду.

Я пишу заявку java, яка генерує фактичний код. Таким чином, ви можете легко використовувати цикл for, щоб створити купу різних версій. Я використовую JavaPoet , що робить його досить простим для створення фактичного коду. Тоді ви можете інтегрувати запуск генерації коду у свою систему збирання.


0

У мене в програмі дуже схожа модель, і наше рішення полягало в тому, щоб просто зберегти карту динамічного розміру, подібну до вашого рішення 2.

Вам просто не потрібно буде турбуватися про продуктивність з таким примітивним масивом java. Ми генеруємо матриці з верхньою межею розмірів 100 стовпців (читайте: 100 розмірних векторів) на 10 000 рядків, і ми мали хороші показники з набагато складнішими типами векторів, ніж ваше рішення 2. Ви можете спробувати запечатати клас або методи маркування як остаточні щоб пришвидшити це, але я думаю, ви передчасно оптимізуєте.

Ви можете отримати економію коду (за рахунок продуктивності), створивши базовий клас для спільного використання коду:

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

Тоді, звичайно, якщо ви перебуваєте на Java-8 +, ви можете використовувати інтерфейси за умовчанням, щоб зробити це ще більш жорстким:

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
}

Зрештою, поза цим вам не вистачає варіантів із JVM. Звичайно, ви можете написати їх на C ++ і використовувати щось на зразок JNA, щоб переадресувати їх - це наше рішення для деяких операцій з швидкою матрицею, де ми використовуємо фортран і інтелект MKL-- але це лише уповільнить справи, якщо ви просто пишете свою матрицю на C ++ і називаєте її getters / setters з Java.


Моя головна проблема - це не ефективність, це перевірка часу збирання. Мені б дуже хотілося рішення, де розмір вектора та операції, які можна виконувати на ньому, визначаються під час компіляції (як, наприклад, із шаблонами C ++). Можливо, ваше рішення найкраще, якщо ви маєте справу з матрицями, які можуть мати розмір до 1000 компонентів, але в цьому випадку я маю справу лише з векторами розміром від 1 до 10.
Parker Hoyes

Якщо ви використовуєте щось на зразок першого чи другого рішення, ви можете створити ці підкласи. Зараз я також просто читаю на Xtend, і, здається, ярмарок, як Котлін. З Котліном ви, ймовірно, можете використовувати data classоб'єкти, щоб легко створити 10 векторних підкласів. З java, якщо припустити, що ви можете перетягнути всю свою функціональність у базовий клас, кожен підклас займе 1-10 рядків. Чому б не створити базовий клас?
Гроостав

Наведений я приклад спрощений, мій фактичний код має безліч методів, визначених для Vector, таких як векторний крапковий продукт, додавання та множення з точки зору компонентів та ін. Хоча я міг би реалізувати їх за допомогою базового класу та вашого asArrayметоду, ці різні методи не перевірялись би під час компіляції (ви можете виконати крапковий продукт між скалярним та декартовим вектором, і він складеться добре, але не вдасться під час виконання) .
Parker Hoyes

0

Розглянемо перерахунок з кожним названим Vector, що має конструктор, що складається з масиву (ініціалізованого у списку параметрів із назвами розмірів або подібним, або, можливо, просто цілим числом для розміру або порожнім масивом компонентів - ваш дизайн) та лямбда для метод getMagnitude. Можна переконати, що enum також реалізує інтерфейс для setComponents / getComponent (s) і просто встановити, який саме компонент був у його використанні, виключаючи getX та ін. Вам потрібно буде ініціалізувати кожен об’єкт з його фактичними значеннями компонентів перед використанням, можливо перевіряючи, чи відповідає розмір вхідного масиву назви розмірів або розміру.

Тоді, якщо ви поширите рішення на інший вимір, ви просто модифікуєте перерахунок і лямбду.


1
Будь ласка, надайте коротке опис фрагменту коду для свого рішення.
Тулен Кордова

0

На основі вашого варіанту 2, чому б просто не зробити цього? Якщо ви хочете запобігти використанню сирої бази, ви можете зробити її абстрактною:

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

Це схоже на "метод 2" у моєму запитанні. Однак ваше рішення дає змогу гарантувати безпеку типу під час компіляції, однак накладні витрати на створення double[]атрибутів небажані порівняно з реалізацією, яка просто використовує 2 примітивні doubles. На такому мінімальному прикладі це здається мікрооптимізацією, але розглянемо набагато складніший випадок, коли задіяно набагато більше метаданих, а відповідний тип має короткий термін експлуатації.
Parker Hoyes

1
Правильно, як мовиться, це ґрунтується на методі 2. На основі вашої дискусії з Грооставом стосовно його відповіді, у мене склалося враження, що ви не турбуєтесь про ефективність. Чи кількісно ви оцінили цю накладну, тобто створили 2 об’єкти замість 1? Що стосується коротких строків життя, то сучасні СВМ оптимізовані для цього випадку і повинні мати нижчу вартість GC (в основному 0), ніж довше живучі об'єкти. Я не впевнений, як метадані в цьому входять. Це метадані скалярні чи розмірні?
JimmyJames

Фактичним проектом, над яким я працював, був геометричний каркас, який буде використовуватися в гіпервимірному рендері. Це означає, що я створював набагато складніші об'єкти, ніж такі вектори, як еліпсоїди, ортотопи та ін., І перетворення, як правило, включали матриці. Складність роботи з вимірною геометрією зробила безпеку типу для матриці та розміру вектора бажаною, хоча все ще було значне бажання максимально уникати створення об'єктів.
Parker Hoyes

Мені здається, що я справді шукав, це більш автоматизоване рішення, яке створило байтовий код, подібний до способу 1, що насправді неможливо в стандартних Java або Xtend. Коли я закінчив робити, я використовував метод 2, де параметри розміру цих об'єктів повинні бути динамічними під час виконання, і втомливо створювати більш ефективні спеціалізовані реалізації для випадків, коли ці параметри були статичними. Реалізація замінить «динамічний» супертип Vectorна більш спеціалізовану реалізацію (наприклад Vector3), якщо його термін експлуатації повинен бути порівняно довгим.
Parker Hoyes

0

Одна ідея:

  1. Абстрактний базовий клас Vector, що забезпечує реалізацію змінних розмірів на основі методу getComponent (i).
  2. Індивідуальні підкласи Vector1, Vector2, Vector3, що охоплюють типові випадки, переосмислюючи векторні методи.
  3. Підклас DynVector для загального випадку.
  4. Фабричні методи із списками аргументів фіксованої довжини для типових випадків, оголошених для повернення Vector1, Vector2 або Vector3.
  5. Фабричний метод var-args, оголошений, що повертає Vector, екземпляр Vector1, Vector2, Vector3 або DynVector, залежно від довжини аргументу.

Це дає хороші показники у типових випадках та певну безпеку часу компіляції (все ще можна покращити) без шкоди для загального випадку.

Скелет коду:

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

}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.