JavaRush /Java Blog /Random-TL /Tinutulungan ka ng FindBugs na matuto ng Java nang mas ma...
articles
Antas

Tinutulungan ka ng FindBugs na matuto ng Java nang mas mahusay

Nai-publish sa grupo
Ang mga static na code analyzer ay sikat dahil nakakatulong sila sa paghahanap ng mga error na ginawa dahil sa kawalang-ingat. Ngunit ang higit na kawili-wili ay ang pagtulong nila sa pagwawasto ng mga pagkakamaling nagawa dahil sa kamangmangan. Kahit na ang lahat ay nakasulat sa opisyal na dokumentasyon para sa wika, ito ay hindi isang katotohanan na lahat ng mga programmer ay binasa ito nang mabuti. At mauunawaan ng mga programmer: mapapagod ka sa pagbabasa ng lahat ng dokumentasyon. Sa bagay na ito, ang isang static na analyzer ay parang isang karanasang kaibigan na nakaupo sa tabi mo at pinapanood kang sumulat ng code. Hindi lang niya sinasabi sa iyo: "Dito ka nagkamali noong kinopya at i-paste mo," ngunit sinabi rin niya: "Hindi, hindi ka maaaring magsulat ng ganyan, tingnan mo ang dokumentasyon mo mismo." Ang gayong kaibigan ay mas kapaki-pakinabang kaysa sa mismong dokumentasyon, dahil iminumungkahi niya lamang ang mga bagay na aktwal mong nakatagpo sa iyong trabaho, at tahimik tungkol sa mga hindi kailanman magiging kapaki-pakinabang sa iyo. Sa post na ito, pag-uusapan ko ang ilan sa mga intricacies ng Java na natutunan ko mula sa paggamit ng FindBugs static analyzer. Marahil ang ilang mga bagay ay hindi inaasahan din para sa iyo. Mahalaga na ang lahat ng mga halimbawa ay hindi haka-haka, ngunit batay sa totoong code.

Operator ng Ternary ?:

Mukhang wala nang mas simple kaysa sa ternary operator, ngunit mayroon itong mga pitfalls. Naniniwala ako na walang pangunahing pagkakaiba sa pagitan ng mga disenyo Type var = condition ? valTrue : valFalse; at Type var; if(condition) var = valTrue; else var = valFalse; ito ay lumabas na mayroong isang subtlety dito. Dahil ang ternary operator ay maaaring maging bahagi ng isang kumplikadong expression, ang resulta nito ay dapat na isang kongkretong uri na tinutukoy sa oras ng pag-compile. Samakatuwid, sabihin nating, na may totoong kundisyon sa if form, ang compiler ay humahantong sa valTrue nang direkta sa uri ng Uri, at sa anyo ng isang ternary operator, ito ay unang humahantong sa karaniwang uri na valTrue at valFalse (sa kabila ng katotohanan na ang valFalse ay hindi nasuri), at pagkatapos ay humahantong ang resulta sa uri ng Uri. Ang mga panuntunan sa pag-cast ay hindi lubos na mahalaga kung ang expression ay nagsasangkot ng mga primitive na uri at wrapper sa ibabaw ng mga ito (Integer, Double, atbp.) Ang lahat ng mga panuntunan ay inilalarawan nang detalyado sa JLS 15.25. Tingnan natin ang ilang halimbawa. Number n = flag ? new Integer(1) : new Double(2.0); Ano ang mangyayari sa n kung nakatakda ang bandila? Isang Double object na may halaga na 1.0. Napag-alaman ng compiler ang aming mga clumsy na pagtatangka na lumikha ng isang bagay na nakakatawa. Dahil ang pangalawa at pangatlong argumento ay mga wrapper sa iba't ibang primitive na uri, binubuksan ng compiler ang mga ito at nagreresulta sa isang mas tumpak na uri (sa kasong ito, doble). At pagkatapos i-execute ang ternary operator para sa assignment, muling isinagawa ang boxing. Sa esensya ang code ay katumbas nito: Number n; if( flag ) n = Double.valueOf((double) ( new Integer(1).intValue() )); else n = Double.valueOf(new Double(2.0).doubleValue()); Mula sa punto ng view ng compiler, ang code ay naglalaman ng walang mga problema at perpektong pinagsama-sama. Ngunit ang FindBugs ay nagbibigay ng babala:
BX_UNBOXED_AND_COERCED_FOR_TERNARY_OPERATOR: Ang primitive na halaga ay na-unbox at pinilit para sa ternary operator sa TestTernary.main(String[]) Ang isang nakabalot na primitive na value ay na-unbox at na-convert sa isa pang primitive na uri bilang bahagi ng pagsusuri ng isang conditional ternary operator (ang operator na e2 b? ). Ang semantics ng Java ay nag-uutos na kung ang e1 at e2 ay nakabalot na mga numeric na halaga, ang mga halaga ay hindi naka-box at na-convert/pinipilit sa kanilang karaniwang uri (hal., kung ang e1 ay may uri na Integer at e2 ay nasa uri ng Float, ang e1 ay naka-unbox, na-convert sa isang floating point value, at naka-box. Tingnan ang JLS Section 15.25. Siyempre, nagbabala rin ang FindBugs na ang Integer.valueOf(1) ay mas mahusay kaysa sa bagong Integer(1), ngunit alam na ng lahat iyon.
O ang halimbawang ito: Integer n = flag ? 1 : null; Nais ng may-akda na ilagay ang null sa n kung ang bandila ay hindi nakatakda. Sa tingin mo ba ito ay gagana? Oo. Ngunit gawing kumplikado ang mga bagay: Integer n = flag1 ? 1 : flag2 ? 2 : null; Mukhang walang gaanong pagkakaiba. Gayunpaman, ngayon kung ang parehong mga flag ay malinaw, ang linyang ito ay nagtatapon ng NullPointerException. Ang mga opsyon para sa tamang operator ng ternary ay int at null, kaya ang uri ng resulta ay Integer. Ang mga pagpipilian para sa kaliwa ay int at Integer, kaya ayon sa mga panuntunan ng Java ang resulta ay int. Upang gawin ito, kailangan mong magsagawa ng pag-unbox sa pamamagitan ng pagtawag sa intValue, na nagbibigay ng exception. Ang code ay katumbas nito: 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()); } Narito ang FindBugs ay gumagawa ng dalawang mensahe, na sapat upang maghinala ng isang error:
BX_UNBOXING_IMMEDIATELY_REBOXED: Ang naka-box na value ay na-unbox at pagkatapos ay agad na nire-rebox sa TestTernary.main(String[]) NP_NULL_ON_SOME_PATH: Posibleng null pointer dereference ng null sa TestTernary.main(String[]) Mayroong isang sangay ng statement na, kung ipapatupad iyon, ginagarantiyahan iyon, kung ipapatupad iyon. Ang null na halaga ay aalisin sa sanggunian, na bubuo ng NullPointerException kapag ang code ay naisakatuparan.
Well, isang huling halimbawa sa paksang ito: double[] vals = new double[] {1.0, 2.0, 3.0}; double getVal(int idx) { return (idx < 0 || idx >= vals.length) ? null : vals[idx]; } Hindi nakakagulat na ang code na ito ay hindi gumagana: paano ang isang function na nagbabalik ng isang primitive na uri ay bumalik na null? Nakakagulat, nag-compile ito nang walang mga problema. Well, naiintindihan mo na kung bakit ito nag-compile.

Format ng Petsa

Upang i-format ang mga petsa at oras sa Java, inirerekomendang gumamit ng mga klase na nagpapatupad ng interface ng DateFormat. Halimbawa, ganito ang hitsura: public String getDate() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); } Kadalasan ang isang klase ay gagamit ng parehong format nang paulit-ulit. Maraming mga tao ang makakaisip ng ideya ng pag-optimize: bakit lumikha ng isang format na object tuwing maaari kang gumamit ng isang karaniwang pagkakataon? private static final DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public String getDate() { return format.format(new Date()); } Ito ay napakaganda at cool, ngunit sa kasamaang-palad ay hindi ito gumagana. Mas tiyak na gumagana ito, ngunit paminsan-minsan ay nasira. Ang katotohanan ay ang dokumentasyon para sa DateFormat ay nagsasabing:
Hindi naka-synchronize ang mga format ng petsa. Inirerekomenda na lumikha ng hiwalay na mga instance ng format para sa bawat thread. Kung maraming thread ang nag-a-access sa isang format nang sabay-sabay, dapat itong i-synchronize sa labas.
At ito ay totoo kung titingnan mo ang panloob na pagpapatupad ng SimpleDateFormat. Sa panahon ng pagpapatupad ng format() na paraan, nagsusulat ang object sa mga field ng klase, kaya ang sabay-sabay na paggamit ng SimpleDateFormat mula sa dalawang thread ay hahantong sa isang hindi tamang resulta na may ilang posibilidad. Narito ang isinulat ng FindBugs tungkol dito:
STCAL_INVOKE_ON_STATIC_DATE_FORMAT_INSTANCE: Tumawag sa paraan ng static na java.text.DateFormat sa TestDate.getDate() Gaya ng isinasaad ng JavaDoc, ang DateFormats ay likas na hindi ligtas para sa multithreaded na paggamit. Nakahanap ang detector ng isang tawag sa isang instance ng DateFormat na nakuha sa pamamagitan ng isang static na field. Mukhang kahina-hinala ito. Para sa higit pang impormasyon tungkol dito tingnan ang Sun Bug #6231579 at Sun Bug #6178997.

Mga Pitfalls ng BigDecimal

Nang malaman na ang klase ng BigDecimal ay nagbibigay-daan sa iyo na mag-imbak ng mga fractional na numero ng arbitraryong katumpakan, at nakikita na mayroon itong constructor para sa doble, ang ilan ay magpapasya na ang lahat ay malinaw at magagawa mo ito tulad nito: System.out.println(new BigDecimal( 1.1)); Walang sinuman ang talagang nagbabawal na gawin ito, ngunit ang resulta ay maaaring mukhang hindi inaasahan: 1.100000000000000088817841970012523233890533447265625. Nangyayari ito dahil ang primitive double ay naka-imbak sa format na IEEE754, kung saan imposibleng kumatawan sa 1.1 na ganap na tumpak (sa binary number system, isang walang katapusang periodic fraction ang nakuha). Samakatuwid, ang halaga na pinakamalapit sa 1.1 ay naka-imbak doon. Sa kabaligtaran, ang BigDecimal(double) constructor ay gumagana nang eksakto: ito ay perpektong nagko-convert ng isang naibigay na numero sa IEEE754 sa decimal form (isang panghuling binary fraction ay palaging kinakatawan bilang isang pangwakas na decimal). Kung gusto mong katawanin ang eksaktong 1.1 bilang isang BigDecimal, maaari mong isulat ang alinman sa bagong BigDecimal("1.1") o BigDecimal.valueOf(1.1). Kung hindi mo agad ipapakita ang numero, ngunit gagawa ng ilang operasyon dito, maaaring hindi mo maintindihan kung saan nanggaling ang error. Nag-isyu ang FindBugs ng babala DMI_BIGDECIMAL_CONSTRUCTED_FROM_DOUBLE, na nagbibigay ng parehong payo. Narito ang isa pang bagay: BigDecimal d1 = new BigDecimal("1.1"); BigDecimal d2 = new BigDecimal("1.10"); System.out.println(d1.equals(d2)); Sa katunayan, ang d1 at d2 ay kumakatawan sa parehong numero, ngunit ang katumbas ay nagbabalik ng false dahil inihahambing nito hindi lamang ang halaga ng mga numero, kundi pati na rin ang kasalukuyang pagkakasunud-sunod (ang bilang ng mga decimal na lugar). Ito ay nakasulat sa dokumentasyon, ngunit kakaunti ang mga tao na magbabasa ng dokumentasyon para sa isang pamilyar na pamamaraan bilang katumbas. Ang ganitong problema ay maaaring hindi kaagad lumitaw. Ang FindBugs mismo, sa kasamaang-palad, ay hindi nagbabala tungkol dito, ngunit mayroong isang tanyag na extension para dito - fb-contrib, na isinasaalang-alang ang bug na ito:
MDM_BIGDECIMAL_EQUALS ay katumbas ng() na tinatawag upang ihambing ang dalawang java.math.BigDecimal na numero. Ito ay karaniwang isang pagkakamali, dahil ang dalawang BigDecimal na bagay ay pantay lamang kung sila ay magkapareho sa parehong halaga at sukat, upang ang 2.0 ay hindi katumbas ng 2.00. Upang paghambingin ang mga BigDecimal na bagay para sa mathematical equality, gamitin ang compareTo() sa halip.

Mga line break at printf

Kadalasan ang mga programmer na lumipat sa Java pagkatapos ng C ay masaya na matuklasan ang PrintStream.printf (pati na rin ang PrintWriter.printf , atbp.). Tulad ng, mahusay, alam ko na, tulad ng sa C, hindi mo kailangang matuto ng bago. May mga pagkakaiba talaga. Ang isa sa mga ito ay namamalagi sa mga pagsasalin ng linya. Ang wikang C ay may dibisyon sa teksto at binary stream. Ang pag-output ng character na '\n' sa isang text stream sa anumang paraan ay awtomatikong mako-convert sa isang newline na umaasa sa system ("\r\n" sa Windows). Walang ganoong paghihiwalay sa Java: ang tamang pagkakasunod-sunod ng mga character ay dapat maipasa sa output stream. Awtomatikong ginagawa ito, halimbawa, sa pamamagitan ng mga pamamaraan ng pamilyang PrintStream.println. Ngunit kapag gumagamit ng printf, ang pagpasa ng '\n' sa format na string ay '\n' lang, hindi isang bagong linya na umaasa sa system. Halimbawa, isulat natin ang sumusunod na code: System.out.printf("%s\n", "str#1"); System.out.println("str#2"); Kapag na-redirect ang resulta sa isang file, makikita natin: Tinutulungan ka ng FindBugs na matuto ng Java nang mas mahusay - 1 Kaya, makakakuha ka ng kakaibang kumbinasyon ng mga line break sa isang thread, na mukhang palpak at maaaring pumutok sa isipan ng ilang parser. Ang error ay maaaring hindi napapansin sa loob ng mahabang panahon, lalo na kung ikaw ay pangunahing nagtatrabaho sa mga Unix system. Upang magpasok ng wastong bagong linya gamit ang printf, ginagamit ang isang espesyal na karakter sa pag-format na "%n". Narito ang isinulat ng FindBugs tungkol dito:
VA_FORMAT_STRING_USES_NEWLINE: Ang format na string ay dapat gumamit ng %n sa halip na \n sa TestNewline.main(String[]) Ang format na string na ito ay may kasamang newline na character (\n). Sa mga string ng format, sa pangkalahatan ay mas mainam na gumamit ng %n, na gagawa ng separator ng linya na tukoy sa platform.
Marahil, para sa ilang mga mambabasa, ang lahat ng nasa itaas ay kilala sa mahabang panahon. Ngunit halos sigurado ako na para sa kanila ay magkakaroon ng isang kawili-wiling babala mula sa static analyzer, na magbubunyag sa kanila ng mga bagong tampok ng programming language na ginamit.
Mga komento
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION