Java 8 lambdas, Function.identity () หรือ t-> t


240

ฉันมีคำถามเกี่ยวกับการใช้Function.identity()วิธีการ

ลองนึกภาพโค้ดต่อไปนี้:

Arrays.asList("a", "b", "c")
          .stream()
          .map(Function.identity()) // <- This,
          .map(str -> str)          // <- is the same as this.
          .collect(Collectors.toMap(
                       Function.identity(), // <-- And this,
                       str -> str));        // <-- is the same as this.

มีเหตุผลใดบ้างที่คุณควรใช้Function.identity()แทนstr->str(หรือกลับกัน) ฉันคิดว่าตัวเลือกที่สองสามารถอ่านได้มากขึ้น (แน่นอนว่าเป็นเรื่องของรสนิยม) แต่มีเหตุผล "ของจริง" ที่ว่าทำไมเราถึงเลือกที่ดีกว่า


6
ในที่สุดไม่มีสิ่งนี้จะไม่สร้างความแตกต่าง
fge

50
ทั้งเป็นเรื่องปกติ ไปไหนก็ได้ที่คุณคิดว่าอ่านได้มากกว่า (ไม่ต้องกังวลมีความสุข)
Brian Goetz

3
ฉันต้องการt -> tเพียงเพราะมันสั้นกระชับ
David Conrad

3
คำถามที่ไม่เกี่ยวข้องเล็กน้อย แต่ไม่มีใครรู้ว่าทำไมผู้ออกแบบภาษาสร้างตัวตน () ส่งคืนอินสแตนซ์ของฟังก์ชั่นแทนที่จะมีพารามิเตอร์ประเภท T และส่งกลับเพื่อให้วิธีการที่สามารถใช้กับการอ้างอิงวิธีการ?
คิริลล์ Rakhman

ฉันจะโต้แย้งว่ามีประโยชน์ในการพูดคุยกับคำว่า "ตัวตน" เนื่องจากมีความหมายที่สำคัญในด้านอื่น ๆ ของการเขียนโปรแกรมการทำงาน
orbfish

คำตอบ:


312

ในฐานะของการใช้ JRE ปัจจุบันFunction.identity()จะส่งคืนอินสแตนซ์เดียวกันเสมอในแต่ละครั้งที่เกิดขึ้นidentifier -> identifierไม่เพียง แต่จะสร้างอินสแตนซ์ของตัวเอง แต่ยังมีคลาสการใช้งานที่แตกต่างกัน สำหรับรายละเอียดเพิ่มเติมโปรดดูที่นี่

เหตุผลก็คือคอมไพเลอร์สร้างวิธีสังเคราะห์ที่เก็บเนื้อความเล็ก ๆ น้อย ๆ ของนิพจน์แลมบ์ดานั้น (ในกรณีที่x->xเทียบเท่าreturn identifier;) และบอกรันไทม์เพื่อสร้างการใช้งานของส่วนต่อประสานการทำงานที่เรียกวิธีนี้ ดังนั้นรันไทม์จึงเห็นเพียงวิธีการเป้าหมายที่แตกต่างกันและการใช้งานในปัจจุบันไม่ได้วิเคราะห์วิธีการเพื่อค้นหาว่าวิธีการบางอย่างจะเทียบเท่า

ดังนั้นการใช้Function.identity()แทนx -> xอาจบันทึกหน่วยความจำบางส่วน แต่ที่ไม่ควรขับรถการตัดสินใจของคุณถ้าคุณคิดว่าสามารถอ่านได้มากกว่าx -> xFunction.identity()

คุณอาจพิจารณาว่าเมื่อเปิดใช้งานการรวบรวมข้อมูลการดีบักวิธีการสังเคราะห์จะมีแอตทริบิวต์การดีบักบรรทัดที่ชี้ไปที่บรรทัดซอร์สโค้ดที่มีการแสดงออกแลมบ์ดาดังนั้นคุณจึงมีโอกาสหาแหล่งที่มาของFunctionอินสแตนซ์เฉพาะขณะทำการดีบัก . ในทางตรงกันข้ามเมื่อพบกับอินสแตนซ์ที่ส่งคืนโดยFunction.identity()ในระหว่างการดีบักการดำเนินการคุณจะไม่ทราบว่าใครได้เรียกวิธีการนั้นและส่งผ่านอินสแตนซ์ไปยังการดำเนินการ


5
คำตอบที่ดี ฉันมีข้อสงสัยเกี่ยวกับการดีบัก มันจะมีประโยชน์อย่างไร เป็นไปได้ยากมากที่จะได้รับการติดตามสแต็กข้อยกเว้นที่เกี่ยวข้องกับx -> xเฟรม คุณแนะนำให้ตั้งค่าเบรกพอยต์เป็นแลมบ์ดานี้หรือไม่? โดยปกติแล้วไม่ใช่เรื่องง่ายที่จะนำเบรกพอยต์ไปใช้กับแลมบ์ดานิพจน์ (อย่างน้อยก็ใน Eclipse) ...
Tagir Valeev

14
@Tagir Valeev: คุณสามารถตรวจแก้จุดบกพร่องรหัสที่ได้รับฟังก์ชั่นโดยพลการและขั้นตอนในวิธีการใช้ของฟังก์ชั่นนั้น จากนั้นคุณอาจสิ้นสุดที่รหัสแหล่งที่มาของการแสดงออกแลมบ์ดา ในกรณีของการแสดงออกแลมบ์ดาที่ชัดเจนคุณจะรู้ว่าฟังก์ชั่นนี้มาจากไหนและมีโอกาสที่จะรับรู้ถึงการตัดสินใจที่จะส่งผ่านแม้ว่าจะมีการสร้างฟังก์ชันเฉพาะตัวขึ้นมา เมื่อใช้Function.identity()ข้อมูลนั้นจะหายไป จากนั้นสายโซ่อาจช่วยในกรณีที่ง่าย แต่คิดว่าเช่นการประเมินแบบมัลติเธรดที่ริเริ่มไม่ได้อยู่ในการติดตามสแต็ก ...
โฮลเกอร์

2
น่าสนใจในบริบทนี้: blog.codefx.org/java/instances-non-capturing-lambdas
Wim Deblauwe

13
@Wim Deblauwe: น่าสนใจ แต่ฉันมักจะเห็นวิธีอื่น ๆ เสมอ: หากวิธีการที่โรงงานไม่ได้ระบุไว้อย่างชัดเจนในเอกสารของมันว่ามันจะส่งคืนอินสแตนซ์ใหม่ในการเรียกใช้ทุกครั้งคุณไม่สามารถสันนิษฐานได้ว่าจะเป็นเช่นนั้น ดังนั้นจึงไม่น่าแปลกใจถ้ามันไม่ได้ ท้ายnewที่สุดนั่นคือเหตุผลหนึ่งที่ทำให้การใช้วิธีการของโรงงานเป็นวิธีการแทน new Foo(…)รับประกันว่าจะสร้างตัวอย่างใหม่ของประเภทที่แน่นอนFooในขณะที่Foo.getInstance(…)อาจส่งคืนอินสแตนซ์ที่มีอยู่ของ (ประเภทย่อย) Foo...
Holger

93

ในตัวอย่างของคุณไม่มีความแตกต่างใหญ่ระหว่างstr -> strและตั้งแต่ภายในมันเป็นเพียงFunction.identity()t->t

แต่บางครั้งเราไม่สามารถใช้เพราะเราไม่สามารถใช้Function.identity Functionลองดูที่นี่:

List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);

นี้จะรวบรวมได้ดี

int[] arrayOK = list.stream().mapToInt(i -> i).toArray();

แต่ถ้าคุณพยายามที่จะรวบรวม

int[] arrayProblem = list.stream().mapToInt(Function.identity()).toArray();

คุณจะได้รับข้อผิดพลาดตั้งแต่การรวบรวมmapToIntคาดซึ่งไม่เกี่ยวข้องกับToIntFunction FunctionยังToIntFunctionไม่มีidentity()วิธี


3
ดูstackoverflow.com/q/38034982/14731สำหรับตัวอย่างอื่นที่การแทนที่i -> iด้วยFunction.identity()จะทำให้เกิดข้อผิดพลาดของคอมไพเลอร์
Gili

19
mapToInt(Integer::intValue)ฉันชอบ
shmosel

4
@shmosel ว่าก็โอเค แต่มันเป็นมูลค่าการกล่าวขวัญว่าการแก้ปัญหาทั้งสองจะทำงานคล้ายตั้งแต่เป็นความเรียบง่ายของmapToInt(i -> i) mapToInt( (Integer i) -> i.intValue())ใช้เวอร์ชันใดที่คุณคิดว่าชัดเจนสำหรับฉันmapToInt(i -> i)ดีกว่าแสดงให้เห็นถึงความตั้งใจของรหัสนี้
Pshemo

1
ฉันคิดว่าอาจมีประโยชน์ด้านประสิทธิภาพในการใช้การอ้างอิงวิธีการ แต่ส่วนใหญ่เป็นเพียงความชอบส่วนตัว ฉันพบว่ามันมีความหมายมากกว่าเพราะi -> iดูเหมือนว่าฟังก์ชันตัวตนซึ่งไม่ได้อยู่ในกรณีนี้
shmosel

@shmosel ฉันไม่สามารถพูดถึงความแตกต่างด้านประสิทธิภาพได้มากนักดังนั้นคุณอาจพูดถูก แต่ถ้าประสิทธิภาพไม่เป็นปัญหาฉันจะอยู่กับi -> iเนื่องจากเป้าหมายของฉันคือการแมปจำนวนเต็มเป็น int (ซึ่งmapToIntแนะนำค่อนข้างดี) ไม่ให้เรียกintValue()วิธีการอย่างชัดเจน วิธีการทำแผนที่นี้จะประสบความสำเร็จไม่สำคัญจริง ๆ ดังนั้นขอเพียงเห็นด้วยที่จะไม่เห็นด้วย แต่ขอขอบคุณสำหรับการชี้ให้เห็นถึงความแตกต่างของประสิทธิภาพการทำงานที่เป็นไปได้ฉันจะต้องดูอย่างใกล้ชิดสักวันหนึ่ง
Pshemo

44

จากแหล่ง JDK :

static <T> Function<T, T> identity() {
    return t -> t;
}

ดังนั้นไม่ตราบใดที่มันถูกต้องทางไวยากรณ์


8
ฉันสงสัยว่าสิ่งนี้จะทำให้คำตอบข้างต้นเกี่ยวข้องกับแลมบ์ดาที่สร้างวัตถุ - หรือถ้านี่เป็นการใช้งานเฉพาะ
orbfish

28
@orbfish: ที่สมบูรณ์แบบในบรรทัด ทุกครั้งที่เกิดt->tในซอร์สโค้ดอาจสร้างวัตถุหนึ่งและการใช้งานของFunction.identity()เป็นหนึ่งที่เกิดขึ้น ดังนั้นไซต์การเรียกใช้ทั้งหมดidentity()จะแชร์วัตถุหนึ่งในขณะที่ทุกไซต์ใช้นิพจน์แลมบ์ดาอย่างชัดเจนt->tจะสร้างวัตถุของตนเอง วิธีการที่Function.identity()ไม่ได้เป็นพิเศษในทางใดเมื่อใดก็ตามที่คุณสร้างวิธีการโรงงานห่อหุ้มเซลล์แสงอาทิตย์แสดงออกแลมบ์ดาที่ใช้กันทั่วไปและเรียกวิธีการที่แทนการทำซ้ำการแสดงออกแลมบ์ดาคุณอาจบันทึกความทรงจำบางอย่างได้รับการดำเนินการในปัจจุบัน
Holger

ฉันเดาว่านี่เป็นเพราะคอมไพเลอร์ปรับการสร้างt->tวัตถุใหม่ให้เหมาะสมในแต่ละครั้งที่มีการเรียกใช้เมธอดและรีไซเคิลสิ่งเดียวกันทุกครั้งที่เรียกว่าเมธอด?
Daniel Gray

1
@DanielGray การตัดสินใจเกิดขึ้นที่ runtime คอมไพเลอร์แทรกการเรียนการสอนที่ได้รับการเชื่อมโยงกับการดำเนินการครั้งแรกโดยการดำเนินการวิธีการบูตที่เรียกว่าซึ่งในกรณีของการแสดงออกแลมบ์ดาตั้งอยู่ในinvokedynamic LambdaMetafactoryการใช้งานนี้ตัดสินใจที่จะส่งกลับหมายเลขอ้างอิงไปยังตัวสร้างวิธีการจากโรงงานหรือรหัสที่ส่งคืนวัตถุเดียวกันเสมอ มันอาจตัดสินใจที่จะคืนลิงค์ไปยังหมายเลขอ้างอิงที่มีอยู่แล้ว (ซึ่งปัจจุบันไม่เกิดขึ้น)
Holger

@ Holger คุณแน่ใจหรือไม่ว่าการเรียกไปยังข้อมูลประจำตัวไม่ถูก inline และอาจถูกทำให้เป็นโมฆะ
JasonN
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.