ทำไม String.chars () กระแสของ ints ใน Java 8


198

ใน Java 8 มีวิธีการใหม่String.chars()ที่ส่งกลับกระแสของints ( IntStream) ที่แสดงถึงรหัสตัวละคร ฉันเดาว่าหลายคนคงคาดหวังกระแสของcharที่นี่แทน อะไรคือแรงจูงใจในการออกแบบ API ด้วยวิธีนี้


4
@RohitJain ฉันไม่ได้หมายถึงสตรีมใดโดยเฉพาะ หากCharStreamไม่มีอยู่สิ่งที่จะเป็นปัญหาในการเพิ่มหรือไม่
Adam Dyga

5
@ AdamDyga: นักออกแบบเลือกที่จะหลีกเลี่ยงการระเบิดของคลาสและวิธีการโดยการ จำกัด การสตรีมดั้งเดิมเป็น 3 ประเภทเนื่องจากประเภทอื่น (char, short, float) สามารถแสดงได้โดยเทียบเท่าขนาดใหญ่กว่า (int, double) โดยไม่มีนัยสำคัญใด ๆ โทษประสิทธิภาพ
JB Nizet

3
@JBNizet ฉันเข้าใจแล้ว แต่มันก็ยังรู้สึกเหมือนเป็นทางออกที่สกปรกเพียงเพื่อรักษาชั้นเรียนใหม่สองสามชั้น
Adam Dyga

9
@JB nizet: สำหรับผมแล้วมันดูเหมือนว่าเราอยู่แล้วมีการระเบิดของอินเตอร์เฟซได้รับกระแสทุกการบรรทุกเกินพิกัดเช่นเดียวกับอินเตอร์เฟซฟังก์ชั่นทั้งหมด ...
โฮล

5
ใช่มีการระเบิดอยู่แล้วแม้จะมีความเชี่ยวชาญเฉพาะทางสายน้ำสามแบบเท่านั้น จะเป็นอย่างไรถ้าทั้งแปดดั้งเดิมมีสายอาชีพเฉพาะทาง กลียุค :-)
Stuart Marks

คำตอบ:


218

ดังที่คนอื่น ๆ ได้กล่าวไปแล้วการตัดสินใจออกแบบด้านหลังนี้ก็เพื่อป้องกันการระเบิดของวิธีการและชั้นเรียน

ถึงกระนั้นโดยส่วนตัวฉันคิดว่านี่เป็นการตัดสินใจที่แย่มากและควรมีเพราะพวกเขาไม่ต้องการที่จะทำCharStreamซึ่งมีเหตุผลและวิธีการที่แตกต่างกันแทนchars()ฉันจะคิดว่า:

  • Stream<Character> chars()ที่ให้กระแสของตัวละครกล่องซึ่งจะมีโทษประสิทธิภาพแสงบางอย่าง
  • IntStream unboxedChars()ซึ่งจะใช้สำหรับรหัสประสิทธิภาพ

อย่างไรก็ตามแทนที่จะมุ่งเน้นไปที่สาเหตุที่ทำแบบนี้ในปัจจุบันฉันคิดว่าคำตอบนี้ควรมุ่งเน้นไปที่การแสดงวิธีการทำกับ API ที่เราได้รับกับ Java 8

ใน Java 7 ฉันจะทำแบบนี้:

for (int i = 0; i < hello.length(); i++) {
    System.out.println(hello.charAt(i));
}

และฉันคิดว่าวิธีการที่สมเหตุสมผลใน Java 8 มีดังต่อไปนี้:

hello.chars()
        .mapToObj(i -> (char)i)
        .forEach(System.out::println);

ที่นี่ฉันได้รับIntStreamและแมปไปยังวัตถุผ่านแลมบ์ดาi -> (char)iซึ่งจะทำกล่องให้โดยอัตโนมัติStream<Character>และจากนั้นเราสามารถทำสิ่งที่เราต้องการและยังคงใช้วิธีการอ้างอิงเป็นข้อดี

ระวังแม้ว่าคุณจะต้องทำmapToObjถ้าคุณลืมและใช้mapแล้วจะไม่มีอะไรบ่น แต่คุณจะยังคงจบลงด้วยการIntStreamและคุณอาจจะสงสัยว่าทำไมมันพิมพ์ค่าจำนวนเต็มแทนสตริงที่เป็นตัวแทนของตัวละคร

ทางเลือกที่น่าเกลียดอื่น ๆ สำหรับ Java 8:

เมื่ออยู่ในIntStreamและต้องการพิมพ์ในที่สุดคุณจะไม่สามารถใช้วิธีการอ้างอิงอีกต่อไปสำหรับการพิมพ์:

hello.chars()
        .forEach(i -> System.out.println((char)i));

ยิ่งกว่านั้นการใช้วิธีการอ้างอิงถึงวิธีการของคุณเองไม่ทำงานอีกต่อไป! พิจารณาสิ่งต่อไปนี้:

private void print(char c) {
    System.out.println(c);
}

แล้ว

hello.chars()
        .forEach(this::print);

สิ่งนี้จะทำให้เกิดข้อผิดพลาดในการคอมไพล์เนื่องจากอาจมีการแปลงแบบเสีย

สรุป:

API ที่ได้รับการออกแบบด้วยวิธีนี้เพราะไม่ต้องการที่จะเพิ่มCharStreamผมเองคิดว่าวิธีการที่จะกลับมาStream<Character>และการแก้ปัญหาในขณะนี้คือการใช้mapToObj(i -> (char)i)บนIntStreamเพื่อให้สามารถทำงานได้อย่างถูกต้องกับพวกเขา


7
บทสรุปของฉัน: ส่วนหนึ่งของ API นี้ขาดจากการออกแบบ แต่ขอบคุณสำหรับคำตอบที่กว้างขวาง
Adam Dyga

27
+1 แต่ข้อเสนอของฉันคือการใช้codePoints()แทนchars()และคุณจะพบมากของฟังก์ชั่นห้องสมุดแล้วยอมรับintสำหรับจุดรหัสนอกจากนี้ยังcharเช่นวิธีการทั้งหมดของjava.lang.Characterเช่นเดียวกับStringBuilder.appendCodePointฯลฯ jdk1.5การสนับสนุนนี้มีอยู่ตั้งแต่
Holger

6
จุดดีเกี่ยวกับจุดรหัส ใช้พวกเขาจะจัดการกับตัวละครเสริมซึ่งจะแสดงเป็นคู่ตัวแทนในหรือString char[]ฉันพนันได้เลยว่าcharการประมวลผลรหัสส่วนใหญ่ผิดพลาดคู่แทน
Stuart Marks

2
@skiwi กำหนดvoid print(int ch) { System.out.println((char)ch); }และจากนั้นคุณสามารถใช้การอ้างอิงวิธีการ
Stuart Marks

2
ดูคำตอบของฉันสำหรับสาเหตุที่Stream<Character>ถูกปฏิเสธ
Stuart Marks

90

คำตอบจาก skiwiปกคลุมหลายจุดสำคัญแล้ว ฉันจะเติมพื้นหลังอีกเล็กน้อย

การออกแบบของ API ใด ๆ คือชุดของการแลกเปลี่ยน ใน Java ปัญหาหนึ่งที่ยากคือการตัดสินใจเกี่ยวกับการออกแบบที่ทำมานานแล้ว

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

นี่คือการแลกเปลี่ยนที่เรายังคงอยู่กับวันนี้เกือบ 20 ปีต่อมา ฟีเจอร์ autoboxing ที่เพิ่มเข้ามาใน Java 5 ส่วนใหญ่ไม่จำเป็นต้องถ่วงซอร์สโค้ดด้วยการชกมวยและการเรียกเมธอด unboxing แต่โอเวอร์เฮดยังคงอยู่ที่นั่น ในหลายกรณีไม่สามารถสังเกตได้ อย่างไรก็ตามหากคุณต้องทำการชกมวยหรือ unboxing ภายในลูปด้านในคุณจะเห็นว่ามันสามารถกำหนดซีพียูและค่าใช้จ่ายในการรวบรวมขยะจำนวนมากได้

เมื่อออกแบบ Streams API เป็นที่ชัดเจนว่าเราต้องให้การสนับสนุนแบบดั้งเดิม ค่าใช้จ่ายมวย / unboxing จะฆ่าผลประโยชน์ใด ๆ จากการขนาน เราไม่ต้องการสนับสนุนการใช้งานดั้งเดิมทั้งหมดเนื่องจากจะเพิ่มความยุ่งเหยิงจำนวนมากให้กับ API (คุณเห็นการใช้งานจริงShortStreamหรือไม่?) "ทั้งหมด" หรือ "ไม่มี" เป็นสถานที่ที่สะดวกสบายสำหรับการออกแบบ แต่ก็ไม่เป็นที่ยอมรับ ดังนั้นเราต้องหาค่าที่สมเหตุสมผลของ "บางอย่าง" เราจบลงด้วยการที่มีความเชี่ยวชาญดั้งเดิมสำหรับint, และlong double(โดยส่วนตัวแล้วฉันจะออกไปintแต่นั่นเป็นเพียงฉัน)

สำหรับCharSequence.chars()เราถือว่าการกลับมาStream<Character>(ต้นแบบต้นอาจนำมาใช้นี้) แต่มันถูกปฏิเสธเพราะค่าใช้จ่ายในการชกมวย เมื่อพิจารณาว่าสายอักขระมีcharค่าเป็นพื้นฐานมันก็ดูเหมือนจะเป็นความผิดพลาดที่จะกำหนดมวยโดยไม่มีเงื่อนไขเมื่อผู้โทรอาจจะทำการประมวลผลเล็กน้อยเกี่ยวกับค่าและ unbox กลับเข้าไปในสตริง

นอกจากนี้เรายังพิจารณาถึงCharStreamความเชี่ยวชาญดั้งเดิม แต่ดูเหมือนว่าการใช้งานจะค่อนข้างแคบเมื่อเทียบกับจำนวนมากที่จะเพิ่มใน API ดูเหมือนว่าจะไม่คุ้มที่จะเพิ่ม

บทลงโทษนี้จะเรียกผู้เรียกว่าพวกเขาต้องรู้ว่าค่าที่IntStreamบรรจุcharเป็นตัวแทนintsและการคัดเลือกนั้นจะต้องทำในสถานที่ที่เหมาะสม สิ่งนี้ทำให้เกิดความสับสนเป็นสองเท่าเนื่องจากมีการเรียก API มากเกินไปPrintStream.print(char)และมีความPrintStream.print(int)แตกต่างอย่างชัดเจนในพฤติกรรมของพวกเขา อาจมีความสับสนเกิดขึ้นเนื่องจากการcodePoints()โทรกลับมาIntStreamแต่ค่าที่มีอยู่นั้นค่อนข้างแตกต่างกัน

ดังนั้นสิ่งนี้จึงทำให้คุณเลือกใช้งานได้ในหลายทางเลือก:

  1. เราไม่สามารถให้บริการเฉพาะทางแบบดั้งเดิมส่งผลให้ API ที่เรียบง่ายสง่างามและสอดคล้องกัน แต่กำหนดให้มีประสิทธิภาพสูงและค่าใช้จ่าย GC;

  2. เราสามารถจัดเตรียมความเชี่ยวชาญเฉพาะทางแบบครบวงจรในราคาที่ทำให้เกิดความยุ่งเหยิงของ API และการกำหนดภาระการบำรุงรักษาให้กับนักพัฒนา JDK หรือ

  3. เราสามารถจัดหาชุดย่อยของความเชี่ยวชาญดั้งเดิมให้ขนาด API ที่มีประสิทธิภาพสูงปานกลางซึ่งกำหนดภาระค่อนข้างน้อยสำหรับผู้โทรในกรณีการใช้งานที่ค่อนข้าง จำกัด (การประมวลผลถ่าน)

เราเลือกอันสุดท้าย


1
คำตอบที่ดี! แต่ก็ไม่ได้คำตอบว่าทำไมไม่สามารถมีสองวิธีที่แตกต่างกันสำหรับchars()หนึ่งที่ผลตอบแทนStream<Character>(มีโทษประสิทธิภาพเล็ก) และความเป็นอยู่อื่น ๆ ที่IntStreamได้รับนี้ถือว่ายัง? มีโอกาสมากที่ผู้คนจะจบลงด้วยการแมปกับมันStream<Character>หากพวกเขาคิดว่าความเชื่อมั่นนั้นคุ้มค่ากับโทษปรับประสิทธิภาพ
skiwi

3
Minimalism เข้ามาที่นี่ หากมีchars()วิธีการที่ส่งคืนค่าถ่านในตัวIntStreamมันจะไม่เพิ่มการเรียก API อีกครั้งที่ได้รับค่าเดียวกัน แต่อยู่ในรูปแบบกล่อง ผู้เรียกใช้สามารถใส่ค่าได้โดยไม่มีปัญหา แน่นอนว่ามันจะสะดวกกว่าที่ไม่ต้องทำในกรณีนี้ (อาจเป็นของหายาก) แต่มีค่าใช้จ่ายในการเพิ่มความยุ่งเหยิงให้กับ API
Stuart Marks

5
ขอบคุณคำถามที่ซ้ำกันฉันสังเกตเห็นนี้ ฉันยอมรับว่าการchars()กลับมาIntStreamไม่ใช่ปัญหาใหญ่โดยเฉพาะอย่างยิ่งเนื่องจากความจริงที่ว่าวิธีนี้มันไม่ค่อยได้ใช้เลย แต่มันจะดีที่จะมีวิธีในตัวการแปลงกลับไปIntStream Stringมันสามารถทำได้ด้วย.reduce(StringBuilder::new, (sb, c) -> sb.append((char)c), StringBuilder::append).toString()แต่มันยาวจริงๆ
Tagir Valeev

7
@TagirValeev ใช่มันค่อนข้างยุ่งยาก กับกระแสของจุดรหัส (เป็น IntStream) collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString()มันไม่ได้เลวร้ายเกินไป: ฉันเดาว่ามันไม่ได้สั้นกว่านี้จริง ๆ แต่การใช้จุดรหัสจะหลีกเลี่ยงการ(char)ปลดเปลื้องและอนุญาตให้ใช้การอ้างอิงวิธีการ รวมทั้งจัดการกับตัวแทนเสมือนอย่างถูกต้อง
Stuart Marks

2
แต่น่าเสียดายที่ @IlyaBystrov ลำธารดั้งเดิมเช่นIntStreamไม่ได้มีวิธีการที่ใช้collect() Collectorพวกเขามีเพียงสามcollect()วิธีหาเรื่องตามที่ระบุไว้ในความคิดเห็นก่อนหน้า
Stuart Marks
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.