Sproglige udvidelser i Java SE 7

Closures kommer i Java 7! Nej, desværre. Heller ikke i denne omgang, men Java SE 7 byder på andre brugbare udvidelser og syntaktisk sukker. Læs mere om de sproglige finurligheder du risikerer at møde i fremtidige Javaprogrammer.
Java SE 7 blev tilgængelig her i sommerferien og byder på mange små og store udvidelser. Denne artikel fokuserer på de sproglige udvidelser, hvoraf flere er både brugbare og elegante. Foreløbig har jeg afprøvet Early Access versionen som i skrivende stund ikke virker med Eclipse, men understøttes af NetBeans.

Hvad kan vi så glæde os til? Der er ikke tale om de store omvæltninger, som vi så i Java 5, men der er nogle lækre nye features og lidt syntaktisk sukker

Strings i switch

Switch blev med Java 5 udvidet til at kunne bruge enums og nu kommer endnu en udvidelse, nemlig Strings i switch:

String s = "Java";
switch(s){
case "jello":
  System.out.println("case 1"); break;
case "java":
  System.out.println("case 2"); break;
case "Java":
  System.out.println("case 3"); break;
default : System.out.println("default");}

Det er jo en fin lille feature. Man skal blot være opmærksom på, at der er nogle begrænsninger. Case-strengene skal, som altid i en switch, være konstante, dvs kunne evalueres på kompiletime. Det betyder, at man ikke kan umiddelbart kan bruge det til at sammenligne to variables streng-værdi:

String s = "Java";
String s2 = "Java";
switch(s){
case "jello": System.out.println("case 1"); break;
case "java": System.out.println("case 2"); break;
case s2: System.out.println("case 3"); break;
default : System.out.println("default");}

Her vil udtrykket ”case s2” ikke kompilere, med mindre man gør variablen s2 final (samme opførsel som med ints i switch.)

Det er heller ikke muligt at teste for null a la

switch(s){
case "jello": System.out.println("case 1"); break;
case "java": System.out.println("case 2"); break;
case null: System.out.println("null");
default : System.out.println("default");
}

hvilket heller ikke er unikt for String-switch, men allerede er defineret i specifikationen for switches. Hvis streng-variablen evalueres til null  kastes en NullPointerException. Samme opførsel kan man opleve med unboxing og ved enums.

Binære litteraler

Tidligere har vi kunnet angive, at et tal var en hexadecimal med præfixet 0x/0X. Nu får vi endnu et præfix, nemlig 0b/0B, der angiver at et tal er binært. Fx

int binær = 0b1010;
int binær2 = 0B1010;

Hvilket så vil repræsentere tallet 10 (10-talssystem).

Underscore i tal

Underscore i tal er ren syntastisk sukker, som ikke ændrer spor på semantikken i vores program. De er introduceret for at gøre større tal lettere at læse.

Fra Java 7 har vi lov at skrive

int i = 12_000;
double d = 12_000.5;

Ovenstående tal er stadig 12000 og 12000,5, men da man normalt ikke kan lave separatorer i kildekoden (a la 12.000/12,500 eller 12.000,5/12,500.5) har man introduceret underscore som separator.

Selv om jeg godt kan se problemet med de manglende separatorer, synes jeg ikke, at underscore er den mest elegante løsning. Det syntaktiske sukker bliver efter min mening lidt for syntetisk her, men det er måske en tilvænnet smag og muligvis anvendeligt for større tal.

Det er imidlertid et problem, at parsemetoderne (Double.parseDouble(s) m.fl.) ikke understøtter brugen af underscore, så følgende vil fejle på runtime:

String s = "12_000.5";
double d2 = Double.parseDouble(s);

Multiple catch i fejlhåndtering

Den næste feature, vi skal kigge på, er til gengæld ret lækker. Vi får nu mulighed for at lave multiple catch i fejlhåndtering. Se bare:

try{
 FileWriter f = new FileWriter(args[0]);
  Date d = DateFormat.getDateInstance().parse("01012001");
}
catch(IOException | ParseException ex){
  // do IO- & Parse-stuff
}
catch(NullPointerException | ArrayIndexOutOfBoundsException ex){
  // do NullPointer- & array-stuff
}

Med multiple catch kan man gruppere specifikke Exceptions i catch-blokke ved brug af almindeligt ”eller-tegn” |, så man slipper for dublikeret kode. Reglen om least upper bound gælder stadig således, at kompileren gennemskuer om en Exception, der er erklæret i en catchblok, er en subtype af en tidligere erklæret Exception. Nedenstående

catch(IOException | FileNotFoundException | ParseException ex){
           // do IO-, File- & Parse-stuff
}

og

catch(IOException | ParseException ex){
 // do IO- & Parse-stuff
}       
 catch(NullPointerException | FileNotFoundException | 
 ArrayIndexOutOfBoundsException ex){
 // do NullPointer-, File- & array-stuff
}

vil derfor ikke kompilere, da FileNotFoundException er en subklasse til IOException.

Præcise re-throws af exceptions

Derudover introduceres mere præcis re-throw af Exceptions. Det bliver interessant, hvis man ønsker at gribe en Exception, foretage sig et eller andet (fx logge) og dernæst viderekaste den. Lad os se på et eksempel:

public void testRethrow() throws IOException, ParseException {
 try {
    FileInputStream stream = new FileInputStream("txt");
    Date d = DateFormat.getDateInstance().parse("01012001");
 } catch (Exception ex) {
    // Log exception
    throw ex;
 }
}

I tidligere versioner af Java, vil dette ikke kompilere, da vi kaster en supertype til de Exceptiontyper vi har erklæret, at metoden kaster. Man kunne løse problemet ved at skrive

public void testRethrowTheHardWay() throws IOException, ParseException {
 try {
    // code which throws IOException and ParseException
    FileInputStream stream = new FileInputStream("txt");
    Date d = DateFormat.getDateInstance().parse("01012001");
 } catch (IOException ex) {
    // Log exception
    throw ex;
 }
 catch(ParseException ex){
    // Log
    throw ex;
 }}

men det bliver lidt bøvlet, hvis der er mange forskellige Exceptions i spil. Med multicatch i Java 7 kunne man løse det ved at skrive

public void testRethrowTheMultipleWay() throws IOException, ParseException {
 try {
 // code which throws IOException and ParseException
    FileInputStream stream = new FileInputStream("txt");
    Date d = DateFormat.getDateInstance().parse("01012001");
 } catch (IOException | ParseException ex) {
 // Log exception
 throw ex;
 }
}

og således gruppere sine Exceptions og spare nogle linjer. Det er dog stadig lidt bøvlet med mange Exceptions. Det, vi får lov at gøre i stedet, er at gribe en generel Exception og viderekaste den som specifik type (gentagelse af det første eksempel):

public void testRethrow() throws IOException, ParseException {
 try {
    FileInputStream stream = new FileInputStream("txt");
    Date d = DateFormat.getDateInstance().parse("01012001");
 } catch (Exception ex) {
    // Log exception
    throw ex;
 }
}

Her erklærer metoden, at den smider med IOExceptions og ParseExceptions, men det der smides er ex, som er af typen Exception, dvs mere generel end det erklærede. Det er i orden, da kompileren gennemskuer at ex er af typen IO eller Parse.

Hvis der er mulighed for, at der kan blive kastet andre checked exceptions, som gribes af catch-betingelsen, vil kompileren brokke sig (nedenstående kompilerer ikke):

public void testMoreRethrows() throws IOException, ParseException {
 try {
    Thread.sleep(1000);
    FileInputStream stream = new FileInputStream("txt");
    Date d = DateFormat.getDateInstance().parse("01012001");
 } catch (Exception ex) {
    // Log exception
    throw ex;
 }
}

idet ex her kan referere til en InterruptedException (smidt fra Thread.sleep()), som ikke kan viderekastes, idet vi ikke har erklæret at smide omkring os med den slags.

Det er god kodestil at gøre ex final, idet følgende ikke er tilladt:

public void testRethrow() throws IOException, ParseException {
 try {
    FileInputStream stream = new FileInputStream("txt");
    Date d = DateFormat.getDateInstance().parse("01012001");
 } catch (Exception ex) {
    ex = new IOException("mere fejl");
    throw ex;
 }
}

Her sættes ex til at referere til en ny IOExceptions-instans og så fralægger kompileren sig ethvert ansvar! Vi får altså ikke lov til at videresende ex, fordi vi har ændret dens værdi og kompileren nu ikke ved hvilken eventuel subtype ex refererer til. Kompileringsfejlen opstår dog ikke i den med rødt markede linje, hvor vi tildeler til ex. Den opstår i linjen hvor ex kastes, hvilket kan virke forvirrende. Det er derfor god stil at skrive

public void testFinalRethrow() throws IOException, ParseException {
 try {
    FileInputStream stream = new FileInputStream("txt");
    Date d = DateFormat.getDateInstance().parse("01012001");
 } catch (final Exception ex) {
    ex = new IOException("mere fejl");
    throw ex;
 }
}

fordi det tydeliggører, at ex ikke må ændres. I ovenstående kode opstår kompileringsfejlen når der tildeles ny værdi til ex og ikke når ex viderekastes, hvilket er i tråd med logikken bag.

I multi-catch-blokke er variablen ex implicit final og derfor unødvendig at skrive:

public void testRethrowTheHardWay() throws IOException, ParseException {
 try {
    // code which throws IOException and ParseException
    FileInputStream stream = new FileInputStream("txt");
    Date d = DateFormat.getDateInstance().parse("01012001");
 } catch (IOException | ParseException ex) {
    // kompilerer ikke:
    ex = new IOException("fejl");
    throw ex;
 }
}

Præcis re-throw er nok ikke en feature, der kommer til at betyde så meget for den almindelige udvikler, men i avanceret fejlhåndtering, fx i forbindelse med udvikling af frameworks, kan den være ret brugbar.

Diamonds are a programmers best friend

Og så en lækker lille sag, som ikke revolutionerer noget, men som jeg som underviser kan skrive under på vil fjerne 10% af forvirringen omkring generics.

Vi har indtil nu skullet skrive

ArrayList<Number> list = new ArrayList<Number>();

hvis vi ønskede en ny ArrayList parameteriseret med Number. Hvis vi undlader at skrive parameteriseringen  på højresiden, får vi en warning, fordi vi lader en parameteriseret variable referere til en raw type instans af ArrayList(). Der er masser af gode grunde til, at kompileren advarer os mod det. Der er også gode grunde til, at venstre og højre side skal parameteriseres med samme type og at normale subtyperegler ikke gælder med parameteriseringer, således at

ArrayList<Number> list = new ArrayList<Integer>();

ikke er lovligt. Dét, vi får med Java 7, er en art typeinference, idet vi nu må skrive

ArrayList<Number> list = new ArrayList<>();

og så gætter kompileren sig selv til, at højresiden er parameteriseret med Number. Det omvendte (ArrayList<> list = new ArrayList<Number>() er ikke tilladt, da kompileren nødvendigvis skal have referencen list parameteriseret for at sikre typesikkerhed i resten af koden).

Og hvad så?

Der er langt fra tale om revolutionerende omvæltninger, men det er heller ikke tanken med Java at sproget skal udvides i tide og utide. Java SE 5 var den store undtagelse, hvor generics og enums blev introduceret, men den generelle filosofi er, at sproget holdes simpelt og rent og de store forandringer kommer som tilføjelser til API’et.

Jeg vil dog mene at Strings i switch og udvidelserne til exceptionhåndtering er såvel brugbare som elegante. Vi hører meget gerne fra jer, hvis I har fået afprøvet Java SE 7 eller blot har en mening om versionen.

Og så kan vi glæde os til slutningen af 2012 hvor Java SE 8 skulle udkomme og der skulle closures være inkluderet. Det er både sikkert og vist….