Programovanie

Programovanie v prostredí Java, 2. časť: Náklady na casting

V tomto druhom článku v našom seriáli o výkone v Jave sa pozornosť zameriava na casting - čo to je, čo to stojí a ako sa mu môžeme (niekedy) vyhnúť. Tento mesiac začíname rýchlou revíziou základov tried, predmetov a odkazov, potom pokračujeme pohľadom na niektoré tvrdé údaje o výkonnosti (na bočnom paneli, aby sme neurazili šteklivé!) A pokyny k typy operácií, ktoré s najväčšou pravdepodobnosťou spôsobia vášmu zariadeniu Java Virtual Machine (JVM) poruchy trávenia. Na záver zakončíme podrobným pohľadom na to, ako sa môžeme vyhnúť bežným efektom štruktúrovania triedy, ktoré môžu spôsobiť obsadenie.

Programovanie v prostredí Java: Prečítajte si celú sériu!

  • Časť 1. Naučte sa, ako znížiť réžiu programu a zlepšiť výkon riadením vytvárania objektov a zhromažďovania odpadu
  • Časť 2. Znížte réžiu a chyby vykonávania pomocou kódu bezpečného pre typ
  • Časť 3. Zistite, ako alternatívy kolekcií zvyšujú výkon, a zistite, ako z každého typu vyťažiť maximum

Typy objektov a odkazov v Jave

Minulý mesiac sme diskutovali o základnom rozlíšení medzi primitívnymi typmi a objektmi v Jave. Počet primitívnych typov aj vzťahy medzi nimi (najmä konverzie medzi typmi) sú stanovené v definícii jazyka. Na druhej strane sú objekty neobmedzeného počtu a môžu súvisieť s akýmkoľvek počtom ďalších typov.

Každá definícia triedy v programe Java definuje nový typ objektu. Patria sem všetky triedy z knižníc Java, takže akýkoľvek program môže používať stovky alebo dokonca tisíce rôznych typov objektov. Niektoré z týchto typov sú definované v definícii jazyka Java ako jazyky, ktoré majú určité špeciálne použitie alebo zaobchádzanie s nimi (napríklad použitie servera java.lang.StringBuffer pre java.lang.String zreťazovacie operácie). Okrem týchto niekoľkých výnimiek však kompilátor Java spracováva všetky typy v zásade rovnako a JVM použitý na vykonanie programu.

Ak definícia triedy nešpecifikuje (pomocou predlžuje klauzula v hlavičke definície triedy) o ďalšiu triedu ako nadradenú alebo nadtriedu, implicitne rozširuje java.lang.Objekt trieda. To znamená, že každá trieda sa nakoniec rozširuje java.lang.Objekt, buď priamo, alebo prostredníctvom sekvencie jednej alebo viacerých úrovní nadradených tried.

Samotné objekty sú vždy inštanciami tried a objektov typu je trieda, ktorej je inštanciou. V Jave však nikdy neriešime priamo objekty; pracujeme s referenciami na objekty. Napríklad riadok:

 java.awt.Component myComponent; 

nevytvára java.awt.Component predmet; vytvára referenčnú premennú typu java.lang.Komponent. Aj keď referencie majú typy rovnako ako objekty, neexistuje presná zhoda medzi referenciou a typmi objektov - referenčná hodnota môže byť nulový, objekt rovnakého typu ako referencia, alebo objekt akejkoľvek podtriedy (t. j. triedy, z ktorej pochádza) typu referencie. V tomto konkrétnom prípade java.awt.Component je abstraktná trieda, takže vieme, že nikdy nemôže existovať objekt rovnakého typu ako naša referencia, ale určite môžu existovať objekty podtried tohto referenčného typu.

Polymorfizmus a odlievanie

Typ odkazu určuje, ako odkazovaný objekt - to znamená objekt, ktorý je hodnotou referencie - je možné použiť. Napríklad v príklade vyššie použite kód myComponent mohol vyvolať ktorúkoľvek z metód definovaných triedou java.awt.Componentalebo ktorákoľvek z jeho nadtried na odkazovaný objekt.

Avšak metóda skutočne vykonaná volaním nie je určená typom samotnej referencie, ale skôr typom odkazovaného objektu. Toto je základný princíp polymorfizmus - podtriedy môžu prepísať metódy definované v nadradenej triede s cieľom implementovať odlišné správanie. V prípade našej ukážkovej premennej, ak by odkazovaný objekt bol skutočne inštanciou java.awt.Button, zmena stavu vyplývajúca z a setLabel („Push Me“) volanie by sa líšilo od výsledku, keby bol odkazovaný objekt inštanciou java.awt.Label.

Okrem definícií tried používajú programy Java aj definície rozhraní. Rozdiel medzi rozhraním a triedou je v tom, že rozhranie určuje iba množinu správania (a v niektorých prípadoch aj konštanty), zatiaľ čo trieda definuje implementáciu. Pretože rozhrania nedefinujú implementácie, objekty nikdy nemôžu byť inštanciami rozhrania. Môžu to byť však inštancie tried, ktoré implementujú rozhranie. Referencie môcť byť typu rozhrania, v takom prípade môžu byť odkazovanými objektmi inštancie akejkoľvek triedy, ktorá implementuje rozhranie (buď priamo, alebo prostredníctvom niektorej triedy predkov).

Casting sa používa na prevod medzi typmi - najmä medzi referenčnými typmi, pre typ operácie odlievania, o ktorý sa tu zaujímame. Zastarané operácie (tiež nazývaný rozširujúce sa konverzie v špecifikácii jazyka Java) prevedie odkaz na podtriedu na odkaz na triedu predkov. Táto castingová operácia je zvyčajne automatická, pretože je vždy bezpečná a môže byť implementovaná priamo kompilátorom.

Prepadnuté operácie (tiež nazývaný zúženie konverzií v špecifikácii jazyka Java) previesť odkaz na triedu predkov na odkaz na podtriedu. Táto operácia odlievania vytvára réžiu vykonávania, pretože Java vyžaduje, aby bola cast za behu skontrolovaná, aby sa ubezpečil, že je platná. Ak odkazovaný objekt nie je inštanciou cieľového typu pre cast alebo podtriedy tohto typu, pokus o cast nie je povolený a musí vrhnúť java.lang.ClassCastException.

The inštancia operátor v Jave vám umožňuje určiť, či je alebo nie je povolená konkrétna operácia prenášania bez toho, aby ste sa o operáciu skutočne pokúsili. Pretože náklady na výkon kontroly sú oveľa nižšie ako náklady na výnimku generovanú nepovoleným pokusom o obsadenie, je všeobecne rozumné použiť inštancia otestujte kedykoľvek, nie ste si istí, že typ referencie je taký, aký by ste chceli. Predtým, ako tak urobíte, by ste sa však mali ubezpečiť, že máte rozumný spôsob riešenia referencie nežiaduceho typu - inak môžete tiež nechať výnimku vyhodiť a spracovať ju na vyššej úrovni v kóde.

Venujte opatrnosť vetrom

Casting umožňuje použitie generického programovania v Jave, kde sa píše kód na prácu so všetkými objektmi tried pochádzajúcich z nejakej základnej triedy (často java.lang.Objekt, pre úžitkové triedy). Použitie odlievania však spôsobuje jedinečný súbor problémov. V nasledujúcej časti sa pozrieme na vplyv na výkon, ale najskôr zvážime vplyv na samotný kód. Tu je ukážka pomocou generického java.lang.Vektor trieda zberu:

 súkromné ​​Vector someNumbers; ... public void doSomething () {... int n = ... Celé číslo = (Celé číslo) someNumbers.elementAt (n); ...} 

Tento kód predstavuje potenciálne problémy z hľadiska jasnosti a udržiavateľnosti. Ak by niekto v inom okamihu ako pôvodný vývojár zmenil kód, mohol by si rozumne myslieť, že by mohol pridať a java.lang.Double do niektoré čísla zbierky, pretože toto je podtrieda java.lang.číslo. Všetko by sa dalo do poriadku, keby to skúsil, ale v určitom okamihu popravy by pravdepodobne dostal java.lang.ClassCastException hodený pri pokuse o vrhnutie na a java.lang.Integer bol popravený pre jeho pridanú hodnotu.

Problém je v tom, že použitie castingu obchádza bezpečnostné kontroly zabudované do kompilátora Java; programátor skončí s hľadaním chýb počas vykonávania, pretože ich kompilátor nezachytí. To samo o sebe nie je katastrofálne, ale tento typ chyby pri používaní sa často pri testovaní vášho kódu skryje dosť dômyselne, len aby sa odhalil, keď je program uvedený do výroby.

Nie je prekvapením, že podpora techniky, ktorá by kompilátoru umožnila zistiť tento typ chyby použitia, je jedným z najžiadanejších vylepšení Java. V procese komunitného procesu Java práve prebieha projekt, ktorý skúma pridanie tejto podpory: číslo projektu JSR-000014, Pridanie generických typov do programovacieho jazyka Java (ďalšie informácie nájdete v časti Zdroje). V pokračovaní tohto článku, Na budúci mesiac sa pozrieme na tento projekt podrobnejšie a budeme diskutovať o tom, ako pravdepodobne pomôže, a kde nás pravdepodobne nechá viac.

Problém s výkonom

Už dávno sa uznáva, že prenášanie môže byť na úkor výkonu v Jave a že výkon môžete vylepšiť minimalizáciou prenášania v často používanom kóde. Hovory metód, najmä hovory cez rozhrania, sa tiež často spomínajú ako potenciálne úzke miesta výkonu. Súčasná generácia JVM však prešla od svojich predchodcov dlhú cestu a stojí za to skontrolovať, ako dobre tieto princípy dnes platia.

Pre tento článok som vyvinul sériu testov, aby som zistil, aké dôležité sú tieto faktory pre výkon so súčasnými JVM. Výsledky testu sú zhrnuté do dvoch tabuliek na bočnom paneli, pričom tabuľka 1 zobrazuje réžiu volania metódy a tabuľka 2 réžiu prenášania. Celý zdrojový kód testovacieho programu je tiež k dispozícii online (ďalšie informácie nájdete v časti Zdroje nižšie).

Aby sme zhrnuli tieto závery pre čitateľov, ktorí sa nechcú brodiť podrobnosťami v tabuľkách, niektoré typy volaní a odovzdávaní metód sú stále pomerne drahé, v niektorých prípadoch trvajú takmer rovnako dlho ako jednoduché prideľovanie objektov. Pokiaľ je to možné, týmto typom operácií by sa malo vyhnúť v kóde, ktorý je potrebné optimalizovať kvôli výkonu.

Najmä volania na prepísané metódy (metódy, ktoré sú prepísané v akejkoľvek načítanej triede, nielen v skutočnej triede objektu) a volania cez rozhrania sú podstatne nákladnejšie ako jednoduché volania metód. HotSpot Server JVM 2.0 beta použitý v teste dokonca prevedie mnoho jednoduchých volaní metód na vložený kód, čím sa zabráni akejkoľvek réžii týchto operácií. HotSpot však vykazuje najhorší výkon medzi testovanými JVM pre prepísané metódy a volania cez rozhrania.

Pokiaľ ide o casting (samozrejme downcasting), testované JVM spravidla udržujú dosiahnutý výkon na rozumnej úrovni. HotSpot s tým robí vo väčšine testovacích testov výnimočnú prácu a rovnako ako pri volaní metód je v mnohých jednoduchých prípadoch schopný takmer úplne eliminovať réžiu prenášania. V zložitejších situáciách, ako sú obsadenia, po ktorých nasledujú volania prepísaných metód, vykazujú všetky testované JVM znateľné zníženie výkonu.

Testovaná verzia HotSpot tiež vykazovala extrémne nízky výkon, keď bol objekt postupne prenášaný na rôzne referenčné typy (namiesto toho, aby bol vždy prenášaný na rovnaký cieľový typ). Táto situácia pravidelne nastáva v knižniciach ako Swing, ktoré používajú hlbokú hierarchiu tried.

Vo väčšine prípadov je réžia oboch volaní metód a prenosu malá v porovnaní s časmi alokácie objektov, na ktoré sa zameral článok z minulého mesiaca. Tieto operácie sa však budú často používať oveľa častejšie ako alokácie objektov, takže stále môžu byť významným zdrojom problémov s výkonom.

Vo zvyšku tohto článku si ukážeme niektoré konkrétne techniky, ktoré znižujú potrebu prenášania do vášho kódu. Konkrétne sa pozrieme na to, ako casting často vzniká zo spôsobu, akým podtriedy interagujú so základnými triedami, a preskúmame niektoré techniky eliminácie tohto typu castingu. Budúci mesiac v druhej časti tohto pohľadu na casting zvážime ďalšiu častú príčinu castingu, použitie všeobecných zbierok.

Základné triedy a casting

V programoch Java existuje niekoľko bežných použití castingu. Napríklad casting sa často používa na všeobecné spracovanie niektorých funkcií v základnej triede, ktoré možno rozšíriť o niekoľko podtried. Nasledujúci kód zobrazuje trochu vymyslenú ilustráciu tohto použitia:

 // jednoduchá základná trieda s podtriedami verejná abstraktná trieda BaseWidget {...} verejná trieda SubWidget rozširuje BaseWidget {... public void doSubWidgetSomething () {...}} ... // základná trieda s podtriedami, s použitím predchádzajúcej množiny tried verejná abstraktná trieda BaseGorph {// Widget spojený s týmto súkromným Gorphom BaseWidget myWidget; ... // nastaviť Widget spojený s touto Gorph (povolený iba pre podtriedy) chránený void setWidget (BaseWidget widget) {myWidget = widget; } // získať Widget spojený s týmto verejným Basewidgetom Gorph getWidget () {return myWidget; } ... // vráti Gorpha s určitým vzťahom k tomuto Gorphovi // toto bude vždy rovnaký typ, ako je vyvolaný, ale môžeme // vrátiť iba inštanciu nášho verejného abstraktu BaseGorph otherGorph () {. ..}} // Gorfova podtrieda používajúca verejnú triedu podtriedy Widget SubGorph rozširuje BaseGorph {// vráti Gorpha s nejakým vzťahom k tejto verejnej Gorph BaseGorph otherGorph () {...} ... public void anyMethod () {.. . // nastavíme Widget, ktorý používame SubWidget widget = ... setWidget (widget); ... // použite náš Widget ((SubWidget) getWidget ()). doSubWidgetSomething (); ... // použite náš otherGorph SubGorph other = (SubGorph) otherGorph (); ...}} 
$config[zx-auto] not found$config[zx-overlay] not found