tifyty

pure Java, what else ?

Ha nem fontos, ne csináld!

Sokszor hallom junior kollégáktól, hogy olyan projekten dolgoznak, ami nem fontos. Rettentően demotiváló, amikor azt látod, hogy a kódot, amivel rengeteget küzdöttél nem használják és kidobják. Kicsit olyan, mintha ásatnának veled egy gödröt, hogy másnap aztán betemethessed. (Aminek persze lehet értelme, mondjuk ha sírásó vagy.)

Eugène Ferdinand Victor Delacroix 018

Egyszer találkoztam egy matematikus programozóval, aki algo-trading algoritmusokat fejlesztett. Ez az, amikor a számítógépek egymással kereskednek a tőzsdén milliszekundumonként hajtva végre tranzakciókat. Elégedett volt a fizetésével, és intellektuális kihívásának is kiváló volt az újabb és újabb algoritmusok kitalálása. Mégis úgy érezte, hogy a munkája nem sok hasznosat teremt az emberiség számára. Úgy gondolta, hogy az algo-trading csak gépek egymás közötti versengése, semmi egyéb.

Mivel hosszan utaztunk együtt és volt idő a beszélgetésre elmondtam, hogy szerintem az alo-trading versengés ugyan, de nem a gépek között, hanem az emberek között, akik az algoritmusokat állítják elő, végrehajtják, karbantartják a tranzakciókat. Ilyen módon az algo-trading a gazdaságunk evolúciós folyamatának a része és nézetem szerint nagyban hozzájárul a tőke likviditásához, hogy a befektetendő pénz ott álljon rendelkezésre, ahol arra szükség van elősegítve az innovációt és a fejlődést.

A matematikus elég nyitott volt arra, hogy végiggondolja amit mondtam, és tulajdonképpen örült, hogy lehet egyéb értelme is a munkájának mint az, hogy a hó végén megérkezik a fizetés. Egy fél évvel később, teljesen véletlenül megint találkoztunk (mind a ketten gyakran utaztunk Budapest és Zürich között vonaton), és elmondta, hogy mióta másképp gondol a munkájára nem csak boldogabb, de attól, hogy lát magasabb értelmet az algoritmusok mögött, hatékonyabban is dolgozik.

Annak ellenére, hogy ez a történet részben kitaláció, részben autoszuggesztió, mégis rávilágít egy fontos pontra: jobban fog menni a munka, ha látod az értelmét. Agnosztikus lévén tudom, hogy az egész dolognak, mint élet, világmindenség meg minden semmi értelme, így az értelem megtalálása csak annak a kérdése, hogy milyen mélyre ásol. Ha sekélyes lelkű vagy elég lehet az is, hogy eleget fizetnek. Ha összetettebb a személyiséged, akkor olyan munka elégít ki, amelyiknél érted, hogy az miért fontos a cégnek, milyen értéket teremt, és hoz hasznot a tulajdonosoknak. Ha humanista vagy, akkor fontos lehet, hogy a munkád az emberiséget szolgálja, egy kis fogaskerék a haladás gépezetében. (Azért nagyon mélyre nem érdemes ásni, mert mentális problémákat okozhat.)

De ami fontos, hogy meg kell találnod a munkád értelmét. Ellenkező esetben az nem lesz hasznára senkinek. A szeniorok tudják.

Mit gondolhatott a költő

Volt már olyan, hogy csúnya kódot kellett refaktorálnom. Ez mindenkivel előfordul az élete során, talán csak a nepáli kecskepásztorok a kivételek, főleg ha birkák. Eleddig azonban ezek a csúnyaságok javarészt tapasztalatlanságból adódtak. Igen, akár az én tapasztalatlanságomból is: elő nem venném a kódot amit 2005-ben gyártottam. Mostanában azonban olyan kódokat látok, amelyiket nem gyárthatott kezdő junior. Meg van a súlya a kódnak, benne van a programozó egyénisége, gondolkodásmódja. Szinte már költészet.

Vannak egyszerűbbek:

String attrib = businessObject.getBusinessAttribute();
if( attrib == null || attrib.length() <= 0 ){
 ...

Már az is egy általánosan felteendő kérdés, hogy miért stringek az üzleti objektumok. Ez azonban általánosan elterjedt kódolási szokás, még abból a korból ered, amikor Perl-ben programoztunk (vagy nem). Végül is egy cím, egy irányítószám, egy bankszámlaszám, egy rendelési azonosító, egy láda térbeli mérete (pl. “22cm x 18cm x 11cm”) mind stringek (vagy nem). De mikor, és hogyan lehet egy string hossza negatív? A nulla hosszal nincs, gond, az is tiszta, hogy lehet a visszaadott érték null. De negatív hosszúságú string? CharSequence még csak csak lehetséges, na de string?

Mire gondolhatott a költő, amikor ezt írta? Mindig ezt a kérdést tették fel irodalom órán, és akkor sem értettem. Mondjuk arra, hogy éppen csikart a hasa, vagy kellett a honorárium a lakbér kifizetéséhez. Nem mindegy? Sokkal fontosabb, hogy milyen gondolatokat ébreszt a vers olvasása bennem. Szép ez a vers? Mert programkódnak csak limitáltan nevezhető, annál több benne a művészi érték. Mit gondoljak a tisztelt kollégáról, aki fentieket írta?

Valószínűleg megfontolt, és óvatos ember. Nem azt nézi, hogy egy string nulla hosszú-e, inkább arra kíváncsi, hogy pozitív számú karakter van-e benne. Biztos, ami biztos. Hátha egyszer szembe jön egy string aminek negatív a hossza. Az, hogy eddig még senki nem látott ilyet, nem bizonyítja, hogy valóban nincs is! Pont mint a jeti. (Már megint Nepál! Lehet ott valami!)

De a fenti kis kód maximum haiku-nak nevezhető. A következő viszont egy igazi szonett, amelyik felkészül a párhuzamosan futó szálra. Van olyan? Nem több szál, hanem egy. Egy szál, ami párhuzamosan fut. Még senki nem látott olyat? Az nem bizonyít semmit!

final ThreadLocal<MyType> lock = new ThreadLocal<MyType>();
	
  public void method() {
    if (lock.get() == null) {
      lock.set(this);
      try {
        init();
      } finally {
        lock.set(null);				
      }
    }
  }

Mit zárol ez a lock? Az adott szálat. Ha tehát az adott szál akarja még egyszer párhuzamosan futtatni a method() metódust, akkor az nem fog menni. Persze ki tudja mi van az init()-ben? Lehet, hogy rekurzívan vissza hív sok rétegen keresztül, és így varrja el a program a végtelen rekurziót. De nem, ezt megnéztük. Ilyet nem tesz. Annak egyébként lehetne értelme, hogy a szálhelyi (threadlocal) változó nem statikus. A rekurzív hívás (már ha lenne) ha másik objektumon hívná meg method() metódust, akkor ott egy másik szálhelyi változó állna strázsát az init() hívás előtt, és így beengedhetné. Tehát lehetne értelme a nem statikus szálhelyi változónak, ha éppen nem lenne az egész teljesen értelmetlen. Vagy én vagyok nepáli helikopter.

Hogy lehet ezt kezelni? Talán sztoikus nyugalommal, amit mi sem fejez ki jobban, mint egy haiku:

Ők programoznak,
Révedünk magunk elé.
Gyűlik sok maszat.

Karácsony előtti hangulat

Utolsó munkanapon, utolsó meeting első perceiben, még nem minden kolléga jött be a terembe, hangulat nem volt túl munkaorientált.

  • Zoli: Pár éve még mekkora truváj volt egy lézer pointer.
  • Péter: De ma már nincsenek pointerek, mert Java-ban programozunk.
  • Zsolt: De a kutyák között vannak.
  • Péter: Viszont ott a garbage colletion-t magadnak kell csinálni.

Hát mindenkinek boldog karácsonyt, aki akar az még dolgozzon, mi már elég fáradtak vagyunk…

Hibajegy besorolás

  • Foglalkoztatok a hibával amit tegnap jelentettünk le?
  • Nem. Még nem jutott rá időnk, magasabb prioritású hibákat kellett fixálni.
  • Ennél magasabb? Miről beszélsz? Ennél nem lehet fontosabb egy fix sem!
  • Akkor miért kozmetikai hibaként jelentettétek le?
  • Kozmetikai????? KOZMIKUS!!!!(eredetiben: cosmetic, cosmic)

Az sem volt feltűnő, hogy a “cosmetic” a ledobó dobozban a “minor” alatt van?

Eclipse vs. NetBeans

A Sonar és a csek sztájl esete kapcsán kérdezte meg sarkiroka, hogy

és eclipse vs netbeans összehasonlításra mit mondanál, ha az nem opció, hogy nem lehet összehasonlítani őket?

Szóval falhoz lettem állítva: nem tehetem meg, hogy ne hasonlítsam össze őket. Akkor viszont megér egy posztot, mert ez egy olyan téma, ami elég személyes, főleg véleményeket lehet megfogalmazni a cikkben, és valószínű, hogy lesz sok komment. Főleg, mivel olyan téma amihez annyira nem értek, így még az is előfordulhat, hogy hülyeségeket írok össze vissza.

Felület

Én magam NetBeans-szel kezdtem a Java fejlesztést, és csak később tértem át Eclipse használatra, főleg kíváncsiságból. Az első lényeges különbség, ami szembetűnik, főleg annak aki Windows-on fejleszt, hogy az Eclipse felülete sokkal natívabb. Nem meglepő, hiszem amíg a NetBeans swinget használ addig az Eclipse SWT-t, ami annyi mindent használ a natív window rétegből amennyit csak lehet. Ennek megfelelően natív implementáció szükséges minden egyes platformra, különben nem működik, viszont van mindenre. Sokan talán nem is realizálják, hogy mennyire fontos, hogy amivel nap, mint nap dolgozunk ergonomikus legyen. Egy szoftver grafikus felülete pedig mindenképpen ide tartozik, és ebben az Eclipse jobb.

Pilótavizsga

A második dolog, hogy egy kezdő számára a NetBeans egyszerű, mint egy faék, az Eclipse meg meg sem szólal, nem tudja, hogy hova nyúljon: pilótavizsga kell hozzá. Amikor naponta használod akkor már ez nem tűnik fel és talán nem is emlékszel rá. De az elején így van: ha csak elindítod, akkor nagyon nem érted, hogy hova kellene nyúlni. Olyan, mint az első szex. De utána már sokkal jobb, mint a NetBeans, legalábbis funkcionálisan, sokkal többet tud.

Repülő versus Autó

És ha pilótavizsga kell hozzá, akkor persze elvárható, hogy repüljön. És repül is. Nagyon sok plugin létezik hozzá, és ezekkel nagyon sok olyasmit tud, amint a Netbeans nem tud. Ha csak azt nézzük, hogy a google a “netbeans plugin” keresésre 3 millió találatot ad, míg az “eclipse plugin”-re 11 milliót: látszik a különbség. Én többet vártam, de ez sem elhanyagolható.

Build Ant vs. maven

A Netbeans és az Eclipse is tud ANT-tal buildelni, de a NetBeans elég natívan kezeli a maven-t is, míg az Eclipse-nek plugin kell hozzá. Valójában az Eclipse a maven-os projektek esetén (meg talán az ant-os projektek esetén sem) a külső built tool-lal fordít, hanem csak feldolgozza a megfelelő plugin, hogy megértse egy fordítás során mit kell csinálni, és utána ezt a saját belső build rendszerével lejátssza. Ennek az a következménye, hogy nem árt frissíteni a build dependenciákat, amikor a build fájl módosul. Hogy ant esetében ez mit jelent nem tudom, mert sosem raktam ösze ant-os projektet. Maven esetében a maven plugin “refresh dependencies” (vagy valami hasonló) menüt kell futtatni, és akkor szépen újra értékeli, hogy a build során mit és hogyan.

Persze el lehet indítani Eclipse-ből is a külső ant-ot vagy maven-t, de ez nem az amit az Eclipse szeret. És a kérdés, hogy miért? Mert elég körülményesnek tűnik a dolog a NetBeans-hez képest. Az Eclise nem is a standard java fordítót használja, hanem egy saját módosítottat, amelyikkel inkrementális fordítást lehet csinálni és így akár még arra is képes (csodákra persze nem), hogy debug közben a módosított forrás alapján folytassa a már futó processz-t. Mágikus, de nem az, csak rocket science.

Összefoglaló

Kezdőként használd a NetBeans-t, mert sokkal hamarabb jutsz vele dűlőre, és használd amíg megfelelő. Nagyvállalati profi környezetben viszont kicsi az esélyed, hogy egy életen át megúszd Eclipse nélkül. Jelenleg az a de-facto IDE standard, rád fogják kényszeríteni. Ha igazi guru vagy, akkor meg IntelliJ.

Sonar és a csek sztájl esete

Mostanában angolul interjúztatok. Java technikai interjú. Ezeknek megvan a menete, és nagyjából ugyanaz, akár magyarul folyik az interjú személyesen, akár interneten keresztül angolul. Mostanában ez a második a jellemző, és nagyon jó élmény, amikor kiváló koponyákkal találkozom. Ilyenkor az első pár perc után az interjú már nem arról szól, hogy megfelel-e az jelölt, inkább csak arról, hogy mennyire felel meg.

Pár napja nagyon vicces dolog történt, egy egyébként nagyon okos programozóval, aki Prágából jelentkezett. (Ez fontos.) Kérdezgettem minden féléről.

Az angollal sem volt gond, bár én sem oxfordi kiejtéssel beszélek, és a jelöltek sem, így akad néha félreértés, de ilyenkor – ha fontos – be lehet gépelni a skype-ba egy-egy szót. Kérdezem a sráctól, hogy milyen szoftvereszközöket használna egy profi környezetben. Ilyenkor jön a Eclipse, maven, ant, git, svn, Jenkins. Kérdezem: Sonar? Cobertura? FindBugs? Mindegyikről hallott valamit. CheckStyle? Erre a válasz, hogy szerinte a csehek ugyanúgy programoznak, mint bárki más nemzet fiai és lányai, nem tud róla, hogy lenne speciálisan cseh stílus…

Hogyan fűzzünk össze stringeket Java8-ban?

Na ez valami olyasmi, mint amit a petúnia gondolt, mielőtt becsapódott volna a földbe:

Jaj ne, már megint!

Szóval volt már erről szó. De hát volt időm, sajnos, és megláttamezt a cikket (eredeti) és nem bírtam megállni. Úgyhogy most haladok:

A Java 8-ban a library-k között van egy StringJoiner osztály. És van egy új statikus metódusa a String osztálynak. Kitalálod a nevét? join()! Ki hitte volna?

Objektum Internálás

Ez a cikk az eredeti angol nyelvű cikk alapján készült.

A Java a string konstansokat egy string medencében tárolja, mégpedig mindegyik stringet pontosan egyszer. Azaz, ha

String a = "I am a string";
String b = "I am a string";

akkor a két változó a és b ugyanarra a stringre fog mutatni. Nem csak azonos lesz a két string, azaz nem csak hogy az equal() metódus eredménye lesz igaz, de pontosan ugyanarra a string objektumra “mutat” a két változó. Java-ban megfogalmazva: a == b. Ez azonban csak stringek-re, kis Integer és Long értékekre igaz. Más objektumokra általában nem igaz, hogy ha ugyanaz az értékük, akkor azonosak is lennének. Ez néha kellemetlen. Például ha előveszünk egy objektumot egy perzisztencia tárolóból (adatbázis). Ha ugyanazt a rekordot többször is elővesszük, mondjuk mert nem tudja a program logika, hogy már elővettük korábban, akkor jó lenne, ha nem egy másolatot kapnánk minden egyes esetben, hanem ugyanazt az objektumot. Más szavakkal: ha ugyanazt az objektumot veszem elő a perzisztenciából akkor ugyanazt az objektumot akarom megkapni a memóriában is. Néhány perzisztencia réteg ezt megteszi magától. Például a JPA implementációk ezt a mintát követik. Más esetekben viszont az alkalmazás szintjén kell ezt megtenni.

Ebben a cikkben részletezek egy egyszerű intern pool (belső medence) implementációt, amelyik a stackoverflow topics cikkben is megtalálható. Ami plusz ebben a cikkben, hogy részletesen leírom a tervezési megfontolásokat is.

Ojjektum medence

az ellen nem véd

Az internáláshoz kell egy objektum medence. Amikor van egy új objektumunk, akkor megnézzük, hogy az objektum medencében benne van-e már egy olyan objektum ami egyenlő a miénkkel, és ha igen, akkor azt használjuk, a miénket pedig eldobjuk. Ha még nincs ilyen objektum a medencében, akkor beletesszük, és használjuk.

Két komoly dologgal kell foglalkozni amikor ezt megvalósítjuk:

  • szemétgyűjtés
  • sokszálú környezet

Amikor az objektumra már nincs szükség, akkor ki kell dobni a medencéből. Ezt a kidobást megteheti az alkalmazás is, de ez felesleges és elavult megközelítés. A Java-ban van szemétgyűjtő, ez az egyik nagy előnye a C++-szal szemben, és ha már van, akkor használjuk. Hagyjuk, hogy a szemétgyűjtő gyűjtse be az objektumokat. Hogy ezt megtehesse nem szabad erős referenciákat tárolnunk az objektumokhoz.

Referenciák

Ha tudod, hogy mi a puha, gyenge és fantom referencia, akkor ugorj a következő fejezetre. Ha nem, akkor sokkal szerencsésebb vagy, mint a csak angolul olvasók, mert itt van egy teljes cikk ezekről magyarul, míg az angol verzióban csak egy rövid kis fejezetben foglaltam össze, hogy mik ezek. Viszont angolul sokkal több helyen megvan egyébként is. Ezért meg ők a szerencsések. Mi mindannyian szerencsések vagyunk.

A JDK-ban van egy osztály java.lang.ref.Reference néven és van másik osztály, amelyek ezt az osztályt kiterjesztik:

  1. PhantomReference
  2. WeakReference and
  3. SoftReference

Ha elolvastad a dokumentációt, esetleg az egy évvel ezelőtti cikkemet, akkor valószínűleg érzed, hogy itt most a medence megvalósításhoz gyenge referenciák kellenek majd. Fantom nem lehet, hiszen azon keresztül nem lehet az objektumhoz hozzáférni, márpedig a medencéből kellenek az objektumok. A puha sem megfelelő, mert ha már nem kell senki másnak, akkor minek tartsuk az objektumot a medencében. Majd ha újra bejön, akkor újra beletesszük. Amúgy meg a puha referencia használata általában is ellenjavallt: ha cache tárolót készítünk (nem készítünk, hanem meglevő keretrendszert használunk) akkor sem kellene a Java rendszerre bízni, hogy mikor mit eviktál (dob ki). Az ilyesmit hatékonyabban elintézi a cash cache implementáció.

A gyenge referencia pont megfelelő, mert azon keresztül el lehet érni az objektumot, de nem befolyásolja a szemétgyűjtő működését.

WeakHashMap

Nem a gyenge referenciát fogjuk direktben használni, hanem a WeakHashMap-et, ami egy olyan Map, amelyiknek a kulcsai gyenge referenciát tartalmaznak a kulcs objektumokra. Amikor internálni akarunk egy objektumot, akkor összehasonlítjuk a medencében levő objektumokkal, és ha valamelyikkel egyenlő (equal) akkor megtaláltuk. A Map pontosan ezt teszi. Az pedig, hogy csak gyenge referenciákat tart a kulcsok felé lehetővé teszi, hogy ha az objektum már nem kell senkinek, akkor a GC begyűjthesse.

Most már tudunk keresni, ami jó. De nem elég keresni: találni is kell. A Map esetében a kulcsra keresünk, és egy értéket találunk. Ebben az esetben az érték ugyanaz az objektum, mint a kulcs. De magát az objektumot nem helyezhetjük be a map-ve értékként, mert akkor van egy erős referenciánk az objektumra a map-ból és így a GC nem tudja begyűjteni, ha már nem kell senkinek. Ezért magunknak létre kell hozni egy gyenge referenciát, és azt tenni el, mint értéket.

WeakPool

Először csak egy gyenge medencét készítünk, amelyik nem rakja el az objektumot, ha nem találta meg. Egyszerűen csak megmondja, hogy van-e ilyen objektum a medencében amilyent keresünk, és ha van, akkor visszaadja. Ha meg nincs, akkor beletehetjük, de ő maga nem teszi bele magától. Egy osztály csináljon csak egy dolgot.

Itt a forráskód. Ez csak annyit mond, hogy ha az objektum amit keresünk benne van a gyenge map-ben, és meg van maga az objektum is, nem csak a gyenge referencia, akkor a get(actualObject) azt vissza fogja adni. Ha nincs benne a map-ben, vagy már csak a referencia van meg (mert pont most ledarálta a gc), akkor null-t ad vissza. A put() meg elteszi az értéket.

public class WeakPool<T> {
  private final WeakHashMap<T, WeakReference<T>> pool = new WeakHashMap<T, WeakReference<T>>();
  public T get(T object){
      final T res;
      WeakReference<T> ref = pool.get(object);
      if (ref != null) {
          res = ref.get();
      }else{
          res = null;
      }
      return res;
  }
  public void put(T object){
      pool.put(object, new WeakReference<T>(object));
  }
}

InternPool

A feladatra a teljes megoldás egy internáló medence (intern pool), amelyet nem is olyan nehéz implementálni ha megvan a gyenge medencénk, a WeakPool. Az InternPool tartalmaz egy gyenge medencét és ezen kívül egyetlen szinkronizált metódus van csak az az osztályban.

public class InternPool<T> {
  private final WeakPool<T> pool = new WeakPool<T>();
  public synchronized T intern(T object) {
    T res = pool.get(object);
    if (res == null) {
        pool.put(object);
        res = object;
    }
    return res;
  }
}

A metódus megnézi, hogy a keresett objektummal egyenlő objektum van-e a medencében, és ha benne van, akkor visszaadja azt, ami a medencében van. Ha nincs benne, akkor beleteszi, és úgy adja vissza a most már a medencében található objektumot.

Multi-thread

A metódusnak szinkronizáltnak kell lennie, hogy biztosítsuk, hogy a pool-ban való keresés és az új objektum belehelyezése atomi művelet legyen. A szinkronizálás nélkül előfordulhatna, hogy két szál is azt találja, hogy nincs az általuk keresettel egyenlő objektum a pool-ban, és mind a kettő berakja a saját verzióját, ami akkor gond, ha ezek egymással is egyenlőek. Ebben az esetben amelyik másodiknak rakja el az objektumot annak a változata lesz a pool-ban, az első viszont a sajátját használja, mint intern-ed verziót. A szinkronizálás megoldja ezt a problémát.

Versengés a szemétgyűjtővel

Annak ellenére, hogy az alkalmazás különböző szálai nem okozhatnak gondot, mivel gyenge referenciákat használunk végig kell gondolni, hogy nem akadhatunk-e össze a szemétgyűjtővel.

Előfordulhat, hogy a gyenge map-ból megkapjuk a gyenge referenciát az objektumunkhoz, de mire a következő utasításban erre meghívjuk a get() metódust addigra az már null-t ad vissza, mert közben az objektumot begyűjtötte a szemetes. Ez még akkor is lehetséges ha a gyenge map úgy van implementálva, hogy null-t adjon vissza ha olyan objektumra keresünk, amelyiket begyűjtött a szemetes. (Egyébként így van.)

Ebben az esetben azonban a WeakPool implementáció null-t ad vissza. Semmi probléma. Így az ezt használó InternPool sem küzd ilyen problémával.

Ha megnézzük azokat a megoldásokat, amelyeket a fent említett stackoverflow cikkben, adtak, az következő kódot láthatjuk:

public class InternPool<T> {

    private WeakHashMap<T, WeakReference<T>> pool = 
        new WeakHashMap<T, WeakReference<T>>();

    public synchronized T intern(T object) {
        T res = null;
        // (The loop is needed to deal with race
        // conditions where the GC runs while we are
        // accessing the 'pool' map or the 'ref' object.)
        do {
            WeakReference<T> ref = pool.get(object);
            if (ref == null) {
                ref = new WeakReference<T>(object);
                pool.put(object, ref);
                res = object;
            } else {
                res = ref.get();
            }
        } while (res == null);
        return res;
    }
}

Ebben a megoldásban a programozó egy végtelen ciklust használ annak az esetnek a kezelésére, amikor a szemetes az objektumot éppen az ahhoz való hozzáférés közben gyűjti be, de a map még visszaadta a gyenge referenciát. Nem valószínű, hogy a ciklus végtelen sokszor lefusson (enyhe irónia), sőt valószínűleg az esetek kozmikusan nagy százalékában teljesítmény gondot sem okoz. Ennek ellenére a kód összetett, nem triviális a megértése.

Tanulság: egy osztály, egy feladat. Bontsuk szét a feladatot alap műveletekre, és az esetek igen nagy százalékában egyszerűbb kódot kapunk.

Sajnos még a Google guava projektje is ezt a ciklusos algoritmust használja. Amint azt az eredeti cikk JCG szindikált megjelenésére írta SH:

Why would you even create a shockingly inferior interner to Guava’s, let alone publish it in an article??
https://code.google.com/p/guava-libraries/source/browse/guava/src/com/google/common/collect/Interners.java?r=cb72618c27773e8aa91238eca87b5e63f08b010f#66

Vagyis: “Minek készít valaki a Guava interner megoldásánál sokkolóan gyengébb megoldást, és még publikálja is?” (Válasz az JCG cikk után angolul.)

Érdemes megnézni a Guava kódot a fenti linken, lehet belőle tanulni, annak ellenére, hogy erős meggyőződésem: nincs szükség a ciklusra a megoldáshoz, és lehet olvashatóbban is kódolni. De feladat adott: ha valaki tudja, hogy miért kell mégis a végtelen ciklus, és miért nem jó az a megoldás, amit ebben a cikkben leírtam, írja meg!

Összefoglalás

Annak ellenére, hogy a Java a string konstansokat pool-ba rakja (ráadásul a permgen-be, amíg még van olyan),
és hasonlóképpen pool-ba rakja néhány primitív típus objektum megfelelőjét néha szükséges egyéb objektumok internálása is. Ezekben az esetekben az internálás nem automatikus, az alkalmazásnak kell azt megtennie. Ez a két osztály alkalmas arra, hogy ezt megtegyük, akár másolt tésztával (copy/pasta) akár repository függőséggel

        <dependency>
          <groupId>com.javax0</groupId>
          <artifactId>intern</artifactId>
          <version>1.0.0</version>
        </dependency>

a központi maven repo-ból. A könyvtár minimális, csak ezt a két osztályt tartalmazza és Apache licensszel használható (vagyis open source erősen). A forráskód a GitHub-on található.

Szavazás

After we managed to have a pool, now lets to have a poll! (Ezt nem tudom lefordítani.) Kérlek, hogy a következő kérdésre őszintén válaszolj:

Agilis, nem tervez!

Volt már erről szó, de most hallottam egy jó hasonlatot. Egy projektről mondta valaki, hogy olyan szintre fejlesztették a nem-tervezést, hogy építkezési hasonlattal élve ásattak velük egy gödröt 12 sprint alatt, majd az utolsó, 13. sprintben át kellett volna vinni a másik telekre. (A nyarakról nem is beszélve.)

A Java8 default metódus hazugsága

Mi az a default metódus?

A Java 8 megjelenésével lehetővé vált, hogy egy interfészt olyan módon módosítsunk új metódus beszúrásával, hogy az közben kompatibilis maradjon az őt implementáló osztályokkal. A Java 8 előtt ez nem volt lehetséges, mert ha egy új metódus került az interfészbe, akkor azt minden olyan osztálynak implementálnia kellett, amelyik az interfészt implementálta és maga nem volt absztrakt. Így ha egy interfészbe új metódus került egy library-ban, akkor csak úgy lehetett az új verzióra áttérni, hogy minden ilyen változást követni kellett a library-t használó kódokban. Ha tehát lett egy computeIfAbsent() metódus az interfészben, akkor azt az alkalmazásban az osztálynak is implementálnia kellett, még akkor is, ha semmit nem csinált vele, nem is használta semmilyen kód.

A Java 8 megjelenésével ez már nem szükséges. És mégis

Ha egy interfészhez egy default metódust adunk hozzá néhány, az interfészt implementáló osztály használhatatlanná válhat.

Először nézzük meg a default metódusok finomságait.

A Java 8-ban lehetőségünk van arra, hogy egy metódust az interfészben implementáljunk. (Statikus metódusok is lehetnek interfészekben, de ez most egy másik történet.) Azt a metódust, amit egy interfészben implementálunk ‘default’ metódusnak hívunk, és a default módosítóval jelöljük meg, jelezve, hogy nem absztrakt. (Amúgy maga a tény, hogy van törzse, és az argumentumot záró zárójele után nem pontosvessző jön eléggé kifejezi, hogy default metódussal van dolgunk, de a Java mindig is redundáns volt kicsit az olvashatóság kedvéért.) Az osztályok, amelyek implementálják ezt az interfészt öröklik a default metódusok megvalósítását is. Ha az osztálynak másképp van szüksége a metódusra semmi nem áll útjában, hogy felüldefiniálja (interfészben metódus nem lehet final).

Ha tehát van egy osztályunk, amelyik implementál egy interfészt, és ebbe az interfészbe belekerül később egy új default metódus az osztályunk továbbra is működőképes.

Többszörös öröklődés?

A dolog akkor kezd bonyolódni amikor a többszörös öröklődés kerül képbe. A Java mindig tartotta magát, hogy többszörös öröklődés csak definíciós szinten van, implementáció esetén nem lehet többszörös öröklődés (ellentétben a C++-szal, ahol van, és elég nagy görcsölés.) Mi történik akkor, ha egy osztály több (legyen az egyszerűség kedvéért kető) interface-t is implementál amelyek mindegyikében megtalálható az m() metódus. Ha mindegyik interface-ben abstrakt a metódus, akkor úgy mint az eddigi Java verziókban az osztálynak implementálnia kell a metódust. Ebben nincs változás. De mi van akkor ha pontosan egy interfész ad default implementációt? Nos a Java 8 válasza az, hogy ebben az esetben is implementálnia kell az osztálynak a metódust, különben nem fog lefordulni. Ezek után mondani sem kell, hogy ha több default implementációt is örököl az osztály, akkor ugyanez a helyzet.

Megjegyzés: Ha az interfészek a default implementációt úgy adják, hogy öröklődésen keresztül végül is ugyanaz az egy default implementáció jelenik meg az osztályban örököltként, akkor az nem probléma. Azaz ha az interfész A implementálja m()-t, majd B és C interfészek kiterjesztik A-t és a D osztály implementálja B és C-t, akkor örökli az m() implementációját is. Úgy érdemes fogalmazni, hogy egy osztály pontosan egy default implementációt tud örökölni, akár több leszármazási úton keresztül de csak akkor ha ugyanezt a metódust nem örökli absztaktként is.

Természetesen ha az osztálynak implementálnia kell a metódust, akkor az történhet magában az osztályban, de örökölheti az implementációt kiterjesztett osztályból is.

Még egy megjegyzés: Bármennyire is vonzó lehetne default implementációt adni például a toString() metódusra: nem lehet. Ami az Object-ből öröklődik, az érinthetetlen. És ez jobb így.

No de akkor most térjünk vissza az előző heti rövid poszthoz

Ha egy C osztály implementál egy I interfészt, és I-ben van egy absztrakt metódus, amit C nem implementál, akkor C nem fordul le, de futni azért még futhat.

valamint amit az előbb írtam, és amin esetleg átsiklott a tisztelt olvasó: “…ebben az esetben is implementálnia kell az osztálynak a metódust, különben nem fog lefordulni…“.

Nem fog lefordulni. De futni fog? Mondjuk ha már korábban lefordult olyan interfész verziókkal, amelyek nem okoztak gondot a fordítónak. A válasz az, hogy fog futni. Vagy nem. A Java 8 nem igazán konzisztens ebben az esetben. Amúgy meg van az oka, de ebbe nem érdemes belemenni, mert a release kijött, és mindenkinek meg volt a lehetősége beleszólni korábban. Ha pedig nem tette, most már késő. (Én sem tettem, de nem is értek annyira a Java-hoz meg a programozáshoz, hogy meg mertem volna tenni.)

  • Mondjuk van két interfészünk, és egy osztály, amelyik implementálja mind a két interfészt.
  • Az egyik interfész implementálja a default metódust m().
  • Az osztály és az interfészek lefordulnak.
  • Változtassuk meg az interfészt, amelyik nem tartalmazta eddig az m() metódust, hogy deklarálja azt absztraktként.
  • Fordítsuk le csak a módosított interfészt.
  • Futtassuk az osztályt.

default method multiple inheritance
Ebben az esetben az osztály fut. Nem tudjuk újra fordítani a módosított interfésszel, de mivel a régivel már le lett fordítva: fut. Most

  • módosítsuk az interfészt amelyikben az absztrakt metódus van, és készítsünk az m() metódushoz egy default implementációt.
  • Fordítsuk le csak a módosított interfészt.
  • Futtassuk az osztály: hiba!

Ha két olyan interfész is van az öröklődési láncban, amelyik default implementációt ad egy metódusra, akkor az implementáló osztályban nem lehet meghívni a metódust, ha az nincs explicite az osztályban vagy egy ősosztályban definiálva.

invalid multiple inharitance of default methods
Ugyanakkor a lefordított .class továbbra is kompatibilis. Be lehet tölteni, és el lehet indítani a futtatását mindaddig, míg nem történik hívás arra a metódusra, amelyet többszörösen definiálnak az interfészek.

Minta kód

directory structure of test

A fentiek demonstrálására készítettem, egy test könyvtárat a C.java osztály számára, és három alkönyvtárat a két I1.java és I2.java interfészek különböző verzióihoz. A base könyvtár tartalmazza azt az interfész verziót, amelyik jó a fordításhoz is, és a futtatáshoz is. I1 tartalmazza az m() metódust default implementációval. Az interfész I2 ebben a változatban nem tartalmaz semmilyen metódust.

Az osztály tartalmaz egy main metódust, így futtatható egyszerűen a tesztünk során. A teszt ellenőrzi, hogy van-e valamilyen parancssori argumentum, így később meg tudjuk hívni úgy is, hogy meghívja az m() metódust, és úgy is, hogy ne.

~/github/test$ cat C.java 
public class C implements I1, I2 {
  public static void main(String[] args) {
    C c = new C();
    if( args.length == 0 ){
      c.m();
    }
  }
}
~/github/test$ cat base/I1.java 
public interface I1 {
  default void m(){
    System.out.println("hello interface 1");
  }	
}
~/github/test$ cat base/I2.java 
public interface I2 {
}

Ezeket a programokat le tudjuk fordítani, és le is tudjuk futtatni:

~/github/test$ javac -cp .:base C.java
~/github/test$ java -cp .:base C
hello interface 1

A compatible könyvtár egy olyan verziót tartalmaz az I2 interfészből, amelyik deklarálja de nem implementálja az m() metódust. Az I1.java tartalma mindegyik könyvtárban ugyanaz.

~/github/test$ cat compatible/I2.java 

public interface I2 {
  void m();
}

This can not be used to compile the class C:

~/github/test$ javac -cp .:compatible C.java 
C.java:1: error: C is not abstract and does not override abstract method m() in I2
public class C implements I1, I2 {
       ^
1 error

A hibaüzenet nagyon precíz. Mivel van egy C.class fájlunk az előző fordításból ha lefordítjuk csak az interfészeket a compatible könyvtárban, akkor még mindig tudjuk futtatni az osztályt:

~/github/test$ javac compatible/I*.java
~/github/test$ java -cp .:compatible C
hello interface 1

A harmadik wrong nevű könyvtár az I2 olyan verzióját tartalmazza, amelyik definiálja az m() metódust:

~/github/test$ cat wrong/I2.java 
public interface I2 {
  default void m(){
    System.out.println("hello interface 2");
  }
}

Azt gondolom, hogy a C osztályt ezekkel az interfészekkel lefordítani megpróbálni sem kell. De ha lefordítjuk az interfészeket, és elindítjuk a kódot az fut mindaddig, míg meg nem akarja hívni a m() metódust. Ennek demonstrálására használjuk a parancssori argumentumot:

~/github/test$ javac wrong/*.java
~/github/test$ java -cp .:wrong C
Exception in thread "main" java.lang.IncompatibleClassChangeError: Conflicting default methods: I1.m I2.m
	at C.m(C.java)
	at C.main(C.java:5)
~/github/test$ java -cp .:wrong C x
~/github/test$

Összefoglalás

Ha most kezded el, kedves olvasó, Java 8 kompatibilisíteni a könyvtáradat, és default metódusokat adsz az interfészekhez, akkor valószínűleg ez nem fog problémát okozni. Legalábbis ebben reménykedtek a JDK8 fejlesztő is, amikor funkcionális metódusokat adtak például a gyűjtemény interfészekhez. Az alkalmazások a Java 7 vagy korábbi implementációkon nyugszanak, és ezekben nincsenek default metódusok, így ha nem akarják újrafordítani az alkalmazásokat, akkor futni fognak még ütközés esetén is. De ha több olyan library is van, amelyik már Java 8 “kompatibilissé” lett téve, akkor azért van esély, hogy két metódus neve, szignatúrája ugyanaz lesz. Hogyan kell elkerülni?

Tervezd meg a library API-t előre. Ne számíts rá, hogy majd a default metódus megoldja később a gondokat ha ki kell egészíteni az API-t. A default metódusok csak az utolsó menedék. Gondosan válaszd meg a metódus neveket. Aztán majd idővel megtanuljuk, hogy a default metódusokat hogyan lehet és érdemes használni Java-ban.

Követem

Értesítést küldünk minden új bejegyzésről a megadott e-mail címre.

Csatlakozz a 962 követőhöz