Programiranje Java performansi, 2. dio: Trošak lijevanja

Za ovaj drugi članak u našoj seriji o performansama Jave fokus se prebacuje na kasting - što je to, što košta i kako ga (ponekad) možemo izbjeći. Ovaj mjesec započinjemo s brzim pregledom osnova klasa, predmeta i referenci, a zatim nastavljamo s pogledom na neke tvrde brojke izvedbe (na bočnoj traci, kako ne bismo uvrijedili gadljive!) I smjernicama o vrste operacija koje će najvjerojatnije probaviti vaš Java Virtual Machine (JVM). Na kraju završavamo detaljnim uvidom u to kako možemo izbjeći uobičajene efekte strukturiranja klasa koji mogu uzrokovati lijevanje.

Programiranje Java performansi: Pročitajte cijelu seriju!

  • Dio 1. Naučite kako smanjiti opće troškove programa i poboljšati izvedbu kontrolirajući stvaranje objekata i odvoz smeća
  • Dio 2. Smanjite režijske troškove i pogreške u izvršenju pomoću koda sigurnog za tip
  • Dio 3. Pogledajte kako se alternative kolekcije mjere u izvedbi i saznajte kako najbolje iskoristiti svaku vrstu

Tipovi objekata i reference u Javi

Prošli smo mjesec razgovarali o osnovnoj razlici između primitivnih tipova i objekata u Javi. I broj primitivnih tipova i odnosi između njih (posebno pretvorbe između tipova) utvrđeni su definicijom jezika. Predmeti su, s druge strane, neograničenih vrsta i mogu se povezati s bilo kojim brojem drugih vrsta.

Svaka definicija klase u programu Java definira novu vrstu objekta. To uključuje sve klase iz Java knjižnica, tako da bilo koji zadani program može koristiti stotine ili čak tisuće različitih vrsta objekata. Nekoliko od ovih vrsta definirano je definicijom jezika Java kao određene vrste upotrebe ili rukovanja (poput upotrebe java.lang.StringBufferza java.lang.Stringoperacije spajanja). Međutim, osim ovih nekoliko iznimaka, Java kompajler i JVM koji se koriste za izvršavanje programa obrađuju sve vrste u osnovi isto.

Ako definicija klase ne specificira (pomoću extendsklauzule u zaglavlju definicije klase) drugu klasu kao roditelj ili superklasu, ona implicitno proširuje java.lang.Objectklasu. To znači da se svaka klasa na kraju proširuje java.lang.Object, bilo izravno ili putem slijeda jedne ili više razina roditeljskih klasa.

Objekti su sami uvijek instance klase, i objekt je tip je klasa koja je instanca. U Javi se, međutim, nikad ne bavimo izravno objektima; radimo s referencama na objekte. Na primjer, linija:

 java.awt.Component myComponent; 

ne stvara java.awt.Componentobjekt; stvara referentnu varijablu tipa java.lang.Component. Iako reference imaju tipove baš kao i objekti, ne postoji točno podudaranje između reference i tipova objekata - referentna vrijednost može biti null, objekt istog tipa kao referenca ili objekt bilo koje podklase (tj. Klasa koja potječe od) vrsta reference. U ovom konkretnom slučaju, riječ java.awt.Componentje o apstraktnoj klasi, pa znamo da nikada ne može postojati objekt istog tipa kao i naša referenca, ali sigurno mogu postojati objekti potklasa tog referentnog tipa.

Polimorfizam i lijevanje

Vrsta reference određuje kako se referencirani objekt - odnosno objekt koji je vrijednost reference - može koristiti. Na primjer, u gornjem primjeru, upotreba koda myComponentmože pozvati bilo koju od metoda definiranih klasom java.awt.Componentili bilo koju od njezinih superklasa na referenciranom objektu.

Međutim, metoda koja se stvarno izvršava pozivom nije određena vrstom same reference, već vrstom objekta na koji se upućuje. Ovo je osnovni princip polimorfizma - podrazredi mogu nadjačati metode definirane u roditeljskoj klasi kako bi se primijenilo drugačije ponašanje. U slučaju naše varijable primjera, ako je referencirani objekt zapravo instanca java.awt.Button, promjena stanja koja je rezultat setLabel("Push Me")poziva bila bi drugačija od one koja bi rezultirala da je referencirani objekt instanca java.awt.Label.

Osim definicija klase, Java programi koriste i definicije sučelja. Razlika između sučelja i klase je u tome što sučelje samo određuje skup ponašanja (i, u nekim slučajevima, konstante), dok klasa definira implementaciju. Budući da sučelja ne definiraju implementacije, objekti nikada ne mogu biti instance sučelja. Međutim, oni mogu biti primjerci klasa koje implementiraju sučelje. Reference mogu biti tipova sučelja, u tom slučaju referencirani objekti mogu biti primjerci bilo koje klase koja implementira sučelje (bilo izravno ili kroz neku klasu pretka).

Lijevanje se koristi za pretvorbu između tipova - posebno između referentnih tipova, za vrstu operacije lijevanja za koju smo ovdje zainteresirani. Ažurirane operacije (koje se u Specifikaciji jezika Java nazivaju i proširivanjem pretvorbe ) pretvaraju referencu podklase u referencu klase pretka. Ova operacija lijevanja obično je automatska, jer je uvijek sigurna i prevodilac je može izravno implementirati.

Operacije spuštanja (koje se u Specifikaciji jezika Java nazivaju i sužavanjem konverzija ) pretvaraju referencu klase pretka u referencu potklase. Ova operacija lijevanja stvara režijske troškove, budući da Java zahtijeva provjeru cast tijekom izvođenja kako bi se osiguralo da je valjana. Ako referencirani objekt nije instanca niti ciljanog tipa za cast, niti potklase te vrste, pokušaj emitiranja nije dopušten i mora baciti a java.lang.ClassCastException.

instanceofOperater u Javi omogućuje da se utvrdilo da li ili ne određeni postupka lijevanja dopušteno bez zapravo pokušavaju operaciju. Budući da su troškovi izvedbe provjere mnogo manji od onih izuzetaka generiranih nedopuštenim pokušajem emitiranja, općenito je pametno koristiti instanceoftest kad god niste sigurni da je vrsta reference onakva kakvu želite . Prije nego što to učinite, trebali biste se pobrinuti da imate razuman način suočavanja s referencom neželjenog tipa - u suprotnom, možete samo dopustiti da se izuzetak baci i riješiti je na višoj razini u vašem kodu.

Bacajući oprez na vjetrove

Casting omogućuje upotrebu generičkog programiranja u Javi, gdje je kôd napisan za rad sa svim objektima klasa potekle iz neke osnovne klase (često java.lang.Objectza korisne klase). Međutim, upotreba lijevanja uzrokuje jedinstveni skup problema. U sljedećem ćemo odjeljku pogledati utjecaj na izvedbu, ali razmotrimo prvo učinak na sam kôd. Evo primjera koji koristi generičku java.lang.Vectorklasu prikupljanja:

private Vector someNumbers; ... javna praznina doSomething () {... int n = ... Integer number = (Integer) someNumbers.elementAt (n); ...}

Ovaj kodeks predstavlja potencijalne probleme u pogledu jasnoće i održavanja. Ako je netko drugi od originalnog razvojnog su izmijeniti kôd u nekom trenutku, on je razumno misliti da je mogao dodati java.lang.Doubleu someNumberszbirkama, budući da je to podvrsta java.lang.Number. Sve bi se fino složilo ako bi ovo pokušao, ali u nekom neodređenom trenutku izvršenja vjerojatno bi ga java.lang.ClassCastExceptionizbacili kad bi pokušaj bacanja na a java.lang.Integerbio izveden zbog njegove dodane vrijednosti.

Ovdje je problem što upotreba lijevanja zaobilazi sigurnosne provjere ugrađene u Java kompajler; programer na kraju lovi na pogreške tijekom izvršavanja, jer ih prevodilac neće uhvatiti. To samo po sebi nije katastrofalno, ali ova vrsta pogreške pri korištenju često se prilično pametno skriva dok testirate svoj kod, da bi se otkrila kad se program pusti u proizvodnju.

Nije iznenađujuće, podrška za tehniku ​​koja bi prevoditelju omogućila da otkrije ovu vrstu pogreške u korištenju jedno je od najtraženijih poboljšanja Java. Trenutno je u tijeku projekt u Procesu Java zajednice koji istražuje dodavanje upravo ove podrške: broj projekta JSR-000014, Dodavanje generičkih tipova u programski jezik Java (za detalje pogledajte odjeljak Resursi u nastavku.) U nastavku ovog članka, dolazeći sljedeći mjesec, detaljnije ćemo razmotriti ovaj projekt i razgovarati o tome kako će vjerojatno pomoći i gdje će nas vjerojatno ostaviti želeći još.

Pitanje izvedbe

Odavno je poznato da lijevanje može štetiti performansama u Javi i da možete poboljšati performanse minimiziranjem lijevanja u jako korištenom kodu. Pozivi metoda, posebno pozivi putem sučelja, često se spominju kao potencijalna uska grla u izvedbi. Trenutna generacija JVM-a daleko je odmakla od svojih prethodnika i vrijedi provjeriti koliko se ti principi danas održavaju.

Za ovaj sam članak razvio niz testova kako bih vidio koliko su ovi čimbenici važni za izvedbu s trenutnim JVM-ovima. Rezultati ispitivanja sažeti su u dvije tablice na bočnoj traci, Tablica 1 koja prikazuje režijske troškove poziva metode i Tablica 2 režijske troškove lijevanja. Cjeloviti izvorni kod za testni program također je dostupan na mreži (za više detalja pogledajte odjeljak Resursi u nastavku).

Da sumiramo ove zaključke za čitatelje koji ne žele prolaziti kroz detalje u tablicama, određene su vrste poziva i uloge još uvijek prilično skupe, u nekim slučajevima traje gotovo toliko dugo koliko i jednostavna dodjela objekata. Gdje je moguće, ove vrste operacija treba izbjegavati u kodu koji treba optimizirati za izvedbu.

Konkretno, pozivi nadjačanih metoda (metode koje su nadjačane u bilo kojoj učitanoj klasi, a ne samo stvarnoj klasi objekta) i pozivi putem sučelja znatno su skuplji od poziva jednostavnih metoda. HotSpot Server JVM 2.0 beta korišten u testu čak će pretvoriti mnoge pozive jednostavnih metoda u ugrađeni kôd, izbjegavajući bilo kakve dodatne troškove za takve operacije. Međutim, HotSpot pokazuje najlošije performanse među testiranim JVM-ima za nadjačane metode i pozive putem sučelja.

Za emitiranje (naravno, downcasting), testirani JVM-ovi uglavnom održavaju učinak na razumnoj razini. HotSpot s tim izvrsno radi u većini referentnih testova i, kao i kod poziva metode, u mnogim je jednostavnim slučajevima u stanju gotovo u potpunosti eliminirati režijske troškove. U kompliciranijim situacijama, poput emitiranja nakon kojih slijede pozivi nadjačanih metoda, svi testirani JVM-ovi pokazuju primjetnu degradaciju performansi.

Testirana verzija HotSpot-a također je pokazala izuzetno lošu izvedbu kada je objekt uzastopno premještan na različite referentne tipove (umjesto da je uvijek prebačen na isti ciljni tip). Ta se situacija redovito javlja u knjižnicama poput Swinga koje koriste duboku hijerarhiju klasa.

U većini slučajeva režijski troškovi i poziva metode i lijevanja mali su u usporedbi s vremenima dodjele objekata koji smo gledali u prošlomjesečnom članku. Međutim, ove će se operacije često koristiti puno češće od dodjele objekata, pa i dalje mogu biti značajan izvor problema s izvedbom.

U ostatku ovog članka razmotrit ćemo neke specifične tehnike za smanjenje potrebe za lijevanjem u vašem kodu. Konkretno, proučit ćemo kako lijevanje često proizlazi iz načina interakcije podrazreda s osnovnim klasama i istražit ćemo neke tehnike za uklanjanje ove vrste lijevanja. Sljedeći mjesec, u drugom dijelu ovog pregleda kastinga, razmotrit ćemo još jedan uobičajeni uzrok lijevanja, upotrebu generičkih zbirki.

Osnovne klase i lijevanje

There are several common uses of casting in Java programs. For instance, casting is often used for the generic handling of some functionality in a base class that may be extended by a number of subclasses. The following code shows a somewhat contrived illustration of this usage:

 // simple base class with subclasses public abstract class BaseWidget { ... } public class SubWidget extends BaseWidget { ... public void doSubWidgetSomething() { ... } } ... // base class with subclasses, using the prior set of classes public abstract class BaseGorph { // the Widget associated with this Gorph private BaseWidget myWidget; ... // set the Widget associated with this Gorph (only allowed for subclasses) protected void setWidget(BaseWidget widget) { myWidget = widget; } // get the Widget associated with this Gorph public BaseWidget getWidget() { return myWidget; } ... // return a Gorph with some relation to this Gorph // this will always be the same type as it's called on, but we can only // return an instance of our base class public abstract BaseGorph otherGorph() { ... } } // Gorph subclass using a Widget subclass public class SubGorph extends BaseGorph { // return a Gorph with some relation to this Gorph public BaseGorph otherGorph() { ... } ... public void anyMethod() { ... // set the Widget we're using SubWidget widget = ... setWidget(widget); ... // use our Widget ((SubWidget)getWidget()).doSubWidgetSomething(); ... // use our otherGorph SubGorph other = (SubGorph) otherGorph(); ... } }