|
|||||||||||||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||||||||||||||||
|
Java 7 - JSR 334 - Project Coin - Strings in switch, Exception Handling, ARM, numerische Literale
|
||||||||||||||||||||||||||||||||
Die Version 7 von Java wird eine Reihe von kleineren Sprachergänzungen bringen, die unter dem Namen "Project Coin" entwickelt wurden. In diesem Beitrag stellen wir einen Teil dieser neuen Sprachmittel aus dem Project Coin vor, nämlich verbesserte Notationen für numerische Literale, Strings in switch-Anweisungen, Verbesserungen beim Exception Handling und das Automatic Resource Management. Die übrigen Verbesserungen im Zusammenhang mit Generics werden wir im nächsten Artikel besprechen. "Project Coin" ist eine Sammlung kleinerer Spracherweiterungen. Die Ideen für diese Ergänzungen stammen aus der Java Community selbst. Sun Microsystems hatte die Community aufgefordert, entsprechende Vorschläge zu machen, und hatte aus der Fülle der Ideen eine Handvoll aufgegriffen. Die Idee dabei war es, die Sprache um Mittel zu erweitern, die nützlich und "klein" sind. Folgende neue Sprachmittel werden mit Java 7 freigegeben:
Verbesserte Schreibweise für numerische LiteraleBinary LiteralsBislang konnten numerische Literale nur dezimal (als 1), octal (als 01) oder hexadezimal (als 0x1) hingeschrieben werden. Mit dem JDK 7 (siehe /JSR334/ und /BINLIT/) gibt es nun auch die Möglichkeit der Binärdarstellung (als 0b1).Hier ist ein Beispiel für die neue Binär-Darstellung: int decimal = 43; Underscores in NumbersIm Java-Source-Code mussten numerische Literale bisher immer dicht geschrieben werden, d.h. ohne Leerzeichen oder andere Strukturierungszeichen zu enthalten. Für das menschliche Auge sind solche Strukturierungselemente aber sehr hilfreich, z.B. wenn man eine Telefonnummer oder eine Kreditkartennummer hinschreiben will. Die Nummer +49721629188 ist schwer verdaulich und in der Schreibweise +49 721 6291 88 deutlich besser als Telefonnummer zu erkennen. Um solche lesbareren Darstellungen zu erlauben, ist es im JDK 7 nun erlaubt, an praktisch allen stellen in einem numerischen Literal Unterstriche einzufügen (siehe /INTLIT/). Man dürfte die obige Telefonnummer nun also so schreiben: +49_721_6291_88. Der Compiler entfernt beim Parsen die Unterstriche, so dass im Byte-Code wieder die alte kompakte Darstellung auftaucht.Hier ein paar Beispiele: long phoneNumber = +49_721_6291_88L; Strings in switch-AnweisungenBislang konnte man in einer switch -Anweisung nur über Werte eines primitiven oder Enum-Typs verzweigen. In Java 7 darf auch über konstante Strings verzweigt werden (siehe /JSR334/ und /STRSWT/). Der Vorteil liegt darin, dass der Compiler aus einem switch über Strings effizienteren Code erzeugen kann, als es mit den bislang zur Verfügung stehenden Sprachmitteln möglich ist.Man darf also in Java 7 folgendes schreiben: private static final String IDLE = "idle";
Der Effizienzzuwachs rührt daher, dass man mit heutigen Sprachmitteln eine if - else -Kaskade mit equals() -Vergleichen für alle Strings hinschreiben müsste. Eine solche Kaskade kann vom Compiler schlecht optimiert werden. Aus einem switch über Strings kann der Compiler hingegen einen switch über die Hashcodes der Strings machen, was dann nur noch einen einzigen equals() -Aufruf statt einer Sequenz von Methodenaufrufen erfordert. Mit herkömmlichen Mitteln wäre die Verzweigung im obigen Beispiel nur mit einer if - else -Kaskade möglich gewesen, in der der variable String state sukzessive mit allen konstanten Strings verglichen wird, bis eine Übereinstimmung gefunden wird. Das hätte dann etwa so ausgesehen: public static void classicStringSwitch(String state) {Die vielen Vergleiche mit String.equals() sind ineffizient. Die Implementierung des neue String- switch eliminiert den Overhead indem sie erst über die HashCodes der String verzweigt und danach (bei Bedarf) noch den Vergleich per String.equals() nutzt. Das heißt, nachem der Compiler den String- switch in Byte-Code übersetzt hat, sieht die Verzweigung sinngemäß so aus: public static void newStringSwitch(String state) {Der neue String- switch ist also nicht nur übersichtlicher, sondern auch noch effizienter. Verbessertes Exception HandlingDie Verbesserungen beim Exception Handling bestehen in einer Flexibilisierung beim Rethrow von Exceptions und einer Multi-Catch-Klausel (siehe /MULCAT/).Flexibilisierung beim Rethrow von ExceptionsManchmal will man eine catch -Klausel schreiben, in der alle Exceptions – egal von welchem Typ – gefangen werden, damit ein Cleanup gemacht werden kann, und danach wird die ursprüngliche Exception weiter geworfen. In einer solchen catch -Klausel würde man gerne Throwable fangen, um alle Exception-Typen zu erwischen. Daraus ergibt sich beim Rethrow ein Problem: der Compiler ist dann nämlich aufgrund der statischen Typinformation Throwable der Meinung, die Methode werfe nun beliebige Exceptions vom Typ Throwable und gibt eine Fehlermeldung aus. Hier ist ein Beispiel:public void someMethod throws SomeSpecificException {In Java 7 werden die Compilerprüfungen dahingehend geändert, dass der Compiler beim Rethrow nicht mehr die statische Typinformation der gefangenen Exception zugrunde legt, sondern davon ausgeht, dass lediglich die Exception-Typen beim Rethrow vorkommen können, die in dem try -Block geworfen werden können. Das sieht dann so aus: public void someMethod throws SomeSpecificException {Nun muss man die throws -Klausel der Methode nicht mehr ändern und throws Throwable deklararieren, sondern die throws -Klausel der Methode kann wie gewünscht die spezifischen, tatsächlich zu erwartenden Exception-Typen deklarieren. Allerdings darf man an die Referenzvariable ex im catch -Block nichts zuweisen, wenn man einen Rethrow machen will. Sobald im catch -Block an die Referenzvariable ex zugewiesen wird, weigert sich der Compiler, einen Rethrow zu akzeptieren. Der Compiler erkennt nämlich daran, dass der Referenzvariablen im catch -Block nichts Neues zugewiesen wurde, dass es sich um einen Rethrow der Original-Exception handelt und dass nicht etwa eine andere Exception geworfen wird. Wenn man eine Zuweisung Referenzvariable im catch -Block macht, dann ist es eben kein Rethrow mehr. In diesem Fall behandelt der Compiler die Exception dann so, wie früher in Java 6 schon: er beschwert sich darüber, dass die Methode eine Exception vom Typ Throwable wirft, diesen Exception-Typ aber nicht in ihrer throws -Klausel deklariert hat und dann muss man die throws -Klausel der Methode ändern und throwsThrowable deklararieren.
Man sich sollte deshalb merken, dass die Referenzvariable ex im
catch
-Block
"effectively final" sein muss, wenn man einen Rethrow machen will.
Das heißt, man muss die Variable zwar nicht ausdrücklich als
final
deklarieren, aber man sollte sie dennoch wie eine
final
-Variable
behandeln.
Multi-CatchMit dem Multi-Catch soll redundanter Code vermieden werden. Anstelle vonpublic void someMethod throws IOException,InterruptedException {darf man nun catch -Klauseln mit identischem Code zu einer einzigen Klausel zusammenfassen. Das sieht dann so aus: public void someMethod throws IOException,InterruptedException {Eine weitere Alternative für die Implementierung, die bereits heute genutzt werden kann, sieht so aus: public void someMethod throws Exception {Der Nachteil ist, dass mehr Exception-Typen gefangen werden, als man eigentlich will. Zusätzlich wird die Deklaration der throws -Klausel der Methode sehr unspezifisch, da der Exception-Supertyps verwendet werden muss. Mit dem Multi-Catch entfällt sowohl die Redundanz als auch die unspezifische throws -Klausel. Es gibt eine Randbedingung bei der Verwendung des neuen Multicatch-Features: die Referenzvariable ex im catch -Block ist "effectively final". Das heißt, man muss die Variable nicht ausdrücklich als final IOException | InterruptedException ex deklarieren, aber sie wird dennoch wie eine final -Variable behandelt und man darf an diese Variable nichts zuweisen. Wenn man es trotzdem tut, bekommt man eine Fehlermeldung vom Compiler, die besagt, man dürfe an den Multicatch-Parameter nichts zuweisen. Automatic Resource ManagementDer Vorschlag für eine automatische Unterstützung solcher Klassen, die dynamisch angeforderte Ressourcen verwalten, stammt von Joshua Bloch und ist als ARM (= Automatic Resource Management ) bekannt (siehe /ARM/ und /ARMUPD/).Damit wird ein Problem gelöst, das schon so alt ist wie Java selbst. In Java gibt es zwar Konstruktoren, in denen ein Objekt zu Beginn seiner Lebenszeit Ressourcen beschaffen kann, aber es gibt kein gutes Instrument, mit dem das Objekt seine Ressourcen beim Ableben automatisch loswerden könnte. Die Finalizer haben sich für diesen Zweck als wenig hilfreich erwiesen und so muss sich der Benutzer des Objekts meist selbst darum kümmern, dass die Ressourcen des Objekts freigegeben werden. Am Beispiel eines Input-Streams sieht es so aus: String readFirstLineFromFile(String path) throws IOException {Der Benutzer des BufferedReader -Objekts muss dafür sorgen, dass die verwendete Datei wieder geschlossen wird. Dazu genügt nicht einmal ein einfacher Aufruf von close() am Ende der Methode, sondern der Benutzer benötigt eine finally -Klausel, damit die Datei auch im Falle einer Exception geschlossen wird. In dieser Situation soll das Automatic Resource Management helfen. Die Idee besteht darin, dass Klassen mit dynamischen Ressourcen, wie z.B. ein BufferedReader , ein spezielles Interface AutoCloseable implementieren können (und im Fall von JDK Klassen auch tun): public interface AutoCloseable {Eine Klasse, die AutoCloseable implementiert, wird als Resource Type bezeichnet. Dazu gibt es eine neue Form des try -Blocks, ein sogenanntes "try-with-resources", bei dessen Verlassen automatisch die close() -Methoden aller spezifizierten Resource-Typen aufgerufen werden. Das sähe zum Beispiel so aus: String readFirstLineFromFile(String path) throws IOException {Bei diesem Beispiel muss man übrigens aufpassen, wie man die try-with-resources-Klausel gestaltet. Die folgende Variante wäre ungünstig: String readFirstLineFromFile(String path) throws IOException {Wenn anstelle von einer Ressource zwei Ressourcen aufgelistet werden, dann wird auch für beide Ressourcen die close() -Methode aufgerufen. Da aber die close() -Methode von BufferedReader ohnehin bereits die close() -Methode von FileReader aufruft, würde letztere zweimal gerufen. Das ist in diesem Fall zwar nicht völlig falsch, aber auch nicht besonders sinnvoll. In anderen Fällen kann es durchaus erwünscht und notwendig sein, dass für jede separat aufgelistete Ressource die jeweilige close() -Methode gerufen wird. Der Aufruf erfolgt übrigens in umgekehrter Reihenfolge der Deklaration, d.h. die erste Ressource wird als letzte weggeräumt. Suppressed ExceptionsBei einem solchen try-with-resources-Konstrukt kann es nun passieren, dass nach einer Exception aus dem try -Block noch eine weitere Exception beim automatischen Aufruf der close() -Methode ausgelöst wird. Bisher hat eine solche zweite Exception (z.B. aus einem finally -Block) die erste unterdrückt, denn es kann immer nur eine Exception an den Aufrufer der Methode weitergeleitet werden. Damit keine Information verloren geht, ist die Klasse Throwable um Information über unterdrückte Exceptions ergänzt worden und hat entsprechende Methoden addSuppressed() und getSuppressed() . Die Method readFirstLineFromFile() im obigen Beispiel würde nun im JDK 7 die erste Exception aus dem try -Block propagieren und diese Exception enthält die zweite unterdrückte Exception aus der close() -Methode.Ohne try-with-resources sähe es so aus: void method() throws Exception {Es würde diejenige Exception weiter propagiert, die von der close() -Methode geworfen wurde. Eine Exception aus dem try - catch -Konstrukt ginge in einem solchen Falle verloren. Mit try-with-resources sähe es so aus: void method() throws Exception {Die c lose() -Methode wird automatisch gerufen. Wenn sie eine Exception auslöst, dann wird diese Exception in eine zuvor im try -Block ausgelöste Exception als "suppressed exception" eingepackt. Propagiert würde dann die initiale Exception aus dem try - catch -Konstrukt mit der unterdrückten Exception aus close() im Gepäck. Auf diese Weise geht keine Exception-Information verloren. Nun stellt sich die Frage: was passiert eigentlich, wenn ein try-with-resources-Konstrukt eine explizite finally -Klausel hat und darin auch eine Exception geworfen wird? Hier ist ein Beispiel: void method() throws Exception {Es sind nun drei Exceptions im Spiel: diejenige aus dem try -Block, eine weitere aus der close() -Methode und noch eine dritte aus dem finally -Block. Welche Exception bekommt der Aufrufer zu sehen? Da die close() -Methoden aller Ressourcen vor den Anweisungen im finally -Block aufgerufen werden, sind wir wieder in der Situation, in der wir ohne das neue try-with-resources-Konstrukt waren: es wird nur die allerletzte Exception aus dem finally -Block geworfen; die Exception aus dem try -Block mit der eingepackten Exception aus der close() -Methode geht verloren. Wie ist die Situation, wenn es noch komplizierter wird und es im try-with-resources-Konstrukt sowohl eine catch - als auch eine finally -Klausel gibt? Im Wesentlichen ist es so wie oben beschrieben: nach Verlassen des try -Block werden die close() -Methoden der Ressourcen gerufen, danach geht es im catch - und anschließend im finally -Block weiter. void method() throws Exception {Die Exception, die nach einer Exception im try -Block von einer close() -Methode ausgelöst wird, ist als "suppressed exception" in die erste Exception eingepackt. Wenn sie im catch -Block mit einem Rethrow propagiert wird, dann geht nichts verloren. Wenn allerdings im catch -Block kein Rethrow gemacht wird, sondern eine andere Exception geworfen wird, dann kommt beim Aufrufer nur diese andere Exception an und der Rest der Exception-Information geht verloren. Dasselbe passiert, wenn danach im finally -Block noch eine Exception geworfen wird: dann wird nur diese allerletzte Exception weitergegeben und alles andere ist verloren. Sinnvoll ist also im catch -Block nur
void method() throws Exception1, Exception2 {Wenn nun beispielsweise im try -Block eine Exception3 ausgelöst wird, dann wird sie im dritten catch -Block gefangen und abgebildet auf eine Exception4 . Der Aufrufer bekommte diese Exception4 , in der als Ursache (siehe Throwable.getCause() ) die initiale Exception steckt, in der die unterdrückte Exception von der close() -Methode enthalten ist. Ursache vs. UnterdrückungEs mag ein wenig verwirrend erscheinen, dass es nun zwei Mechanismen für das Einpacken von Exceptions in andere Exceptions gibt: unterdrückte Exceptions (siehe (siehe Throwable.getSuppressed() ) und die Ursache (siehe Throwable.getCause() ).Eine unterdrückte Exception wird automatisch erzeugt im Zusammenhang mit dem neuen try-with-resources-Konstrukt. Man kann weitere unterdrückte Exceptions selber hinzufügen mit Throwable.addSuppressed() . Eine Exception kann also beliebig viele unterdrückte Exceptions enthalten. Unterdrückte Exceptions entstehen "gleichzeitig" und gehören zur selben Fehlersituation; es handelt sich um einen initialen Fehler und seine Folgefehler.
Eine Ursache hingegen entsteht beim Abbilden einer Exception auf eine
andere Exception. Das ist der Exception-Chaining-Mechanismus, den
wir schon seit Java 1.4 kennen und der in geschichteten Architekturen häufig
vorkommt. Die Exception-Kette entsteht nie automatisch, sondern sie
muss ausdrücklich hergestellt werden mit
Throwable.initCause()
oder durch die Benutzung des Exception-Konstruktor. Dabei enthält
jede Exception nur genau eine Ursache, die wiederum genau eine Ursache
enthält, usw. Die Ursachen entstehen "nacheinander" und beschreiben
eine Abbildungskette über einen Call-Stack hinweg.
Zusammenfassung und AusblickIn diesem Beitrag haben wir einige der neuen Sprachmittel aus dem Project Coin betrachtet. Die verbesserten Notationen für numerische Literale sind so naheliegend, dass man sich fragt, warum es sie nicht schon immer gegeben hat. Strings in switch -Anweisungen sorgen für bessere Lesbarkeit des Codes und sind zudem auch noch effizienter als die herkömmlichen Sprachmittel. Die Verbesserungen beim Exception Handling bestehen aus einem Multi-Catch, der redundanten Code beim Exception Handling vermeidet, und einem verbesserten Rethrow, bei dem der Compiler die Exception-Typen automatisch deduziert. Das Automatic Resource Management sorgt dafür, dass für sogenannte Ressource-Typen -die Ressourcen-Freigabe automatisch angestoßen wird und dabei keine Exceptions verloren gehen. Dazu wurde ein try-with-resources-Konstrukt erfunden und die Exceptions können nun unterdrückte Exceptions enthalten. Die übrigen Sprachverbesserungen im Zusammenhang mit Generics werden wir im nächsten Artikel besprechen.Literaturverweise
Die gesamte Serie über Java 7:
|
|||||||||||||||||||||||||||||||||
© Copyright 1995-2013 by Angelika Langer. All Rights Reserved. URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/58.Java7.Coin1/58.Java7.Coin1.html> last update: 24 Jan 2013 |