ฉันจะคัดลอกคอลเลกชันอย่างปลอดภัยได้อย่างไร


9

ในอดีตฉันได้กล่าวว่าการคัดลอกคอลเลกชันอย่างปลอดภัยทำบางสิ่งเช่น:

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

หรือ

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

แต่ตัวสร้าง "สำเนา" เหล่านี้มีวิธีการสร้างและสตรีมแบบสแตติกที่คล้ายกันปลอดภัยจริงๆและมีการระบุกฎไว้ที่ไหนบ้าง โดยความปลอดภัยฉันหมายถึงการรับประกันความสมบูรณ์แบบพื้นฐานทางความหมายของภาษาจาวาและคอลเลกชันที่บังคับใช้กับผู้โทรที่เป็นอันตรายโดยสมมติว่าได้รับการสนับสนุนจากเหตุผลที่สมเหตุสมผลSecurityManagerและไม่มีข้อบกพร่องใด ๆ

ฉันมีความสุขด้วยวิธีการขว้างปาConcurrentModificationException, NullPointerException, IllegalArgumentException, ClassCastExceptionฯลฯ หรือบางทีอาจจะแขวน

ฉันได้เลือกStringเป็นตัวอย่างของการโต้แย้งประเภทไม่เปลี่ยนรูป สำหรับคำถามนี้ฉันไม่สนใจสำเนาชุดลึกสำหรับคอลเล็กชั่นที่ไม่แน่นอนซึ่งมี gotchas ของตัวเอง

(เพื่อความชัดเจนฉันได้ดูซอร์สโค้ด OpenJDK และมีคำตอบบางอย่างสำหรับArrayListและTreeSet.)


2
คุณหมายถึงอะไรโดยปลอดภัย ? โดยทั่วไปการพูดคลาสในกรอบงานคอลเลกชันมักจะทำงานในทำนองเดียวกันโดยมีข้อยกเว้นที่ระบุใน javadocs ตัวสร้างสำเนานั้นเหมือนกับ "ปลอดภัย" เหมือนกับตัวสร้างอื่น ๆ มีบางสิ่งที่คุณมีอยู่ในใจเพราะถามว่าตัวสร้างคอลเลกชันสำเนามีความปลอดภัยหรือไม่
Kayaman

1
ดีNavigableSetและComparableคอลเลกชันอื่น ๆ ที่ใช้ในบางครั้งสามารถตรวจพบว่าชั้นเรียนไม่ได้ดำเนินการcompareTo()อย่างถูกต้องและโยนข้อยกเว้น มันค่อนข้างชัดเจนว่าคุณหมายถึงอะไรจากข้อโต้แย้งที่ไม่น่าเชื่อถือ คุณหมายถึงงานฝีมือของคนชั่วในการสะสมของ Strings ที่ไม่ดีและเมื่อคุณคัดลอกมันไปยังคอลเล็กชั่นของคุณ ไม่กรอบของคอลเลกชันนั้นค่อนข้างมั่นคงมาตั้งแต่ 1.2
Kayaman

1
@JesseWilson คุณสามารถประนีประนอมมากของคอลเลกชันมาตรฐานโดยไม่ต้องเจาะเข้าไปใน internals ของพวกเขาHashSet(และทุกคอลเลกชันคร่ำเครียดอื่น ๆ ทั่วไป) อาศัยอยู่กับความถูกต้อง / ความสมบูรณ์ของhashCodeการดำเนินงานขององค์ประกอบTreeSetและPriorityQueueขึ้นอยู่กับComparator(และคุณไม่สามารถแม้แต่จะ สร้างสำเนาที่เทียบเท่าโดยไม่ยอมรับตัวเปรียบเทียบที่กำหนดเองหากมี) ให้EnumSetเชื่อมั่นในความสมบูรณ์ของenumประเภทเฉพาะที่ไม่เคยตรวจสอบหลังจากคอมไพล์ดังนั้นไฟล์คลาสที่ไม่ได้สร้างjavacหรือทำด้วยมือสามารถล้มล้างได้
Holger

1
ในตัวอย่างของคุณคุณมีnew TreeSet<>(strs)ที่เป็นstrs NavigableSetนี่ไม่ใช่สำเนาจำนวนมากเนื่องจากผลลัพธ์TreeSetจะใช้ตัวเปรียบเทียบของแหล่งที่มาซึ่งจำเป็นต้องมีเพื่อเก็บความหมายไว้ หากคุณพอใจกับการประมวลผลองค์ประกอบที่toArray()มีอยู่เป็นวิธีที่จะไป มันจะรักษาลำดับการวนซ้ำ เมื่อคุณพอใจกับ“ รับองค์ประกอบตรวจสอบองค์ประกอบใช้องค์ประกอบ” คุณไม่จำเป็นต้องทำสำเนา ปัญหาเริ่มต้นเมื่อคุณต้องการตรวจสอบองค์ประกอบทั้งหมดตามด้วยการใช้องค์ประกอบทั้งหมด จากนั้นคุณไม่สามารถเชื่อถือTreeSetสำเนาเปรียบเทียบที่กำหนดเองได้
Holger

1
การดำเนินการคัดลอกจำนวนมากเท่านั้นที่มีผลกระทบของ a checkcastสำหรับแต่ละองค์ประกอบนั้นขึ้นอยู่toArrayกับประเภทเฉพาะ เรามักจะจบลงที่มัน คอลเลกชันทั่วไปไม่รู้จักประเภทองค์ประกอบจริงดังนั้นตัวสร้างสำเนาของพวกเขาจึงไม่สามารถใช้ฟังก์ชันการทำงานที่คล้ายกันได้ แน่นอนคุณสามารถเลื่อนการตรวจสอบใด ๆ ไปก่อนการใช้งานได้ แต่จากนั้นฉันไม่รู้ว่าคำถามของคุณมีจุดประสงค์อะไร คุณไม่ต้องการ "semantic integrity" เมื่อคุณตรวจสอบและล้มเหลวทันทีก่อนใช้องค์ประกอบ
Holger

คำตอบ:


12

ไม่มีการป้องกันที่แท้จริงสำหรับโค้ดที่ประสงค์ร้ายที่ทำงานอยู่ภายใน JVM เดียวกันใน API ทั่วไปเช่น API การรวบรวม

สามารถแสดงให้เห็นได้อย่างง่ายดาย:

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]

อย่างที่คุณเห็นการคาดหวังว่าList<String>จะไม่รับประกันว่าจะได้รับรายชื่อStringอินสแตนซ์ เนื่องจากการลบประเภทและประเภทดิบจึงไม่มีแม้แต่การแก้ไขในด้านการใช้งานรายการ

สิ่งอื่น ๆ ที่คุณสามารถตำหนิArrayListคอนสตรัค 's สำหรับความไว้วางใจในการเก็บสะสมเข้ามาของtoArrayการดำเนินงาน TreeMapไม่ได้รับผลกระทบในทางเดียวกัน ArrayListแต่เพียงเพราะไม่มีกำไรจากผลการดำเนินงานดังกล่าวจากการส่งผ่านอาร์เรย์เช่นในการก่อสร้างนั้น คลาสทั้งสองไม่รับประกันการป้องกันในตัวสร้าง

โดยปกติแล้วจะไม่มีประเด็นในการพยายามเขียนโค้ดโดยสมมติว่ามีรหัสที่เป็นอันตรายรอบ ๆ มุม มีมากเกินไปที่จะทำเพื่อปกป้องทุกสิ่ง การป้องกันดังกล่าวมีประโยชน์เฉพาะกับรหัสที่ทำในแค็ปซูลการกระทำที่อาจทำให้ผู้เรียกที่เป็นอันตรายสามารถเข้าถึงบางสิ่งได้ แต่ไม่สามารถเข้าถึงได้หากไม่มีรหัสนี้

หากคุณต้องการความปลอดภัยสำหรับรหัสเฉพาะให้ใช้

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

จากนั้นคุณสามารถมั่นใจได้ว่าnewStrsจะมีเพียงสตริงและไม่สามารถแก้ไขได้โดยรหัสอื่นหลังจากการสร้าง

หรือใช้List<String> newStrs = List.of(strs.toArray(new String[0]));กับ Java 9 หรือใหม่กว่า
โปรดทราบว่า Java 10 นั้นList.copyOf(strs)ทำเหมือนกัน แต่เอกสารของมันไม่ได้ระบุว่ารับประกันว่าจะไม่เชื่อถือtoArrayวิธีการรวบรวมข้อมูลที่เข้ามา ดังนั้นการโทรList.of(…)ซึ่งจะทำสำเนาอย่างแน่นอนในกรณีที่ส่งคืนรายการตามอาเรย์นั้นปลอดภัยกว่า

เนื่องจากผู้โทรไม่สามารถเปลี่ยนแปลงวิธีการทำงานได้อาร์เรย์การทิ้งคอลเล็กชันขาเข้าลงในอาร์เรย์แล้วตามด้วยการเติมคอลเล็กชันใหม่ด้วยจะทำให้การคัดลอกปลอดภัยเสมอ เนื่องจากคอลเลกชันสามารถเก็บการอ้างอิงไปยังอาร์เรย์ที่ส่งคืนตามที่แสดงไว้ด้านบนมันจึงสามารถเปลี่ยนแปลงได้ในระหว่างขั้นตอนการคัดลอก แต่จะไม่ส่งผลกระทบต่อการคัดลอกในคอลเลกชัน

ดังนั้นการตรวจสอบความสอดคล้องใด ๆ ควรทำหลังจากองค์ประกอบเฉพาะถูกดึงออกมาจากอาร์เรย์หรือในการรวบรวมผลลัพธ์โดยรวม


2
โมเดลความปลอดภัยของ Java ทำงานโดยการให้รหัสจุดตัดของชุดการอนุญาตของรหัสทั้งหมดในสแต็กดังนั้นเมื่อผู้เรียกรหัสของคุณทำให้โค้ดของคุณทำสิ่งที่ไม่ได้ตั้งใจมันยังคงไม่ได้รับอนุญาตมากกว่าตอนแรก ดังนั้นจะทำให้โค้ดของคุณทำสิ่งที่โค้ดอันตรายสามารถทำได้โดยไม่ต้องใช้โค้ดของคุณเช่นกัน คุณจะต้องทำให้โค้ดที่คุณตั้งใจจะใช้ทำงานหนักขึ้นด้วยสิทธิ์ระดับสูงผ่านทางAccessController.doPrivileged(…)อื่น ๆ แต่ข้อผิดพลาดที่เกี่ยวข้องกับการรักษาความปลอดภัยของแอปเพล็ตทำให้เรามีคำใบ้ว่าเทคโนโลยีนี้ถูกทอดทิ้ง ...
Holger

1
แต่ฉันควรจะแทรก“ ใน API ธรรมดาเช่น Collection API” นั่นคือสิ่งที่ฉันมุ่งเน้นไปที่คำตอบ
Holger

2
ทำไมคุณควรทำให้รหัสของคุณแข็งซึ่งเห็นได้ชัดว่าไม่เกี่ยวข้องกับความปลอดภัยเทียบกับรหัสที่มีสิทธิ์พิเศษซึ่งอนุญาตให้มีการรวบรวมการดำเนินการที่เป็นอันตราย ผู้โทรสมมุตินั้นจะยังคงมีพฤติกรรมที่เป็นอันตรายทั้งก่อนและหลังการเรียกรหัสของคุณ มันจะไม่ได้สังเกตด้วยซ้ำว่าโค้ดของคุณเป็นเพียงพฤติกรรมเดียวที่ถูกต้อง การใช้new ArrayList<>(…)เป็นตัวสร้างการคัดลอกถือว่าใช้งานคอลเลกชันที่ถูกต้อง ไม่ใช่หน้าที่ของคุณที่จะแก้ไขปัญหาด้านความปลอดภัยเมื่อมันสายเกินไป ฮาร์ดแวร์ที่ถูกบุกรุกมีอะไรบ้าง ระบบปฏิบัติการ? มัลติเธรดล่ะ
Holger

2
ฉันไม่สนับสนุน "ไม่มีความปลอดภัย" แต่ความปลอดภัยในสถานที่ที่เหมาะสมแทนที่จะพยายามแก้ไขสภาพแวดล้อมที่แตกหลังจากข้อเท็จจริง มันมีการกล่าวอ้างที่น่าสนใจว่า“ มีคอลเล็กชั่นมากมายที่ใช้ซูเปอร์ไทป์ไม่ถูกต้อง ” แต่มันไปไกลเกินกว่าที่จะขอพิสูจน์ได้ คำถามเดิมได้รับคำตอบอย่างสมบูรณ์; จุดที่คุณนำตอนนี้ไม่เคยเป็นส่วนหนึ่งของมัน ดังที่ได้กล่าวมาแล้วว่าList.copyOf(strs)ไม่ต้องพึ่งพาความถูกต้องของคอลเล็กชั่นที่เข้ามาในราคาที่เห็นได้ชัด ArrayListเป็นการประนีประนอมที่สมเหตุสมผลสำหรับทุกวัน
Holger

4
มันชัดเจนบอกว่าไม่มีข้อกำหนดดังกล่าวสำหรับ "วิธีการสร้างและสตรีมแบบคงที่ที่คล้ายกันทั้งหมด" ดังนั้นถ้าคุณต้องการที่จะปลอดภัยอย่างแน่นอนคุณจะต้องเรียกtoArray()ตัวเองเพราะไม่สามารถอาร์เรย์มีพฤติกรรมแทนที่ตามด้วยการสร้างสำเนาคอลเลกชันของอาร์เรย์เช่นหรือnew ArrayList<>(Arrays.asList( strs.toArray(new String[0]))) List.of(strs.toArray(new String[0]))ทั้งสองยังมีผลข้างเคียงของการบังคับใช้ประเภทองค์ประกอบ โดยส่วนตัวฉันไม่คิดว่าพวกเขาจะยอมให้มีcopyOfการประนีประนอมกับคอลเลกชันที่ไม่เปลี่ยนรูป แต่ทางเลือกมีอยู่ในคำตอบ
Holger

1

ฉันอยากจะทิ้งข้อมูลนี้ไว้ในความคิดเห็น แต่ฉันไม่มีชื่อเสียงพอขอโทษ :) ฉันจะพยายามอธิบายให้มากที่สุดเท่าที่จะทำได้

แทนที่จะเป็นบางสิ่งอย่างเช่นโมดิconstฟายเออร์ที่ใช้ใน C ++ เพื่อทำเครื่องหมายฟังก์ชั่นสมาชิกที่ไม่ควรแก้ไขเนื้อหาของวัตถุ แต่เดิมทีใน Java นั้นใช้แนวคิดของ "การไม่เปลี่ยนแปลง" Encapsulation (หรือ OCP, Open-Closed Principle) ควรที่จะป้องกันการกลายพันธุ์ (การเปลี่ยนแปลง) ของวัตถุที่ไม่คาดคิด แน่นอนว่าการสะท้อน API นั้นเดินไปรอบ ๆ การเข้าถึงหน่วยความจำโดยตรงไม่เหมือนกัน; นั่นเป็นเรื่องของการยิงขาของตัวเอง :)

java.util.Collectionตัวเองเป็นอินเตอร์เฟซที่ไม่แน่นอน: มันมีaddวิธีการที่ควรจะแก้ไขการเก็บรวบรวม แน่นอนว่าโปรแกรมเมอร์อาจห่อรวบรวมสิ่งที่จะโยน ... และข้อยกเว้นรันไทม์ทั้งหมดจะเกิดขึ้นเพราะโปรแกรมเมอร์คนอื่นไม่สามารถอ่าน javadoc ซึ่งเห็นได้ชัดว่าคอลเลกชันนั้นไม่เปลี่ยนรูป

ฉันตัดสินใจใช้java.util.Iterabletype เพื่อแสดงการรวบรวมที่ไม่เปลี่ยนแปลงในอินเตอร์เฟสของฉัน ความหมายIterableไม่มีลักษณะของการรวบรวมว่า "ไม่แน่นอน" ยังคุณจะ (ส่วนใหญ่) จะสามารถปรับเปลี่ยนคอลเลกชันพื้นฐานผ่านสตรีม


JIC เพื่อแสดงแผนที่ในลักษณะที่ไม่เปลี่ยนรูปแบบjava.util.Function<K,V>สามารถใช้ ( getวิธีของแผนที่ตรงกับคำจำกัดความนี้)


แนวคิดของอินเทอร์เฟซสำหรับอ่านอย่างเดียวและความไม่สามารถเปลี่ยนได้คือ orthogonal จุด C ++ และ C คือการที่พวกเขาไม่สนับสนุนความสมบูรณ์ของความหมาย คัดลอกอาร์กิวเมนต์วัตถุ / struct - const & คือการเพิ่มประสิทธิภาพซึ่งหลบสำหรับที่ หากคุณผ่านไปIteratorแล้วนั่นเป็นการบังคับให้สำเนาทวนเข็มนาฬิกาเป็นองค์ประกอบ แต่มันก็ไม่ดี การใช้forEachRemaining/ forEachเห็นได้ชัดว่าเป็นหายนะที่สมบูรณ์ (ฉันต้องพูดถึงว่าIteratorมีremoveวิธีการ)
Tom Hawtin - tackline

หากดูที่ห้องสมุดคอลเลกชัน Scala มีความแตกต่างที่เข้มงวดระหว่างอินเทอร์เฟซที่ไม่แน่นอนและไม่เปลี่ยนรูปได้ แม้ว่า (ฉันคิดว่า) มันถูกสร้างขึ้นมาเพราะเหตุผลที่แตกต่างกันโดยสิ้นเชิง แต่ก็ยังเป็นการสาธิตถึงความปลอดภัยที่สามารถทำได้ อินเทอร์เฟซแบบอ่านอย่างเดียวในเชิงความหมายถือว่าไม่สามารถเปลี่ยนได้นั่นคือสิ่งที่ฉันพยายามจะพูด (ฉันเห็นด้วยกับการIterableที่ไม่ได้เปลี่ยนแปลงไม่ได้จริง ๆ แต่ไม่เห็นปัญหาใด ๆforEach*)
Alexander
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.