การใช้ขอบเขตและการปิดคำศัพท์
นักพัฒนาหลายคนมีความเข้าใจผิดนี้ โดยเชื่อว่าการใช้นิพจน์แลมบ์ดาจะทำให้เกิดความซ้ำซ้อนของโค้ดและลดคุณภาพของโค้ด ในทางตรงกันข้าม ไม่ว่าโค้ดจะซับซ้อนแค่ไหน เราจะไม่ประนีประนอมกับคุณภาพของโค้ดเพื่อความเรียบง่าย ดังที่เราจะเห็นด้านล่าง
เราสามารถใช้นิพจน์แลมบ์ดาซ้ำได้ในตัวอย่างก่อนหน้านี้ อย่างไรก็ตาม ถ้าเราจับคู่ตัวอักษรอื่น ปัญหาของความซ้ำซ้อนของโค้ดจะกลับมาอย่างรวดเร็ว มาวิเคราะห์ปัญหานี้เพิ่มเติมก่อน จากนั้นใช้ขอบเขตและการปิดคำศัพท์เพื่อแก้ไข
ความซ้ำซ้อนที่เกิดจากนิพจน์แลมบ์ดา
มากรองตัวอักษรที่ขึ้นต้นด้วย N หรือ B จากเพื่อนกัน จากตัวอย่างข้างต้น โค้ดที่เราเขียนอาจมีลักษณะดังนี้:
คัดลอกรหัสรหัสดังต่อไปนี้:
ภาคแสดงสุดท้าย <String> startWithN = name -> name.startsWith("N");
ภาคแสดงสุดท้าย <String> startWithB = name -> name.startsWith("B");
countFriendsStartN ยาวครั้งสุดท้าย =
เพื่อน.สตรีม()
.filter(startsWithN).count();
countFriendsStartB ยาวสุดท้าย =
เพื่อน.สตรีม()
.filter(startsWithB).count();
ภาคแรกกำหนดว่าชื่อขึ้นต้นด้วย N หรือไม่ และภาคที่สองกำหนดว่าชื่อขึ้นต้นด้วย B หรือไม่ เราส่งผ่านทั้งสองอินสแตนซ์นี้ไปยังการเรียกเมธอดตัวกรองสองตัวตามลำดับ ดูเหมือนสมเหตุสมผล แต่ภาคแสดงทั้งสองนั้นซ้ำซ้อน เป็นเพียงตัวอักษรที่แตกต่างกันในเช็ค มาดูกันว่าเราจะหลีกเลี่ยงความซ้ำซ้อนนี้ได้อย่างไร
ใช้ขอบเขตคำศัพท์เพื่อหลีกเลี่ยงความซ้ำซ้อน
ในแนวทางแรก เราสามารถแยกตัวอักษรเป็นพารามิเตอร์ของฟังก์ชันและส่งฟังก์ชันนี้ไปยังวิธีกรองได้ นี่เป็นวิธีที่ดี แต่บางฟังก์ชันไม่ยอมรับตัวกรอง ยอมรับเฉพาะฟังก์ชันที่มีพารามิเตอร์เพียงตัวเดียวเท่านั้น พารามิเตอร์นั้นสอดคล้องกับองค์ประกอบในคอลเลกชันและส่งกลับค่าบูลีน โดยหวังว่าสิ่งที่ส่งผ่านเข้ามาจะเป็นภาคแสดง
เราหวังว่าจะมีที่ที่สามารถแคชตัวอักษรนี้ได้จนกว่าพารามิเตอร์จะถูกส่งผ่าน (ในกรณีนี้คือพารามิเตอร์ชื่อ) มาสร้างฟังก์ชั่นใหม่แบบนี้กันดีกว่า
คัดลอกรหัสรหัสดังต่อไปนี้:
สาธารณะคงเพรดิเคต <String> checkIfStartsWith (ตัวอักษรสตริงสุดท้าย) {
ชื่อกลับ -> name.startsWith (ตัวอักษร);
-
เรากำหนดฟังก์ชันคงที่ checkIfStartsWith ซึ่งรับพารามิเตอร์ String และส่งกลับอ็อบเจ็กต์ภาคแสดง ซึ่งสามารถส่งผ่านไปยังวิธีการกรองเพื่อใช้ในภายหลังได้ ซึ่งแตกต่างจากฟังก์ชันลำดับที่สูงกว่าที่เราเห็นก่อนหน้านี้ ซึ่งรับฟังก์ชันเป็นพารามิเตอร์ เมธอดนี้จะส่งคืนฟังก์ชัน แต่มันก็เป็นฟังก์ชันที่มีลำดับสูงกว่าด้วย ซึ่งเราได้กล่าวไว้แล้วในวิวัฒนาการ ไม่ใช่การเปลี่ยนแปลง ในหน้าที่ 12
อ็อบเจ็กต์เพรดิเคตที่ส่งคืนโดยเมธอด checkIfStartsWith ค่อนข้างแตกต่างจากนิพจน์แลมบ์ดาอื่นๆ ในคำสั่ง return name -> name.startsWith(letter) เรารู้ว่าชื่ออะไร เป็นพารามิเตอร์ที่ส่งผ่านไปยังนิพจน์ lambda แต่ตัวอักษรตัวแปรคืออะไรกันแน่? มันอยู่นอกโดเมนของฟังก์ชันที่ไม่ระบุชื่อ Java ค้นหาโดเมนที่มีการกำหนดนิพจน์แลมบ์ดาและค้นหาตัวอักษรตัวแปร สิ่งนี้เรียกว่าขอบเขตคำศัพท์ ขอบเขตคำศัพท์เป็นสิ่งที่มีประโยชน์มาก โดยช่วยให้เราสามารถแคชตัวแปรในขอบเขตหนึ่งเพื่อใช้ในภายหลังในบริบทอื่น เนื่องจากนิพจน์ lambda นี้ใช้ตัวแปรในขอบเขต สถานการณ์นี้จึงเรียกว่าการปิด เกี่ยวกับการจำกัดการเข้าถึงขอบเขตคำศัพท์ คุณสามารถอ่านข้อจำกัดเกี่ยวกับขอบเขตคำศัพท์ในหน้า 31 ได้หรือไม่
มีข้อจำกัดเกี่ยวกับขอบเขตคำศัพท์หรือไม่?
ในนิพจน์แลมบ์ดา เราสามารถเข้าถึงเฉพาะประเภทสุดท้ายในขอบเขตหรือตัวแปรเฉพาะที่ของประเภทสุดท้ายเท่านั้น
นิพจน์แลมบ์ดาอาจถูกเรียกทันที ล่าช้า หรือจากเธรดอื่น เพื่อหลีกเลี่ยงความขัดแย้งด้านเชื้อชาติ ตัวแปรท้องถิ่นในโดเมนที่เราเข้าถึงไม่ได้รับอนุญาตให้แก้ไขเมื่อเตรียมใช้งานแล้ว การดำเนินการแก้ไขใดๆ จะทำให้เกิดข้อยกเว้นในการคอมไพล์
การทำเครื่องหมายว่าเป็นขั้นสุดท้ายช่วยแก้ปัญหานี้ได้ แต่ Java ไม่ได้บังคับให้เราทำเครื่องหมายด้วยวิธีนี้ ในความเป็นจริง Java มองสองสิ่ง ประการแรกคือตัวแปรที่เข้าถึงจะต้องเริ่มต้นในวิธีการที่กำหนดไว้ และก่อนที่จะกำหนดนิพจน์แลมบ์ดา ประการที่สอง ค่าของตัวแปรเหล่านี้ไม่สามารถแก้ไขได้ - นั่นคือในความเป็นจริงแล้วเป็นประเภทสุดท้ายแม้ว่าจะไม่ได้ทำเครื่องหมายไว้ก็ตาม
นิพจน์แลมบ์ดาแบบไม่ระบุสถานะเป็นค่าคงที่รันไทม์ ในขณะที่นิพจน์แลมบ์ดาที่ใช้ตัวแปรเฉพาะที่มีค่าใช้จ่ายในการคำนวณเพิ่มเติม
เมื่อเรียกเมธอด filter เราสามารถใช้นิพจน์แลมบ์ดาที่ส่งคืนโดยเมธอด checkIfStartsWith ได้ดังนี้
คัดลอกรหัสรหัสดังต่อไปนี้:
countFriendsStartN ยาวครั้งสุดท้าย =
friends.stream() .filter(checkIfStartsWith("N")).count();
countFriendsStartB ยาวครั้งสุดท้าย = friends.stream()
.filter(checkIfStartsWith("B")).count();
ก่อนที่จะเรียกเมธอด filter เราเรียกเมธอด checkIfStartsWith() ก่อนและส่งผ่านตัวอักษรที่ต้องการ การเรียกนี้จะส่งคืนนิพจน์แลมบ์ดาอย่างรวดเร็ว ซึ่งเราจะส่งต่อไปยังวิธีการกรอง
ด้วยการสร้างฟังก์ชันลำดับที่สูงกว่า (checkIfStartsWith ในกรณีนี้) และใช้ขอบเขตคำศัพท์ เราก็สามารถลบความซ้ำซ้อนออกจากโค้ดได้สำเร็จ เราไม่จำเป็นต้องกำหนดซ้ำๆ อีกต่อไปว่าชื่อขึ้นต้นด้วยตัวอักษรตัวใดตัวหนึ่งอีกต่อไป
ปรับโครงสร้างใหม่ ลดขอบเขต
ในตัวอย่างก่อนหน้านี้ เราใช้วิธีคงที่ แต่เราไม่ต้องการใช้วิธีคงที่เพื่อแคชตัวแปร ซึ่งจะทำให้โค้ดของเรายุ่งเหยิง ทางที่ดีควรจำกัดขอบเขตของฟังก์ชันนี้ให้แคบลงให้เหลือเฉพาะตำแหน่งที่ใช้งาน เราสามารถใช้อินเทอร์เฟซฟังก์ชันเพื่อให้บรรลุเป้าหมายนี้ได้
คัดลอกรหัสรหัสดังต่อไปนี้:
ฟังก์ชั่นสุดท้าย <สตริง, ภาคแสดง <สตริง>> startWithLetter = (ตัวอักษรสตริง) -> {
เพรดิเคต<String> checkStarts = (ชื่อสตริง) -> name.startsWith(letter);
กลับตรวจสอบเริ่มต้น; };
นิพจน์แลมบ์ดานี้จะแทนที่วิธีสแตติกดั้งเดิม โดยสามารถวางในฟังก์ชันและกำหนดไว้ก่อนที่จะจำเป็น ตัวแปร startWithLetter อ้างอิงถึงฟังก์ชันที่มีพารามิเตอร์อินพุตเป็น String และมีพารามิเตอร์เอาต์พุตเป็น Predicate
เมื่อเปรียบเทียบกับการใช้วิธีคงที่ เวอร์ชันนี้ง่ายกว่ามาก แต่เราสามารถจัดโครงสร้างใหม่ต่อไปเพื่อให้กระชับยิ่งขึ้นได้ จากมุมมองเชิงปฏิบัติ ฟังก์ชันนี้จะเหมือนกับวิธีแบบคงที่ก่อนหน้านี้ โดยทั้งคู่จะได้รับสตริงและส่งกลับภาคแสดง แทนที่จะประกาศภาคแสดงอย่างชัดเจน เราจะแทนที่มันทั้งหมดด้วยนิพจน์แลมบ์ดา
คัดลอกรหัสรหัสดังต่อไปนี้:
ฟังก์ชั่นสุดท้าย <สตริง, ภาคแสดง <สตริง>> startWithLetter = (ตัวอักษรสตริง) -> (ชื่อสตริง) -> name.startsWith (ตัวอักษร);
เราได้กำจัดความยุ่งเหยิงออกไปแล้ว แต่เรายังสามารถลบการประกาศประเภทเพื่อให้กระชับยิ่งขึ้นได้ และคอมไพเลอร์ Java จะดำเนินการหักประเภทตามบริบท มาดูรุ่นที่ปรับปรุงกันดีกว่า
คัดลอกรหัสรหัสดังต่อไปนี้:
ฟังก์ชั่นสุดท้าย <สตริง, ภาคแสดง <สตริง>> startWithLetter =
ตัวอักษร -> ชื่อ -> name.startsWith (ตัวอักษร);
ต้องใช้ความพยายามพอสมควรในการปรับให้เข้ากับไวยากรณ์ที่กระชับนี้ ถ้ามันทำให้คุณตาบอดให้มองหาที่อื่นก่อน เราได้ปรับโครงสร้างโค้ดใหม่เสร็จสิ้นแล้ว และตอนนี้สามารถใช้เพื่อแทนที่เมธอด checkIfStartsWith() ดั้งเดิมได้ เช่นนี้
คัดลอกรหัสรหัสดังต่อไปนี้:
countFriendsStartN ยาวครั้งสุดท้าย = friends.stream()
.filter(startsWithLetter.apply("N")).count();
countFriendsStartB ยาวครั้งสุดท้าย = friends.stream()
.filter(startsWithLetter.apply("B")).count();
ในส่วนนี้เราใช้ฟังก์ชันลำดับที่สูงกว่า เราเห็นวิธีการสร้างฟังก์ชันภายในฟังก์ชันหากเราส่งฟังก์ชันไปยังฟังก์ชันอื่น และวิธีการคืนค่าฟังก์ชันจากฟังก์ชัน ตัวอย่างเหล่านี้ล้วนแสดงให้เห็นถึงความเรียบง่ายและการนำกลับมาใช้ซ้ำได้ซึ่งเกิดจากนิพจน์แลมบ์ดา
ในส่วนนี้ เราได้ใช้ฟังก์ชันของ Function และ Predicate อย่างเต็มที่แล้ว แต่ลองมาดูความแตกต่างระหว่างฟังก์ชันเหล่านี้กัน เพรดิเคตยอมรับพารามิเตอร์ประเภท T และส่งคืนค่าบูลีนเพื่อแสดงค่าจริงหรือเท็จของเงื่อนไขการตัดสินที่สอดคล้องกัน เมื่อเราจำเป็นต้องตัดสินแบบมีเงื่อนไข เราสามารถใช้ Predicateg เพื่อดำเนินการให้เสร็จสิ้นได้ วิธีการเช่นตัวกรองที่องค์ประกอบตัวกรองได้รับภาคแสดงเป็นพารามิเตอร์ Funciton แสดงถึงฟังก์ชันที่พารามิเตอร์อินพุตเป็นตัวแปรประเภท T และส่งกลับผลลัพธ์เป็นประเภท R มีลักษณะทั่วไปมากกว่าภาคแสดงซึ่งสามารถคืนค่าบูลีนได้เท่านั้น ตราบใดที่อินพุตถูกแปลงเป็นเอาต์พุต เราสามารถใช้ Function ได้ ดังนั้นจึงสมเหตุสมผลที่ map จะใช้ Function เป็นพารามิเตอร์
อย่างที่คุณเห็น การเลือกองค์ประกอบจากคอลเลกชันนั้นง่ายมาก ด้านล่างนี้เราจะแนะนำวิธีการเลือกองค์ประกอบเดียวจากคอลเลกชัน