使用编译时值参数生成Java类


10

考虑一个类实现相同的基本行为,方法等的情况,但是该类可能存在多种不同版本以用于不同用途。在我的特定情况下,我有一个向量(一个几何向量,而不是一个列表),并且该向量可以应用于任何N维欧几里德空间(1维,2维等)。如何定义此类/类型?

在C ++中,这很容易,因为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)。我目前有超过2,000行代码,尽管每个向量实际上只需要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];
        }
    }
    

    不幸的是,这种解决方案会损害代码性能,导致代码比以前的解决方案更为混乱,并且在编译时也不那么安全(在编译时无法保证您要处理的向量实际上是二维的,例如)。

我目前实际上在Xtend中进行开发,因此,如果有任何Xtend解决方案可用,它们也是可以接受的。


由于您使用的是Xtend,您是否在Xtext DSL的上下文中执行此操作?
Dan1701 2016年

2
DSL非常适合代码生成应用。简而言之,您将创建一个小的语言语法,该语言的一个实例(在这种情况下,描述各种向量)以及一些在保存该实例时执行的代码(生成Java代码)。Xtext网站上有很多资源和示例。
Dan1701

2
使用依赖类型(或多或少是为它们创建的)可以很好地解决此问题,但是可惜Java中没有。如果您只有少量固定数量的类(例如,您仅使用一维,二维和3维向量),那么我会选择第一种解决方案,而后一种解决方案的解决方案则更多。显然,如果不运行代码就无法确定,但是我认为您担心的性能不会受到影响
gardenhead

1
这两个类没有相同的接口,它们不是多态的,但是您正在尝试以多态使用它们。
马丁·斯帕默

1
如果您正在编写线性代数数学并且关注性能,那么为什么选择Java。除了问题我什么都看不到。
Sopel

Answers:


1

在这种情况下,我将使用代码生成。

我编写了一个生成实际代码的Java应用程序。这样,您可以轻松地使用for循环来生成许多不同的版本。我使用JavaPoet,这使得构建实际代码非常简单。然后,您可以将运行代码生成集成到构建系统中。


0

我的应用程序上有一个非常相似的模型,我们的解决方案是仅保留一个动态大小的地图,类似于您的解决方案2。

您根本不需要担心像这样的Java数组基元的性能。我们生成的矩阵的上限大小为100列(读取:100维向量)乘以10,000行,并且在比您的解决方案2复杂得多的向量类型上都具有良好的性能。您可以尝试将类或标记方法密封为final加快速度,但我认为您过早地进行了优化。

通过创建一个基类来共享您的代码,可以节省一些代码(以性能为代价):

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之类的东西来桥接它们-这是我们针对某些快速矩阵运算的解决方案,其中我们使用了fortran和intel的MKL-但这只会减慢速度,您只需用C ++编写矩阵,然后从Java调用其getter / setter。


我主要关心的不是性能,而是编译时检查。我真的很想一个解决方案,其中向量的大小和可以在向量上执行的操作在编译时确定(例如C ++模板)。也许你的解决方案是最好的,如果你要处理的矩阵,可能是面积达1000个组件,但在这种情况下,我只处理矢量大小为1 - 10
帕克Hoyes

如果您使用第一种或第二种解决方案,则可以创建这些子类。现在,我也正在阅读Xtend,似乎有点像Kotlin。使用Kotlin,您可能可以使用这些data class对象轻松创建10个向量子类。使用Java,假设您可以将所有功能都拉入基类,则每个子类将占用1-10行。为什么不创建基类?
Groostav '16

我提供的示例过于简化,我的实际代码中有许多为Vector定义的方法,例如矢量点乘积,逐分量加法和乘法等。尽管我可以使用基类和您的asArray方法来实现这些方法,但是不会在编译时检查各种方法(您可以在标量和笛卡尔矢量之间执行点积运算,并且可以很好地编译,但在运行时会失败) 。
派克·霍伊斯,2016年

0

考虑一个枚举,每个命名的Vector都有一个构造函数,该构造函数由一个数组(在参数列表中使用尺寸名称或类似名称初始化,或者可能只是大小的整数或空组件数组-您的设计)以及一个lambda getMagnitude方法。您可以让枚举还实现setComponents / getComponent(s)的接口,并只需确定使用哪个组件即可,从而消除getX等。在使用之前,您需要使用其实际组件值初始化每个对象,可能需要检查输入数组的大小是否与维度名称或大小匹配。

然后,如果将解决方案扩展到另一个维度,则只需修改枚举和lambda。


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 的实现相比,创建a的开销是不希望的。在一个最小的示例中,这似乎是微优化,但是考虑一个更复杂的情况,其中涉及更多的元数据,并且所讨论的类型的寿命很短。
派克·霍伊斯,2016年

1
正确,正如它所说,这是基于方法2的。基于您与Groostav关于他的回答的讨论,我给您的印象是,您关心的不是性能。您是否量化了此开销,即创建2个对象而不是1个?对于短寿命,现代JVM已针对这种情况进行了优化,并且与寿命更长的对象相比,其GC成本(基本上为0)更低。我不确定元数据如何发挥作用。此元数据是标量还是维?
JimmyJames

我正在从事的实际项目是要在超尺寸渲染器中使用的几何框架。这意味着我创建的对象要比诸如椭圆体,正交位等的向量复杂得多,而转换通常涉及矩阵。处理高维几何的复杂性使矩阵和向量大小的类型安全性成为可取的,同时仍然强烈希望尽可能避免创建对象。
帕克·霍伊斯,2016年

我想我真正要寻找的是一种更自动化的解决方案,该解决方案产生类似于方法1的字节码,这在标准Java或Xtend中实际上是不可能的。最后,我使用的是方法2,其中这些对象的大小参数在运行时需要是动态的,并且繁琐地为这些参数是静态的情况创建了更有效,更专业的实现。如果其生存期相对较长,则该实现将Vector使用更专业的实现(例如Vector3)替换“动态”超类型。
帕克·霍伊斯,2016年

0

一个想法:

  1. 一个抽象基类Vector,它基于getComponent(i)方法提供可变维度的实现。
  2. 单个子类Vector1,Vector2,Vector3,涵盖了典型情况,覆盖了Vector方法。
  3. 一般情况下的DynVector子类。
  4. 对于典型情况,带有固定长度参数列表的工厂方法声明为返回Vector1,Vector2或Vector3。
  5. 一个var-args工厂方法,声明为返回Vector,根据arglist的长度实例化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;
    }

}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.