Java 8 เป็นวิธีที่ดีในการทำซ้ำค่าหรือฟังก์ชันหรือไม่?


118

ในภาษาอื่น ๆ อีกมากมายเช่น Haskell เป็นเรื่องง่ายที่จะทำซ้ำค่าหรือฟังก์ชันหลาย ๆ ครั้งเช่น เพื่อรับรายการ 8 สำเนาของค่า 1:

take 8 (repeat 1)

แต่ฉันยังไม่พบสิ่งนี้ใน Java 8 มีฟังก์ชันดังกล่าวใน JDK ของ Java 8 หรือไม่?

หรืออีกทางเลือกหนึ่งที่เทียบเท่ากับช่วงเช่น

[1..8]

ดูเหมือนว่าจะแทนที่คำสั่ง verbose ใน Java ได้อย่างชัดเจนเช่น

for (int i = 1; i <= 8; i++) {
    System.out.println(i);
}

จะมีบางอย่างเช่น

Range.from(1, 8).forEach(i -> System.out.println(i))

แม้ว่าตัวอย่างนี้จะดูไม่กระชับเท่าไหร่นัก ... แต่หวังว่าจะอ่านได้ง่ายขึ้น


2
คุณศึกษาStreams API แล้วหรือยัง นั่นควรเป็นทางออกที่ดีที่สุดของคุณเท่าที่ JDK เกี่ยวข้อง มันมีฟังก์ชันrangeนั่นคือสิ่งที่ฉันพบจนถึงตอนนี้
Marko Topolnik

1
@MarkoTopolnik คลาส Streams ถูกลบออก (แม่นยำกว่านั้นมันถูกแบ่งออกเป็นคลาสอื่น ๆ และวิธีการบางอย่างได้ถูกลบออกอย่างสมบูรณ์)
assylias

3
คุณเรียกว่าเป็นห่วง verbose! เป็นเรื่องดีที่คุณไม่ได้อยู่ใกล้ ๆ ในสมัย ​​Cobol มันใช้เวลามากกว่า 10 ข้อความประกาศใน Cobol เพื่อแสดงตัวเลขจากน้อยไปมาก คนหนุ่มสาวสมัยนี้ไม่เห็นคุณค่าของสิ่งที่ดี
Gilbert Le Blanc

1
@GilbertLeBlanc verbosity ไม่มีอะไรเกี่ยวข้องกับมัน ลูปไม่สามารถประกอบสตรีมได้ ลูปนำไปสู่การทำซ้ำอย่างหลีกเลี่ยงไม่ได้ในขณะที่สตรีมอนุญาตให้นำมาใช้ซ้ำ เนื่องจากสตรีมดังกล่าวเป็นนามธรรมเชิงปริมาณที่ดีกว่าลูปและควรเป็นที่ต้องการ
Alain O'Dea

2
@GilbertLeBlanc และเราต้องเขียนโค้ดด้วยเท้าเปล่าท่ามกลางหิมะ
Dawood ibn Kareem

คำตอบ:


155

สำหรับตัวอย่างเฉพาะนี้คุณสามารถทำได้:

IntStream.rangeClosed(1, 8)
         .forEach(System.out::println);

หากคุณต้องการขั้นตอนที่แตกต่างจาก 1 คุณสามารถใช้ฟังก์ชันการแมปตัวอย่างเช่นขั้นตอนที่ 2:

IntStream.rangeClosed(1, 8)
         .map(i -> 2 * i - 1)
         .forEach(System.out::println);

หรือสร้างการทำซ้ำแบบกำหนดเองและ จำกัด ขนาดของการวนซ้ำ:

IntStream.iterate(1, i -> i + 2)
         .limit(8)
         .forEach(System.out::println);

4
การปิดจะเปลี่ยนโค้ด Java อย่างสมบูรณ์ให้ดีขึ้น รอคอยวันนั้น ...
Marko Topolnik

1
@jwenting มันขึ้นอยู่กับ - โดยทั่วไปแล้วจะมีสิ่ง GUI (Swing หรือ JavaFX) ซึ่งจะลบแผ่นหม้อไอน้ำจำนวนมากออกเนื่องจากคลาสที่ไม่ระบุชื่อ
assylias

8
@jwenting สำหรับทุกคนที่มีประสบการณ์ใน FP โค้ดที่หมุนรอบฟังก์ชันลำดับที่สูงกว่าถือเป็นชัยชนะที่แท้จริง สำหรับใครก็ตามที่ไม่มีประสบการณ์นั้นถึงเวลาอัพเกรดทักษะของคุณหรือเสี่ยงต่อการถูกทิ้งให้เป็นฝุ่น
Marko Topolnik

2
@MarkoTopolnik คุณอาจต้องการใช้ javadoc เวอร์ชันที่ใหม่กว่าเล็กน้อย (คุณกำลังชี้ไปที่รุ่น 78 ล่าสุดคือรุ่น 105: download.java.net/lambda/b105/docs/api/java/util/stream/… )
Mark Rotteveel

1
@GraemeMoss คุณยังคงสามารถใช้รูปแบบเดิมได้ ( IntStream.rangeClosed(1, 8).forEach(i -> methodNoArgs());) แต่มันทำให้ IMO สับสนและในกรณีนั้นดูเหมือนว่ามีการระบุลูป
assylias

65

นี่เป็นอีกเทคนิคหนึ่งที่ฉันวิ่งข้ามวัน:

Collections.nCopies(8, 1)
           .stream()
           .forEach(i -> System.out.println(i));

Collections.nCopiesโทรสร้างListที่มีnสำเนาของสิ่งที่คุณให้คุ้มค่า ในกรณีนี้เป็นIntegerค่าบรรจุกล่อง1 แน่นอนว่ามันไม่ได้สร้างรายการที่มีnองค์ประกอบ จะสร้างรายการ "virtualized" ที่มีเฉพาะค่าและความยาวและการเรียกไปยังgetภายในช่วงจะส่งกลับค่า nCopiesวิธีการได้รับรอบตั้งแต่คอลเลกชันกรอบได้รับการแนะนำทางกลับใน JDK 1.2 แน่นอนความสามารถในการสร้างสตรีมจากผลลัพธ์ได้ถูกเพิ่มใน Java SE 8

เรื่องใหญ่อีกวิธีหนึ่งในการทำสิ่งเดียวกันในจำนวนบรรทัดเดียวกัน

แต่เทคนิคนี้จะเร็วกว่าIntStream.generateและIntStream.iterateวิธีการและที่น่าแปลกใจก็ยังเร็วกว่าIntStream.rangeวิธีการ

สำหรับiterateและgenerateผลลัพธ์อาจไม่น่าแปลกใจเกินไป เฟรมเวิร์กของสตรีม (จริงๆคือ Spliterators สำหรับสตรีมเหล่านี้) สร้างขึ้นจากสมมติฐานที่ว่า lambdas อาจสร้างค่าที่แตกต่างกันในแต่ละครั้งและจะสร้างผลลัพธ์ที่ไม่ถูกผูกมัด ทำให้การแยกขนานยากเป็นพิเศษ iterateวิธีการยังเป็นปัญหาสำหรับกรณีนี้เพราะการโทรแต่ละครั้งต้องมีผลมาจากการอย่างใดอย่างหนึ่งก่อนหน้านี้ ดังนั้นสตรีมที่ใช้generateและiterateทำได้ไม่ดีนักในการสร้างค่าคงที่ซ้ำ ๆ

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

Collections.nCopiesเทคนิคมีการทำมวย / unboxing Listเพื่อจัดการค่าเนื่องจากไม่มีความเชี่ยวชาญดั้งเดิมของ เนื่องจากค่าเหมือนกันทุกครั้งโดยทั่วไปจะใส่กล่องครั้งเดียวและกล่องนั้นจะแชร์โดยnสำเนาทั้งหมด ฉันสงสัยว่าการชกมวย / การแกะกล่องนั้นได้รับการปรับให้เหมาะสมอย่างมากแม้จะอยู่ภายในและสามารถสอดใส่ได้ดี

นี่คือรหัส:

    public static final int LIMIT = 500_000_000;
    public static final long VALUE = 3L;

    public long range() {
        return
            LongStream.range(0, LIMIT)
                .parallel()
                .map(i -> VALUE)
                .map(i -> i % 73 % 13)
                .sum();
}

    public long ncopies() {
        return
            Collections.nCopies(LIMIT, VALUE)
                .parallelStream()
                .mapToLong(i -> i)
                .map(i -> i % 73 % 13)
                .sum();
}

และนี่คือผลลัพธ์ JMH: (2.8GHz Core2Duo)

Benchmark                    Mode   Samples         Mean   Mean error    Units
c.s.q.SO18532488.ncopies    thrpt         5        7.547        2.904    ops/s
c.s.q.SO18532488.range      thrpt         5        0.317        0.064    ops/s

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

ฉันประหลาดใจที่nCopiesเทคนิคนี้ทำงานได้ดีเพียงใด ภายในไม่ได้ทำอะไรพิเศษมากนักด้วยสตรีมของรายการเวอร์ชวลไลซ์เพียงแค่ใช้งานIntStream.range! ฉันคาดไว้ว่าจะต้องสร้างตัวแยกเฉพาะเพื่อให้มันทำงานได้เร็ว แต่ดูเหมือนว่าจะดีอยู่แล้ว


6
นักพัฒนาที่มีประสบการณ์น้อยอาจสับสนหรือมีปัญหาเมื่อพวกเขาเรียนรู้nCopiesว่าไม่ได้คัดลอกอะไรเลยและ "สำเนา" ทั้งหมดชี้ไปที่วัตถุชิ้นเดียวนั้น จะปลอดภัยเสมอหากวัตถุนั้นไม่เปลี่ยนรูปเช่นแบบดั้งเดิมชนิดบรรจุกล่องในตัวอย่างนี้ คุณพูดถึงสิ่งนี้ในคำสั่ง "บ็อกซ์ครั้งเดียว" ของคุณ แต่อาจเป็นการดีที่จะระบุข้อควรระวังในที่นี้อย่างชัดเจนเนื่องจากพฤติกรรมดังกล่าวไม่เฉพาะเจาะจงสำหรับการชกมวยอัตโนมัติ
William Price

1
นั่นหมายความว่าLongStream.rangeช้ากว่าอย่างมีนัยสำคัญIntStream.range? ดังนั้นจึงเป็นเรื่องดีที่แนวคิดในการไม่เสนอIntStream(แต่ใช้LongStreamกับจำนวนเต็มทุกประเภท) ได้ถูกทิ้ง โปรดทราบว่าสำหรับกรณีการใช้งานตามลำดับไม่มีเหตุผลที่จะใช้สตรีมเลยCollections.nCopies(8, 1).forEach(i -> System.out.println(i));เช่นเดียวกับCollections.nCopies(8, 1).stream().forEach(i -> System.out.println(i));แต่อาจมีประสิทธิภาพมากกว่าCollections.<Runnable>nCopies(8, () -> System.out.println(1)).forEach(Runnable::run);
Holger

1
@ Holger การทดสอบเหล่านี้ดำเนินการกับโปรไฟล์ประเภทที่สะอาดดังนั้นจึงไม่เกี่ยวข้องกับโลกแห่งความจริง อาจจะLongStream.rangeทำงานได้ไม่ดีเพราะมันมีสองแผนที่ที่มีLongFunctionอยู่ภายในขณะที่ncopiesมีสามแผนที่ที่มีIntFunction, ToLongFunctionและLongFunctionจึง lambdas ทั้งหมดเป็น monomorphic การเรียกใช้การทดสอบนี้ในโปรไฟล์ประเภทก่อนปนเปื้อน (ซึ่งใกล้เคียงกับกรณีจริงมากขึ้น) แสดงว่าncopiesช้ากว่า 1.5 เท่า
Tagir Valeev

1
Premature Optimization FTW
Rafael Bugajewski

1
เพื่อความสมบูรณ์จะเป็นการดีหากได้เห็นเกณฑ์มาตรฐานที่เปรียบเทียบทั้งสองเทคนิคนี้กับการforวนซ้ำแบบเก่า แม้ว่าโซลูชันของคุณจะเร็วกว่าStreamโค้ด แต่ฉันเดาว่าforลูปจะเอาชนะอย่างใดอย่างหนึ่งด้วยระยะขอบที่สำคัญ
typeracer

35

เพื่อความสมบูรณ์และเพราะยังช่วยตัวเองไม่ได้ :)

การสร้างลำดับค่าคงที่ จำกัด นั้นค่อนข้างใกล้เคียงกับสิ่งที่คุณเห็นใน Haskell โดยใช้คำฟุ่มเฟือยในระดับ Java เท่านั้น

IntStream.generate(() -> 1)
         .limit(8)
         .forEach(System.out::println);

() -> 1จะสร้าง 1 เท่านั้นสิ่งนี้ตั้งใจหรือไม่ 1 1 1 1 1 1 1 1ดังนั้นการส่งออกจะเป็น
Christian Ullenboom

4
ใช่ต่อ OP take 8 (repeat 1)แรกของตัวอย่าง assylias ค่อนข้างครอบคลุมกรณีอื่น ๆ ทั้งหมด
clstrfsck

3
Stream<T>นอกจากนี้ยังมีgenerateวิธีการทั่วไปในการรับสตรีมแบบไม่สิ้นสุดของประเภทอื่นซึ่งสามารถ จำกัด ได้เช่นเดียวกัน
zstewart

11

เมื่อฟังก์ชันการทำซ้ำถูกกำหนดให้เป็น

public static BiConsumer<Integer, Runnable> repeat = (n, f) -> {
    for (int i = 1; i <= n; i++)
        f.run();
};

คุณสามารถใช้งานได้แล้วด้วยวิธีนี้เช่น:

repeat.accept(8, () -> System.out.println("Yes"));

เพื่อให้ได้และเทียบเท่ากับ Haskell's

take 8 (repeat 1)

คุณสามารถเขียน

StringBuilder s = new StringBuilder();
repeat.accept(8, () -> s.append("1"));

2
อันนี้สุดยอดมาก แต่ฉันมีการปรับเปลี่ยนให้มันเพื่อให้จำนวนซ้ำกลับโดยการเปลี่ยนRunnableไปแล้วใช้Function<Integer, ?> f.apply(i)
Fons

0

นี่คือวิธีแก้ปัญหาของฉันในการใช้ฟังก์ชัน times ฉันเป็นจูเนียร์ดังนั้นฉันยอมรับว่ามันอาจไม่เหมาะฉันยินดีที่จะได้ยินหากนี่ไม่ใช่ความคิดที่ดีไม่ว่าจะด้วยเหตุผลใดก็ตาม

public static <T extends Object, R extends Void> R times(int count, Function<T, R> f, T t) {
    while (count > 0) {
        f.apply(t);
        count--;
    }
    return null;
}

นี่คือตัวอย่างการใช้งานบางส่วน:

Function<String, Void> greet = greeting -> {
    System.out.println(greeting);
    return null;
};

times(3, greet, "Hello World!");
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.