Tạo các lớp Java với các tham số giá trị thời gian biên dịch


10

Xem xét một tình huống trong đó một lớp thực hiện cùng một hành vi cơ bản, các phương thức, et cetera, nhưng nhiều phiên bản khác nhau của lớp đó có thể tồn tại cho các mục đích sử dụng khác nhau. Trong trường hợp cụ thể của tôi, tôi có một vectơ (vectơ hình học, không phải danh sách) và vectơ đó có thể áp dụng cho bất kỳ không gian Euclide N chiều nào (1 chiều, 2 chiều, ...). Làm thế nào có thể định nghĩa lớp / loại này?

Điều này sẽ dễ dàng trong C ++ khi các mẫu lớp có thể có các giá trị thực tế làm tham số, nhưng chúng ta không có sự sang trọng đó trong Java.

Hai cách tiếp cận tôi có thể nghĩ ra có thể được thực hiện để giải quyết vấn đề này là:

  1. Có một thực hiện của từng trường hợp có thể tại thời gian biên dịch.

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

    Giải pháp này rõ ràng là rất tốn thời gian và cực kỳ tẻ nhạt để viết mã. Trong ví dụ này có vẻ không tệ lắm, nhưng trong mã thực tế của tôi, tôi đang xử lý các vectơ có nhiều triển khai mỗi cái, với tối đa bốn chiều (x, y, z và w). Tôi hiện có hơn 2.000 dòng mã, mặc dù mỗi vector chỉ thực sự cần 500.

  2. Chỉ định tham số khi chạy.

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

    Thật không may, giải pháp này làm tổn hại đến hiệu suất mã, dẫn đến mã lộn xộn hơn so với giải pháp trước đây và không an toàn ở thời gian biên dịch (không thể đảm bảo tại thời điểm biên dịch rằng vectơ bạn xử lý thực sự là 2 chiều, ví dụ).

Tôi hiện đang thực sự phát triển trong Xtend, vì vậy nếu có bất kỳ giải pháp Xtend nào có sẵn, chúng cũng sẽ được chấp nhận.


Vì bạn đang sử dụng Xtend, bạn có đang thực hiện việc này trong bối cảnh DSL Xtext không?
Dan1701

2
DSL rất tốt cho các ứng dụng mã gen. Tóm lại, bạn tạo một ngữ pháp ngôn ngữ nhỏ, một thể hiện của ngôn ngữ đó (mô tả các vectơ khác nhau, trong trường hợp này) và một số mã thực thi khi cá thể được lưu (tạo mã Java của bạn). Có rất nhiều tài nguyên và ví dụ trên trang Xtext .
Dan1701

2
Có một giải pháp hoàn hảo cho vấn đề này bằng cách sử dụng các loại phụ thuộc (nó ít nhiều là những gì chúng được tạo ra), nhưng than ôi không có sẵn trong Java. Tôi sẽ đi với giải pháp đầu tiên nếu bạn chỉ có một số lượng nhỏ các lớp cố định (giả sử bạn chỉ sử dụng các vectơ 1-, 2- và 3 chiều) và giải pháp sau cho nhiều hơn thế. Rõ ràng tôi không thể nói chắc chắn mà không chạy mã của bạn, nhưng tôi không nghĩ sẽ có tác động hiệu suất mà bạn lo lắng
vườn

1
Hai lớp này không có cùng giao diện, chúng không đa hình nhưng bạn đang cố gắng sử dụng chúng đa hình.
Martin Spamer

1
Nếu bạn đang viết toán đại số tuyến tính và quan tâm đến hiệu suất thì tại sao java. Tôi không thể thấy bất cứ điều gì ngoài vấn đề trong đó.
Sốt

Câu trả lời:


1

Trong trường hợp như thế này, tôi sử dụng tạo mã.

Tôi viết một ứng dụng java tạo mã thực tế. Bằng cách đó, bạn có thể dễ dàng sử dụng vòng lặp for để tạo ra một loạt các phiên bản khác nhau. Tôi sử dụng JavaPoet , điều này làm cho việc xây dựng mã thực tế trở nên khá đơn giản. Sau đó, bạn có thể tích hợp chạy việc tạo mã vào hệ thống xây dựng của mình.


0

Tôi có một mô hình rất giống với ứng dụng của mình và giải pháp của chúng tôi là chỉ cần giữ một bản đồ có kích thước động, tương tự như giải pháp 2 của bạn.

Đơn giản là bạn sẽ không cần phải lo lắng về hiệu năng với một mảng java nguyên thủy như thế. Chúng tôi tạo ma trận với kích thước giới hạn trên 100 cột (đọc: vectơ 100 chiều) bằng 10.000 hàng và chúng tôi đã có hiệu suất tốt với các loại vectơ phức tạp hơn nhiều so với giải pháp của bạn 2. Bạn có thể thử niêm phong lớp hoặc phương thức đánh dấu là cuối cùng để tăng tốc, nhưng tôi nghĩ bạn đang tối ưu hóa sớm.

Bạn có thể nhận được một số tiết kiệm mã (với chi phí hiệu suất) bằng cách tạo một lớp cơ sở để chia sẻ mã của bạn:

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

Tất nhiên, nếu bạn đang sử dụng Java-8 +, bạn có thể sử dụng các giao diện mặc định để làm cho điều này thậm chí còn chặt chẽ hơn:

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
}

Cuối cùng, bạn không còn lựa chọn nào với JVM. Tất nhiên bạn có thể viết chúng bằng C ++ và sử dụng một cái gì đó như JNA để kết nối chúng với nhau - đây là giải pháp của chúng tôi cho một số hoạt động ma trận nhanh, nơi chúng tôi sử dụng MKL của fortran và intel-- nhưng điều này sẽ làm mọi thứ chậm lại nếu bạn chỉ cần viết ma trận của bạn trong C ++ và gọi getters / setters của nó từ java.


Mối quan tâm chính của tôi không phải là hiệu suất, đó là kiểm tra thời gian biên dịch. Tôi thực sự muốn một giải pháp trong đó kích thước của vectơ và các thao tác có thể được thực hiện trên nó được xác định tại thời gian biên dịch (như với các mẫu C ++). Có lẽ giải pháp của bạn là tốt nhất nếu bạn xử lý các ma trận có kích thước lên tới 1000 thành phần, nhưng trong trường hợp này tôi chỉ xử lý các vectơ có kích thước 1 - 10.
Parker Hoyes 16/2/2016

Nếu bạn sử dụng một cái gì đó như giải pháp thứ nhất hoặc thứ hai, bạn có thể tạo các lớp con đó. Bây giờ tôi cũng chỉ đọc trên Xtend, và nó có vẻ hơi công bằng như Kotlin. Với Kotlin, có lẽ bạn có thể sử dụng các data classđối tượng để dễ dàng tạo 10 lớp con vector. Với java, giả sử bạn có thể kéo tất cả các chức năng của mình vào lớp cơ sở, mỗi lớp con sẽ mất 1-10 dòng. Tại sao không tạo một lớp cơ sở?
Groostav

Ví dụ tôi cung cấp là quá đơn giản, mã thực tế của tôi có rất nhiều phương thức được định nghĩa cho Vector như sản phẩm chấm vector, phép cộng và phép nhân thành phần, et cetera. Mặc dù tôi có thể triển khai các asArrayphương thức này bằng cách sử dụng một lớp cơ sở và phương thức của bạn , các phương thức khác nhau đó sẽ không được kiểm tra tại thời điểm biên dịch (bạn có thể thực hiện một sản phẩm chấm giữa vô hướng và vectơ cartesian và nó sẽ biên dịch tốt, nhưng thất bại khi chạy) .
Parker Hoyes 16/2/2016

0

Hãy xem xét một enum với mỗi Vector có tên có một hàm tạo bao gồm một mảng (được khởi tạo trong danh sách tham số có tên kích thước hoặc tương tự, hoặc có lẽ chỉ là một số nguyên cho kích thước hoặc mảng thành phần trống - thiết kế của bạn) và lambda cho phương thức getMagnitude. Bạn cũng có thể yêu cầu enum thực hiện giao diện cho setComponents / getComponent (s) và chỉ cần thiết lập thành phần nào trong cách sử dụng của nó, loại bỏ getX, et al. Bạn sẽ cần khởi tạo từng đối tượng với các giá trị thành phần thực tế của nó trước khi sử dụng, có thể kiểm tra xem kích thước mảng đầu vào có khớp với tên hoặc kích thước không.

Sau đó, nếu bạn mở rộng giải pháp sang một chiều khác, bạn chỉ cần sửa đổi enum và lambda.


1
Xin vui lòng, cung cấp một đoạn mã ngắn minh họa giải pháp của bạn.
Tulains Córdova

0

Dựa trên lựa chọn 2 của bạn, tại sao không chỉ đơn giản là làm điều này? Nếu bạn muốn ngăn chặn việc sử dụng cơ sở thô, bạn có thể làm cho nó trừu tượng:

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

Điều này tương tự như "phương pháp 2" trong câu hỏi của tôi. Tuy nhiên, giải pháp của bạn đưa ra một cách đảm bảo an toàn loại tại thời gian biên dịch, tuy nhiên, chi phí tạo ra double[]là không mong muốn so với việc triển khai chỉ sử dụng 2 doubles nguyên thủy . Trong một ví dụ tối thiểu như thế này có vẻ như là một vi mô hóa, nhưng hãy xem xét một trường hợp phức tạp hơn nhiều khi có nhiều siêu dữ liệu hơn và loại câu hỏi có thời gian tồn tại ngắn.
Parker Hoyes

1
Đúng như đã nói, điều này dựa trên phương pháp 2. Dựa trên cuộc thảo luận của bạn với Groostav liên quan đến câu trả lời của anh ấy, tôi có ấn tượng rằng mối quan tâm của bạn không liên quan đến hiệu suất. Bạn đã định lượng chi phí này tức là tạo 2 đối tượng thay vì 1 chưa? Đối với các vòng đời ngắn, các JVM hiện đại được tối ưu hóa cho trường hợp này và nên có chi phí GC thấp hơn (về cơ bản là 0) so với các đối tượng sống lâu hơn. Tôi không chắc làm thế nào siêu dữ liệu chơi trong này. Đây là siêu dữ liệu vô hướng hay chiều?
JimmyJames

Dự án thực tế mà tôi đang thực hiện là một khung hình học sẽ được sử dụng trong trình kết xuất tăng chiều. Điều này có nghĩa là tôi đã tạo ra các đối tượng phức tạp hơn nhiều so với các vectơ như ellipsoids, orthotopes et cetera và các phép biến đổi thường liên quan đến ma trận. Sự phức tạp của việc làm việc với hình học chiều cao hơn đã tạo ra sự an toàn cho loại ma trận và kích thước vectơ trong khi vẫn có mong muốn đáng kể để tránh việc tạo ra các đối tượng càng nhiều càng tốt.
Parker Hoyes

Điều tôi nghĩ rằng tôi thực sự đang tìm kiếm là một giải pháp tự động hơn được tạo ra bởi mã byte tương tự như phương thức 1, điều này thực sự không khả thi trong Java hoặc Xtend tiêu chuẩn. Khi tôi kết thúc việc sử dụng phương thức 2 trong đó các tham số kích thước của các đối tượng này cần phải động khi chạy và tạo ra các triển khai chuyên biệt, hiệu quả hơn cho các trường hợp trong đó các tham số này là tĩnh. Việc triển khai sẽ thay thế siêu kiểu "động" Vectorbằng một triển khai chuyên biệt hơn (ví dụ Vector3) nếu thời gian tồn tại của nó tương đối dài.
Parker Hoyes

0

Một ý tưởng:

  1. Một lớp cơ sở trừu tượng Vector cung cấp các triển khai kích thước thay đổi dựa trên phương thức getComponent (i).
  2. Các lớp con Indiviual Vector1, Vector2, Vector3, bao gồm các trường hợp điển hình, ghi đè các phương thức Vector.
  3. Một lớp con DynVector cho trường hợp chung.
  4. Các phương thức xuất xưởng với danh sách đối số có độ dài cố định cho các trường hợp điển hình, được khai báo để trả về Vector1, Vector2 hoặc Vector3.
  5. Một phương thức nhà máy var-args, được khai báo để trả về Vector, khởi tạo Vector1, Vector2, Vector3 hoặc DynVector, tùy thuộc vào độ dài của đối số.

Điều này mang lại cho bạn hiệu suất tốt trong các trường hợp điển hình và một số an toàn trong thời gian biên dịch (vẫn có thể được cải thiện) mà không phải hy sinh trường hợp chung.

Bộ xương mã:

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

}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.