ทำไมคุณต้องอัปเกรดเป็น Java 8 แล้ว
เผยแพร่แล้ว: 2022-03-11Java 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;
โครงสร้างมีความคล้ายคลึงกันมากกับฟังก์ชัน ในวงเล็บ เราสร้างรายการอาร์กิวเมนต์ ไวยากรณ์ ->
แสดงว่านี่คือแลมบ์ดา และในส่วนขวามือของนิพจน์นี้ เราตั้งค่าพฤติกรรมของแลมบ์ดาของเรา
ตอนนี้เราสามารถปรับปรุงตัวอย่างก่อนหน้าของเราได้:
... 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
)

Stream API มีความสามารถอื่นๆ มากมาย เอกสารฉบับสมบูรณ์สามารถพบได้ที่นี่ ฉันแนะนำให้อ่านเพิ่มเติมเพื่อทำความเข้าใจเครื่องมืออันทรงพลังทั้งหมดที่แพ็คเกจนี้มีให้
การเชื่อมโยงงานแบบอะซิงโครนัสกับ CompletableFuture
ที่สมบูรณ์
ในแพ็คเกจ java.util.concurrent
ของ Java 7 มีอินเทอร์เฟซ Future<T>
ซึ่งช่วยให้เราได้รับสถานะหรือผลลัพธ์ของงานอะซิงโครนัสในอนาคต ในการใช้ฟังก์ชันนี้ เราต้อง:
- สร้าง
ExecutorService
ซึ่งจัดการการทำงานของงานแบบอะซิงโครนัส และสามารถสร้างวัตถุFuture
เพื่อติดตามความคืบหน้าได้ - สร้างงาน
Runnable
แบบอะซิงโครนัส - เรียกใช้งานใน
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 จะล้มเหลวโดยมีข้อยกเว้น แต่ผลลัพธ์สุดท้ายจะถูกคำนวณและพิมพ์สำเร็จ
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.time
จะจัดเตรียม TemporalAdjuster
คลาสพิเศษ คลาส TemporalAdjuster
มีชุดตัวปรับแต่งมาตรฐาน ซึ่งพร้อมใช้งานเป็นวิธีการแบบคงที่ สิ่งเหล่านี้ช่วยให้เราสามารถ:
- ค้นหาวันแรกหรือวันสุดท้ายของเดือน
- ค้นหาวันแรกหรือวันสุดท้ายของเดือนถัดไปหรือก่อนหน้า
- ค้นหาวันแรกหรือวันสุดท้ายของปี
- ค้นหาวันแรกหรือวันสุดท้ายของปีถัดไปหรือปีก่อนหน้า
- ค้นหาวันแรกหรือวันสุดท้ายของสัปดาห์ภายในหนึ่งเดือน เช่น "วันพุธแรกของเดือนมิถุนายน"
- ค้นหาวันในสัปดาห์ถัดไปหรือก่อนหน้า เช่น "วันพฤหัสบดีหน้า"
ต่อไปนี้คือตัวอย่างสั้นๆ วิธีรับวันอังคารแรกของเดือน:
LocalDate getFirstTuesday(int year, int month) { return LocalDate.of(year, month, 1) .with(TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY)); }
Java 8 ในบทสรุป
ดังที่เราเห็น Java 8 เป็นการเปิดตัวแพลตฟอร์ม Java ในยุคสมัย มีการเปลี่ยนแปลงทางภาษามากมาย โดยเฉพาะอย่างยิ่งเมื่อมีการแนะนำแลมบ์ดาส ซึ่งแสดงถึงการย้ายเพื่อนำความสามารถในการเขียนโปรแกรมที่ใช้งานได้จริงมาสู่ Java Stream API เป็นตัวอย่างที่ดีที่ lambdas สามารถเปลี่ยนวิธีที่เราทำงานกับเครื่องมือ Java มาตรฐานที่เราคุ้นเคย
นอกจากนี้ Java 8 ยังนำเสนอคุณสมบัติใหม่สำหรับการทำงานกับการเขียนโปรแกรมแบบอะซิงโครนัสและการยกเครื่องเครื่องมือวันที่และเวลาที่จำเป็นมาก
เมื่อรวมกันแล้ว การเปลี่ยนแปลงเหล่านี้ถือเป็นก้าวสำคัญสำหรับภาษา Java ทำให้การพัฒนา Java มีความน่าสนใจและมีประสิทธิภาพมากขึ้น