Gerando classes Java com parâmetros de valor em tempo de compilação


10

Considere uma situação em que uma classe implemente o mesmo comportamento básico, métodos etc., mas várias versões diferentes dessa classe podem existir para diferentes usos. No meu caso particular, eu tenho um vetor (um vetor geométrico, não uma lista) e esse vetor pode ser aplicado a qualquer espaço euclidiano N-dimensional (1 dimensional, 2 dimensional, ...). Como essa classe / tipo pode ser definida?

Isso seria fácil em C ++, onde os modelos de classe podem ter valores reais como parâmetros, mas não temos esse luxo em Java.

As duas abordagens que posso pensar para resolver esse problema são:

  1. Ter uma implementação de cada caso possível em tempo de compilação.

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

    Esta solução é obviamente muito demorada e extremamente tediosa para codificar. Neste exemplo, não parece tão ruim, mas no meu código real, estou lidando com vetores que têm várias implementações cada, com até quatro dimensões (x, y, z ew). Atualmente, tenho mais de 2.000 linhas de código, embora cada vetor precise realmente de 500.

  2. Especificando parâmetros em tempo de execução.

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

    Infelizmente, essa solução prejudica o desempenho do código, resulta em um código mais confuso do que a solução anterior e não é tão segura no tempo de compilação (não é possível garantir no tempo de compilação que o vetor com o qual você está lidando é bidimensional, por exemplo).

No momento, estou atualmente desenvolvendo no Xtend, portanto, se houver alguma solução disponível no Xtend, elas também serão aceitáveis.


Como você está usando o Xtend, está fazendo isso no contexto de uma DSL do Xtext?
Dan1701 15/02

2
DSLs são ótimos para aplicativos de geração de código. Em poucas palavras, você cria uma pequena gramática de linguagem, uma instância dessa linguagem (descrevendo vários vetores, neste caso) e algum código que é executado quando a instância é salva (gerando seu código Java). Há muitos recursos e exemplos no site Xtext .
Dan1701

2
Existe uma solução perfeita para esse problema usando tipos dependentes (é mais ou menos para o que eles foram criados), mas infelizmente isso não está disponível em Java. Eu usaria a primeira solução se você tiver apenas um número pequeno e fixo de classes (digamos que você use apenas vetores 1, 2 e 3-dimensionais) e a última solução por mais que isso. Obviamente eu não posso dizer com certeza sem executar o código, mas eu não acho que haverá o impacto de desempenho que você está preocupado com
gardenhead

1
Essas duas classes não têm a mesma interface, elas não são polimórficas, mas você está tentando usá-las polimorficamente.
Martin Spamer

1
Se você está escrevendo matemática de álgebra linear e está preocupado com o desempenho, por que java. Não vejo nada além de problemas nisso.
Sopel

Respostas:


1

Em casos como esse, eu uso geração de código.

Eu escrevo um aplicativo java que gera o código real. Dessa forma, você pode facilmente usar um loop for para gerar várias versões diferentes. Eu uso o JavaPoet , o que torna bastante simples a criação do código real. Em seguida, você pode integrar a execução da geração de código ao seu sistema de construção.


0

Eu tenho um modelo muito semelhante no meu aplicativo e nossa solução foi simplesmente manter um mapa de tamanho dinâmico, semelhante à sua solução 2.

Você simplesmente não precisará se preocupar com o desempenho com uma matriz java primitiva como essa. Geramos matrizes com tamanhos de limite superior de 100 colunas (leia-se: 100 vetores dimensionais) por 10.000 linhas e tivemos um bom desempenho com tipos de vetores muito mais complexos que a sua solução 2. Você pode tentar selar os métodos de classe ou marcação como final. para acelerar, mas acho que você está otimizando prematuramente.

Você pode obter algumas economias de código (ao custo do desempenho) criando uma classe base para compartilhar seu 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}; }
}

Então, é claro, se você estiver no Java-8 +, poderá usar interfaces padrão para tornar isso ainda mais difícil:

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
}

Em última análise, além disso, você está sem opções com a JVM. É claro que você pode escrevê-los em C ++ e usar algo como JNA para conectá-los - esta é a nossa solução para algumas das operações de matriz rápida, onde usamos o fortran e o MKL da intel-- mas isso só vai desacelerar as coisas se você simplesmente escreve sua matriz em C ++ e chama seus getters / setters de java.


Minha principal preocupação não é o desempenho, é a verificação em tempo de compilação. Eu realmente gostaria de uma solução em que o tamanho do vetor e as operações que podem ser executadas nele sejam determinados em tempo de compilação (como nos modelos C ++). Talvez sua solução é melhor se você está lidando com matrizes que podem ser até 1000 componentes em tamanho, mas neste caso eu só estou lidando com vetores com um tamanho de 1 - 10.
Parker Hoyes

Se você usar algo como a primeira ou a segunda solução, poderá criar essas subclasses. Agora também estou lendo no Xtend, e parece um pouco com o Kotlin. Com o Kotlin, você provavelmente pode usar os data classobjetos para criar facilmente 10 subclasses de vetores. Com o java, supondo que você possa puxar toda a sua funcionalidade para a classe base, cada subclasse terá de 1 a 10 linhas. Por que não criar uma classe base?
Groostav 16/02

O exemplo que forneci é simplificado demais, meu código real tem muitos métodos definidos para Vector, como o produto com pontos vetoriais, adição e multiplicação por componentes, etc. Embora eu pudesse implementá-los usando uma classe base e seu asArraymétodo, esses vários métodos não seriam verificados em tempo de compilação (você pode executar um produto escalar entre um vetor escalar e um vetor cartesiano e ele compilará bem, mas falhará em tempo de execução) .
Parker Hoyes

0

Considere uma enumeração com cada vetor nomeado com um construtor que consiste em uma matriz (inicializada na lista de parâmetros com os nomes de dimensão ou similar, ou talvez apenas um número inteiro para o tamanho ou uma matriz de componentes vazios - seu design) e uma lambda para o método getMagnitude. Você poderia fazer com que o enum também implementasse uma interface para setComponents / getComponent (s) e apenas estabelecesse qual componente era qual em seu uso, eliminando getX, et al. Você precisaria inicializar cada objeto com seus valores reais de componentes antes de usar, possivelmente verificando se o tamanho da matriz de entrada corresponde aos nomes ou tamanho da dimensão.

Então, se você estender a solução para outra dimensão, basta modificar o enum e o lambda.


1
Forneça uma ilustração do snippet de código curto para sua solução.
Tulains Córdova 13/09/16

0

Com base na sua opção 2, por que não fazer isso simplesmente? Se você deseja impedir o uso da base bruta, você pode torná-la abstrata:

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

Isso é semelhante ao "método 2" na minha pergunta. Sua solução, no entanto, fornece uma maneira de garantir a segurança do tipo no tempo de compilação, no entanto, a sobrecarga de criar a double[]é indesejável em comparação com uma implementação que simplesmente usa 2 primitivos double. Em um exemplo tão mínimo quanto isso, parece uma microoptimização, mas considere um caso muito mais complexo em que muito mais metadados estão envolvidos e o tipo em questão tem uma vida útil curta.
Parker Hoyes

1
Certo, como diz, isso se baseia no método 2. Com base na sua discussão com Groostav em relação à resposta dele, tive a impressão de que sua preocupação não era com o desempenho. Você quantificou essa sobrecarga, ou seja, criou 2 objetos em vez de 1? Quanto aos curtos períodos de vida, as JVMs modernas são otimizadas para este caso e devem ter um custo de GC mais baixo (basicamente 0) do que objetos com vida útil mais longa. Não tenho certeza de como os metadados se encaixam nisso. Esses metadados são escalares ou dimensionais?
21716 JimmyJames

O projeto real no qual eu estava trabalhando era uma estrutura de geometria para ser usada em um renderizador hiperdimensional. Isso significa que eu estava criando objetos muito mais complexos do que vetores como elipsóides, ortótopos etc., e transformações geralmente envolviam matrizes. A complexidade de trabalhar com geometria dimensional mais alta tornou desejável a segurança de tipo para o tamanho da matriz e do vetor, enquanto ainda havia um desejo significativo de evitar a criação de objetos o máximo possível.
Parker Hoyes

O que eu acho que realmente estava procurando era uma solução mais automatizada que produzisse um código semelhante ao método 1, o que não é realmente possível no Java ou Xtend padrão. Quando terminei, estava usando o método 2, em que os parâmetros de tamanho desses objetos precisavam ser dinâmicos em tempo de execução e criando tediosamente implementações mais eficientes e especializadas para casos em que esses parâmetros eram estáticos. A implementação substituiria o supertipo "dinâmico" Vectorpor uma implementação mais especializada (por exemplo Vector3) se sua vida útil fosse relativamente longa.
Parker Hoyes

0

Uma ideia:

  1. Um vetor de classe base abstrato que fornece implementações de dimensão variável com base no método getComponent (i).
  2. Subclasses individuais Vector1, Vector2, Vector3, cobrindo os casos típicos, substituindo os métodos Vector.
  3. Uma subclasse DynVector para o caso geral.
  4. Métodos de fábrica com listas de argumentos de comprimento fixo para os casos típicos, declarados para retornar Vector1, Vector2 ou Vector3.
  5. Um método de fábrica var-args, declarado para retornar Vector, instanciando Vector1, Vector2, Vector3 ou DynVector, dependendo do comprimento do arglist.

Isso fornece bom desempenho em casos típicos e alguma segurança em tempo de compilação (ainda pode ser aprimorada) sem sacrificar o caso geral.

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

}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.