JavaRush /Blog Java /Random-VI /Nghỉ giải lao #130. Cách làm việc chính xác với mảng Java...

Nghỉ giải lao #130. Cách làm việc chính xác với mảng Java - mẹo từ Oracle

Xuất bản trong nhóm
Nguồn: Oracle Làm việc với mảng có thể bao gồm các biểu thức phản chiếu, tổng quát và lambda. Gần đây tôi đã nói chuyện với một đồng nghiệp phát triển bằng C. Cuộc trò chuyện chuyển sang mảng và cách chúng hoạt động trong Java so với C. Tôi thấy điều này hơi lạ vì Java được coi là ngôn ngữ giống C. Họ thực sự có rất nhiều điểm tương đồng, nhưng cũng có những khác biệt. Hãy bắt đầu đơn giản. Nghỉ giải lao #130.  Cách làm việc chính xác với mảng Java - mẹo từ Oracle - 1

Khai báo mảng

Nếu bạn làm theo hướng dẫn Java, bạn sẽ thấy có hai cách để khai báo một mảng. Điều đầu tiên rất đơn giản:
int[] array; // a Java array declaration
Bạn có thể thấy nó khác với C như thế nào, với cú pháp là:
int array[]; // a C array declaration
Hãy quay lại với Java. Sau khi khai báo một mảng, bạn cần cấp phát mảng:
array = new int[10]; // Java array allocation
Có thể khai báo và khởi tạo một mảng cùng một lúc không? Thực ra là không:
int[10] array; // NOPE, ERROR!
Tuy nhiên, bạn có thể khai báo và khởi tạo mảng ngay nếu đã biết các giá trị:
int[] array = { 0, 1, 1, 2, 3, 5, 8 };
Nếu bạn không biết ý nghĩa thì sao? Đây là đoạn mã bạn thường thấy nhất để khai báo, phân bổ và sử dụng mảng int :
int[] array;
array = new int[10];
array[0] = 0;
array[1] = 1;
array[2] = 1;
array[3] = 2;
array[4] = 3;
array[5] = 5;
array[6] = 8;
...
Lưu ý rằng tôi đã chỉ định một mảng int , là một mảng các kiểu dữ liệu nguyên thủy của Java . Hãy xem điều gì sẽ xảy ra nếu bạn thử quy trình tương tự với một mảng các đối tượng Java thay vì các đối tượng nguyên thủy:
class SomeClass {
    int val;
    // …
}
SomeClass[] array = new SomeClass[10];
array[0].val = 0;
array[1].val = 1;
array[2].val = 1;
array[3].val = 2;
array[4].val = 3;
array[5].val = 5;
array[6].val = 8;
Nếu chạy đoạn mã trên, chúng ta sẽ nhận được một ngoại lệ ngay sau khi thử sử dụng phần tử đầu tiên của mảng. Tại sao? Mặc dù mảng được phân bổ, mỗi phân đoạn của mảng đều chứa các tham chiếu đối tượng trống. Nếu bạn nhập mã này vào IDE, nó thậm chí sẽ tự động điền .val cho bạn nên lỗi có thể gây nhầm lẫn. Để sửa lỗi, hãy làm theo các bước sau:
SomeClass[] array = new SomeClass[10];
for ( int i = 0; i < array.length; i++ ) {  //new code
    array[i] = new SomeClass();             //new code
}                                           //new code
array[0].val = 0;
array[1].val = 1;
array[2].val = 1;
array[3].val = 2;
array[4].val = 3;
array[5].val = 5;
array[6].val = 8;
Nhưng nó không thanh lịch. Tôi tự hỏi tại sao tôi không thể dễ dàng phân bổ một mảng và các đối tượng trong mảng đó với ít mã hơn, thậm chí có thể tất cả trên một dòng. Để tìm câu trả lời, tôi đã tiến hành một số thí nghiệm.

Tìm niết bàn giữa các mảng Java

Mục tiêu của chúng tôi là viết mã một cách tao nhã. Tuân theo quy tắc “mã sạch”, tôi quyết định tạo mã có thể tái sử dụng để dọn dẹp mẫu phân bổ mảng. Đây là lần thử đầu tiên:
public class MyArray {

    public static Object[] toArray(Class cls, int size)
      throws Exception {
        Constructor ctor = cls.getConstructors()[0];
        Object[] objects = new Object[size];
        for ( int i = 0; i < size; i++ ) {
            objects[i] = ctor.newInstance();
        }

        return objects;
    }

    public static void main(String[] args) throws Exception {
        SomeClass[] array1 = (SomeClass[])MyArray.toArray(SomeClass.class, 32); // see this
        System.out.println(array1);
    }
}
Dòng mã được đánh dấu “xem cái này” trông chính xác như tôi mong muốn nhờ triển khai toArray . Cách tiếp cận này sử dụng sự phản chiếu để tìm hàm tạo mặc định cho lớp được cung cấp, sau đó gọi hàm tạo đó để khởi tạo một đối tượng của lớp đó. Quá trình gọi hàm tạo một lần cho mỗi phần tử mảng. Tuyệt vời! Thật đáng tiếc là nó không hoạt động. Mã biên dịch tốt nhưng đưa ra lỗi ClassCastException khi chạy. Để sử dụng mã này, bạn cần tạo một mảng các phần tử Object , sau đó chuyển từng phần tử của mảng sang một lớp SomeClass như thế này:
Object[] objects = MyArray.toArray(SomeClass.class, 32);
SomeClass scObj = (SomeClass)objects[0];
...
Điều này không thanh lịch! Sau nhiều thử nghiệm hơn, tôi đã phát triển một số giải pháp sử dụng biểu thức phản chiếu, tổng quát và lambda.

Giải pháp 1: Sử dụng sự phản chiếu

Ở đây chúng ta đang sử dụng lớp java.lang.reflect.Array để khởi tạo một mảng của lớp mà bạn chỉ định thay vì sử dụng lớp java.lang.Object cơ sở . Đây thực chất là một thay đổi mã một dòng:
public static Object[] toArray(Class cls, int size) throws Exception {
    Constructor ctor = cls.getConstructors()[0];
    Object array = Array.newInstance(cls, size);  // new code
    for ( int i = 0; i < size; i++ ) {
        Array.set(array, i, ctor.newInstance());  // new code
    }
    return (Object[])array;
}
Bạn có thể sử dụng phương pháp này để có được một mảng của lớp mong muốn và sau đó làm việc với nó như sau:
SomeClass[] array1 = (SomeClass[])MyArray.toArray(SomeClass.class, 32);
Mặc dù đây không phải là thay đổi bắt buộc nhưng dòng thứ hai đã được thay đổi để sử dụng lớp Phản chiếu mảng nhằm đặt nội dung của từng thành phần mảng. Thật đáng kinh ngạc! Nhưng còn một chi tiết nữa có vẻ không ổn lắm: dàn diễn viên của SomeClass[] trông không đẹp lắm. May mắn thay, có một giải pháp với thuốc generic.

Giải pháp 2: Sử dụng thuốc generic

Khung Bộ sưu tập sử dụng các kiểu tổng quát để liên kết kiểu và loại bỏ các kiểu ép kiểu đối với chúng trong nhiều hoạt động của nó. Generics cũng có thể được sử dụng ở đây. Hãy lấy java.util.List làm ví dụ .
List list = new ArrayList();
list.add( new SomeClass() );
SomeClass sc = list.get(0); // Error, needs a cast unless...
Dòng thứ ba trong đoạn mã trên sẽ báo lỗi trừ khi bạn cập nhật dòng đầu tiên như thế này:
List<SomeClass> = new ArrayList();
Bạn có thể đạt được kết quả tương tự bằng cách sử dụng generic trong lớp MyArray . Đây là phiên bản mới:
public class MyArray<E> {
    public <E> E[] toArray(Class cls, int size) throws Exception {
        E[] array = (E[])Array.newInstance(cls, size);
        Constructor ctor = cls.getConstructors()[0];
        for ( int element = 0; element < array.length; element++ ) {
            Array.set(array, element, ctor.newInstance());
        }
        return arrayOfGenericType;
    }
}
// ...
MyArray<SomeClass> a1 = new MyArray(SomeClass.class, 32);
SomeClass[] array1 = a1.toArray();
Nó có vẻ tốt. Bằng cách sử dụng generics và bao gồm loại mục tiêu trong khai báo, loại này có thể được suy ra trong các hoạt động khác. Ngoài ra, mã này có thể được giảm xuống còn một dòng bằng cách thực hiện điều này:
SomeClass[] array = new MyArray<SomeClass>(SomeClass.class, 32).toArray();
Nhiệm vụ đã hoàn thành phải không? Vâng, không hẳn. Điều này tốt nếu bạn không quan tâm đến việc bạn gọi hàm tạo của lớp nào, nhưng nếu bạn muốn gọi một hàm tạo cụ thể thì giải pháp này không hoạt động. Bạn có thể tiếp tục sử dụng sự phản chiếu để giải quyết vấn đề này, nhưng khi đó mã sẽ trở nên phức tạp. May mắn thay, có những biểu thức lambda đưa ra một giải pháp khác.

Giải pháp 3: Sử dụng biểu thức lambda

Tôi phải thừa nhận rằng trước đây tôi không đặc biệt hào hứng với các biểu thức lambda, nhưng tôi đã học cách đánh giá cao chúng. Đặc biệt, tôi thích giao diện java.util.stream.Stream , giao diện xử lý các bộ sưu tập đối tượng. Stream đã giúp tôi đạt được niết bàn mảng Java. Đây là nỗ lực đầu tiên của tôi khi sử dụng lambdas:
SomeClass[] array =
    Stream.generate(() -> new SomeClass())
    .toArray(SomeClass[]::new);
Tôi đã chia mã này thành ba dòng để dễ đọc hơn. Bạn có thể thấy rằng nó đáp ứng tất cả các yêu cầu: nó đơn giản và trang nhã, tạo ra một mảng phổ biến các phiên bản đối tượng và cho phép bạn gọi một hàm tạo cụ thể. Hãy chú ý đến tham số phương thức toArray : SomeClass[]::new . Đây là hàm tạo được sử dụng để phân bổ một mảng thuộc loại đã chỉ định. Tuy nhiên, như hiện tại, đoạn mã này có một vấn đề nhỏ: nó tạo ra một mảng có kích thước vô hạn. Điều này không tối ưu lắm. Nhưng vấn đề có thể được giải quyết bằng cách gọi phương thức limit :
SomeClass[] array =
    Stream.generate(() -> new SomeClass())
    .limit(32)   // calling the limit method
    .toArray(SomeClass[]::new);
Mảng bây giờ được giới hạn ở 32 phần tử. Bạn thậm chí có thể đặt các giá trị đối tượng cụ thể cho từng phần tử của mảng, như hiển thị bên dưới:
SomeClass[] array = Stream.generate(() -> {
    SomeClass result = new SomeClass();
    result.val = 16;
    return result;
    })
    .limit(32)
    .toArray(SomeClass[]::new);
Mã này thể hiện sức mạnh của biểu thức lambda, nhưng mã này không gọn gàng hoặc cô đọng. Theo tôi, việc gọi một hàm tạo khác để đặt giá trị sẽ tốt hơn nhiều.
SomeClass[] array6 = Stream.generate( () -> new SomeClass(16) )
    .limit(32)
    .toArray(SomeClass[]::new);
Tôi thích giải pháp dựa trên biểu thức lambda. Thật lý tưởng khi bạn cần gọi một hàm tạo cụ thể hoặc làm việc với từng phần tử của một mảng. Khi tôi cần thứ gì đó đơn giản hơn, tôi thường sử dụng giải pháp dựa trên thuốc generic vì nó đơn giản hơn. Tuy nhiên, bạn có thể tự mình thấy rằng biểu thức lambda cung cấp một giải pháp tinh tế và linh hoạt.

Phần kết luận

Hôm nay chúng ta đã học cách khai báo và phân bổ các mảng nguyên thủy, phân bổ mảng các phần tử Object , sử dụng các biểu thức phản chiếu, tổng quát và lambda trong Java.
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION