บทความนี้เดิมถูกโพสต์ในบล็อกของฉัน
คุณควรอ่านบทช่วยสอน Java 11 ของฉันด้วย (รวมถึงภาษาใหม่และคุณสมบัติ API จาก Java 9, 10 และ 11)
ยินดีต้อนรับสู่การแนะนำ Java 8 ของฉัน บทช่วยสอนนี้จะแนะนำคุณทีละขั้นตอนเกี่ยวกับฟีเจอร์ภาษาใหม่ทั้งหมด ด้วยตัวอย่างโค้ดที่สั้นและเรียบง่าย คุณจะได้เรียนรู้วิธีใช้วิธีการอินเทอร์เฟซเริ่มต้น นิพจน์ lambda การอ้างอิงวิธีการ และคำอธิบายประกอบที่ทำซ้ำได้ ในตอนท้ายของบทความ คุณจะคุ้นเคยกับการเปลี่ยนแปลงล่าสุดของ API เช่น สตรีม อินเทอร์เฟซการทำงาน ส่วนขยายแผนที่ และ Date API ใหม่ ไม่มีกำแพงข้อความ มีเพียงตัวอย่างโค้ดแสดงความคิดเห็นจำนวนหนึ่ง สนุก!
★★★ ชอบโครงการนี้ไหม? ฝากดาว ติดตามบน Twitter หรือบริจาคเพื่อสนับสนุนงานของฉัน ขอบคุณ!
Java 8 ช่วยให้เราสามารถเพิ่มการใช้งานเมธอดที่ไม่ใช่นามธรรมให้กับอินเทอร์เฟซโดยใช้คีย์เวิร์ด default
คุณลักษณะนี้เรียกอีกอย่างว่าวิธีการขยายเสมือน
นี่คือตัวอย่างแรกของเรา:
interface Formula {
double calculate ( int a );
default double sqrt ( int a ) {
return Math . sqrt ( a );
}
}
นอกจากวิธีนามธรรมแล้ว calculate
อินเทอร์เฟ Formula
ยังกำหนดวิธีเริ่มต้น sqrt
คลาสที่เป็นรูปธรรมจะต้องใช้วิธี calculate
เชิงนามธรรมเท่านั้น คุณสามารถใช้วิธีเริ่มต้น sqrt
ได้ทันที
Formula formula = new Formula () {
@ Override
public double calculate ( int a ) {
return sqrt ( a * 100 );
}
};
formula . calculate ( 100 ); // 100.0
formula . sqrt ( 16 ); // 4.0
สูตรถูกนำมาใช้เป็นวัตถุที่ไม่ระบุชื่อ รหัสค่อนข้างละเอียด: โค้ด 6 บรรทัดสำหรับการคำนวณอย่างง่าย ๆ sqrt(a * 100)
ดังที่เราจะเห็นในหัวข้อถัดไป มีวิธีที่ดีกว่ามากในการใช้ single method object ใน Java 8
เริ่มจากตัวอย่างง่ายๆ ของวิธีเรียงลำดับรายการสตริงใน Java เวอร์ชันก่อนหน้า:
List < String > names = Arrays . asList ( "peter" , "anna" , "mike" , "xenia" );
Collections . sort ( names , new Comparator < String >() {
@ Override
public int compare ( String a , String b ) {
return b . compareTo ( a );
}
});
เมธอดยูทิลิตี้คง Collections.sort
ยอมรับรายการและตัวเปรียบเทียบเพื่อเรียงลำดับองค์ประกอบของรายการที่กำหนด คุณมักจะพบว่าตัวเองกำลังสร้างตัวเปรียบเทียบที่ไม่ระบุชื่อและส่งต่อไปยังวิธีการเรียงลำดับ
แทนที่จะสร้างวัตถุนิรนามตลอดทั้งวัน Java 8 มาพร้อมกับไวยากรณ์ที่สั้นกว่ามาก นิพจน์แลมบ์ดา :
Collections . sort ( names , ( String a , String b ) -> {
return b . compareTo ( a );
});
อย่างที่คุณเห็นโค้ดนั้นสั้นกว่าและอ่านง่ายกว่ามาก แต่มันสั้นลงอีก:
Collections . sort ( names , ( String a , String b ) -> b . compareTo ( a ));
สำหรับเนื้อหาวิธีการบรรทัดเดียว คุณสามารถข้ามทั้งเครื่องหมายปีกกา {}
และคีย์เวิร์ด return
แต่มันสั้นลงอีก:
names . sort (( a , b ) -> b . compareTo ( a ));
รายการตอนนี้มีวิธี sort
นอกจากนี้คอมไพเลอร์ Java ยังรับรู้ถึงประเภทพารามิเตอร์ ดังนั้นคุณจึงสามารถข้ามมันได้เช่นกัน มาเจาะลึกถึงวิธีการใช้นิพจน์ lambda ในป่ากัน
นิพจน์แลมบ์ดาเหมาะสมกับระบบประเภทของ Java อย่างไร แลมบ์ดาแต่ละตัวสอดคล้องกับประเภทที่กำหนด ซึ่งระบุโดยอินเทอร์เฟซ อินเทอร์เฟซที่เรียกว่าฟังก์ชัน จะต้องมีการประกาศ วิธีนามธรรมเพียงรายการเดียว นิพจน์แลมบ์ดาแต่ละรายการจะถูกจับคู่กับวิธีนามธรรมนี้ เนื่องจากวิธีการเริ่มต้นไม่ใช่นามธรรม คุณจึงมีอิสระที่จะเพิ่มวิธีการเริ่มต้นให้กับอินเทอร์เฟซการทำงานของคุณ
เราสามารถใช้อินเทอร์เฟซตามอำเภอใจเป็นนิพจน์แลมบ์ดาได้ ตราบใดที่อินเทอร์เฟซมีเพียงวิธีนามธรรมเพียงวิธีเดียวเท่านั้น เพื่อให้แน่ใจว่าอินเทอร์เฟซของคุณตรงตามข้อกำหนด คุณควรเพิ่มคำอธิบายประกอบ @FunctionalInterface
คอมไพลเลอร์ทราบถึงคำอธิบายประกอบนี้และแสดงข้อผิดพลาดของคอมไพเลอร์ทันทีที่คุณพยายามเพิ่มการประกาศเมธอดนามธรรมที่สองให้กับอินเทอร์เฟซ
ตัวอย่าง:
@ FunctionalInterface
interface Converter < F , T > {
T convert ( F from );
}
Converter < String , Integer > converter = ( from ) -> Integer . valueOf ( from );
Integer converted = converter . convert ( "123" );
System . out . println ( converted ); // 123
โปรดทราบว่าโค้ดยังใช้ได้หากใส่คำอธิบายประกอบ @FunctionalInterface
ไว้
โค้ดตัวอย่างข้างต้นสามารถทำให้ง่ายขึ้นได้โดยใช้การอ้างอิงวิธีการแบบคงที่:
Converter < String , Integer > converter = Integer :: valueOf ;
Integer converted = converter . convert ( "123" );
System . out . println ( converted ); // 123
Java 8 ช่วยให้คุณสามารถส่งการอ้างอิงเมธอดหรือคอนสตรัคเตอร์ผ่านคีย์เวิร์ด ::
ตัวอย่างข้างต้นแสดงวิธีการอ้างอิงวิธีการคงที่ แต่เรายังสามารถอ้างอิงวิธีการของวัตถุได้:
class Something {
String startsWith ( String s ) {
return String . valueOf ( s . charAt ( 0 ));
}
}
Something something = new Something ();
Converter < String , String > converter = something :: startsWith ;
String converted = converter . convert ( "Java" );
System . out . println ( converted ); // "J"
มาดูกันว่าคีย์เวิร์ด ::
ทำงานอย่างไรกับตัวสร้าง ขั้นแรกเรากำหนดคลาสตัวอย่างด้วย Constructor ที่แตกต่างกัน:
class Person {
String firstName ;
String lastName ;
Person () {}
Person ( String firstName , String lastName ) {
this . firstName = firstName ;
this . lastName = lastName ;
}
}
ต่อไปเราจะระบุอินเทอร์เฟซโรงงานบุคคลที่จะใช้สำหรับการสร้างบุคคลใหม่:
interface PersonFactory < P extends Person > {
P create ( String firstName , String lastName );
}
แทนที่จะดำเนินการโรงงานด้วยตนเอง เราจะรวมทุกอย่างเข้าด้วยกันผ่านการอ้างอิงตัวสร้าง:
PersonFactory < Person > personFactory = Person :: new ;
Person person = personFactory . create ( "Peter" , "Parker" );
เราสร้างการอ้างอิงถึงตัวสร้างบุคคลผ่าน Person::new
คอมไพเลอร์ Java จะเลือกตัวสร้างที่ถูกต้องโดยอัตโนมัติโดยการจับคู่ลายเซ็นของ PersonFactory.create
การเข้าถึงตัวแปรขอบเขตภายนอกจากนิพจน์แลมบ์ดานั้นคล้ายกับอ็อบเจ็กต์ที่ไม่ระบุชื่อมาก คุณสามารถเข้าถึงตัวแปรสุดท้ายได้จากขอบเขตภายนอกภายในเครื่อง เช่นเดียวกับฟิลด์อินสแตนซ์และตัวแปรคงที่
เราสามารถอ่านตัวแปรโลคัลสุดท้ายได้จากขอบเขตภายนอกของนิพจน์แลมบ์ดา:
final int num = 1 ;
Converter < Integer , String > stringConverter =
( from ) -> String . valueOf ( from + num );
stringConverter . convert ( 2 ); // 3
แต่แตกต่างจากวัตถุที่ไม่ระบุตัวตน ตัวแปร num
ไม่จำเป็นต้องประกาศขั้นสุดท้าย รหัสนี้ยังใช้ได้:
int num = 1 ;
Converter < Integer , String > stringConverter =
( from ) -> String . valueOf ( from + num );
stringConverter . convert ( 2 ); // 3
อย่างไรก็ตาม num
ต้องเป็นค่าสุดท้ายโดยปริยายจึงจะสามารถคอมไพล์โค้ดได้ รหัสต่อไปนี้ ไม่ได้ รวบรวม:
int num = 1 ;
Converter < Integer , String > stringConverter =
( from ) -> String . valueOf ( from + num );
num = 3 ;
ห้ามเขียนถึง num
จากภายในนิพจน์แลมบ์ดาเช่นกัน
ตรงกันข้ามกับตัวแปรภายในเครื่อง เรามีทั้งการเข้าถึงแบบอ่านและเขียนในฟิลด์อินสแตนซ์และตัวแปรคงที่จากภายในนิพจน์แลมบ์ดา ลักษณะการทำงานนี้เป็นที่ทราบกันดีจากวัตถุที่ไม่ระบุชื่อ
class Lambda4 {
static int outerStaticNum ;
int outerNum ;
void testScopes () {
Converter < Integer , String > stringConverter1 = ( from ) -> {
outerNum = 23 ;
return String . valueOf ( from );
};
Converter < Integer , String > stringConverter2 = ( from ) -> {
outerStaticNum = 72 ;
return String . valueOf ( from );
};
}
}
จำตัวอย่างสูตรจากส่วนแรกได้ไหม Formula
อินเทอร์เฟซกำหนดวิธีการเริ่มต้น sqrt
ซึ่งสามารถเข้าถึงได้จากแต่ละอินสแตนซ์ของสูตร รวมถึงอ็อบเจ็กต์ที่ไม่ระบุชื่อ สิ่งนี้ใช้ไม่ได้กับนิพจน์แลมบ์ดา
ไม่สามารถ เข้าถึงวิธีการเริ่มต้นจากภายในนิพจน์แลมบ์ดา รหัสต่อไปนี้ไม่ได้รวบรวม:
Formula formula = ( a ) -> sqrt ( a * 100 );
JDK 1.8 API มีอินเทอร์เฟซการทำงานในตัวมากมาย บางส่วนเป็นที่รู้จักกันดีจาก Java เวอร์ชันเก่าเช่น Comparator
หรือ Runnable
อินเทอร์เฟซที่มีอยู่เหล่านั้นได้รับการขยายเพื่อเปิดใช้งานการรองรับ Lambda ผ่านคำอธิบายประกอบ @FunctionalInterface
แต่ Java 8 API ยังเต็มไปด้วยอินเทอร์เฟซการทำงานใหม่ๆ เพื่อทำให้ชีวิตของคุณง่ายขึ้น อินเทอร์เฟซใหม่บางส่วนเป็นที่รู้จักจากห้องสมุด Google Guava แม้ว่าคุณจะคุ้นเคยกับไลบรารีนี้ คุณก็ควรจับตาดูอย่างใกล้ชิดว่าอินเทอร์เฟซเหล่านั้นถูกขยายด้วยส่วนขยายเมธอดที่มีประโยชน์บางอย่างอย่างไร
เพรดิเคตเป็นฟังก์ชันค่าบูลีนของอาร์กิวเมนต์หนึ่งตัว อินเทอร์เฟซประกอบด้วยวิธีการเริ่มต้นต่างๆ สำหรับการเขียนเพรดิเคตให้เป็นเงื่อนไขเชิงตรรกะที่ซับซ้อน (และ หรือ ลบล้าง)
Predicate < String > predicate = ( s ) -> s . length () > 0 ;
predicate . test ( "foo" ); // true
predicate . negate (). test ( "foo" ); // false
Predicate < Boolean > nonNull = Objects :: nonNull ;
Predicate < Boolean > isNull = Objects :: isNull ;
Predicate < String > isEmpty = String :: isEmpty ;
Predicate < String > isNotEmpty = isEmpty . negate ();
ฟังก์ชันยอมรับหนึ่งอาร์กิวเมนต์และสร้างผลลัพธ์ วิธีการเริ่มต้นสามารถใช้เพื่อเชื่อมโยงหลายฟังก์ชันเข้าด้วยกัน (เขียนแล้ว)
Function < String , Integer > toInteger = Integer :: valueOf ;
Function < String , String > backToString = toInteger . andThen ( String :: valueOf );
backToString . apply ( "123" ); // "123"
ซัพพลายเออร์ผลิตผลลัพธ์ของประเภททั่วไปที่กำหนด ต่างจากฟังก์ชันตรงที่ซัพพลายเออร์ไม่ยอมรับข้อโต้แย้ง
Supplier < Person > personSupplier = Person :: new ;
personSupplier . get (); // new Person
ผู้บริโภคเป็นตัวแทนของการดำเนินการที่จะดำเนินการในอาร์กิวเมนต์อินพุตเดียว
Consumer < Person > greeter = ( p ) -> System . out . println ( "Hello, " + p . firstName );
greeter . accept ( new Person ( "Luke" , "Skywalker" ));
เครื่องมือเปรียบเทียบเป็นที่รู้จักกันดีจาก Java เวอร์ชันเก่า Java 8 เพิ่มวิธีการเริ่มต้นต่างๆ ให้กับอินเทอร์เฟซ
Comparator < Person > comparator = ( p1 , p2 ) -> p1 . firstName . compareTo ( p2 . firstName );
Person p1 = new Person ( "John" , "Doe" );
Person p2 = new Person ( "Alice" , "Wonderland" );
comparator . compare ( p1 , p2 ); // > 0
comparator . reversed (). compare ( p1 , p2 ); // < 0
ตัวเลือกไม่ใช่อินเทอร์เฟซที่ใช้งานได้ แต่เป็นยูทิลิตี้ที่ดีเพื่อป้องกัน NullPointerException
นี่เป็นแนวคิดที่สำคัญสำหรับส่วนถัดไป ดังนั้นเรามาดูกันว่าตัวเลือกทำงานอย่างไรโดยสรุป
ทางเลือกคือคอนเทนเนอร์อย่างง่ายสำหรับค่าที่อาจเป็นโมฆะหรือไม่ใช่โมฆะ ลองนึกถึงวิธีการที่อาจส่งคืนผลลัพธ์ที่ไม่เป็นค่าว่าง แต่บางครั้งก็ไม่ได้ผลลัพธ์เลย แทนที่จะคืนค่า null
คุณจะส่งคืน Optional
ใน Java 8
Optional < String > optional = Optional . of ( "bam" );
optional . isPresent (); // true
optional . get (); // "bam"
optional . orElse ( "fallback" ); // "bam"
optional . ifPresent (( s ) -> System . out . println ( s . charAt ( 0 ))); // "b"
java.util.Stream
แสดงถึงลำดับขององค์ประกอบที่สามารถดำเนินการได้ตั้งแต่หนึ่งรายการขึ้นไป การดำเนินการสตรีมอาจเป็น สื่อกลาง หรือ เทอร์มินัล ในขณะที่การดำเนินการเทอร์มินัลส่งคืนผลลัพธ์เป็นประเภทใดประเภทหนึ่ง การดำเนินการระดับกลางจะส่งคืนสตรีมเอง เพื่อให้คุณสามารถโยงการเรียกเมธอดหลายรายการในแถวได้ สตรีมถูกสร้างขึ้นบนแหล่งที่มา เช่น java.util.Collection
เช่นรายการหรือชุด (ไม่รองรับแผนที่) การดำเนินการสตรีมสามารถดำเนินการตามลำดับหรือแบบขนานก็ได้
สตรีมมีประสิทธิภาพอย่างมาก ดังนั้นฉันจึงเขียนบทช่วยสอน Java 8 Streams แยกต่างหาก คุณควรตรวจสอบ Sequency เป็นไลบรารีที่คล้ายกันสำหรับเว็บ
ก่อนอื่นเรามาดูกันว่ากระแสข้อมูลตามลำดับทำงานอย่างไร ขั้นแรกเราสร้างแหล่งตัวอย่างในรูปแบบของรายการสตริง:
List < String > stringCollection = new ArrayList <>();
stringCollection . add ( "ddd2" );
stringCollection . add ( "aaa2" );
stringCollection . add ( "bbb1" );
stringCollection . add ( "aaa1" );
stringCollection . add ( "bbb3" );
stringCollection . add ( "ccc" );
stringCollection . add ( "bbb2" );
stringCollection . add ( "ddd1" );
คอลเลกชันใน Java 8 ได้รับการขยายเพื่อให้คุณสามารถสร้างสตรีมได้โดยการโทร Collection.stream()
หรือ Collection.parallelStream()
ส่วนต่อไปนี้จะอธิบายการดำเนินการสตรีมที่พบบ่อยที่สุด
ตัวกรองยอมรับภาคแสดงเพื่อกรององค์ประกอบทั้งหมดของสตรีม การดำเนินการนี้เป็นการ ดำเนินการระดับกลาง ซึ่งช่วยให้เราสามารถเรียกการดำเนินการสตรีมอื่น ( forEach
) กับผลลัพธ์ได้ ForEach ยอมรับผู้บริโภคที่จะดำเนินการสำหรับแต่ละองค์ประกอบในสตรีมที่กรอง ForEach เป็นการดำเนินการเทอร์มินัล มันเป็น void
ดังนั้นเราจึงไม่สามารถเรียกใช้การดำเนินการสตรีมอื่นได้
stringCollection
. stream ()
. filter (( s ) -> s . startsWith ( "a" ))
. forEach ( System . out :: println );
// "aaa2", "aaa1"
Sorted เป็นการดำเนินการ ระดับกลาง ซึ่งส่งคืนมุมมองที่เรียงลำดับของสตรีม องค์ประกอบจะถูกจัดเรียงตามลำดับปกติ เว้นแต่คุณจะผ่าน Comparator
ที่กำหนดเอง
stringCollection
. stream ()
. sorted ()
. filter (( s ) -> s . startsWith ( "a" ))
. forEach ( System . out :: println );
// "aaa1", "aaa2"
โปรดทราบว่า sorted
จะสร้างมุมมองที่เรียงลำดับของสตรีมเท่านั้น โดยไม่ปรับเปลี่ยนการเรียงลำดับของคอลเลกชันที่ได้รับการสนับสนุน การเรียงลำดับของ stringCollection
ไม่ได้ถูกแตะต้อง:
System . out . println ( stringCollection );
// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
map
การดำเนินการ ระดับกลาง จะแปลงแต่ละองค์ประกอบให้เป็นวัตถุอื่นผ่านฟังก์ชันที่กำหนด ตัวอย่างต่อไปนี้จะแปลงแต่ละสตริงให้เป็นสตริงตัวพิมพ์ใหญ่ แต่คุณยังสามารถใช้ map
เพื่อแปลงแต่ละวัตถุให้เป็นประเภทอื่นได้ ประเภททั่วไปของสตรีมผลลัพธ์จะขึ้นอยู่กับประเภททั่วไปของฟังก์ชันที่คุณส่งไปยัง map
stringCollection
. stream ()
. map ( String :: toUpperCase )
. sorted (( a , b ) -> b . compareTo ( a ))
. forEach ( System . out :: println );
// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
การดำเนินการจับคู่ต่างๆ สามารถใช้เพื่อตรวจสอบว่าภาคแสดงบางอย่างตรงกับสตรีมหรือไม่ การดำเนินการทั้งหมดเหล่านี้เป็น เทอร์มินัล และส่งคืนผลลัพธ์บูลีน
boolean anyStartsWithA =
stringCollection
. stream ()
. anyMatch (( s ) -> s . startsWith ( "a" ));
System . out . println ( anyStartsWithA ); // true
boolean allStartsWithA =
stringCollection
. stream ()
. allMatch (( s ) -> s . startsWith ( "a" ));
System . out . println ( allStartsWithA ); // false
boolean noneStartsWithZ =
stringCollection
. stream ()
. noneMatch (( s ) -> s . startsWith ( "z" ));
System . out . println ( noneStartsWithZ ); // true
Count คือการดำเนินการ เทอร์มินัล ที่ส่งคืนจำนวนองค์ประกอบในสตรีมแบบ long
long startsWithB =
stringCollection
. stream ()
. filter (( s ) -> s . startsWith ( "b" ))
. count ();
System . out . println ( startsWithB ); // 3
การดำเนินการ เทอร์มินัล นี้จะลดองค์ประกอบของสตรีมด้วยฟังก์ชันที่กำหนด ผลลัพธ์ที่ได้คือ Optional
ในการถือครองค่าที่ลดลง
Optional < String > reduced =
stringCollection
. stream ()
. sorted ()
. reduce (( s1 , s2 ) -> s1 + "#" + s2 );
reduced . ifPresent ( System . out :: println );
// "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"
ดังที่ได้กล่าวไว้ข้างต้นสตรีมอาจเป็นแบบต่อเนื่องหรือแบบขนานก็ได้ การดำเนินการบนสตรีมตามลำดับจะดำเนินการบนเธรดเดียว ในขณะที่การดำเนินการบนสตรีมแบบขนานจะดำเนินการพร้อมกันบนหลายเธรด
ตัวอย่างต่อไปนี้สาธิตความง่ายในการเพิ่มประสิทธิภาพโดยใช้สตรีมแบบขนาน
ขั้นแรก เราสร้างรายการองค์ประกอบเฉพาะจำนวนมาก:
int max = 1000000 ;
List < String > values = new ArrayList <>( max );
for ( int i = 0 ; i < max ; i ++) {
UUID uuid = UUID . randomUUID ();
values . add ( uuid . toString ());
}
ตอนนี้เราวัดเวลาที่ใช้ในการจัดเรียงสตรีมของคอลเลกชันนี้
long t0 = System . nanoTime ();
long count = values . stream (). sorted (). count ();
System . out . println ( count );
long t1 = System . nanoTime ();
long millis = TimeUnit . NANOSECONDS . toMillis ( t1 - t0 );
System . out . println ( String . format ( "sequential sort took: %d ms" , millis ));
// sequential sort took: 899 ms
long t0 = System . nanoTime ();
long count = values . parallelStream (). sorted (). count ();
System . out . println ( count );
long t1 = System . nanoTime ();
long millis = TimeUnit . NANOSECONDS . toMillis ( t1 - t0 );
System . out . println ( String . format ( "parallel sort took: %d ms" , millis ));
// parallel sort took: 472 ms
ดังที่คุณเห็นว่าข้อมูลโค้ดทั้งสองเกือบจะเหมือนกัน แต่การเรียงลำดับแบบขนานนั้นเร็วกว่าประมาณ 50% สิ่งที่คุณต้องทำคือเปลี่ยน stream()
เป็น parallelStream()
ดังที่กล่าวไปแล้วว่าแผนที่ไม่รองรับการสตรีมโดยตรง ไม่มีวิธี stream()
บนอินเทอร์เฟซ Map
เอง อย่างไรก็ตาม คุณสามารถสร้างสตรีมพิเศษตามคีย์ ค่า หรือรายการของแผนที่ผ่าน map.keySet().stream()
, map.values().stream()
และ map.entrySet().stream()
นอกจากนี้ แผนที่ยังสนับสนุนวิธีการใหม่ๆ ที่เป็นประโยชน์มากมายสำหรับการทำงานทั่วไป
Map < Integer , String > map = new HashMap <>();
for ( int i = 0 ; i < 10 ; i ++) {
map . putIfAbsent ( i , "val" + i );
}
map . forEach (( id , val ) -> System . out . println ( val ));
โค้ดด้านบนควรอธิบายได้ในตัว: putIfAbsent
จะป้องกันไม่ให้เราเขียนเพิ่มเติมหากตรวจสอบค่าว่าง forEach
ยอมรับผู้บริโภคเพื่อดำเนินการกับแต่ละค่าของแผนที่
ตัวอย่างนี้แสดงวิธีคำนวณโค้ดบนแผนที่โดยใช้ฟังก์ชัน:
map . computeIfPresent ( 3 , ( num , val ) -> val + num );
map . get ( 3 ); // val33
map . computeIfPresent ( 9 , ( num , val ) -> null );
map . containsKey ( 9 ); // false
map . computeIfAbsent ( 23 , num -> "val" + num );
map . containsKey ( 23 ); // true
map . computeIfAbsent ( 3 , num -> "bam" );
map . get ( 3 ); // val33
ต่อไป เราจะเรียนรู้วิธีลบรายการสำหรับคีย์ที่ระบุ เฉพาะเมื่อคีย์นั้นถูกแมปกับค่าที่กำหนดในปัจจุบันเท่านั้น:
map . remove ( 3 , "val3" );
map . get ( 3 ); // val33
map . remove ( 3 , "val33" );
map . get ( 3 ); // null
อีกวิธีที่เป็นประโยชน์:
map . getOrDefault ( 42 , "not found" ); // not found
การรวมรายการแผนที่นั้นค่อนข้างง่าย:
map . merge ( 9 , "val9" , ( value , newValue ) -> value . concat ( newValue ));
map . get ( 9 ); // val9
map . merge ( 9 , "concat" , ( value , newValue ) -> value . concat ( newValue ));
map . get ( 9 ); // val9concat
ผสานโดยใส่คีย์/ค่าลงในแผนที่หากไม่มีรายการสำหรับคีย์ ไม่เช่นนั้นฟังก์ชันการรวมจะถูกเรียกให้เปลี่ยนค่าที่มีอยู่
Java 8 มี API วันที่และเวลาใหม่ล่าสุดภายใต้แพ็คเกจ java.time
Date API ใหม่สามารถเทียบเคียงได้กับไลบรารี Joda-Time แต่ก็ไม่เหมือนกัน ตัวอย่างต่อไปนี้ครอบคลุมส่วนที่สำคัญที่สุดของ API ใหม่นี้
นาฬิกาช่วยให้เข้าถึงวันที่และเวลาปัจจุบันได้ นาฬิกาทราบเขตเวลาและอาจใช้แทน System.currentTimeMillis()
เพื่อดึงข้อมูลเวลาปัจจุบันในหน่วยมิลลิวินาทีตั้งแต่ Unix EPOCH จุดที่เกิดขึ้นทันทีบนไทม์ไลน์ดังกล่าวยังแสดงด้วยคลาส Instant
อีกด้วย สามารถใช้เพื่อสร้างวัตถุ java.util.Date
ดั้งเดิมได้
Clock clock = Clock . systemDefaultZone ();
long millis = clock . millis ();
Instant instant = clock . instant ();
Date legacyDate = Date . from ( instant ); // legacy java.util.Date
เขตเวลาจะแสดงด้วย ZoneId
สามารถเข้าถึงได้ง่ายผ่านวิธีการแบบคงที่ของโรงงาน เขตเวลาจะกำหนดออฟเซ็ตซึ่งมีความสำคัญในการแปลงระหว่างวันที่และเวลาท้องถิ่นในทันที
System . out . println ( ZoneId . getAvailableZoneIds ());
// prints all available timezone ids
ZoneId zone1 = ZoneId . of ( "Europe/Berlin" );
ZoneId zone2 = ZoneId . of ( "Brazil/East" );
System . out . println ( zone1 . getRules ());
System . out . println ( zone2 . getRules ());
// ZoneRules[currentStandardOffset=+01:00]
// ZoneRules[currentStandardOffset=-03:00]
LocalTime แสดงถึงเวลาที่ไม่มีเขตเวลา เช่น 22.00 น. หรือ 17.30:15 น. ตัวอย่างต่อไปนี้สร้างเวลาท้องถิ่นสองครั้งสำหรับเขตเวลาที่กำหนดไว้ข้างต้น จากนั้นเราจะเปรียบเทียบทั้งสองเวลาและคำนวณความแตกต่างเป็นชั่วโมงและนาทีระหว่างทั้งสองเวลา
LocalTime now1 = LocalTime . now ( zone1 );
LocalTime now2 = LocalTime . now ( zone2 );
System . out . println ( now1 . isBefore ( now2 )); // false
long hoursBetween = ChronoUnit . HOURS . between ( now1 , now2 );
long minutesBetween = ChronoUnit . MINUTES . between ( now1 , now2 );
System . out . println ( hoursBetween ); // -3
System . out . println ( minutesBetween ); // -239
LocalTime มาพร้อมกับวิธีการจากโรงงานที่หลากหลายเพื่อทำให้การสร้างอินสแตนซ์ใหม่ง่ายขึ้น รวมถึงการแยกวิเคราะห์สตริงเวลา
LocalTime late = LocalTime . of ( 23 , 59 , 59 );
System . out . println ( late ); // 23:59:59
DateTimeFormatter germanFormatter =
DateTimeFormatter
. ofLocalizedTime ( FormatStyle . SHORT )
. withLocale ( Locale . GERMAN );
LocalTime leetTime = LocalTime . parse ( "13:37" , germanFormatter );
System . out . println ( leetTime ); // 13:37
LocalDate แสดงถึงวันที่ที่แตกต่างกัน เช่น 2014-03-11 มันไม่เปลี่ยนรูปและทำงานแบบอะนาล็อกกับ LocalTime ทุกประการ ตัวอย่างนี้สาธิตวิธีการคำนวณวันที่ใหม่โดยการบวกหรือลบวัน เดือน หรือปี โปรดทราบว่าการจัดการแต่ละครั้งจะส่งคืนอินสแตนซ์ใหม่
LocalDate today = LocalDate . now ();
LocalDate tomorrow = today . plus ( 1 , ChronoUnit . DAYS );
LocalDate yesterday = tomorrow . minusDays ( 2 );
LocalDate independenceDay = LocalDate . of ( 2014 , Month . JULY , 4 );
DayOfWeek dayOfWeek = independenceDay . getDayOfWeek ();
System . out . println ( dayOfWeek ); // FRIDAY
การแยกวิเคราะห์ LocalDate จากสตริงนั้นทำได้ง่ายพอๆ กับการแยกวิเคราะห์ LocalTime:
DateTimeFormatter germanFormatter =
DateTimeFormatter
. ofLocalizedDate ( FormatStyle . MEDIUM )
. withLocale ( Locale . GERMAN );
LocalDate xmas = LocalDate . parse ( "24.12.2014" , germanFormatter );
System . out . println ( xmas ); // 2014-12-24
LocalDateTime แสดงถึงวันที่-เวลา โดยจะรวมวันที่และเวลาตามที่เห็นในส่วนด้านบนไว้ในอินสแตนซ์เดียว LocalDateTime
จะไม่เปลี่ยนรูปและทำงานคล้ายกับ LocalTime และ LocalDate เราสามารถใช้วิธีการดึงข้อมูลบางช่องจากวันที่-เวลา:
LocalDateTime sylvester = LocalDateTime . of ( 2014 , Month . DECEMBER , 31 , 23 , 59 , 59 );
DayOfWeek dayOfWeek = sylvester . getDayOfWeek ();
System . out . println ( dayOfWeek ); // WEDNESDAY
Month month = sylvester . getMonth ();
System . out . println ( month ); // DECEMBER
long minuteOfDay = sylvester . getLong ( ChronoField . MINUTE_OF_DAY );
System . out . println ( minuteOfDay ); // 1439
ด้วยข้อมูลเพิ่มเติมของเขตเวลา จึงสามารถแปลงเป็นแบบทันทีได้ Instants สามารถแปลงเป็นวันที่ดั้งเดิมประเภท java.util.Date
ได้อย่างง่ายดาย
Instant instant = sylvester
. atZone ( ZoneId . systemDefault ())
. toInstant ();
Date legacyDate = Date . from ( instant );
System . out . println ( legacyDate ); // Wed Dec 31 23:59:59 CET 2014
การจัดรูปแบบวันที่-เวลาทำงานเหมือนกับการจัดรูปแบบวันที่หรือเวลา แทนที่จะใช้รูปแบบที่กำหนดไว้ล่วงหน้า เราสามารถสร้างตัวจัดรูปแบบจากรูปแบบที่กำหนดเองได้
DateTimeFormatter formatter =
DateTimeFormatter
. ofPattern ( "MMM dd, yyyy - HH:mm" );
LocalDateTime parsed = LocalDateTime . parse ( "Nov 03, 2014 - 07:13" , formatter );
String string = formatter . format ( parsed );
System . out . println ( string ); // Nov 03, 2014 - 07:13
ไม่เหมือนกับ java.text.NumberFormat
DateTimeFormatter
ใหม่จะไม่เปลี่ยนรูปและ ปลอดภัยต่อเธรด
สำหรับรายละเอียดเกี่ยวกับไวยากรณ์รูปแบบ โปรดอ่านที่นี่
คำอธิบายประกอบใน Java 8 สามารถทำซ้ำได้ ลองมาดูตัวอย่างโดยตรงเพื่อหาคำตอบกัน
ขั้นแรก เรากำหนดคำอธิบายประกอบแบบ wrapper ซึ่งเก็บอาร์เรย์ของคำอธิบายประกอบจริง:
@interface Hints {
Hint [] value ();
}
@ Repeatable ( Hints . class )
@interface Hint {
String value ();
}
Java 8 ช่วยให้เราใช้คำอธิบายประกอบหลายรายการที่เป็นประเภทเดียวกันได้โดยการประกาศคำอธิบายประกอบ @Repeatable
@ Hints ({ @ Hint ( "hint1" ), @ Hint ( "hint2" )})
class Person {}
@ Hint ( "hint1" )
@ Hint ( "hint2" )
class Person {}
การใช้ตัวแปร 2 คอมไพเลอร์ Java จะตั้งค่าคำอธิบายประกอบ @Hints
โดยปริยายภายใต้ประทุน นั่นเป็นสิ่งสำคัญสำหรับการอ่านข้อมูลคำอธิบายประกอบผ่านการไตร่ตรอง
Hint hint = Person . class . getAnnotation ( Hint . class );
System . out . println ( hint ); // null
Hints hints1 = Person . class . getAnnotation ( Hints . class );
System . out . println ( hints1 . value (). length ); // 2
Hint [] hints2 = Person . class . getAnnotationsByType ( Hint . class );
System . out . println ( hints2 . length ); // 2
แม้ว่าเราจะไม่เคยประกาศคำอธิบายประกอบ @Hints
ในคลาส Person
แต่ก็ยังสามารถอ่านได้ผ่าน getAnnotation(Hints.class)
อย่างไรก็ตาม วิธีที่สะดวกกว่าคือ getAnnotationsByType
ซึ่งให้สิทธิ์การเข้าถึงโดยตรงไปยังคำอธิบายประกอบ @Hint
ที่มีคำอธิบายประกอบทั้งหมด
นอกจากนี้ การใช้คำอธิบายประกอบใน Java 8 ยังขยายไปสู่เป้าหมายใหม่สองประการ:
@ Target ({ ElementType . TYPE_PARAMETER , ElementType . TYPE_USE })
@interface MyAnnotation {}
คู่มือการเขียนโปรแกรม Java 8 ของฉันสิ้นสุดที่นี่ หากคุณต้องการเรียนรู้เพิ่มเติมเกี่ยวกับคลาสและคุณสมบัติใหม่ทั้งหมดของ JDK 8 API โปรดดูที่ JDK8 API Explorer ของฉัน มันช่วยให้คุณค้นหาคลาสใหม่และอัญมณีที่ซ่อนอยู่ของ JDK 8 เช่น Arrays.parallelSort
, StampedLock
และ CompletableFuture
- และอื่นๆ อีกมากมาย
ฉันยังได้เผยแพร่บทความติดตามผลจำนวนมากในบล็อกของฉันซึ่งอาจน่าสนใจสำหรับคุณ:
คุณควรติดตามฉันบน Twitter ขอบคุณสำหรับการอ่าน!