tifyty

pure Java, what else ?

Havi archívumok: szeptember 2012

Hamis ébresztés

Amikor wait-et használunk, azt mindig ciklusban kell tennünk. Valahogy így:

Ez egy pszeudo kód

while( someConditionIsNotMet ){
  wait();
  if( conditionIsMet ){
    doSomething();
    }
  }

De miért?


Megjegyzés: amikor wait-et használunk, akkor mielőtt nekilátnánk, előtte erősen elgondolkozunk azon, hogy miért is tesszük ezt. Nem lehet, hogy éppen keretrendszert fejlesztek? Mélyen magamba nézek, és elolvasom Viczián István cikkét, minden szava arany. És abbahagyom a keretrendszer írását.

Ez a blog azonban pont olyan dolgokkal foglalkozik, amik ilyen “érdekes” Java technológiák. Ez pedig nem jelenti azt, hogy ezeket az eszközöket használni kell, de érdemes ismerni őket, azért, hogy ha egyszer szembejönnek, akkor ne érjen meglepetés. Végül is, a Java open source és néha előfordul, hogy debuggolás közben le kell ásni akár egy keretrendszer mélyére is. Néha. És akkor jó, ha legalább esélyünk van, hogy megértsük mi történik ott a mélyben.

Az egyik mondás az, hogy azért, mert a notifyAll() meghívása esetén minden olyan szál felébred, amelyik ebben a wait()-ben van, és futtatható, de nem feltétlenül futó állapotba kerül. Egész pontosan egy szál kerül futó állapotba, hiszen az ébredés után meg kell szereznie a futásához azt a monitort, amelyik a wait végrehajtásakor megvolt. A futó szál elvégzi amit kell, és utána a someConditionIsNotMet már lehet ismét true a második, sokadik szálnak nem feltétlenül marad mit tennie.

Ez logikus, (nem én találtam ki, coderanch-on írták ezt a marhaságot) de sajnos félrevezető. Lehetne ez is az ok, ám ha ez lenne az ok, akkor a notifyAll() száműzésével a probléma megoldódott. Ha csak egy notify() hívás történik, akkor csak az egyik szál ébred fel, elvégzi a dolgát, majd alszik tovább, vagy kiszáll, bent marad, teszi, amit akar, vagy pontosabban amit a programozó leprogramozott (és nem azt amit a programozó akart, mert a kettő nem mindig ugyan az 🙂 ).

A valóság az, hogy azért kell a ciklus, mert egy wait() felébredhet úgy is, hogy egyetlen egy notify() vagy notifyAll() sem történt a rendszerben, sőt a szál interruptot sem kapott. (Az interrupt egy másik történet, mot abba nem megyünk bele.) A Java szabvány az 566. oldalon azt mondja, hogy

Implementations are permitted, although not encouraged, to perform “spurious wake-ups”, that is, to remove threads from wait sets and thus enable resumption without explicit instructions to do so.

Azaz, az implementációk (java környezetek) számára megengedett, bár nem javasolt, hogy “hamis ébresztést” hajtsanak végre, azaz kivegyék az alvó szálat a várakozók közül, és engedélyezzék a folytatást anélkül, hogy ilyen utasítást kaptak volna.

Ezzel véget is érhetne a történet ha a programozók katonák lennének, és parancsot hajtanának végre: ez van, így kell, kész. Végrehajtani. Minden wait() ciklusba kerül és kész.

A programozó (a jó programozó) azonban nem ilyen. Sokkal inkább hasonlít egy három éves gyerekre, aki mindenre azt kérdezi “miért?”. Miért megy az autó? Mert a belső égésű motor… Megérti ezt egy gyerek? Nem. Akkor mit mond a szülő? Vagy azt, hogy fogd be a szádat édes fiam/lányom, és ne kérdezzél, mert attól csak dühös lesz az apád, vagy kitalál valamit. (Szerintem mind a kettő rossz, azt kell elmondani ami igaz, és meg fogsz lepődni, hogy milyen sokat megért belőle.) A kitalálás az itt is megy. Spurious wakeup. Mi az, miért van? Mert a kis manók, azok… Aha!

Az egyik ilyen történet az volt, hogy a processz kaphat egy signal-t. A másik, hogy egyes processzor megvalósításokon elég költséges (idő, memória whatever) lenne ennek az elkerülése. Na ez utóbbit nem tudom ellenőrizni, ám a signal-t igen! És akkor most jöjjön a recept: végy egy egyosztályos programot:

package tifyty;

public class App {

	private synchronized void  run() {

		try {
			wait();
			System.out.println("Woke up");
		} catch (InterruptedException e) {
			System.out.println("Interrupted.");
		}
	}

	public static void main(String[] args) {
		new App().run();
	}
}

(Ez most tényleges copy/paste, minden egyes sor.) Fordítsd le maven-nel:

$ mvn clean install
[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building spurious 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- maven-clean-plugin:2.4.1:clean (default-clean) @ spurious ---
[INFO] Deleting /Users/verhasp/blogcode/spurious/target
[INFO] 
[INFO] --- maven-resources-plugin:2.4.3:resources (default-resources) @ spurious ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /Users/verhasp/blogcode/spurious/src/main/resources
[INFO] 
[INFO] --- maven-compiler-plugin:2.3.2:compile (default-compile) @ spurious ---
[INFO] Compiling 1 source file to /Users/verhasp/blogcode/spurious/target/classes
[INFO] 
[INFO] --- maven-resources-plugin:2.4.3:testResources (default-testResources) @ spurious ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /Users/verhasp/blogcode/spurious/src/test/resources
[INFO] 
[INFO] --- maven-compiler-plugin:2.3.2:testCompile (default-testCompile) @ spurious ---
[INFO] Compiling 1 source file to /Users/verhasp/blogcode/spurious/target/test-classes
[INFO] 
[INFO] --- maven-surefire-plugin:2.7.2:test (default-test) @ spurious ---
[INFO] Surefire report directory: /Users/verhasp/blogcode/spurious/target/surefire-reports

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running tifyty.AppTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.02 sec

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO] 
[INFO] --- maven-jar-plugin:2.3.1:jar (default-jar) @ spurious ---
[INFO] Building jar: /Users/verhasp/blogcode/spurious/target/spurious-1.0-SNAPSHOT.jar
[INFO] 
[INFO] --- maven-install-plugin:2.3.1:install (default-install) @ spurious ---
[INFO] Installing /Users/verhasp/blogcode/spurious/target/spurious-1.0-SNAPSHOT.jar to /Users/verhasp/.m2/repository/tifyty/spurious/1.0-SNAPSHOT/spurious-1.0-SNAPSHOT.jar
[INFO] Installing /Users/verhasp/blogcode/spurious/pom.xml to /Users/verhasp/.m2/repository/tifyty/spurious/1.0-SNAPSHOT/spurious-1.0-SNAPSHOT.pom
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 5.156s
[INFO] Finished at: Wed Aug 15 09:30:57 CEST 2012
[INFO] Final Memory: 15M/138M
[INFO] ------------------------------------------------------------------------

Utána indítsd el a programot,

$ java -cp target/spurious-1.0-SNAPSHOT.jar tifyty.App

és egy másik terminál ablakból küldj neki signal-okat:

$ kill -1 `jps |grep App|awk '{print $1;}'`

És a program kilép. Még lehet neki küldeni más signal-okat is. Ezek közül

$ kill -3 `jps |grep App|awk '{print $1;}'`

esetén a program nem áll le, viszont érdekes dolgokat ír ki:

2012-08-15 09:38:52
Full thread dump Java HotSpot(TM) 64-Bit Server VM (23.0-b21 mixed mode):

"Service Thread" daemon prio=5 tid=0x00007f904984f000 nid=0x1162ef000 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" daemon prio=5 tid=0x00007f904984e000 nid=0x1161ec000 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" daemon prio=5 tid=0x00007f904984c800 nid=0x1160e9000 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" daemon prio=5 tid=0x00007f9049849800 nid=0x115fe6000 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" daemon prio=5 tid=0x00007f904905c000 nid=0x115cde000 in Object.wait() [0x0000000115cdd000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x00000007e4e057f0> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:135)
	- locked <0x00000007e4e057f0> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:151)
	at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:177)

"Reference Handler" daemon prio=5 tid=0x00007f904905b000 nid=0x115bdb000 in Object.wait() [0x0000000115bda000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x00000007e4e05370> (a java.lang.ref.Reference$Lock)
	at java.lang.Object.wait(Object.java:503)
	at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:133)
	- locked <0x00000007e4e05370> (a java.lang.ref.Reference$Lock)

"main" prio=5 tid=0x00007f9049800800 nid=0x10e0a1000 in Object.wait() [0x000000010e0a0000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x00000007e4eb3708> (a tifyty.App)
	at java.lang.Object.wait(Object.java:503)
	at tifyty.App.run(App.java:12)
	- locked <0x00000007e4eb3708> (a tifyty.App)
	at tifyty.App.main(App.java:20)

"VM Thread" prio=5 tid=0x00007f904982f800 nid=0x115ad8000 runnable 

"GC task thread#0 (ParallelGC)" prio=5 tid=0x00007f904980d800 nid=0x1116f4000 runnable 

"GC task thread#1 (ParallelGC)" prio=5 tid=0x00007f904980e000 nid=0x1117f7000 runnable 

"VM Periodic Task Thread" prio=5 tid=0x00007f9049837800 nid=0x1163f2000 waiting on condition 

JNI global references: 117

Heap
 PSYoungGen      total 24320K, used 858K [0x00000007e4e00000, 0x00000007e6920000, 0x0000000800000000)
  eden space 20864K, 4% used [0x00000007e4e00000,0x00000007e4ed6b60,0x00000007e6260000)
  from space 3456K, 0% used [0x00000007e65c0000,0x00000007e65c0000,0x00000007e6920000)
  to   space 3456K, 0% used [0x00000007e6260000,0x00000007e6260000,0x00000007e65c0000)
 ParOldGen       total 55552K, used 0K [0x00000007aea00000, 0x00000007b2040000, 0x00000007e4e00000)
  object space 55552K, 0% used [0x00000007aea00000,0x00000007aea00000,0x00000007b2040000)
 PSPermGen       total 21248K, used 2692K [0x00000007a9800000, 0x00000007aacc0000, 0x00000007aea00000)
  object space 21248K, 12% used [0x00000007a9800000,0x00000007a9aa10d0,0x00000007aacc0000)

Ezzel tehát lehet használni futó programból információk kinyerni? Igen, lehet, dokumentált lehetőség.

$ kill -4 `jps |grep App|awk '{print $1;}'`

esetén a program

#
# A fatal error has been detected by the Java Runtime Environment:
#
#  SIGILL (0x4) at pc=0x00007fff939e667a, pid=928, tid=140735251044704
#
# JRE version: 7.0_04-b21
# Java VM: Java HotSpot(TM) 64-Bit Server VM (23.0-b21 mixed mode bsd-amd64 compressed oops)
# Problematic frame:
# C  [libsystem_kernel.dylib+0x1567a]  mach_msg_trap+0xa
#
# Failed to write core dump. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again
#
# An error report file with more information is saved as:
# /Users/verhasp/blogcode/spurious/hs_err_pid928.log
#
# If you would like to submit a bug report, please visit:
#   http://bugreport.sun.com/bugreport/crash.jsp
#
Abort trap: 6

ezt írja ki a konzolra. Ez is érdekes. A hs_err_pid928.log fájl tartalmát nem másolom ide. 🙂

Ki lehet próbálgatni a többi signal-t is. Én megtettem. Egyik esetben sem írta ki, hogy Woke up.

Konklúzió

Továbbra sem tudjuk, hogy miért ébredhet fel egy szál notify vagy intterupt nélkül, de nem az operációs rendszer szintű signal az, ami miatt. De felébredhet, mert a szabvány ezt mondja, és a szabvány, az szabvány.

Zártosztály

Java-ban nem programoznak bolondok, ezért ott ismeretlen fogalom a zártosztály (closure). Bár ha a Java programozás és a későbbi megbolondulás közötti korrelációt nézzük…

Ellenben vannak olyan osztályok, amik másik osztályon belül vannak. Sőt nem csak osztályon, hanem osztály metóduson belül is lehetnek további belső osztályok. Például a következő:

public class WhyFinal {
	interface Outer {
		String method();
	}

	public Outer method(final String s) {
		class Inner implements Outer {
			public String method() {
				return s + " wonka";
			}
		}
		return new Inner();
	}

	public static void main(String[] args) {
		System.out.println(new WhyFinal().method("bonka").method());
	}
}

Ami ebben a posztban igazából érdekel engem az az, hogy miért kell a final kulcsszó a method metódus String argumentuma elé.

Ez az s nevű változó különleges, nem is nagyon látunk máshol hasonlót Java-ban. Elérhető az Inner osztályon belülről, pedig az osztályban nincs deklarálva. Ez már önmagában is különleges, de még inkább az, hogy ez a változó megszűnik, amikor a method() visszatér. Azonban a létrehozott Inner példány tovább él, és eléri ezt a változót. Ami már megszűnt. Vagy nem?

Valójában nem ez történik. A fenti program a következő programmal egyenértékű:

public class WhyFinal {
	interface Outer {
		String method();
	}

	public Outer method(final String s) {
		class Inner implements Outer {
			private String s;
			public Inner(String s){
				this.s = s;
			}
			public String method() {
				return s + " wonka";
			}
		}
		return new Inner(s);
	}

	public static void main(String[] args) {
		System.out.println(new WhyFinal().method("bonka").method());
	}
}

Itt nem a külső method metódus lokális változójára hivatkozunk a belső osztályban, hanem explicit módon átadjuk az s változóértékét a konstruktorban, amelyik azt jól elteszi egy mezőbe, és onnan kezdve azt használja a program.

Ebben az esetben final kulcsszót akár ki is törölhetjük a method(final String s) argumentumból, hiszen explicit módon megmondtuk, hogy amikor létrehozzuk az Inner objektumot, akkor adjuk át az s értékét, és ha utána változik az s ahhoz az Inner példánynak semmi köze. Ha azonban az eredeti példánál maradunk, akkor ez a paraméter átadás nincs explicite kiírva, és annak ellenére, hogy valójában ez történik, a kód úgy néz ki, mintha a belső osztályunk a metódus lokális s változóját használná.

Nézzük meg az eredeti, tehát default Inner konstruktoros példa bájt kódját:

class WhyFinal$1Inner implements WhyFinal$Outer
  SourceFile: "WhyFinal.java"
  EnclosingMethod: #27.#28                // WhyFinal.method
  InnerClasses:
       #37= #9; //Inner=class WhyFinal$1Inner
       static #41= #11 of #27; //Outer=class WhyFinal$Outer of class WhyFinal
  minor version: 0
  major version: 51
  flags: ACC_SUPER
Constant pool:
   #1 = Fieldref           #9.#29         //  WhyFinal$1Inner.this$0:LWhyFinal;
   #2 = Fieldref           #9.#30         //  WhyFinal$1Inner.val$s:Ljava/lang/String;
   #3 = Methodref          #10.#31        //  java/lang/Object."<init>":()V
   #4 = Class              #32            //  java/lang/StringBuilder
   #5 = Methodref          #4.#31         //  java/lang/StringBuilder."<init>":()V
   #6 = Methodref          #4.#33         //  java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #7 = String             #34            //   wonka
   #8 = Methodref          #4.#35         //  java/lang/StringBuilder.toString:()Ljava/lang/String;
   #9 = Class              #36            //  WhyFinal$1Inner
  #10 = Class              #39            //  java/lang/Object
  #11 = Class              #40            //  WhyFinal$Outer
  #12 = Utf8               val$s
  #13 = Utf8               Ljava/lang/String;
  #14 = Utf8               this$0
  #15 = Utf8               LWhyFinal;
  #16 = Utf8               <init>
  #17 = Utf8               (LWhyFinal;Ljava/lang/String;)V
  #18 = Utf8               Code
  #19 = Utf8               LineNumberTable
  #20 = Utf8               Signature
  #21 = Utf8               ()V
  #22 = Utf8               method
  #23 = Utf8               ()Ljava/lang/String;
  #24 = Utf8               SourceFile
  #25 = Utf8               WhyFinal.java
  #26 = Utf8               EnclosingMethod
  #27 = Class              #42            //  WhyFinal
  #28 = NameAndType        #22:#43        //  method:(Ljava/lang/String;)LWhyFinal$Outer;
  #29 = NameAndType        #14:#15        //  this$0:LWhyFinal;
  #30 = NameAndType        #12:#13        //  val$s:Ljava/lang/String;
  #31 = NameAndType        #16:#21        //  "<init>":()V
  #32 = Utf8               java/lang/StringBuilder
  #33 = NameAndType        #44:#45        //  append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #34 = Utf8                wonka
  #35 = NameAndType        #46:#23        //  toString:()Ljava/lang/String;
  #36 = Utf8               WhyFinal$1Inner
  #37 = Utf8               Inner
  #38 = Utf8               InnerClasses
  #39 = Utf8               java/lang/Object
  #40 = Utf8               WhyFinal$Outer
  #41 = Utf8               Outer
  #42 = Utf8               WhyFinal
  #43 = Utf8               (Ljava/lang/String;)LWhyFinal$Outer;
  #44 = Utf8               append
  #45 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #46 = Utf8               toString
{
  final java.lang.String val$s;
    flags: ACC_FINAL, ACC_SYNTHETIC


  final WhyFinal this$0;
    flags: ACC_FINAL, ACC_SYNTHETIC


  WhyFinal$1Inner();
    flags: 
    Code:
      stack=2, locals=3, args_size=3
         0: aload_0       
         1: aload_1       
         2: putfield      #1                  // Field this$0:LWhyFinal;
         5: aload_0       
         6: aload_2       
         7: putfield      #2                  // Field val$s:Ljava/lang/String;
        10: aload_0       
        11: invokespecial #3                  // Method java/lang/Object."<init>":()V
        14: return        
      LineNumberTable:
        line 7: 0
    Signature: #21                          // ()V

  public java.lang.String method();
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: new           #4                  // class java/lang/StringBuilder
         3: dup           
         4: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
         7: aload_0       
         8: getfield      #2                  // Field val$s:Ljava/lang/String;
        11: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        14: ldc           #7                  // String  wonka
        16: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        19: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        22: areturn       
      LineNumberTable:
        line 9: 0
}

Amit érdemes megnézni, az az, hogy a 12-es sorban deklarál a kód egy WhyFinal$1Inner.val$s nevű és String típusú változót. A belső osztály ezt fogja használni, és nem a külső method
lokális a változóját.

A következő a 70. sornál kezdődik. Itt az Inner konstruktora, amelyik ugye default konstruktor, és ezért nincs argumentuma, két argumentumot is elpakol mezőkbe, amiket szintén nem deklaráltunk. Az első a külső osztály “this” értéke, azaz maga a külső osztály, hiszen az Inner nem statikus, tehát mindig meg kell, hogy legyen a WhyFinal példány, ami őt létrehozta, és el is érhető Java szintről a WhyFinal.this hivatkozással. A másik pedig az s amit a WhyFinal$1Inner.val$s névvel illet a fordító.

Amikor pedig a 91-es sorban az s értékéhez akar hozzáférni a program, akkor ehhez a változóhoz nyúl, hiszen a lokális változó ekkor már régen a szemétgyűjtőé.

Szeretjük-e az import static-ot?

Az 1.5 Java-val együtt jött be az import static lehetősége. Aki nem tudná, hogy ez micsoda, annak számára elmesélem, hogy ezzel más osztályokból static metódusokat, mezőket lehet beimportálni az osztályunkba és ott úgy lehet ezeket használni, mintha a saját osztályunkban lettek volna megírva.

Persze valami hasonló volt korábban is, hiszen 1.5 előtt is volt olyan, hogy meghívtunk egy metódust, és hiába kereste a vi editor, hogy hol is van az a metódus, csak nem találta az éppen nyitott fájlban: azért, mert örököltük a metódust, vagy mert absztrakt az osztályunk, és csak egy leszármazott fogja implementálni azt a metódust.

Az import static pedig formailag (hangsúlyozom, hogy csak formailag) más, tök idegen osztályokból “származtat” át statikus metódusokat, mezőket a mi osztályunkba. Innen kezdve aztán már gondolkodhatunk, hogy egy metódus, amit nem talál a vi az örökölt, absztrakt, vagy statikus import. Ez olvashatatlanná teszi a kódot és ezért nem szeretjük az import static lehetőségét.

Na de hát ki programoz ma már vi-ban? Maximum Dénes, de már ő is belátta, hogy a vi elavult, és áttért emacs-ra. Eclipse-t, Netbeans-t használunk, és ha tudni akarjuk, hogy egy metódus hol is van definiálva, akkor csak lenyomjuk a Cmd gombot, és az egérkurzor máris átváltozik, a mezőnév aktív link lesz, egy kattintás, és már ott is vagyunk a metódus definíciójánál, vagy valamelyik implementációjánál, amit éppen szeretnénk. Az előbbi indok tehát nem állja meg a helyét. Ugyanakkor nézzük meg a következő kódot:

    @Test
    public void testGetConfigValueWhenEnvironmentKeyExists() {
        new Business() {
            private String actual;
            private Properties props;

            @Override
            public void given() {
                props = PowerMockito.mock(Properties.class);
                PowerMockito.mockStatic(System.class);
                config.setConfigProperties(props);
                Mockito.when(props.containsKey(key)).thenReturn(true);
                Mockito.when(props.getProperty(key)).thenReturn(badValue);
                Mockito.when(System.getenv("sb4j." + key)).thenReturn(value);
            }

            @Override
            public void when() {
                actual = config.getConfigValue(key);
            }

            @Override
            public void then() {
                Mockito.verify(props).containsKey(key);
                Mockito.verify(props).getProperty(key);
                PowerMockito.verifyStatic();
                System.getenv("sb4j." + key);
                Assert.assertEquals(value, actual);
            }

        }.execute();
    }

Nem lenne sokkal szebb, ha az elejére beszúrnánk némi import static utasítást, és akkor már így nézne ki:

    @Test
    public void testGetConfigValueWhenEnvironmentKeyExists() {
        new Business() {
            private String actual;
            private Properties props;

            @Override
            public void given() {
                props = mock(Properties.class);
                mockStatic(System.class);
                config.setConfigProperties(props);
                when(props.containsKey(key)).thenReturn(true);
                when(props.getProperty(key)).thenReturn(badValue);
                when(System.getenv("sb4j." + key)).thenReturn(value);
            }

            @Override
            public void when() {
                actual = config.getConfigValue(key);
            }

            @Override
            public void then() {
                verify(props).containsKey(key);
                verify(props).getProperty(key);
                verifyStatic();
                System.getenv("sb4j." + key);
                assertEquals(value, actual);
            }

        }.execute();
    }

Az biztos, hogy rövidebb. De lehetne még rövidebb is, ha még a System class getenv metódusát is statikusan importálom. Hopsz… akkor viszont már nem működik az egész. (Most itt nem fejteném ki, hogy miért, el kell olvasni a How to mock the system class cikket, abból kiderül.)

Szóval ebben a speciális esetben tesztelhetetlenné teszi a kódot a statikus import, de legalábbis megakadályozza azt a fajta mókolást, amit az említett cikkben leírtam.

Hogy szeretjük-e vagy nem szeretjük a statikus importot nem egzakt kérdés. Nem olyan, hogy működik, vagy nem működik. Nem lehet eldönteni a kérdést, hogy jó-e vagy sem. Azt lehetne csinálni, amit TAO-ban: mérni rettentően sok programozó munkáját, és megnézni, hogy hol keletkezik jobb kód. Mi a jobb kód? Hol lesz kevesebb bug. Vagy a hibák száma nem is számít, inkább az, hogy mennyi óra kijavítani őket. De vajon lassabban dolgoznak-e akik használnak statikus importot? És kit mérjünk? Csak open source projekteket? Vagy céges fejlesztéseket is? Azt elég nehéz lesz. Akiknek mostanában dolgozom legszívesebben azt szeretnék ha azt is eltitkolnám, hogy ez alatt az idő alatt létezem. (Néha úgy érzem magam, mint egy Ben Affleck, csak nincs közben mellettem egy Uma Thurman. Nah, de felejtsük is el ezt a témát.) Mennyivel egyszerűbb a TAO. Ott minden egyes kezelésről készül hibajegy, és csak nagyon ritkán fordul elő, hogy ha valaki meghal, akkor ezt az eseményt ne vinnék fel a ticketing rendszerbe. Cserébe viszont nem működik a jó öreg control-alt-del és újraindítás.

get(Object) miért?

Késő nyári este volt, amikor a forróság már alábbhagyott, az árnyékok is hosszabbak lettek. A birkák is felkeltek, hogy a legelőről átvonulva éjjeli szálláshelyükre menjenek, ahol az éjszakai hideget összebújva vészelhetik át. Pásztoraik nem nagyon figyeltek rájuk, arra ott voltak a kutyák, meg aztán ők is érezték, hogy jön be a hideg, hát az éjszakai karámok mellett a szokott helyen tüzet raktak, főztek, és régmúlt időkről beszélgettek.

  • Én azt nem értettem soha – kezdte a beszéd fonalát az egyikük — hogy amikor bejött a Java 1.5 verzió, akkor a kollekcióknál, például a Map interfészen miért maradt a get metódus argumentuma Object. Amikor az lehetett volna generikus. Voltak olyan hibák, amikor refaktor után ez hagyott bent hibát. Azt hittem, hogy a fordító majd mindenhol kiabálni fog, ahol nem megfelelő az argumentum típusa, de nem kiabált.

Hallgattak egy sort. Jó volt megrágni az emlékeket, és minden egyes mondatot. Itt Nepálban lassan telt az idő, nem úgy, mint régen, a mikor még programozóként naponta jöttek az új patch-ek, release-k és sprintekben loholtak a megrendelő kívánságait kielégíteni. Itt ilyen nem volt, ezért is jöttek ide. Mégis jó volt elmerengeni a régi időkön.

  • Hát én azt gondolom – mondta egy másik -, hogy azért nem jelzett a fordító hibát, mert az argumentum típusa megfelelő volt. Object. Hiszen az volt a runtime specifikációban. Ha jól emlékszem.
  • Persze, az volt! – fortyant fel az első, bár ez a felfortyanás is aludttej volt a hajdani felfortyanások forralt borához képest – De éppen azt kérdem, hogy miért volt Object? Hülyék voltak az ORACLE programozói?
  • Az akkor még SUN volt. – bökte közbe egy harmadik, de csak egy pillanatra akasztotta meg a párbeszédet, nem törődtek vele.
  • Nem voltak hülyék. Olyan szépen összetették azt a nyelvet. Még ma is a karám ajtó vezérlése Java-ban megy!

Erre néhányan elkezdtek kuncogni, mert eszükbe jutott, hogy minap a karámajtó nem akart kinyílni és ki kellett várni egy teljes garbage collection ciklust, mire ki lehetett engedni a birkákat.

  • Hát ha nem voltak hülyék, akkor bizonyára volt logikus magyarázat, és ha volt, akkor az meg van ma is. A jövő változik, a történelmet meg lehet változtatni de a tények azok makacs dolgok, azok lassan változnak. Ha nekik volt racionális okuk, akkor arra mi is rájöhetünk. – mondta a legöregebb, akit a csapat vezetőjeként viselkedett. Nem nevezték ugyan ki, de ha valamit mondott, akkor ha nem is értettek mind vele egyet többnyire, nem mindig, de kiderült, hogy neki volt igaza. Ő volt nem hivatalosan a birkanyáj architekt. – Most pedig aludjunk! Majd a kutyák vigyáznak ránk!

Ez volt mindig az utolsó szó elalvás előtt. Amikor ez elhangzott utána már nem volt több szó, csak szuszogás a híg levegőben, ahogy a tüdők próbálták az alvó testet ellátni oxigénnel.

Másnap este ismét beszélgettek a tűz körül.

  • Azt hiszem, rájöttem mi a válasz a tegnapi kérdésre. – kezdte megint a beszédet aki tegnap is.

Hümmögtek, és a birka pörkölttek foglalkoztak, amit még tegnap főztek, és közelebb húzódtak a tűzhöz. Ma nem kellett főzni, csak kicsit megmelegíteni ami tegnapról maradt, így kisebb tűz is elég volt, kevesebb fa. A fa ritka volt ilyen magasan, és ezért optimalizálni kellett az erőforrások felhasználását.

  • Szóval az van, hogy ott van a Map. Mondjuk belerakunk valamit. Ahogy a specifikáció mondja put(K key, V value), generikus típusok. Viszont nem biztos, hogy amikor ki akarom venni az értéket, akkor ugyanolyan lesz a key objektumom típusa. Lehet például a K leszármazottja. Ezért a get argumentuma Object.

Ettek egy sort, néhányan egymásra néztek, mint akik értik egymás gondolatait is, majd az egyikük megadta az aznapi választ:

  • Akkor lehetne get(Z key) ahol a Z extends K generikus. Szóval, asszem, … ezen még dolgozzá! Mi meg aludjunk, a kutyák meg…

Nem fejezte be a mondatot. Aznap éjjel az egyik volt programozó birkapásztor csak sokára aludt el.

Másnap valahogy nem ment úgy a birkaterelés, mint máskor. Sok hibát vétettek, és ezt még a kutyák is észrevették. Nekik általában nem volt sok dolguk, csak akkor, amikor a pásztorok hibáztak. Ilyenkor úgy tettek, mintha visszaterelnék a birkát a nyájhoz, de igazából a pásztor volt az akinek ki kellett javítania a hibát, ők csak jeleztek. Szorgalmas, folyamatos figyelést igénylő, unalmas munka.

Este amikor összegyűltek megint nagy lángot raktak, mert bár fogytán volt a fa, de nagy hideg volt várható, és nem akartak megfagyni. Ha olyanok az igények, akkor alá kell tenni az erőforrást, másként ez nem megy. Újabb adagot is kellett főzni, mert napközben a napra rendelt adaggal meglépett az egyik kutya, amelyik pásztornak képzelte magát.

  • Sokat gondolkodtam napközben. – kezdte megint a programozó, az a bizonyos, de a többiek erre elkezdtek kuncogni, és az egyikük közbe is vetette:
  • Ne gondolkodj Woyczek fiam, az nem tesz jót!

Ezen többen nevettek, bár többnyire nem értették, hogy miért hívta Woyczek-nek a társukat az öreg, hiszen nem az volt a neve, de mindez nem számított, nem is kérdezték meg.

  • Szóval gondolkodtam, és arra jutottam, hogy a nem generikus get meg contains definíciójában kell keresni az okot. És eszembe jutott, hogy a javadoc dokumentáció azt írta, hogy akkor ad vissza egy értéket a get ha a paramétere egyenlő az equals szerint azzal a kulccsal amivel beraktuk az értéket a map-be. És hát az equals pedig Object-et vár, nem is várhat mást, hiszen két objektum egyenlő lehet egymással akkor is, ha az osztályai közül egyik sem leszármazottja a másiknak. Nem jól gondolom?

Hümmögtek. Ez most már nem sokat számított. Olyan szavak kavarogtak a fejükben, mint javadoc, meg equals. Milyen jó volt ezeket a szavakat újra hallani. Végül megszólalt az öreg:

  • Örülök neki, hogy sikerült végiggondolnod. Remélem, hogy ezzel elégedett is leszel, és holnap kevesebb lesz a bug a terelés közben. – Ezen megint sokan kuncogtak. – Most meg aludjuk. A kutyák meg…