JavaRush /จาวาบล็อก /Random-TH /FindBugs ช่วยให้คุณเรียนรู้ Java ได้ดีขึ้น
articles
ระดับ

FindBugs ช่วยให้คุณเรียนรู้ Java ได้ดีขึ้น

เผยแพร่ในกลุ่ม
เครื่องวิเคราะห์โค้ดแบบคงที่ได้รับความนิยมเนื่องจากช่วยค้นหาข้อผิดพลาดที่เกิดขึ้นเนื่องจากความประมาท แต่สิ่งที่น่าสนใจกว่ามากคือช่วยแก้ไขข้อผิดพลาดที่เกิดจากความไม่รู้ แม้ว่าทุกอย่างจะเขียนไว้ในเอกสารอย่างเป็นทางการสำหรับภาษานั้น แต่ก็ไม่ใช่ความจริงที่ว่าโปรแกรมเมอร์ทุกคนจะอ่านอย่างละเอียด และโปรแกรมเมอร์ก็สามารถเข้าใจได้: คุณจะเบื่อหน่ายกับการอ่านเอกสารทั้งหมด ในเรื่องนี้ เครื่องวิเคราะห์แบบคงที่เปรียบเสมือนเพื่อนที่มีประสบการณ์ซึ่งนั่งข้างคุณและเฝ้าดูคุณเขียนโค้ด เขาไม่เพียงบอกคุณว่า: "นี่คือจุดที่คุณทำผิดพลาดเมื่อคุณคัดลอกและวาง" แต่ยังพูดว่า: "ไม่ คุณไม่สามารถเขียนแบบนั้นได้ ดูเอกสารประกอบด้วยตัวคุณเอง" เพื่อนแบบนี้มีประโยชน์มากกว่าเอกสารเพราะเขาแนะนำเฉพาะสิ่งที่คุณพบจริงในงานของคุณและเงียบเกี่ยวกับสิ่งที่จะไม่เป็นประโยชน์กับคุณ ในโพสต์นี้ ฉันจะพูดถึงความซับซ้อนของ Java บางอย่างที่ฉันเรียนรู้จากการใช้เครื่องวิเคราะห์สแตติก FindBugs บางทีบางสิ่งก็อาจเป็นเรื่องที่ไม่คาดคิดสำหรับคุณเช่นกัน สิ่งสำคัญคือตัวอย่างทั้งหมดไม่เป็นการคาดเดา แต่อิงจากโค้ดจริง

ตัวดำเนินการแบบไตรภาค ?:

ดูเหมือนว่าไม่มีอะไรจะง่ายไปกว่าตัวดำเนินการที่ประกอบไปด้วย แต่ก็มีข้อผิดพลาดอยู่ ฉันเชื่อว่าไม่มีความแตกต่างพื้นฐานระหว่างการออกแบบ Type var = condition ? valTrue : valFalse; และ Type var; if(condition) var = valTrue; else var = valFalse; ปรากฏว่ามีความละเอียดอ่อนที่นี่ เนื่องจากตัวดำเนินการแบบไตรภาคสามารถเป็นส่วนหนึ่งของนิพจน์ที่ซับซ้อนได้ ผลลัพธ์จึงต้องเป็นประเภทที่เป็นรูปธรรมซึ่งกำหนด ณ เวลารวบรวม ดังนั้น สมมติว่าด้วยเงื่อนไขที่แท้จริงในรูปแบบ if คอมไพลเลอร์จะนำ valTrue ไปยังประเภท Type โดยตรง และในรูปแบบของตัวดำเนินการแบบไตรภาค คอมไพเลอร์จะนำไปสู่ประเภททั่วไป valTrue และ valFalse ก่อน (แม้ว่าข้อเท็จจริงที่ว่า valFalse ไม่ใช่ ประเมินผล) จากนั้นผลลัพธ์จะนำไปสู่ประเภทประเภท กฎการคัดเลือกไม่ใช่เรื่องเล็กน้อยเลยหากนิพจน์เกี่ยวข้องกับประเภทดั้งเดิมและส่วนที่หุ้มทับ (จำนวนเต็ม, สอง ฯลฯ ) กฎทั้งหมดได้รับการอธิบายโดยละเอียดใน JLS 15.25 ลองดูตัวอย่างบางส่วน Number n = flag ? new Integer(1) : new Double(2.0); จะเกิดอะไรขึ้นกับ n หากตั้งค่าสถานะ? วัตถุคู่ที่มีค่า 1.0 คอมไพเลอร์พบว่าความพยายามที่งุ่มง่ามของเราในการสร้างวัตถุตลก เนื่องจากอาร์กิวเมนต์ที่สองและสามเป็นการหุ้มทับประเภทดั้งเดิมที่แตกต่างกัน คอมไพลเลอร์จึงคลายอาร์กิวเมนต์เหล่านั้นและให้ผลลัพธ์เป็นประเภทที่แม่นยำยิ่งขึ้น (ในกรณีนี้คือ double) และหลังจากดำเนินการโอเปอเรเตอร์ที่ประกอบไปด้วยการชกมวยก็จะดำเนินการอีกครั้ง โดยพื้นฐานแล้วโค้ดจะเทียบเท่ากับสิ่งนี้: Number n; if( flag ) n = Double.valueOf((double) ( new Integer(1).intValue() )); else n = Double.valueOf(new Double(2.0).doubleValue()); จากมุมมองของคอมไพเลอร์ โค้ดไม่มีปัญหาใด ๆ และคอมไพล์ได้อย่างสมบูรณ์แบบ แต่ FindBugs ให้คำเตือน:
BX_UNBOXED_AND_COERCED_FOR_TERNARY_OPERATOR: ค่าดั้งเดิมจะถูกแกะกล่องและถูกบังคับสำหรับตัวดำเนินการแบบไตรภาคใน TestTernary.main(String[]) ค่าดั้งเดิมที่ห่อไว้จะถูกแกะกล่องและแปลงเป็นประเภทดั้งเดิมอื่นโดยเป็นส่วนหนึ่งของการประเมินตัวดำเนินการแบบไตรภาคที่มีเงื่อนไข (ตัวดำเนินการ b? e1: e2 ). ความหมายของคำสั่ง Java ว่าถ้า e1 และ e2 ถูกห่อค่าตัวเลข ค่าต่างๆ จะถูก unbox และแปลง/บังคับเป็นประเภททั่วไป (เช่น ถ้า e1 เป็นประเภท Integer และ e2 เป็นประเภท Float ดังนั้น e1 จะถูก unboxed แปลงเป็นค่าทศนิยมและบรรจุกล่อง ดู JLS มาตรา 15.25 แน่นอนว่า FindBugs ยังเตือนด้วยว่า Integer.valueOf(1) มีประสิทธิภาพมากกว่า Integer(1) ใหม่ แต่ทุกคนก็รู้อยู่แล้ว
หรือตัวอย่างนี้: Integer n = flag ? 1 : null; ผู้เขียนต้องการใส่ค่าว่างใน n หากไม่ได้ตั้งค่าสถานะ คุณคิดว่ามันจะได้ผลไหม? ใช่. แต่มาทำให้เรื่องซับซ้อนกันดีกว่า: Integer n = flag1 ? 1 : flag2 ? 2 : null; ดูเหมือนว่าจะไม่มีความแตกต่างกันมากนัก อย่างไรก็ตาม ตอนนี้ถ้าธงทั้งสองชัดเจน บรรทัดนี้จะส่ง NullPointerException ตัวเลือกสำหรับตัวดำเนินการไตรภาคที่ถูกต้องคือ int และ null ดังนั้นประเภทผลลัพธ์จึงเป็น Integer ตัวเลือกทางซ้ายคือ int และ Integer ดังนั้นตามกฎของ Java ผลลัพธ์จะเป็น int ในการดำเนินการนี้ คุณจะต้องทำการแกะกล่องโดยการเรียก intValue ซึ่งจะทำให้เกิดข้อยกเว้น รหัสเทียบเท่ากับสิ่งนี้: Integer n; if( flag1 ) n = Integer.valueOf(1); else { if( flag2 ) n = Integer.valueOf(Integer.valueOf(2).intValue()); else n = Integer.valueOf(((Integer)null).intValue()); } ที่นี่ FindBugs จะสร้างข้อความสองข้อความ ซึ่งเพียงพอที่จะสงสัยว่าเกิดข้อผิดพลาด:
BX_UNBOXING_IMMEDIATELY_REBOXED: ค่า Boxed จะถูก unboxed แล้ว reboxed ทันทีใน TestTernary.main(String[]) NP_NULL_ON_SOME_PATH: Possible null pointer dereference of null in TestTernary.main(String[]) มีสาขาของคำสั่งที่ หากดำเนินการ รับประกันว่า ค่า Null จะถูก dereferenced ซึ่งจะสร้าง NullPointerException เมื่อโค้ดถูกดำเนินการ
ตัวอย่างสุดท้ายในหัวข้อนี้: double[] vals = new double[] {1.0, 2.0, 3.0}; double getVal(int idx) { return (idx < 0 || idx >= vals.length) ? null : vals[idx]; } ไม่น่าแปลกใจเลยที่โค้ดนี้ใช้งานไม่ได้: ฟังก์ชันที่ส่งคืนประเภทดั้งเดิมจะคืนค่า null ได้อย่างไร น่าแปลกที่มันคอมไพล์ได้โดยไม่มีปัญหา คุณเข้าใจแล้วว่าทำไมมันถึงคอมไพล์

รูปแบบวันที่

หากต้องการจัดรูปแบบวันที่และเวลาใน Java ขอแนะนำให้ใช้คลาสที่ใช้อินเทอร์เฟซ DateFormat ตัวอย่างเช่น มีลักษณะดังนี้: public String getDate() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); } บ่อยครั้งที่ชั้นเรียนจะใช้รูปแบบเดียวกันซ้ำแล้วซ้ำเล่า หลายๆ คนจะเกิดแนวคิดเรื่องการเพิ่มประสิทธิภาพ: ทำไมต้องสร้างออบเจ็กต์รูปแบบทุกครั้งในเมื่อคุณสามารถใช้อินสแตนซ์ทั่วไปได้ private static final DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public String getDate() { return format.format(new Date()); } มันสวยงามและเท่มาก แต่น่าเสียดายที่มันไม่ได้ผล มันใช้งานได้แม่นยำยิ่งขึ้น แต่บางครั้งก็พัง ความจริงก็คือเอกสารสำหรับ DateFormat บอกว่า:
รูปแบบวันที่ไม่ได้รับการซิงโครไนซ์ ขอแนะนำให้สร้างอินสแตนซ์รูปแบบแยกกันสำหรับแต่ละเธรด หากหลายเธรดเข้าถึงรูปแบบพร้อมกัน จะต้องซิงโครไนซ์จากภายนอก
และสิ่งนี้จะเกิดขึ้นจริงหากคุณดูการใช้งาน SimpleDateFormat ภายใน ในระหว่างการดำเนินการของเมธอด format() วัตถุจะเขียนลงในฟิลด์คลาส ดังนั้นการใช้ SimpleDateFormat จากสองเธรดพร้อมกันจะนำไปสู่ผลลัพธ์ที่ไม่ถูกต้องและมีความน่าจะเป็นบางประการ นี่คือสิ่งที่ FindBugs เขียนเกี่ยวกับเรื่องนี้:
STCAL_INVOKE_ON_STATIC_DATE_FORMAT_INSTANCE: การเรียกใช้เมธอดของ java.text.DateFormat แบบคงที่ใน TestDate.getDate() ตามที่ระบุใน JavaDoc DateFormats จะไม่ปลอดภัยสำหรับการใช้งานแบบมัลติเธรดโดยเนื้อแท้ ตัวตรวจจับพบการเรียกไปยังอินสแตนซ์ของ DateFormat ที่ได้รับผ่านฟิลด์แบบคงที่ เรื่องนี้ดูน่าสงสัย สำหรับข้อมูลเพิ่มเติมเกี่ยวกับเรื่องนี้ โปรดดูที่ Sun Bug #6231579 และ Sun Bug #6178997

ข้อผิดพลาดของ BigDecimal

เมื่อได้เรียนรู้ว่าคลาส BigDecimal ช่วยให้คุณสามารถจัดเก็บจำนวนเศษส่วนของความแม่นยำตามอำเภอใจ และเมื่อเห็นว่ามี Constructor สำหรับ Double บางคนจึงตัดสินใจว่าทุกอย่างชัดเจนและคุณสามารถทำเช่นนี้: System.out.println(new BigDecimal( 1.1)); ไม่มีใครห้ามการทำเช่นนี้จริงๆ แต่ผลลัพธ์อาจดูเหมือนไม่คาดคิด: 1.100000000000000088817841970012523233890533447265625 สิ่งนี้เกิดขึ้นเนื่องจากค่าสองเท่าดั้งเดิมถูกจัดเก็บในรูปแบบ IEEE754 ซึ่งเป็นไปไม่ได้ที่จะแทน 1.1 อย่างแม่นยำอย่างสมบูรณ์ (ในระบบเลขฐานสองจะได้เศษส่วนคาบที่ไม่มีที่สิ้นสุด) ดังนั้นค่าที่ใกล้เคียงที่สุดกับ 1.1 จึงถูกเก็บไว้ที่นั่น ในทางตรงกันข้าม ตัวสร้าง BigDecimal(double) ทำงานได้อย่างแน่นอน โดยแปลงตัวเลขที่กำหนดใน IEEE754 ให้เป็นรูปแบบทศนิยมได้อย่างสมบูรณ์แบบ (เศษส่วนไบนารีสุดท้ายจะแสดงเป็นทศนิยมสุดท้ายเสมอ) หากคุณต้องการแสดง 1.1 เป็น BigDecimal ทุกประการ คุณสามารถเขียน BigDecimal("1.1") ใหม่หรือ BigDecimal.valueOf(1.1) ได้ หากคุณไม่แสดงหมายเลขทันที แต่ดำเนินการบางอย่างกับหมายเลขดังกล่าว คุณอาจไม่เข้าใจว่าข้อผิดพลาดมาจากไหน FindBugs ออกคำเตือน DMI_BIGDECIMAL_CONSTRUCTED_FROM_DOUBLE ซึ่งให้คำแนะนำเดียวกัน อีกประการหนึ่ง: อัน BigDecimal d1 = new BigDecimal("1.1"); BigDecimal d2 = new BigDecimal("1.10"); System.out.println(d1.equals(d2)); ที่จริง d1 และ d2 แทนจำนวนเดียวกัน แต่จะคืนค่าเป็นเท็จ เนื่องจากไม่เพียงเปรียบเทียบค่าของตัวเลขเท่านั้น แต่ยังเปรียบเทียบลำดับปัจจุบันด้วย (จำนวนตำแหน่งทศนิยม) สิ่งนี้เขียนไว้ในเอกสารประกอบ แต่มีเพียงไม่กี่คนที่จะอ่านเอกสารประกอบสำหรับวิธีที่คุ้นเคยเช่นนี้ ปัญหาดังกล่าวอาจไม่เกิดขึ้นทันที น่าเสียดายที่ FindBugs เองไม่ได้เตือนเกี่ยวกับเรื่องนี้ แต่มีส่วนขยายยอดนิยมสำหรับมัน - fb-contrib ซึ่งคำนึงถึงข้อบกพร่องนี้:
MDM_BIGDECIMAL_EQUALS เท่ากับ() ถูกเรียกเพื่อเปรียบเทียบตัวเลข java.math.BigDecimal สองตัว โดยปกติจะเป็นข้อผิดพลาด เนื่องจากออบเจ็กต์ BigDecimal สองตัวจะเท่ากันก็ต่อเมื่อทั้งสองค่าและขนาดเท่ากัน ดังนั้น 2.0 จึงไม่เท่ากับ 2.00 หากต้องการเปรียบเทียบวัตถุ BigDecimal เพื่อความเท่าเทียมกันทางคณิตศาสตร์ ให้ใช้ CompareTo() แทน

ตัวแบ่งบรรทัดและ printf

บ่อยครั้งที่โปรแกรมเมอร์ที่เปลี่ยนมาใช้ Java หลังจากภาษา C ยินดีที่จะค้นพบPrintStream.printf (เช่นเดียวกับPrintWriter.printfเป็นต้น) เยี่ยมเลย ฉันรู้ว่า เช่นเดียวกับใน C คุณไม่จำเป็นต้องเรียนรู้อะไรใหม่ มีความแตกต่างจริงๆ หนึ่งในนั้นอยู่ในการแปลบรรทัด ภาษา C แบ่งออกเป็นข้อความและสตรีมไบนารี การส่งออกอักขระ '\n' ไปยังสตรีมข้อความด้วยวิธีใดก็ตามจะถูกแปลงเป็นการขึ้นบรรทัดใหม่ขึ้นอยู่กับระบบโดยอัตโนมัติ ("\r\n" บน Windows) ไม่มีการแยกดังกล่าวใน Java: จะต้องส่งลำดับอักขระที่ถูกต้องไปยังเอาต์พุตสตรีม ซึ่งจะดำเนินการโดยอัตโนมัติ เช่น โดยวิธีการของตระกูล PrintStream.println แต่เมื่อใช้ printf การส่ง '\n' ในสตริงรูปแบบเป็นเพียง '\n' ไม่ใช่การขึ้นบรรทัดใหม่ขึ้นอยู่กับระบบ ตัวอย่างเช่น เรามาเขียนโค้ดต่อไปนี้: System.out.printf("%s\n", "str#1"); System.out.println("str#2"); เมื่อเปลี่ยนเส้นทางผลลัพธ์ไปยังไฟล์แล้ว เราจะเห็น: FindBugs ช่วยให้คุณเรียนรู้ Java ได้ดีขึ้น - 1 ดังนั้น คุณสามารถได้รับการขึ้นบรรทัดใหม่พร้อมกันในเธรดเดียว ซึ่งดูเลอะเทอะและอาจทำให้ parser บางส่วนทึ่งได้ ข้อผิดพลาดนี้อาจไม่มีใครสังเกตเห็นเป็นเวลานาน โดยเฉพาะอย่างยิ่งหากคุณทำงานบนระบบ Unix เป็นหลัก หากต้องการแทรกบรรทัดใหม่ที่ถูกต้องโดยใช้ printf จะใช้อักขระการจัดรูปแบบพิเศษ "%n" นี่คือสิ่งที่ FindBugs เขียนเกี่ยวกับเรื่องนี้:
VA_FORMAT_STRING_USES_NEWLINE: สตริงรูปแบบควรใช้ %n แทนที่จะเป็น \n ใน TestNewline.main(String[]) สตริงรูปแบบนี้ประกอบด้วยอักขระขึ้นบรรทัดใหม่ (\n) ในรูปแบบสตริง โดยทั่วไปควรใช้ %n ดีกว่า ซึ่งจะสร้างตัวคั่นบรรทัดเฉพาะแพลตฟอร์ม
บางทีสำหรับผู้อ่านบางคนสิ่งที่กล่าวมาทั้งหมดอาจเป็นที่รู้กันมานานแล้ว แต่ฉันเกือบจะแน่ใจว่าสำหรับพวกเขาแล้วจะมีคำเตือนที่น่าสนใจจากตัววิเคราะห์แบบคงที่ซึ่งจะเปิดเผยคุณสมบัติใหม่ของภาษาการเขียนโปรแกรมที่ใช้ให้พวกเขาทราบ
ความคิดเห็น
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION