ทำไมคุณต้องอัปเกรดเป็น Java 8 แล้ว

เผยแพร่แล้ว: 2022-03-11

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

มีคุณลักษณะใหม่มากมายใน Java 8 ฉันจะแสดงคุณลักษณะที่มีประโยชน์และน่าสนใจจำนวนหนึ่งให้คุณดู:

  • นิพจน์แลมบ์ดา
  • สตรีม API สำหรับการทำงานกับคอลเล็กชัน
  • การเชื่อมโยงงานแบบอะซิงโครนัสด้วย CompletableFuture
  • API เวลาใหม่ล่าสุด

นิพจน์แลมบ์ดา

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

มาดูตัวอย่างกันว่าจะปรับปรุงโค้ดของเราได้อย่างไร ที่นี่เรามีตัวเปรียบเทียบอย่างง่ายซึ่งเปรียบเทียบค่า Integer สองค่าด้วยโมดูโล 2:

 class BinaryComparator implements Comparator<Integer>{ @Override public int compare(Integer i1, Integer i2) { return i1 % 2 - i2 % 2; } }

ในอนาคต ตัวอย่างของคลาสนี้อาจถูกเรียกในโค้ดที่ต้องการตัวเปรียบเทียบ เช่นนี้

 ... List<Integer> list = ...; Comparator<Integer> comparator = new BinaryComparator(); Collections.sort(list, comparator); ...

ไวยากรณ์แลมบ์ดาใหม่ช่วยให้เราทำสิ่งนี้ได้ง่ายขึ้น นี่คือนิพจน์แลมบ์ดาอย่างง่ายซึ่งทำสิ่งเดียวกับวิธี compare จาก BinaryComparator :

 (Integer i1, Integer i2) -> i1 % 2 - i2 % 2;

โครงสร้างมีความคล้ายคลึงกันมากกับฟังก์ชัน ในวงเล็บ เราสร้างรายการอาร์กิวเมนต์ ไวยากรณ์ -> แสดงว่านี่คือแลมบ์ดา และในส่วนขวามือของนิพจน์นี้ เราตั้งค่าพฤติกรรมของแลมบ์ดาของเรา

จาวา 8 แลมบ์ดา เอ็กซ์เพรสชั่น

ตอนนี้เราสามารถปรับปรุงตัวอย่างก่อนหน้าของเราได้:

 ... List<Integer> list = ...; Collections.sort(list, (Integer i1, Integer i2) -> i1 % 2 - i2 % 2); ...

เราอาจกำหนดตัวแปรด้วยวัตถุนี้ มาดูกันว่ามันมีลักษณะอย่างไร:

 Comparator<Integer> comparator = (Integer i1, Integer i2) -> i1 % 2 - i2 % 2;

ตอนนี้เราสามารถนำฟังก์ชันนี้กลับมาใช้ใหม่ได้ดังนี้:

 ... List<Integer> list1 = ...; List<Integer> list2 = ...; Collections.sort(list1, comparator); Collections.sort(list2, comparator); ...

โปรดสังเกตว่าในตัวอย่างเหล่านี้ แลมบ์ดากำลังถูกส่งผ่านไปยังเมธอด sort() ในลักษณะเดียวกับที่อินสแตนซ์ของ BinaryComparator ถูกส่งผ่านในตัวอย่างก่อนหน้านี้ JVM รู้ได้อย่างไรว่าตีความแลมบ์ดาอย่างถูกต้อง?

เพื่อให้ฟังก์ชันรับ lambdas เป็นอาร์กิวเมนต์ Java 8 ได้แนะนำแนวคิดใหม่: functional interface อินเทอร์เฟซที่ใช้งานได้คืออินเทอร์เฟซที่มีวิธีนามธรรมเพียงวิธีเดียว อันที่จริง Java 8 ถือว่านิพจน์แลมบ์ดาเป็นการใช้งานพิเศษของอินเทอร์เฟซที่ใช้งานได้ ซึ่งหมายความว่า ในการรับแลมบ์ดาเป็นอาร์กิวเมนต์ของเมธอด ประเภทที่ประกาศของอาร์กิวเมนต์นั้นจะต้องเป็นส่วนต่อประสานที่ใช้งานได้เท่านั้น

เมื่อเราประกาศอินเทอร์เฟซที่ใช้งานได้ เราอาจเพิ่มสัญลักษณ์ @FunctionalInterface เพื่อแสดงให้นักพัฒนาเห็นว่ามันคืออะไร:

 @FunctionalInterface private interface DTOSender { void send(String accountId, DTO dto); } void sendDTO(BisnessModel object, DTOSender dtoSender) { //some logic for sending... ... dtoSender.send(id, dto); ... }

ตอนนี้ เราสามารถเรียกเมธอด sendDTO โดยส่งผ่านแลมบ์ดาที่แตกต่างกันเพื่อให้เกิดพฤติกรรมที่แตกต่างกันดังนี้:

 sendDTO(object, ((accountId, dto) -> sendToAndroid(accountId, dto))); sendDTO(object, ((accountId, dto) -> sendToIos(accountId, dto)));

การอ้างอิงวิธีการ

อาร์กิวเมนต์แลมบ์ดาช่วยให้เราปรับเปลี่ยนพฤติกรรมของฟังก์ชันหรือเมธอดได้ ดังที่เราเห็นในตัวอย่างที่แล้ว บางครั้งแลมบ์ดาใช้เรียกวิธีการอื่นเท่านั้น ( sendToAndroid หรือ sendToIos ) สำหรับกรณีพิเศษนี้ Java 8 ขอแนะนำชวเลขที่สะดวก: การอ้างอิงเมธอด ไวยากรณ์ย่อนี้แสดงถึงแลมบ์ดาที่เรียกใช้เมธอด และมีรูปแบบ objectName::methodName ซึ่งช่วยให้เราสร้างตัวอย่างก่อนหน้านี้ที่กระชับและอ่านง่ายยิ่งขึ้น:

 sendDTO(object, this::sendToAndroid); sendDTO(object, this::sendToIos);

ในกรณีนี้ เมธอด sendToAndroid และ sendToIos ถูกนำมาใช้ในคลาส this เราอาจอ้างอิงวิธีการของวัตถุหรือคลาสอื่นด้วย

สตรีม API

Java 8 นำความสามารถใหม่มาใช้กับ Collections ในรูปแบบของ Stream API ใหม่ล่าสุด ฟังก์ชันการทำงานใหม่นี้มีให้โดยแพ็คเกจ java.util.stream และมีวัตถุประสงค์เพื่อเปิดใช้งานแนวทางการทำงานที่มากขึ้นในการเขียนโปรแกรมด้วยคอลเลกชัน อย่างที่เราจะได้เห็นกัน เป็นไปได้มากว่าต้องขอบคุณไวยากรณ์แลมบ์ดาใหม่ที่เราเพิ่งพูดถึง

Stream API นำเสนอการกรอง การนับ และการแมปคอลเล็กชันที่ง่ายดาย ตลอดจนวิธีต่างๆ ในการแยกส่วนและชุดย่อยของข้อมูลออกจากสิ่งเหล่านี้ ด้วยรูปแบบการใช้งาน Stream API ช่วยให้โค้ดที่สั้นลงและสวยงามยิ่งขึ้นสำหรับการทำงานกับคอลเลกชัน

มาเริ่มกันด้วยตัวอย่างสั้นๆ เราจะใช้โมเดลข้อมูลนี้ในตัวอย่างทั้งหมด:

 class Author { String name; int countOfBooks; } class Book { String name; int year; Author author; }

ลองนึกภาพว่าเราจำเป็นต้องพิมพ์ผู้แต่งทั้งหมดในชุด books ที่เขียนหนังสือหลังปี 2548 เราจะทำใน Java 7 ได้อย่างไร

 for (Book book : books) { if (book.author != null && book.year > 2005){ System.out.println(book.author.name); } }

และเราจะทำอย่างไรใน Java 8?

 books.stream() .filter(book -> book.year > 2005) // filter out books published in or before 2005 .map(Book::getAuthor) // get the list of authors for the remaining books .filter(Objects::nonNull) // remove null authors from the list .map(Author::getName) // get the list of names for the remaining authors .forEach(System.out::println); // print the value of each remaining element

มันเป็นเพียงหนึ่งการแสดงออก! การเรียกใช้เมธอด stream() บน Collection นใดๆ จะส่งคืนอ็อบเจ็กต์ Stream ที่ห่อหุ้มองค์ประกอบทั้งหมดของคอลเล็กชันนั้น สิ่งนี้สามารถจัดการได้ด้วยตัวปรับแต่งต่างๆ จาก Stream API เช่น filter() และ map() ตัวปรับแต่งแต่ละรายการจะส่งกลับวัตถุ Stream ใหม่พร้อมผลลัพธ์ของการแก้ไข ซึ่งสามารถจัดการเพิ่มเติมได้ เมธอด . .forEach() ช่วยให้เราดำเนินการบางอย่างกับสตรีมผลลัพธ์แต่ละอินสแตนซ์ได้

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

Stream API ช่วยให้นักพัฒนาดูคอลเล็กชัน Java จากมุมมองใหม่ ลองนึกภาพว่าตอนนี้เราจำเป็นต้องได้รับ Map ของภาษาที่ใช้ได้ในแต่ละประเทศ สิ่งนี้จะถูกนำไปใช้ใน Java 7 อย่างไร

 Map<String, Set<String>> countryToSetOfLanguages = new HashMap<>(); for (Locale locale : Locale.getAvailableLocales()){ String country = locale.getDisplayCountry(); if (!countryToSetOfLanguages.containsKey(country)){ countryToSetOfLanguages.put(country, new HashSet<>()); } countryToSetOfLanguages.get(country).add(locale.getDisplayLanguage()); }

ใน Java 8 สิ่งต่าง ๆ ค่อนข้างเรียบร้อย:

 import java.util.stream.*; import static java.util.stream.Collectors.*; ... Map<String, Set<String>> countryToSetOfLanguages = Stream.of(Locale.getAvailableLocales()) .collect(groupingBy(Locale::getDisplayCountry, mapping(Locale::getDisplayLanguage, toSet())));

วิธีการ collect() ช่วยให้เราสามารถรวบรวมผลลัพธ์ของสตรีมได้หลายวิธี ในที่นี้ เราจะเห็นได้ว่าอันดับแรกจะจัดกลุ่มตามประเทศ แล้วจึงจับคู่แต่ละกลุ่มตามภาษา ( groupingBy() และ toSet() เป็นทั้งวิธีการคงที่จากคลาส Collectors )

JAVA 8 สตรีม API

Stream API มีความสามารถอื่นๆ มากมาย เอกสารฉบับสมบูรณ์สามารถพบได้ที่นี่ ฉันแนะนำให้อ่านเพิ่มเติมเพื่อทำความเข้าใจเครื่องมืออันทรงพลังทั้งหมดที่แพ็คเกจนี้มีให้

การเชื่อมโยงงานแบบอะซิงโครนัสกับ CompletableFuture ที่สมบูรณ์

ในแพ็คเกจ java.util.concurrent ของ Java 7 มีอินเทอร์เฟซ Future<T> ซึ่งช่วยให้เราได้รับสถานะหรือผลลัพธ์ของงานอะซิงโครนัสในอนาคต ในการใช้ฟังก์ชันนี้ เราต้อง:

  1. สร้าง ExecutorService ซึ่งจัดการการทำงานของงานแบบอะซิงโครนัส และสามารถสร้างวัตถุ Future เพื่อติดตามความคืบหน้าได้
  2. สร้างงาน Runnable แบบอะซิงโครนัส
  3. เรียกใช้งานใน ExecutorService ซึ่งจะให้การเข้าถึงสถานะหรือผลลัพธ์ Future

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

อย่างไรก็ตาม ใน Java 8 นั้น แนวคิด Future นั้นถูกนำไปเพิ่มเติมด้วยอินเทอร์เฟซ CompletableFuture<T> ซึ่งอนุญาตให้สร้างและดำเนินการลูกโซ่ของงานแบบอะซิงโครนัส เป็นกลไกที่มีประสิทธิภาพในการสร้างแอปพลิเคชันแบบอะซิงโครนัสใน Java 8 เนื่องจากช่วยให้เราประมวลผลผลลัพธ์ของแต่ละงานโดยอัตโนมัติเมื่อเสร็จสิ้น

มาดูตัวอย่างกัน:

 import java.util.concurrent.CompletableFuture; ... CompletableFuture<Void> voidCompletableFuture = CompletableFuture.supplyAsync(() -> blockingReadPage()) .thenApply(this::getLinks) .thenAccept(System.out::println);

เมธอด CompletableFuture.supplyAsync สร้างงานแบบอะซิงโครนัสใหม่ใน Executor เริ่มต้น (โดยทั่วไปคือ ForkJoinPool ) เมื่องานเสร็จสิ้น ผลลัพธ์ของงานจะถูกส่งโดยอัตโนมัติเป็นอาร์กิวเมนต์ของฟังก์ชัน this::getLinks ซึ่งรันในงานอะซิงโครนัสใหม่ด้วย สุดท้าย ผลลัพธ์ของขั้นตอนที่สองนี้จะถูกพิมพ์ไปยัง System.out โดยอัตโนมัติ thenApply() และ thenAccept() เป็นเพียงสองวิธีที่มีประโยชน์มากมายที่จะช่วยคุณสร้างงานพร้อมกันโดยไม่ต้องใช้ Executors ด้วยตนเอง

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

 import static java.util.concurrent.CompletableFuture.*; ... Supplier<Integer> task1 = (...) -> { ... // some complex calculation return 1; // example result }; Supplier<Integer> task2 = (...) -> { ... // some complex calculation throw new RuntimeException(); // example exception }; Supplier<Integer> task3 = (...) -> { ... // some complex calculation return 3; // example result }; supplyAsync(task1) // run task1 .applyToEither( // use whichever result is ready first, result of task1 or supplyAsync(task2), // result of task2 (Integer i) -> i) // return result as-is .thenCombine( // combine result supplyAsync(task3), // with result of task3 Integer::sum) // using summation .thenAccept(System.out::println); // print final result after execution

หากเราตรวจสอบว่า Java 8 จัดการกับสิ่งนี้อย่างไร เราจะเห็นว่างานทั้งสามจะทำงานพร้อมกันแบบอะซิงโครนัส แม้ว่า ภารกิจที่ 2 จะล้มเหลวโดยมีข้อยกเว้น แต่ผลลัพธ์สุดท้ายจะถูกคำนวณและพิมพ์สำเร็จ

การเขียนโปรแกรมแบบอะซิงโครนัส JAVA 8 พร้อมอนาคตที่สมบูรณ์

CompletableFuture ทำให้ง่ายต่อการสร้างงานแบบอะซิงโครนัสที่มีหลายขั้นตอน และให้อินเทอร์เฟซที่ง่ายแก่เราสำหรับการกำหนดว่าควรดำเนินการใดเมื่อเสร็จสิ้นแต่ละขั้นตอน

Java วันที่และเวลา API

ตามที่ระบุไว้โดยการรับเข้าเรียนของ Java:

ก่อนการเปิดตัว Java SE 8 กลไกวันที่และเวลาของ Java ถูกจัดเตรียมโดยคลาส java.util.Date , java.util.Calendar และ java.util.TimeZone รวมถึงคลาสย่อย เช่น java.util.GregorianCalendar ชั้นเรียนเหล่านี้มีข้อเสียหลายประการ ได้แก่

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

ในที่สุด Java 8 ก็แก้ปัญหาที่มีมายาวนานเหล่านี้ด้วยแพ็คเกจ java.time ใหม่ ซึ่งมีคลาสสำหรับการทำงานกับวันที่และเวลา ทั้งหมดนี้ไม่เปลี่ยนรูปแบบและมี API ที่คล้ายกับเฟรมเวิร์กยอดนิยม Joda-Time ซึ่งนักพัฒนา Java เกือบทั้งหมดใช้ในแอปพลิเคชันของตนแทน Date , Calendar และ TimeZone ดั้งเดิม

นี่คือคลาสที่มีประโยชน์บางส่วนในแพ็คเกจนี้:

  • Clock - นาฬิกาสำหรับบอกเวลาปัจจุบัน รวมถึงช่วงเวลาปัจจุบัน วันที่ และเวลาพร้อมเขตเวลา
  • Duration และ Period - ระยะเวลา Duration ใช้ค่าตามเวลา เช่น “76.8 วินาที และ Period ตามวันที่ เช่น “4 ปี 6 เดือน และ 12 วัน”
  • Instant - ช่วงเวลาหนึ่งชั่วขณะในหลายรูปแบบ
  • LocalDate , LocalDateTime , LocalTime , Year , YearMonth - วันที่ เวลา ปี เดือน หรือค่าผสมกัน โดยไม่มีเขตเวลาในระบบปฏิทิน ISO-8601
  • OffsetDateTime , OffsetTime - วันที่-เวลาที่มีการชดเชยจาก UTC/Greenwich ในระบบปฏิทิน ISO-8601 เช่น “2015-08-29T14:15:30+01:00”
  • ZonedDateTime - วันที่-เวลาที่มีเขตเวลาที่เกี่ยวข้องในระบบปฏิทิน ISO-8601 เช่น “1986-08-29T10:15:30+01:00 Europe/Paris”

JAVA 8 TIME API

บางครั้ง เราจำเป็นต้องค้นหาวันที่สัมพันธ์กัน เช่น “วันอังคารแรกของเดือน” สำหรับกรณีเหล่านี้ java.time จะจัดเตรียม TemporalAdjuster คลาสพิเศษ คลาส TemporalAdjuster มีชุดตัวปรับแต่งมาตรฐาน ซึ่งพร้อมใช้งานเป็นวิธีการแบบคงที่ สิ่งเหล่านี้ช่วยให้เราสามารถ:

  • ค้นหาวันแรกหรือวันสุดท้ายของเดือน
  • ค้นหาวันแรกหรือวันสุดท้ายของเดือนถัดไปหรือก่อนหน้า
  • ค้นหาวันแรกหรือวันสุดท้ายของปี
  • ค้นหาวันแรกหรือวันสุดท้ายของปีถัดไปหรือปีก่อนหน้า
  • ค้นหาวันแรกหรือวันสุดท้ายของสัปดาห์ภายในหนึ่งเดือน เช่น "วันพุธแรกของเดือนมิถุนายน"
  • ค้นหาวันในสัปดาห์ถัดไปหรือก่อนหน้า เช่น "วันพฤหัสบดีหน้า"

ต่อไปนี้คือตัวอย่างสั้นๆ วิธีรับวันอังคารแรกของเดือน:

 LocalDate getFirstTuesday(int year, int month) { return LocalDate.of(year, month, 1) .with(TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY)); }
ยังใช้ Java 7 อยู่ใช่หรือไม่ รับกับโปรแกรม! #Java8
ทวีต

Java 8 ในบทสรุป

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

นอกจากนี้ Java 8 ยังนำเสนอคุณสมบัติใหม่สำหรับการทำงานกับการเขียนโปรแกรมแบบอะซิงโครนัสและการยกเครื่องเครื่องมือวันที่และเวลาที่จำเป็นมาก

เมื่อรวมกันแล้ว การเปลี่ยนแปลงเหล่านี้ถือเป็นก้าวสำคัญสำหรับภาษา Java ทำให้การพัฒนา Java มีความน่าสนใจและมีประสิทธิภาพมากขึ้น