Menghasilkan Kelas Java dengan Parameter Nilai waktu kompilasi


10

Pertimbangkan situasi di mana kelas mengimplementasikan perilaku dasar yang sama, metode, dan lain-lain, tetapi beberapa versi berbeda dari kelas itu bisa ada untuk penggunaan yang berbeda. Dalam kasus khusus saya, saya memiliki vektor (vektor geometris, bukan daftar) dan vektor itu dapat berlaku untuk ruang Euclidean N-dimensi (1 dimensi, 2 dimensi, ...). Bagaimana kelas / tipe ini dapat didefinisikan?

Ini akan mudah di C ++ di mana templat kelas dapat memiliki nilai aktual sebagai parameter, tetapi kami tidak memiliki kemewahan di Jawa.

Dua pendekatan yang dapat saya pikirkan yang dapat diambil untuk menyelesaikan masalah ini adalah:

  1. Memiliki implementasi dari setiap kasus yang mungkin pada waktu kompilasi.

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

    Solusi ini jelas sangat memakan waktu dan sangat membosankan untuk kode. Dalam contoh ini sepertinya tidak terlalu buruk, tetapi dalam kode saya yang sebenarnya saya berurusan dengan vektor yang masing-masing memiliki beberapa implementasi, dengan hingga empat dimensi (x, y, z, dan w). Saat ini saya memiliki lebih dari 2.000 baris kode, meskipun setiap vektor hanya membutuhkan 500.

  2. Menentukan parameter saat runtime.

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

    Sayangnya solusi ini merusak kinerja kode, menghasilkan kode yang lebih berantakan daripada solusi sebelumnya, dan tidak aman pada saat kompilasi (tidak dapat dijamin pada waktu kompilasi bahwa vektor yang Anda hadapi sebenarnya adalah 2-dimensi, sebagai contoh).

Saya saat ini benar-benar berkembang di Xtend, jadi jika ada solusi Xtend yang tersedia, mereka juga dapat diterima.


Karena Anda menggunakan Xtend, apakah Anda melakukan ini dalam konteks Xtext DSL?
Dan1701

2
DSL sangat bagus untuk aplikasi kode-gen. Singkatnya, Anda membuat tata bahasa sedikit bahasa, contoh dari bahasa itu (menggambarkan berbagai vektor, dalam kasus ini), dan beberapa kode yang dieksekusi ketika instance disimpan (menghasilkan kode Java Anda). Ada banyak sumber daya dan contoh di situs Xtext .
Dan1701

2
Ada solusi sempurna untuk masalah ini menggunakan tipe dependen (ini lebih atau kurang untuk apa mereka diciptakan), tetapi sayangnya itu tidak tersedia di Jawa. Saya akan pergi dengan solusi pertama jika Anda hanya memiliki sejumlah kecil, kelas tetap (katakanlah Anda hanya menggunakan vektor 1, 2, dan 3 dimensi), dan solusi terakhir untuk lebih dari itu. Jelas saya tidak bisa mengatakan dengan pasti tanpa menjalankan kode Anda, tetapi saya tidak berpikir akan ada dampak kinerja yang Anda khawatirkan
gardenhead

1
Kedua kelas tidak memiliki antarmuka yang sama, mereka bukan polimorfik tetapi Anda mencoba menggunakannya secara polimorfik.
Martin Spamer

1
Jika Anda menulis matematika aljabar linier dan khawatir tentang kinerja maka mengapa java. Saya tidak bisa melihat apa pun kecuali masalah dalam hal itu.
Sopel

Jawaban:


1

Dalam kasus seperti ini, saya menggunakan pembuatan kode.

Saya menulis aplikasi java yang menghasilkan kode aktual. Dengan begitu Anda dapat dengan mudah menggunakan for for untuk menghasilkan banyak versi yang berbeda. Saya menggunakan JavaPoet , yang membuatnya cukup mudah untuk membangun kode aktual. Kemudian Anda dapat mengintegrasikan menjalankan pembuatan kode ke dalam sistem build Anda.


0

Saya memiliki model yang sangat mirip pada aplikasi saya dan solusi kami adalah hanya menyimpan peta dengan ukuran yang dinamis, mirip dengan solusi Anda 2.

Anda tidak perlu khawatir tentang kinerja dengan java array primitif seperti itu. Kami menghasilkan matriks dengan ukuran batas atas 100 kolom (baca: vektor 100 dimensi) sebanyak 10.000 baris, dan kami telah memiliki kinerja yang baik dengan jenis vektor yang jauh lebih kompleks daripada solusi Anda 2. Anda dapat mencoba menyegel kelas atau menandai metode sebagai final untuk mempercepatnya, tapi saya pikir Anda mengoptimalkan secara prematur.

Anda bisa mendapatkan penghematan kode (dengan biaya kinerja) dengan membuat kelas dasar untuk membagikan kode Anda:

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

Maka tentu saja, jika Anda menggunakan Java-8 +, Anda dapat menggunakan antarmuka bawaan untuk membuat ini lebih ketat:

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
}

Lebih dari itu, Anda kehabisan pilihan dengan JVM. Anda tentu saja dapat menuliskannya dalam C ++ dan menggunakan sesuatu seperti JNA untuk menjembatani mereka - ini adalah solusi kami untuk beberapa operasi matriks cepat, di mana kami menggunakan Fortran dan intel MKL - tetapi ini hanya akan memperlambat segalanya jika Anda cukup menulis matriks Anda di C ++ dan memanggil getter / setternya dari java.


Perhatian utama saya bukan kinerja, ini memeriksa waktu kompilasi. Saya benar-benar ingin solusi di mana ukuran vektor dan operasi yang dapat dilakukan ditentukan pada waktu kompilasi (seperti dengan template C ++). Mungkin solusi Anda adalah yang terbaik jika Anda berurusan dengan matriks yang bisa berukuran hingga 1000 komponen, tetapi dalam hal ini saya hanya berurusan dengan vektor dengan ukuran 1 - 10.
Parker Hoyes

Jika Anda menggunakan sesuatu seperti solusi pertama atau kedua, Anda dapat membuat subclass tersebut. Sekarang saya juga baru membaca Xtend, dan sepertinya agak adil seperti Kotlin. Dengan Kotlin, Anda mungkin dapat menggunakan data classobjek untuk dengan mudah membuat 10 subclass vektor. Dengan java, dengan asumsi Anda dapat menarik semua fungsionalitas Anda ke kelas dasar, setiap subclass akan mengambil 1-10 baris. Mengapa tidak membuat kelas dasar?
Groostav

Contoh yang saya berikan terlalu disederhanakan, kode saya yang sebenarnya memiliki banyak metode yang didefinisikan untuk Vector seperti produk titik vektor, penambahan dan perkalian komponen-bijaksana, dan sebagainya. Meskipun saya dapat mengimplementasikan ini menggunakan kelas dasar dan asArraymetode Anda , berbagai metode tersebut tidak akan diperiksa pada waktu kompilasi (Anda dapat melakukan dot-produk antara skalar dan vektor kartesius dan itu akan dikompilasi dengan baik, tetapi gagal saat runtime) .
Parker Hoyes

0

Pertimbangkan enum dengan masing-masing bernama Vektor yang memiliki konstruktor yang terdiri dari array (diinisialisasi dalam daftar parameter dengan nama dimensi atau serupa, atau mungkin hanya bilangan bulat untuk ukuran atau array komponen kosong - desain Anda), dan lambda untuk metode getMagnitude. Anda dapat memiliki enum juga mengimplementasikan antarmuka untuk setComponents / getComponent (s), dan hanya menetapkan komponen mana yang dalam penggunaannya, menghilangkan getX, et al. Anda perlu menginisialisasi setiap objek dengan nilai komponen yang sebenarnya sebelum digunakan, mungkin memeriksa bahwa ukuran array input cocok dengan nama dimensi atau ukuran.

Kemudian jika Anda memperluas solusi ke dimensi lain, Anda cukup memodifikasi enum dan lambda.


1
Harap berikan ilustrasi cuplikan kode singkat solusi Anda.
Tulains Córdova

0

Berdasarkan pilihan Anda 2, mengapa tidak melakukan ini saja? Jika Anda ingin mencegah penggunaan basis mentah, Anda dapat membuatnya abstrak:

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

Ini mirip dengan "metode 2" dalam pertanyaan saya. Solusi Anda, bagaimanapun, memberikan cara untuk menjamin keamanan tipe pada waktu kompilasi, namun biaya overhead pembuatan double[]tidak diinginkan dibandingkan dengan implementasi yang hanya menggunakan 2 primitif double. Dalam contoh seminimal ini, ini terlihat seperti optimasi mikro, tetapi pertimbangkan kasus yang jauh lebih kompleks di mana lebih banyak metadata terlibat dan jenis yang dimaksud memiliki masa hidup yang singkat.
Parker Hoyes

1
Benar, seperti yang dikatakan, ini didasarkan pada metode 2. Berdasarkan diskusi Anda dengan Groostav sehubungan dengan jawabannya, saya mendapat kesan bahwa kekhawatiran Anda bukan pada kinerja. Sudahkah Anda menghitung overhead ini dengan membuat 2 objek, bukan 1? Adapun rentang hidup yang pendek, JVM modern dioptimalkan untuk kasus ini dan harus memiliki biaya GC yang lebih rendah (pada dasarnya 0) daripada objek yang berumur panjang. Saya tidak yakin bagaimana metadata berperan dalam hal ini. Apakah skadar atau dimensi metadata ini?
JimmyJames

Proyek aktual yang saya kerjakan adalah kerangka kerja geometri yang akan digunakan dalam penyaji hyperdimensional. Ini berarti saya membuat objek yang jauh lebih kompleks daripada vektor seperti ellipsoids, orthotopes et cetera dan transformasi yang biasanya melibatkan matriks. Kompleksitas bekerja dengan geometri dimensi yang lebih tinggi membuat jenis-keselamatan untuk ukuran matriks dan vektor diinginkan sementara masih ada keinginan yang signifikan untuk menghindari penciptaan objek sebanyak mungkin.
Parker Hoyes

Apa yang saya pikir saya benar-benar cari adalah solusi yang lebih otomatis yang menghasilkan bytecode mirip dengan metode 1, yang tidak benar-benar mungkin di Jawa standar atau Xtend. Ketika akhirnya saya lakukan adalah menggunakan metode 2 di mana parameter ukuran objek-objek ini harus dinamis saat runtime, dan membuat implementasi yang lebih efisien, khusus untuk kasus-kasus di mana parameter ini statis. Implementasi akan menggantikan supertipe "dinamis" Vectordengan implementasi yang lebih terspesialisasi (misalnya Vector3) jika masa pakainya relatif lama.
Parker Hoyes

0

Satu ide:

  1. Vektor kelas dasar abstrak yang menyediakan implementasi variabel-dimensi berdasarkan metode getComponent (i).
  2. Subclass Indiviual Vector1, Vector2, Vector3, meliputi kasus-kasus khas, menimpa metode Vector.
  3. Subclass DynVector untuk kasus umum.
  4. Metode pabrik dengan daftar argumen panjang tetap untuk kasus-kasus tipikal, dinyatakan untuk mengembalikan Vector1, Vector2 atau Vector3.
  5. Metode pabrik var-args, dinyatakan untuk mengembalikan Vector, instantiating Vector1, Vector2, Vector3, atau DynVector, tergantung pada panjang arglist.

Ini memberi Anda kinerja yang baik dalam kasus-kasus tertentu dan beberapa keamanan waktu kompilasi (masih dapat ditingkatkan) tanpa mengorbankan kasus umum.

Kerangka kode:

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

}
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.