Làm thế nào tôi có thể sao chép bộ sưu tập một cách an toàn?


9

Trước đây, tôi từng nói sẽ sao chép một cách an toàn một bộ sưu tập làm một việc như:

public static void doThing(List<String> strs) {
    List<String> newStrs = new ArrayList<>(strs);

hoặc là

public static void doThing(NavigableSet<String> strs) {
    NavigableSet<String> newStrs = new TreeSet<>(strs);

Nhưng các hàm tạo "sao chép" này, các phương thức và luồng tạo tĩnh tương tự, có thực sự an toàn không và các quy tắc được chỉ định ở đâu? Nói một cách an toàn, ý tôi là các bảo đảm toàn vẹn ngữ nghĩa cơ bản được cung cấp bởi ngôn ngữ Java và các bộ sưu tập được thi hành chống lại một người gọi độc hại, giả sử được hỗ trợ bởi một hợp lý SecurityManagervà không có sai sót.

Tôi hài lòng với phương pháp ném ConcurrentModificationException, NullPointerException, IllegalArgumentException, ClassCastException, vv, hoặc thậm chí treo.

Tôi đã chọn Stringlàm một ví dụ về một đối số loại bất biến. Đối với câu hỏi này, tôi không quan tâm đến các bản sao sâu cho các bộ sưu tập các loại có thể thay đổi có các vấn đề riêng.

(Để rõ ràng, tôi đã xem mã nguồn OpenJDK và có một số loại câu trả lời cho ArrayListTreeSet.)


2
Bạn có ý nghĩa gì bởi an toàn ? Nói chung, các lớp trong khung bộ sưu tập có xu hướng hoạt động tương tự, với các ngoại lệ được chỉ định trong javadocs. Các hàm tạo sao chép cũng "an toàn" như mọi hàm tạo khác. Có một điều đặc biệt nào bạn có trong đầu, bởi vì hỏi liệu một nhà xây dựng bản sao bộ sưu tập có an toàn nghe có vẻ rất cụ thể không?
Kayaman

1
Chà, NavigableSetvà các Comparablebộ sưu tập dựa trên khác đôi khi có thể phát hiện nếu một lớp không thực hiện compareTo()đúng và đưa ra một ngoại lệ. Có một chút không rõ ý của bạn là gì bởi những lý lẽ không đáng tin cậy. Bạn có nghĩa là một kẻ bất lương thủ công một bộ sưu tập các chuỗi xấu và khi bạn sao chép chúng vào bộ sưu tập của bạn thì điều gì đó xấu xảy ra? Không, khung bộ sưu tập khá chắc chắn, nó đã có từ ngày 1.2.
Kayaman

1
@JesseWilson bạn có thể thỏa hiệp rất nhiều bộ sưu tập tiêu chuẩn mà không xâm nhập vào phần bên trong của chúng, HashSet(và tất cả các bộ sưu tập băm khác nói chung) phụ thuộc vào tính chính xác / tính toàn vẹn của việc hashCodetriển khai các phần tử TreeSetPriorityQueuephụ thuộc vào Comparator(và thậm chí bạn không thể tạo một bản sao tương đương mà không chấp nhận bộ so sánh tùy chỉnh nếu có), EnumSettin tưởng vào tính toàn vẹn của enumloại cụ thể không bao giờ được xác minh sau khi biên dịch, do đó, một tệp lớp, không được tạo javachoặc làm thủ công, có thể lật đổ nó.
Holger

1
Trong ví dụ của bạn, bạn có new TreeSet<>(strs)nơi strslà a NavigableSet. Đây không phải là một bản sao số lượng lớn, vì kết quả TreeSetsẽ sử dụng bộ so sánh của nguồn, thậm chí còn cần thiết để giữ lại ngữ nghĩa. Nếu bạn ổn chỉ với việc xử lý các yếu tố có trong đó, toArray()là cách để đi; nó thậm chí sẽ giữ thứ tự lặp. Khi bạn ổn với phần tử lấy, xác thực phần tử, sử dụng phần tử, bạn thậm chí không cần tạo một bản sao. Các vấn đề bắt đầu khi bạn muốn xác minh tất cả các yếu tố, tiếp theo là sử dụng tất cả các yếu tố. Sau đó, bạn không thể tin tưởng vào một bộ TreeSetso sánh tùy chỉnh sao chép
Holger

1
Hoạt động sao chép số lượng lớn duy nhất có hiệu lực của checkcastmỗi phần tử, toArrayvới một loại cụ thể. Chúng tôi luôn luôn kết thúc ở đó. Các bộ sưu tập chung thậm chí không biết loại phần tử thực tế của chúng, vì vậy các hàm tạo sao chép của chúng không thể cung cấp chức năng tương tự. Tất nhiên, bạn có thể trì hoãn bất kỳ kiểm tra nào để sử dụng đúng trước đó, nhưng sau đó, tôi không biết câu hỏi của bạn đang nhắm đến là gì. Bạn không cần "tính toàn vẹn ngữ nghĩa", khi bạn ổn với việc kiểm tra và thất bại ngay lập tức trước khi sử dụng các yếu tố.
Holger

Câu trả lời:


12

Không có sự bảo vệ thực sự chống lại mã độc hại cố ý chạy trong cùng một JVM trong các API thông thường, như API Bộ sưu tập.

Như có thể dễ dàng được chứng minh:

public static void main(String[] args) throws InterruptedException {
    Object[] array = { "foo", "bar", "baz", "and", "another", "string" };
    array[array.length - 1] = new Object() {
        @Override
        public String toString() {
            Collections.shuffle(Arrays.asList(array));
            return "string";
        }
    };
    doThing(new ArrayList<String>() {
        @Override public Object[] toArray() {
            return array;
        }
    });
}

public static void doThing(List<String> strs) {
    List<String> newStrs = new ArrayList<>(strs);

    System.out.println("made a safe copy " + newStrs);
    for(int i = 0; i < 10; i++) {
        System.out.println(newStrs);
    }
}
made a safe copy [foo, bar, baz, and, another, string]
[bar, and, string, string, another, foo]
[and, baz, bar, string, string, string]
[another, baz, and, foo, bar, string]
[another, bar, and, foo, string, and]
[another, baz, string, another, and, foo]
[string, and, another, foo, string, foo]
[baz, string, foo, and, baz, string]
[bar, another, string, and, another, baz]
[bar, string, foo, string, baz, and]
[bar, string, bar, another, and, foo]

Như bạn có thể thấy, hy vọng List<String>không đảm bảo sẽ thực sự có được một danh sách các Stringtrường hợp. Do loại xóa và loại thô, thậm chí không thể khắc phục được về phía thực hiện danh sách.

Một điều khác, bạn có thể đổ lỗi cho nhà ArrayListxây dựng của mình, là sự tin tưởng vào việc toArraytriển khai bộ sưu tập sắp tới . TreeMapkhông bị ảnh hưởng theo cùng một cách, nhưng chỉ vì không có hiệu suất như vậy khi truyền mảng, như khi xây dựng một ArrayList. Cả hai lớp đều không đảm bảo sự bảo vệ trong hàm tạo.

Thông thường, không có điểm nào trong việc cố gắng viết mã giả định mã độc cố ý xung quanh mọi góc. Có quá nhiều thứ có thể làm, để bảo vệ chống lại mọi thứ. Việc bảo vệ như vậy chỉ hữu ích đối với mã thực sự gói gọn một hành động có thể cho phép người gọi độc hại truy cập vào một cái gì đó, nó không thể truy cập mà không có mã này.

Nếu bạn cần sự an toàn cho một mã cụ thể, hãy sử dụng

public static void doThing(List<String> strs) {
    String[] content = strs.toArray(new String[0]);
    List<String> newStrs = new ArrayList<>(Arrays.asList(content));

    System.out.println("made a safe copy " + newStrs);
    for(int i = 0; i < 10; i++) {
        System.out.println(newStrs);
    }
}

Sau đó, bạn có thể chắc chắn rằng newStrsnó chỉ chứa các chuỗi và không thể được sửa đổi bởi mã khác sau khi xây dựng nó.

Hoặc sử dụng List<String> newStrs = List.of(strs.toArray(new String[0]));với Java 9 hoặc mới hơn
Lưu ý rằng Java 10 cũng List.copyOf(strs)làm như vậy, nhưng tài liệu của nó không nói rằng nó được đảm bảo không tin vào toArrayphương thức của bộ sưu tập đến . Vì vậy, việc gọi List.of(…), chắc chắn sẽ tạo một bản sao trong trường hợp nó trả về một danh sách dựa trên mảng, sẽ an toàn hơn.

Vì không có người gọi nào có thể thay đổi cách thức, các mảng hoạt động, bỏ bộ sưu tập đến vào một mảng, sau đó điền vào bộ sưu tập mới với nó, sẽ luôn làm cho bản sao an toàn. Vì bộ sưu tập có thể giữ một tham chiếu đến mảng được trả về như đã trình bày ở trên, nên nó có thể thay đổi nó trong giai đoạn sao chép, nhưng nó không thể ảnh hưởng đến bản sao trong bộ sưu tập.

Vì vậy, bất kỳ kiểm tra tính nhất quán nên được thực hiện sau khi phần tử cụ thể đã được truy xuất từ ​​mảng hoặc trên toàn bộ bộ sưu tập kết quả.


2
Mô hình bảo mật của Java hoạt động bằng cách cấp mã cho giao điểm của các bộ quyền của tất cả mã trên ngăn xếp, do đó, khi người gọi mã của bạn làm cho mã của bạn làm những việc ngoài ý muốn, nó vẫn không nhận được nhiều quyền hơn so với ban đầu. Vì vậy, nó chỉ làm cho mã của bạn làm những việc mà mã độc có thể đã làm mà không có mã của bạn. Bạn chỉ phải làm cứng mã mà bạn định chạy với các đặc quyền nâng cao thông qua AccessController.doPrivileged(…)vv. Nhưng danh sách dài các lỗi liên quan đến bảo mật applet cho chúng tôi một gợi ý tại sao công nghệ này đã bị bỏ rơi
Holger

1
Nhưng tôi nên chèn vào các API thông thường như API API Bộ sưu tập, vì đó là điều tôi đang tập trung vào câu trả lời.
Holger

2
Tại sao bạn nên làm cứng mã của mình, dường như không liên quan đến bảo mật, chống lại mã đặc quyền cho phép triển khai bộ sưu tập độc hại? Người gọi giả định đó vẫn sẽ phải chịu hành vi độc hại trước và sau khi gọi mã của bạn. Nó thậm chí sẽ không nhận thấy rằng mã của bạn là người duy nhất hành xử đúng. Sử dụng new ArrayList<>(…)như constructor sao chép là tốt giả sử thực hiện bộ sưu tập chính xác. Bạn không có nghĩa vụ phải khắc phục các sự cố bảo mật khi đã quá muộn. Phần cứng bị xâm nhập thì sao? Hệ điều hành? Làm thế nào về đa luồng?
Holger

2
Tôi không ủng hộ việc không có bảo mật, nhưng bảo mật ở đúng nơi, thay vì cố gắng khắc phục một môi trường bị hỏng sau thực tế. Đó là một tuyên bố thú vị rằng có rất nhiều bộ sưu tập không thực hiện chính xác các siêu tập của họ nhưng nó đã đi quá xa, để yêu cầu chứng minh, mở rộng điều này hơn nữa. Câu hỏi ban đầu đã được trả lời hoàn toàn; những điểm bạn đang mang đến bây giờ không bao giờ là một phần của nó. Như đã nói, List.copyOf(strs)không dựa vào tính đúng đắn của bộ sưu tập đến trong vấn đề đó, với mức giá rõ ràng. ArrayListlà một sự thỏa hiệp hợp lý cho hàng ngày.
Holger

4
Nó nói rõ rằng không có thông số kỹ thuật như vậy, đối với tất cả các phương thức tạo tĩnh tương tự và dòng Stream. Vì vậy, nếu bạn muốn an toàn tuyệt đối, bạn phải tự gọi toArray()mình, vì các mảng không thể có hành vi bị ghi đè, tiếp theo là tạo một bản sao bộ sưu tập của mảng, như new ArrayList<>(Arrays.asList( strs.toArray(new String[0])))hoặc List.of(strs.toArray(new String[0])). Cả hai cũng có tác dụng phụ của việc thực thi loại phần tử. Cá nhân tôi không nghĩ họ sẽ cho phép copyOfthỏa hiệp các bộ sưu tập bất biến, nhưng câu trả lời thay thế là có, trong câu trả lời.
Holger

1

Tôi muốn để lại thông tin này trong bình luận, nhưng tôi không có đủ danh tiếng, xin lỗi :) Tôi sẽ cố gắng giải thích nó dài dòng nhất có thể sau đó.

Thay vì một cái gì đó như const sửa đổi được sử dụng trong C ++ để đánh dấu các hàm thành viên không được phép sửa đổi nội dung đối tượng, trong Java ban đầu được sử dụng khái niệm "bất biến". Đóng gói (hoặc OCP, Nguyên tắc đóng mở) được cho là để bảo vệ chống lại mọi đột biến (thay đổi) bất ngờ của một đối tượng. Tất nhiên API phản chiếu đi xung quanh điều này; truy cập bộ nhớ trực tiếp làm như vậy; đó là nhiều hơn về chụp chân của chính mình :)

java.util.Collection chính nó là giao diện có thể thay đổi: nó có add phương thức được cho là sửa đổi bộ sưu tập. Tất nhiên, lập trình viên có thể gói bộ sưu tập vào thứ gì đó sẽ ném ... và tất cả các ngoại lệ thời gian chạy sẽ xảy ra do một lập trình viên khác không thể đọc javadoc, trong đó nói rõ rằng bộ sưu tập là bất biến.

Tôi quyết định sử dụng java.util.Iterableloại để trưng bày bộ sưu tập bất biến trong các giao diện của mình. Về mặt ngữ nghĩa Iterablekhông có đặc tính của bộ sưu tập là "tính đột biến". Tuy nhiên, rất có thể bạn sẽ có thể sửa đổi các bộ sưu tập cơ bản thông qua các luồng.


JIC, để hiển thị bản đồ theo cách không thay đổi java.util.Function<K,V>có thể được sử dụng ( getphương pháp của bản đồ phù hợp với định nghĩa này)


Các khái niệm về giao diện chỉ đọc và tính bất biến là trực giao. Quan điểm của C ++ và C là chúng không hỗ trợ tính toàn vẹn ngữ nghĩa . Đối tượng cũng sao chép đối tượng / struct - const & là một tối ưu hóa tinh ranh cho điều đó. Nếu bạn đã vượt qua Iteratorthì điều đó thực tế buộc một bản sao nguyên tố, nhưng điều đó không tốt. Sử dụng forEachRemaining/ forEachrõ ràng sẽ là một thảm họa hoàn chỉnh. (Tôi cũng phải đề cập rằng Iteratorcó một removephương pháp.)
Tom Hawtin - tackline

Nếu nhìn vào thư viện bộ sưu tập Scala, có sự phân biệt nghiêm ngặt giữa các giao diện có thể thay đổi và bất biến. Mặc dù (tôi cho rằng) nó đã được thực hiện vì những lý do hoàn toàn khác nhau, nhưng vẫn là một minh chứng về cách an toàn có thể đạt được. Giao diện chỉ đọc về mặt ngữ nghĩa giả định tính bất biến, đó là những gì tôi đang cố gắng nói. (Tôi đồng ý về việc Iterablekhông thực sự bất biến, nhưng không thấy có vấn đề gì với forEach*)
Alexander
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.