การจัดการ Java NullPointerException ที่มีประสิทธิภาพ

ไม่ต้องใช้ประสบการณ์ในการพัฒนา Java มากนักในการเรียนรู้โดยตรงว่า NullPointerException เกี่ยวกับอะไร ในความเป็นจริงมีคนหนึ่งให้ความสำคัญกับการจัดการกับสิ่งนี้เนื่องจากนักพัฒนา Java ทำผิดพลาดอันดับหนึ่ง ก่อนหน้านี้ฉันบล็อกเกี่ยวกับการใช้ String.value (Object) เพื่อลด NullPointerExceptions ที่ไม่ต้องการ มีเทคนิคง่ายๆอื่น ๆ อีกมากมายที่สามารถใช้เพื่อลดหรือกำจัดการเกิดขึ้นของ RuntimeException ประเภททั่วไปที่อยู่กับเรามาตั้งแต่ JDK 1.0 บล็อกโพสต์นี้รวบรวมและสรุปเทคนิคยอดนิยมบางส่วน

ตรวจสอบวัตถุแต่ละชิ้นเป็นค่าว่างก่อนใช้

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

final String causeStr = "adding String to Deque that is set to null."; final String elementStr = "Fudd"; Deque deque = null; try { deque.push(elementStr); log("Successful at " + causeStr, System.out); } catch (NullPointerException nullPointer) { log(causeStr, nullPointer, System.out); } try { if (deque == null) { deque = new LinkedList(); } deque.push(elementStr); log( "Successful at " + causeStr + " (by checking first for null and instantiating Deque implementation)", System.out); } catch (NullPointerException nullPointer) { log(causeStr, nullPointer, System.out); } 

ในโค้ดด้านบน Deque ที่ใช้มีเจตนาเริ่มต้นเป็น null เพื่ออำนวยความสะดวกในตัวอย่าง รหัสในtryบล็อกแรกไม่ตรวจสอบค่าว่างก่อนที่จะพยายามเข้าถึงวิธีการ Deque รหัสในtryบล็อกที่สองตรวจสอบค่าว่างและสร้างอินสแตนซ์การนำไปใช้Deque(LinkedList) หากเป็นโมฆะ ผลลัพธ์จากทั้งสองตัวอย่างมีลักษณะดังนี้:

ERROR: NullPointerException encountered while trying to adding String to Deque that is set to null. java.lang.NullPointerException INFO: Successful at adding String to Deque that is set to null. (by checking first for null and instantiating Deque implementation) 

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

วิธีนี้มักใช้และดังที่แสดงไว้ข้างต้นมีประโยชน์มากในการหลีกเลี่ยงNullPointerExceptionอินสแตนซ์ที่ไม่ต้องการ (ไม่คาดคิด) อย่างไรก็ตามมันไม่ได้ไม่มีค่าใช้จ่าย การตรวจสอบค่าว่างก่อนใช้ทุกออบเจ็กต์อาจทำให้โค้ดขยายใหญ่ขึ้นอาจเป็นเรื่องที่น่าเบื่อในการเขียนและเปิดพื้นที่มากขึ้นสำหรับปัญหาในการพัฒนาและการบำรุงรักษาโค้ดเพิ่มเติม ด้วยเหตุนี้จึงมีการพูดถึงการแนะนำการสนับสนุนภาษา Java สำหรับการตรวจจับ null ในตัวการเพิ่มการตรวจสอบค่า null โดยอัตโนมัติหลังจากการเข้ารหัสเริ่มต้นประเภท null-safe การใช้ Aspect-Oriented Programming (AOP) เพื่อเพิ่มการตรวจสอบ null เป็นรหัสไบต์และเครื่องมือตรวจหาโมฆะอื่น ๆ

Groovy มีกลไกที่สะดวกสำหรับจัดการกับการอ้างอิงอ็อบเจ็กต์ที่อาจเป็นโมฆะอยู่แล้ว ตัวดำเนินการนำทางที่ปลอดภัยของ Groovy ( ?.) ส่งคืนค่าว่างแทนการโยน a NullPointerExceptionเมื่อมีการเข้าถึงการอ้างอิงวัตถุว่าง

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

นี่คือสถานการณ์ที่ตัวดำเนินการ ternary มีประโยชน์อย่างยิ่ง แทน

// retrieved a BigDecimal called someObject String returnString; if (someObject != null) { returnString = someObject.toEngineeringString(); } else { returnString = ""; } 

ตัวดำเนินการ ternary สนับสนุนไวยากรณ์ที่กระชับมากขึ้นนี้

// retrieved a BigDecimal called someObject final String returnString = (someObject != null) ? someObject.toEngineeringString() : ""; } 

ตรวจสอบข้อโต้แย้งของวิธีการสำหรับ Null

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

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

 /** * Append predefined text String to the provided StringBuilder. * * @param builder The StringBuilder that will have text appended to it; should * be non-null. * @throws IllegalArgumentException Thrown if the provided StringBuilder is * null. */ private void appendPredefinedTextToProvidedBuilderCheckForNull( final StringBuilder builder) { if (builder == null) { throw new IllegalArgumentException( "The provided StringBuilder was null; non-null value must be provided."); } builder.append("Thanks for supplying a StringBuilder."); } /** * Append predefined text String to the provided StringBuilder. * * @param builder The StringBuilder that will have text appended to it; should * be non-null. */ private void appendPredefinedTextToProvidedBuilderNoCheckForNull( final StringBuilder builder) { builder.append("Thanks for supplying a StringBuilder."); } /** * Demonstrate effect of checking parameters for null before trying to use * passed-in parameters that are potentially null. */ public void demonstrateCheckingArgumentsForNull() { final String causeStr = "provide null to method as argument."; logHeader("DEMONSTRATING CHECKING METHOD PARAMETERS FOR NULL", System.out); try { appendPredefinedTextToProvidedBuilderNoCheckForNull(null); } catch (NullPointerException nullPointer) { log(causeStr, nullPointer, System.out); } try { appendPredefinedTextToProvidedBuilderCheckForNull(null); } catch (IllegalArgumentException illegalArgument) { log(causeStr, illegalArgument, System.out); } } 

เมื่อดำเนินการโค้ดด้านบนผลลัพธ์จะปรากฏดังที่แสดงถัดไป

ERROR: NullPointerException encountered while trying to provide null to method as argument. java.lang.NullPointerException ERROR: IllegalArgumentException encountered while trying to provide null to method as argument. java.lang.IllegalArgumentException: The provided StringBuilder was null; non-null value must be provided. 

ในทั้งสองกรณีข้อความแสดงข้อผิดพลาดถูกบันทึก อย่างไรก็ตามกรณีที่มีการตรวจสอบค่าว่างเพื่อโยน IllegalArgumentException ที่โฆษณาซึ่งรวมข้อมูลบริบทเพิ่มเติมเกี่ยวกับเวลาที่พบค่าว่าง หรืออีกทางหนึ่งพารามิเตอร์ null นี้สามารถจัดการได้หลายวิธี สำหรับกรณีที่ไม่ได้จัดการกับพารามิเตอร์ null ไม่มีตัวเลือกสำหรับวิธีจัดการ หลายคนชอบที่จะโยนNullPolinterExceptionข้อมูลบริบทเพิ่มเติมเมื่อมีการค้นพบโมฆะอย่างชัดเจน (ดู Item # 60 ใน Second Edition of Effective Javaหรือ Item # 42 ใน First Edition) แต่ฉันมีความต้องการเล็กน้อยIllegalArgumentExceptionเมื่อมีการระบุอย่างชัดเจน อาร์กิวเมนต์เมธอดที่เป็นโมฆะเพราะฉันคิดว่าข้อยกเว้นมากจะเพิ่มรายละเอียดบริบทและง่ายต่อการรวม "null" ในหัวเรื่อง

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

การตรวจสอบพารามิเตอร์เมธอดสำหรับ null ยังเป็นส่วนย่อยของวิธีปฏิบัติทั่วไปในการตรวจสอบพารามิเตอร์เมธอดสำหรับความถูกต้องทั่วไปตามที่กล่าวไว้ในรายการ # 38 ของ Second Edition of Effective Java (รายการ 23 ใน First Edition)

พิจารณา Primitives มากกว่า Objects

ฉันไม่คิดว่าเป็นความคิดที่ดีที่จะเลือกประเภทข้อมูลดั้งเดิม (เช่นint) เหนือประเภทการอ้างอิงวัตถุที่เกี่ยวข้อง (เช่นจำนวนเต็ม) เพียงเพื่อหลีกเลี่ยงความเป็นไปได้ของ a NullPointerExceptionแต่ไม่มีการปฏิเสธว่าข้อดีอย่างหนึ่งของ ประเภทดั้งเดิมคือพวกเขาไม่ได้นำไปสู่NullPointerExceptions อย่างไรก็ตามยังคงต้องมีการตรวจสอบความถูกต้องของ primitives (หนึ่งเดือนไม่สามารถเป็นจำนวนเต็มลบได้) ดังนั้นประโยชน์นี้จึงอาจน้อย ในทางกลับกันไม่สามารถใช้ primitives ใน Java Collections ได้และมีบางครั้งที่เราต้องการความสามารถในการตั้งค่าเป็น null

สิ่งที่สำคัญที่สุดคือต้องระมัดระวังอย่างมากเกี่ยวกับการผสมผสานระหว่างสิ่งดั้งเดิมประเภทการอ้างอิงและการทำกล่องอัตโนมัติ มีคำเตือนในEffective Java (Second Edition, Item # 49) เกี่ยวกับอันตรายรวมถึงการขว้างปาNullPointerExceptionที่เกี่ยวข้องกับการผสมแบบดั้งเดิมและประเภทอ้างอิงอย่างไม่ระมัดระวัง

พิจารณาการโทรแบบถูกล่ามโซ่อย่างรอบคอบ

A NullPointerExceptionสามารถค้นหาได้ง่ายมากเนื่องจากหมายเลขบรรทัดจะระบุตำแหน่งที่เกิดขึ้น ตัวอย่างเช่นการติดตามสแต็กอาจมีลักษณะดังนี้:

java.lang.NullPointerException at dustin.examples.AvoidingNullPointerExamples.demonstrateNullPointerExceptionStackTrace(AvoidingNullPointerExamples.java:222) at dustin.examples.AvoidingNullPointerExamples.main(AvoidingNullPointerExamples.java:247) 

การติดตามสแต็กทำให้เห็นได้ชัดว่าสิ่งที่NullPointerExceptionถูกโยนเป็นผลมาจากรหัสที่เรียกใช้ในบรรทัด 222 ของAvoidingNullPointerExamples.java. แม้จะมีหมายเลขบรรทัดที่ระบุไว้ก็ยังสามารถ จำกัด ให้แคบลงได้ยากหากมีวัตถุหลายรายการที่มีวิธีการหรือช่องที่เข้าถึงในบรรทัดเดียวกัน

ตัวอย่างเช่นคำสั่งเช่นsomeObject.getObjectA().getObjectB().getObjectC().toString();มีการเรียกที่เป็นไปได้สี่สายที่อาจส่งสัญญาณที่NullPointerExceptionมาจากบรรทัดเดียวกันของรหัส การใช้ดีบักเกอร์สามารถช่วยในเรื่องนี้ได้ แต่อาจมีสถานการณ์ที่ดีกว่าที่จะทำลายโค้ดด้านบนเพื่อให้แต่ละสายดำเนินการแยกกัน สิ่งนี้ช่วยให้หมายเลขบรรทัดที่มีอยู่ในการติดตามสแต็กสามารถระบุได้อย่างง่ายดายว่าการโทรที่แน่นอนคือปัญหา นอกจากนี้ยังอำนวยความสะดวกในการตรวจสอบแต่ละวัตถุอย่างชัดเจนว่าเป็นโมฆะ อย่างไรก็ตามในทางกลับกันการแบ่งโค้ดจะเพิ่มจำนวนโค้ด (สำหรับบางโค้ดที่เป็นค่าบวก!) และอาจไม่เป็นที่ต้องการเสมอไปโดยเฉพาะอย่างยิ่งหากมีวิธีใดวิธีหนึ่งที่เป็นปัญหาจะไม่เป็นโมฆะ

ทำให้ NullPointerExceptions มีข้อมูลมากขึ้น

In the above recommendation, the warning was to consider carefully use of method call chaining primarily because it made having the line number in the stack trace for a NullPointerException less helpful than it otherwise might be. However, the line number is only shown in a stack trace when the code was compiled with the debug flag turned on. If it was compiled without debug, the stack trace looks like that shown next:

java.lang.NullPointerException at dustin.examples.AvoidingNullPointerExamples.demonstrateNullPointerExceptionStackTrace(Unknown Source) at dustin.examples.AvoidingNullPointerExamples.main(Unknown Source) 

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

ตัวอย่างต่อไปนี้แสดงให้เห็นถึงหลักการนี้

final Calendar nullCalendar = null; try { final Date date = nullCalendar.getTime(); } catch (NullPointerException nullPointer) { log("NullPointerException with useful data", nullPointer, System.out); } try { if (nullCalendar == null) { throw new NullPointerException("Could not extract Date from provided Calendar"); } final Date date = nullCalendar.getTime(); } catch (NullPointerException nullPointer) { log("NullPointerException with useful data", nullPointer, System.out); } 

ผลลัพธ์จากการรันโค้ดด้านบนมีลักษณะดังนี้

ERROR: NullPointerException encountered while trying to NullPointerException with useful data java.lang.NullPointerException ERROR: NullPointerException encountered while trying to NullPointerException with useful data java.lang.NullPointerException: Could not extract Date from provided Calendar