gzip Kommandozeilenprogramm mit wenigen Zeilen Quellcode

package com.tutego.insel.io.zip;

import java.io.*;
import java.nio.file.*;
import java.util.zip.GZIPOutputStream;

public class gzip {

  public static void main( String[] args ) {
    if ( args.length != 1 ) {
      System.err.println( "Benutzung: gzip <source>" );
      return;
    }

    try ( OutputStream gos = new GZIPOutputStream( Files.newOutputStream( Paths.get( args[ 0 ] + ".gz" ) ) ) ) {
      Files.copy( Paths.get( args[ 0 ] ), gos );
    }
    catch ( IOException e ) {
      System.err.println( "Fehler: Kann nicht packen " + args[ 0 ] );
    }
  }
}

java.io.File und NIO.2-Path: wo beide zusammenpassen und wo nicht

Die Klasse File ist schon immer da gewesen und stark mit dem lokalen Dateisystem verbunden. So findet sich der Typ File weiterhin bei vielen Operationen. Wenn Runtime.exec(String[] cmdarray, String[] envp, File dir) einen Hintergrundprozess startet, dann ist dir genau das Startverzeichnis. Eine Abstraktion auf virtuelle Dateisysteme ist unpassend und File passt als Typ sehr gut. Doch obwohl sich Pfade vom NIO.2-Typ Path schon an einigen Stellen in der Java-API finden lassen, sind doch immer noch viele APIs mit File ausgestattet. Dass ImageIO.read(File input) nur ein File-Objekt annimmt, aber kein Path-Objekt, um ein Bild zu laden ist schade, wo es doch auch eine read(InputStream) und read(URL)-Methode gibt. Die Bibliotheksdesigner haben bisher keine Notwendigkeit gesehen, das nachzubessern, vielleicht aus deswegen nicht, weil Entwickler die Möglichkeit haben, etwa mit Files.newInputStream(path) von einem Pfad einen Eingabestrom zu erfragen. Der Weg ist auch der Beste, denn vom Path ein File-Objekt zu erfragen und dann Methoden aufzurufen, die das File-Objekt annehmen, birgt eine Gefahr: von ein NIO.2-Dateisystem, etwa Zip, ein File-Objekt zu erfragen wird nicht funktionieren, weil es die Datei vom File-Objekt ja gar nicht im lokalen Verzeichnis gibt! Einige Klasse erwarten nur File-Objekte und nichts anderes, also auch kein Strom, und hier zeigt sich, dass diese Klassen nicht auf virtuellen Dateisystemen funktionieren können. Etwa der JFileChooser. Der operiert nur auf dem lokalen Dateisystem, was sich an JFileChooser.getSelectedFile() und JFileChooser.setCurrentDirectory(File dir) ablesen lässt.

Java und Sequenzpunkte in C(++)

Je mehr Freiheiten ein Compiler hat, desto ungenierter kann er optimieren. Besonders Schreibzugriffe interessieren Compiler, denn kann er diese einsparen kann, läuft das Programm später ein bisschen schneller. Damit das Resultat eines Compilers jedoch beherrschbar bleibt, definiert der C(++)-Standard Sequenzpunkte (eng. sequence point), an dem alle Schreibzugriffe klar zugewiesen wurden. (Dass der Compiler später Optimierungen macht ist eine andere Geschichte; die Sequenzpunkte gehören zum semantischen Modell, Optimierungen verändern das nicht). Das Semikolon als Abschluss von Anweisungen bildet zum Beispiel einen Sequenzpunkt. Im Ausdruck wie i = i + 1; j = i; muss der Schreizugriff auf i aufgelöst sein, bevor ein Lesezugriff für die Zuweisung zu j erfolgt. Problematisch ist, dass es gar nicht so viele Sequenzpunkte gibt, und es passieren kann, dass zwischen zwei Sequenzpunkten zwei mehrdeutige Schreibzugriffe auf die gleiche Variable stattfinden. Da das jedoch in C(++) undefiniert ist, kann sich der Compiler so verhalten wie er will – er muss sich ja nun an den Sequenzpunkten so verhalten wie gefordert. Problemfälle sind: (i=j) + i oder, weil ein Inkrement/Dekrement ein Lese-/Schreibzugriff ist, auch i = i++, was ja nichts anderes als i = (i = i + 1) ist. Bedauerlicherweise bildet die Zuweisung keinen Sequenzpunkt. Bei Zuweisungen der Art i = ++i + –i kann alles Mögliche später in i stehen, je nachdem, was der Compiler zu welchem Zeitpunkt ausführt. In Java sind diese Dinge von der Spezifikation klar geregelt, in C(++) ist nur geregelt, dass das Verhalten zwischen den Sequenzpunkten klar sein muss. Doch moderne Compiler erkennen konkurrierende Schreibzugriffe zwischen zwei Sequenzpunkten und mahnen sie (bei entsprechender Warnstufe) an.[1]

 



[1]       Beim GCC ist der Schalter -Wsequence-point (der auch bei -Wall mitgenommen wird), siehe dazu http://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html.

Putziger kleiner OR-Mapper: ORMLite, für JDBC und auch für Android

package tutego;

import java.sql.SQLException;
import com.j256.ormlite.dao.*;
import com.j256.ormlite.db.HsqldbDatabaseType;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.jdbc.JdbcConnectionSource;
import com.j256.ormlite.support.ConnectionSource;
import com.j256.ormlite.table.*;

@DatabaseTable
class Contact
{
  @DatabaseField( generatedId = true )
  Long id;

  @DatabaseField
  String name;

  // Setter/Getter sparen
}

public class ORMLiteDemo
{
  public static void main( String[] args )
  {
    System.setProperty( "tutegoHsqldbDatabasePath", "TutegoDB" );
    String url = "jdbc:hsqldb:file:${tutegoHsqldbDatabasePath};shutdown=true";
    ConnectionSource connectionSource;
    try {
      connectionSource = new JdbcConnectionSource( url, "sa", "", new HsqldbDatabaseType() );
      Dao<Contact, String> dao = DaoManager.createDao( connectionSource, Contact.class );
//      TableUtils.createTable( connectionSource, Contact.class );

      Contact c1 = new Contact();
      c1.name = "Chris";
      dao.create( c1 );
      Contact c2 = new Contact();
      c2.name = "Juvy";
      dao.create( c2 );

      Contact c3 = dao.queryForId( "1" );
      System.out.println( c3.name );
      connectionSource.close();
    }
    catch ( SQLException e ) {
      e.printStackTrace();
    }
  }
}

1. Klassen annotieren mit den ORMLite-Annotationen oder mit JPA-Annotationen

2. Sind die Tabellen nicht da, muss man TableUtils.createTable( connectionSource, Contact.class ); aufrufen, dann erzeugt ORMLite die Tabellen.

3. Der Rest ist einfach, siehe Beispiel 🙂

Mehr unter http://ormlite.com/.

Neues Eclipse Release: 4.2 (Juno) ist fertig

Download wie üblich unter http://www.eclipse.org/downloads/. Die Neuigkeiten sind groß, http://download.eclipse.org/eclipse/downloads/drops4/R-4.2-201206081400/news/index.html, insbesondere für RCP-Entwickler:

Der 3er Zweig läuft aus.

PS: Unser tutego Eclipse Kurs http://tutego.de/g/ECLPSRCP/ schult auf Wunsch komplett auf Eclipse 4.

Java 8 bekommt auch eine Optional-Klasse (wie Guava und Scala)

+/*

+ * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved.

+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.

+ *

+ * This code is free software; you can redistribute it and/or modify it

+ * under the terms of the GNU General Public License version 2 only, as

+ * published by the Free Software Foundation. Oracle designates this

+ * particular file as subject to the "Classpath" exception as provided

+ * by Oracle in the LICENSE file that accompanied this code.

+ *

+ * This code is distributed in the hope that it will be useful, but WITHOUT

+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or

+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License

+ * version 2 for more details (a copy is included in the LICENSE file that

+ * accompanied this code).

+ *

+ * You should have received a copy of the GNU General Public License version

+ * 2 along with this work; if not, write to the Free Software Foundation,

+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.

+ *

+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA

+ * or visit www.oracle.com if you need additional information or have any

+ * questions.

+ */

+package java.util;

+

+import java.util.functions.Factory;

+import java.util.functions.Mapper;

+

+/**

+ * Optional

+ *

+ * @author Brian Goetz

+ */

+public class Optional<T> {

+ private final static Optional<?> EMPTY = new Optional<>();

+

+ private final T value;

+ private final boolean present;

+

+ public Optional(T value) {

+ this.value = value;

+ this.present = true;

+ }

+

+ private Optional() {

+ this.value = null;

+ this.present = false;

+ }

+

+ public static<T> Optional<T> empty() {

+ return (Optional<T>) EMPTY;

+ }

+

+ public T get() {

+ if (!present)

+ throw new NoSuchElementException();

+ return value;

+ }

+

+ public boolean isPresent() {

+ return present;

+ }

+

+ public T orElse(T other) {

+ return present ? value : other;

+ }

+

+ public T orElse(Factory<T> other) {

+ return present ? value : other.make();

+ }

+

+ public<V extends Throwable> T orElseThrow(Factory<V> exceptionFactory) throws V {

+ if (present)

+ return value;

+ else

+ throw exceptionFactory.make();

+ }

+

+ public<V extends Throwable> T orElseThrow(Class<V> exceptionClass) throws V {

+ if (present)

+ return value;

+ else

+ try {

+ throw exceptionClass.newInstance();

+ }

+ catch (InstantiationException | IllegalAccessException e) {

+ throw new IllegalStateException("Unexpected exception: " + e, e);

+ }

+ }

+

+ public<V> Optional<V> map(Mapper<T, V> mapper) {

+ return present ? new Optional<>(mapper.map(value)) : Optional.<V>empty();

+ }

+

+ @Override

+ public boolean equals(Object o) {

+ if (this == o) return true;

+ if (o == null || getClass() != o.getClass()) return false;

+

+ Optional optional = (Optional) o;

+

+ if (present != optional.present) return false;

+ if (value != null ? !value.equals(optional.value) : optional.value != null) return false;

+

+ return true;

+ }

+

+ @Override

+ public int hashCode() {

+ int result = value != null ? value.hashCode() : 0;

+ result = 31 * result + (present ? 1 : 0);

+ return result;

+ }

+}


Änderungen an Schnittstellen: Code-Kompatibilität und Binär-Kompatibilität

Sind Schnittstellen einmal deklariert und in einer großen Anwendung verbreitet, so sind Änderungen nur schwer möglich, da sie schnell die Kompatibilität brechen. Wird der Name einer Parametervariablen umbenannt, ist das kein Problem, aber bekommt eine Schnittstelle eine neue Operation, führt das zu einem Übersetzungsfehler, wenn nicht automatisch alle implementierenden Klassen diese neue Methode implementieren. Framework-Entwickler müssen also sehr drauf achten, wie sie Schnittstellen modifizieren, doch sie haben es in der Hand, wie weit die Kompatibilität gebrochen wird.

Geschichtsstunde

Schnittstellen später zu ändern, wenn schon viele Klassen die Schnittstelle implementieren, ist eine schlechte Idee. Denn erneuert sich die Schnittstelle, etwa wenn nur eine Operation hinzukommt oder sich ein Parametertyp ändert, dann sind plötzlich alle implementierenden Klassen kaputt. Sun selbst hat dies bei der Schnittstelle java.sql.Connection riskiert. Beim Übergang von Java 5 auf Java 6 wurde die Schnittstelle erweitert, und keine Treiberimplementierungen konnten mehr compiliert werden.

Code-Kompatibilität und Binär-Kompatibilität

Es gibt Änderungen, die führen zwar zu Compilerfehlern, wie neu eingeführten Operationen, sind aber zur Laufzeit in Ordnung. Bekommt eine Schnittstelle eine neue Methode, so ist das für die JVM überhaupt kein Problem. Die Laufzeitumgebung arbeitet auf den Klassendateien selbst und sie interessiert es nicht, ob eine Klasse brav alle Methoden der Schnittstelle implementiert; sie löst nur Methodenverweise auf. Wenn eine Schnittstelle plötzlich „mehr“ vorschreibt, hat sie damit kein Problem.

Während also fast alle Änderungen an Schnittstellten zum Bruch der Codebasis führen, sind doch einige Änderungen für die JVM in Ordnung. Wir nennen das Binär-Kompatibilität. Zu den binär-kompatiblen Änderungen zählen:

  • Neue Methode hinzufügen
  • Schnittstelle erbt von einer zusätzlichen Schnittstelle
  • Hinzufügen/Löschen einer throws-Ausnahme
  • Letzten Parametertyp von T[] in T… ändern
  • Neue Konstanten, also statische Variablen hinzufügen
  • Die Anzahl der binär-inkompatiblen Änderungen sind jedoch gradierender. Verboten sind:

  • Ändern des Methodennamens
  • Ändern der Parametertypen und Umsortieren der Parameter
  • Formalen Parameter hinzunehmen oder entfernen
  • Strategien zum Ändern von Schnittstellen

    Falls die Schnittstelle nicht groß veröffentlicht wurde, so lassen sich einfacher Änderungen vornehmen. Ist der Name einer Operation zum Beispiel schlecht gewählt, wird ein Refactoring in der IDE den Namen in der Schnittstelle genauso ändern wie auch alle Bezeichner in den implementierenden Klassen. Problematischer ist es, wenn externe Nutzer sich auf die Schnittstelle verlassen. Eine Lösung ist, diese Klienten ebenfalls zur Änderung zu zwingen, oder auf „Schönheitsänderungen“, wie dem Ändern des Methodenamens, einfach zu zu verzichten.

    Kommen Operationen hinzu, hat sich eine Konvention etabliert, die im Java-Universum oft anzutreffen ist: Soll eine Schnittstelle um Operationen erweitert werden, so gibt es eine neue Schnittstelle, die die alte erweitert, und auf „2“ endet; java.awt.LayoutManager2 ist so ein Beispiel aus dem Bereich der grafischen Oberflächen, Attributes2, EntityResolver2, Locator2 für XML-Verarbeitung sind weitere. Ein Blick auf die API vom Eclipse-Framework (http://help.eclipse.org/juno/topic/org.eclipse.platform.doc.isv/reference/api/index.html?overview-summary.html) zeigt, dass bei mehr als 3500 Typen dieses Muster um die 70 Mal angewendet wurde.

    Seit Java 8 gibt es eine weitere Möglichkeit Operationen in Schnittstellen hinzuzufügen, sogenannte Virtuelle Erweiterungsmethoden. Sie erweitern die Schnittstelle, fügen aber gleich schon eine vorgefertigte Implementierung mit, sodass Unterklassen nicht zwingend eine Implementierung anbieten müssen.

    Ressourcen aus dem Klassenpfad und aus Jar‑Archiven laden

    Um Ressourcen wie Grafiken oder Konfigurationsdateien aus Jar-Archiven zu laden gibt es eine Methoden am Class-Objekt: getResourceAsStream().

     

    class java.lang.Class

    implements Serializable, GenericDeclaration, Type, AnnotatedElement

    – InputStream getResourceAsStream(String name)
    Gibt einen Eingabestrom auf die Datei mit dem Namen name zurück, oder null, falls es keine Ressource mit dem Namen im Klassepfad gibt.

     

    Da der Klassenlader die Ressource findet, entdeckt er alle Dateien, die im Pfad des Klassenladers eingetragen sind. Das gilt auch für Jar-Archive, weil dort vom Klassenlader alles verfügbar ist. Die Methode getResourceAsStream() liefert auch null, wenn die Sicherheitsrichtlinien das Lesen verbieten. Da die Methode keine Ausnahme auslöst, muss auf jeden Fall getestet werden, ob die Rückgabe ungleich null war.

    Das folgende Programm liest ein Byte ein und gibt es auf dem Bildschirm aus:

     

    package com.tutego.insel.io.stream;

    import java.io.*;

    import java.util.Objects;

    public class GetResourceAsStreamDemo {

    public static void main( String[] args ) {

      String filename = "onebyte.txt";

      InputStream is = Objects.requireNonNull(

       GetResourceAsStreamDemo.class.getResourceAsStream( filename ),

        "Datei gibt es nicht!" );

      try {

       System.out.println( is.read() ); // 49

      }

      catch ( IOException e ) {

       e.printStackTrace();

      }

    }

    }

    Die Datei onebyte.txt befindet sich im gleichen Pfad wie auch die Klasse, liegt also in com/tutego/insel/io/stream/onebyte.txt. Liegt sie zum Beispiel im Wurzelverzeichnis des Pakets, muss sie mit "/onebyte.txt" angegeben werden. Liegen die Ressourcen außerhalb des Klassenpfades, können sie nicht gelesen werden. Der große Vorteil ist aber, dass die Methode alle Ressourcen anzapfen kann, die über den Klassenlader zugänglich sind, und das ist insbesondere der Fall, wenn die Dateien aus Jar-Archiven kommen – hier gibt es keinen üblichen Pfad im Dateisystem, der hört in der Regel beim Jar-Archiv selbst auf.

    Zum Nutzen der getResourceAsStream()-Methoden ist ein Class-Objekt nötig, was wir in unserem Fall über Klassenname.class besorgen. Das ist nötig, weil die Methode main() statisch ist. Andernfalls kann innerhalb von Objektmethoden auch getClass() eingesetzt werden, eine Methode, die jede Klasse aus der Basisklasse java.lang.Object erbt.

    Lesen aus Dateien und Schreiben in Dateien

    Um Daten aus Dateien lesen, oder sie schreiben zu können, ist eine Strom-Klasse nötig, die es schafft, die Operationen von Reader, Writer, InputStream und OutputStream auf Dateien abzubilden. Um an solche Implementierungen zu kommen gibt es drei verschiedene Ansätze:

    · Die Utility-Klasse Files bietet vier newXXX()-Methoden, um Lese-/Schreib-Datenströme für Zeichen- und Byte-orientierte Dateien zu bekommen.

    · Ein Class-Objekt bietet getResourceAsStream() und liefert einen InputStream, um Bytes aus Dateien im Klassenpfad zu lesen. Zum Schreiben gibt es nichts Vergleichbares und falls keine Bytes sondern Unicode-Zeichen gelesen werden sollen, muss der InputStream in einen Reader konvertiert werden.

    · Die speziellen Klassen FileInputStream, FileReader, FileOutputStream, FileWriter sind Strom-Klassen, die read()/write()-Methoden auf Dateien abbilden.

    Jede der Varianten hat Vor-/und Nachteile.

    Primitiv- und Verweis-Typ und der Vergleich mit Smalltalk und .NET

    Die Datentypen in Java zerfallen in zwei Kategorien:

    • Primitive Typen: Die primitiven (einfachen) Typen sind die eingebauten Datentypen für Zahlen, Unicode-Zeichen und Wahrheitswerte.
    • Referenztypen: Mit diesem Datentyp lassen sich Objektverweise etwa auf Zeichenketten, Datenstrukturen oder Zwergpinscher verwalten.

    Warum sich damals Sun für diese Teilung entschieden hat, lässt sich mit einem einfachen Grund erklären: Java ist als Programmiersprache entworfen worden, die kleine schwache Geräte unterstützen sollte, und auf denen musste die Java-Software, die am Anfang noch interpretierte wurde, so schnell wie möglich laufen. Unterscheidet der Compiler zwischen primitiven Typen und Referenztypen, so kann der er relativ leicht Bytecode erzeugen, der ebenfalls zwischen den beiden Typen unterscheidet. Damit kann die Laufzeitumgebung auch den Programmcode viel schneller ausführen, und das mit einem relativen einfachen Compiler. Das war für die Anfangszeit ein wichtiges Kriterium.

    Sprachvergleich mit Smalltalk und .NET

    In Smalltalk ist alles ein Objekt, auch die eingebauten Sprachdatentypen. Für Zahlen gibt es einen Basistyp Number und Integer, Float, Fraction als Untertypen. Immer noch gibt es arithmetische Operatoren (+, -, *, /, //, \\ um sie alle aufzuzählen), aber das sind nur Methoden der Klasse Number.[1] Für Java-Entwickler sind Methodennamen wie + oder – ungewöhnlich, doch in Smalltalk ist es das nicht. Syntaktisch unterscheidet sich ein 1 + 2 in Java und Smalltalk nicht, nur in Smalltalk ist die Addition ein Nachrichtenaufruf an das Integer-Objekt 1 an die Methode + mit dem Argument 2, was wiederum ein Integer-Objekt ist – die Objekte baut der Compiler selbständig aus den Literalen auf. Eine Klasse Integer für Ganzzahlen besitzt weitere Methoden wie asCharacter, floor usw.[2] Es ist wichtig zu verstehen, dass dies nur das semantische Modell auf der Sprachseite ist; das hat nichts damit zu tun, wie später die Laufzeitumgebung diese speziellen Nachrichtenaufrufe optimiert. Moderne Smalltalk-Laufzeitumgebungen mit Just-In-Time-Compilation sind bei arithmetischen Operationen auf einen ähnlichen Level wie C oder Java. Durch die Einteilung von Java in primitive Datentypen und Referenztypen haben die Sprachschöpfer einen objektorientierten Bruch in Kauf genommen, um die interpretierte Laufzeit Anfang der 1990er zu optimieren – eine Optimierung, die aus heutiger Sicht unnötig war.

    In .NET ist es eher wie in Java. Der Compiler kennt eingebauten Datentypen und gibt ihnen eine Sonderbehandlung, es sind keine Methodenaufrufe. Auch im Bytecode (Common Intermediate Language, kurz CIL in .NET genannt) finden sich Anweisungen wie Addition, Subtraktion wieder. Doch es gibt noch einen Unterschied zu Java. Der Compilder bildet Datentypen der .NET-Sprachen auf .NET-Klassen ab, und diese Klassen haben Methoden. In C# ist der eingebaute Datentyp float mit dem Datentyp Single (aus dem .NET-Paket System) identisch und es ist egal, ob Entwickler float f oder Single f schreiben. Doch Single (respektive float) hat im Vergleich zu Smalltalk keine mathematischen Operationen, aber dennoch ein paar wenige Methoden wie ToString()[3]. In .NET verhalten sich folglich die eingebauten Datentypen wie Objekte, sie haben Methoden, haben aber die gleiche Wertsemantik zum Beispiel bei Methodenaufrufen wie in Java, und sehen auch im Bytecode ähnlich aus, was ihnen die gleiche gute Performance gibt.


    [1] Die Dokumentation für das GNU Smalltalk zeigt auf: http://www.gnu.org/software/smalltalk/manual-base/html_node/Number_002darithmetic.html#Number_002darithmetic

    [2] http://www.gnu.org/software/smalltalk/manual-base/html_node/Integer.html.

    [3] Siehe http://msdn.microsoft.com/en-us/library/system.int32_members(v=vs.71).aspx.

    Besondere Rückgaben oder Ausnahmen?

    Nicht immer ist eine Ausnahme nötig, doch wann es eine Rückgabe wie null oder -1 gibt, und wann eine Ausnahme ausgelöst werden soll, ist nicht immer einfach zu beantworten und hängt vom Kontext ab. Ein Beispiel: Eine Methode liest eine Datei ein und führt eine Suche durch. Wenn eine bestimmte Teilzeichenkette nicht vorhanden ist, soll die Methode dann eine Ausnahme werfen oder nicht? Hier kommt es drauf an.

    1. Wenn das Dokument in der ersten Zeile eine Kennung tragen muss und der Test prüft auf diese Kennung, dann liegt ein Protokollfehler vor, wenn diese Kennung nicht vorhanden ist.

    2. Im Dokument gibt es eine einfache Textsuche. Ein Suchwort kann enthalten sein, muss aber nicht.

    Im ersten Fall passt eine Ausnahme gut, da ein interner Fehler vorliegt. Muss die Kennung in der Datei sein, ist sie aber nicht, darf dieser Fehler nicht untergehen und eine Ausnahme zeigt das perfekt an. Ob geprüft oder ungeprüft steht auf einem anderen Blatt. Im zweiten Fall ist eine Ausnahme unangebracht, da es kein Fehler ist, wenn der Suchstring nicht im Dokument ist; das kann vorkommen. Das ist das gleiche wie bei indexOf() oder matches() von String – die Methoden würden ja auch keine Ausnahmen werfen, wenn es keine Übereinstimmung gibt.

    Überlaufe

    Bei einigen mathematischen Fragestellungen muss sich feststellen lassen, ob Operationen wie die Addition, Subtraktion oder Multiplikation den Zahlenbereich sprengen, also etwa den Ganzzahlenbereich eines Integers von 32 Bit verlassen. Passt das Ergebnis einer Berechnung nicht in den Wertebereich einer Zahl, so wird dieser Fehler standardmäßig nicht von Java angezeigt; weder der Compiler noch die Laufzeitumgebung melden dieses Problem. Es gibt auch keine Ausnahme, Java hat keine eingebaute Überlaufkontrolle.

    Beispiel

    Mathematisch gilt a × a / a = a, also zum Beispiel 100 000 × 100 000 / 100 000 = 100 000. In Java ist das anders, da wir bei 100 000 × 100 000 einen Überlauf im int haben.

    System.out.println( 100000 * 100000 / 100000 );     // 14100

    liefert daher 14100. Wenn wir den Datentyp auf long erhöhen, indem wir hinter ein 100 000 ein L setzen, sind wir bei dieser Multiplikation noch sicher, da ein long das Ergebnis aufnehmen kann.

    System.out.println( 100000L * 100000 / 100000 );    // 100000

    Hinweis

    Ein Test auf Überlauf könnte aussehen: boolean canMultiply(int a, int b) { return a * b / b == a; } reichen. Doch eine JVM kann das zu return a == a; optimieren und somit zu return true; machen, sodass der Test nicht funktioniert.

    Überlauf erkennen

    Für eine Operation wie die Addition oder Subtraktion lässt sich relativ leicht erkennen, ob das Ergebnis über das Ziel hinausschießt. Eine Möglichkeit ist, bei der Addition zweier ints diese erst auf long zu bringen und dann den long mit der Konstanten Integer.MAX_VALUE/Integer.MIN_VALUE zu vergleichen. Aber über die Interna brauchen wir uns keine großen Gedanken machen, denn ab Java 8 kommen neue Methoden hinzu, die eine Überlauferkennung ermöglichen. Die Methoden gibt es in Math und StrictMath:

    · static int addExact(int x, int y)

    · static long addExact(long x, long y)

    · static int subtractExact(int x, int y)

    · static long subtractExact(long x, long y)

    · static int multiplyExact(int x, int y)

    · static long multiplyExact(long x, long y)

    · static int toIntExact(long value)

    Alle Methoden werfen eine ArithmeticException, falls die Operation nicht durchführbar ist, die letzte, wenn (int)value != value ist. Leider deklariert Java keine Unterklassen wie UnderflowException oder OverflowException, und Java meldet nur alles vom Typ ArithmeticException mit der Fehlermeldung „xxx overflow“, auch wenn es eigentlich ein Unterlauf ist:

    System.out.println( subtractExact( Integer.MIN_VALUE, 1 ) ); // ArithmeticException

    Inselupdate: Vorzeichenlos arbeiten

    Bis auf char sind in Java alle integralen Datentypen vorzeichenbehaftet und kodiert im Zweierkomplement. Bei einem byte stehen 8 Bit für die Kodierung eines Wertes zur Verfügung, jedoch sind es eigentlich nur 7 Bit, denn über ein Bit erfolgt die Kodierung des Vorzeichens. Der Wertebereich ist von -128 bis +127. Über einen Umweg ist es möglich, den vollen Wertebereich auszuschöpfen und so zu tun, als ob Java vorzeichenlose Datentypen hätte.

    byte als vorzeichenlosen Datentyp nutzen

    Eine wichtige Eigenschaft der expliziten Typanpassung bei Ganzzahltypen ist es, dass die überschüssigen Bytes einfach abgeschnitten werden. Betrachten wir die Typanpassung an einem Beispiel:

    int l = 0xABCD6F;

    byte b = (byte) 0xABCD6F;

    System.out.println( Integer.toBinaryString( l ) ); // 101010111100110101101111

    System.out.println( Integer.toBinaryString( b ) ); // 1101111

    Liegt eine Zahl im Bereich von 0 bis 255, so kann ein byte diese durch seine 8 Bit grundsätzlich speichern. Java muss jedoch mit der expliziten Typanpassung gezwungen werden, das Vorzeichenbit zu ignorieren. Erst dann entspricht die Zahl 255 acht gesetzten Bits, denn sie mit byte b = 255; zu belegen, funktioniert nicht.

    Damit die Weiterverarbeitung gelingt, muss noch eine andere Eigenschaft berücksichtigt werden. Sehen wir uns dazu folgende Ausgabe an:

    byte b1 = (byte) 255;

    byte b2 = -1;

    System.out.println( b1 ); // -1

    System.out.println( b2 ); // -1

    Das Bitmuster ist in beiden Fällen gleich, alle Bits sind gesetzt. Dass die Konsolenausgabe aber negativ ist, hat mit einer anderen Java-Eigenschaft zu tun: Java konvertiert das Byte, welches vorzeichenbehaftet ist, in ein int (der Parametertyp bei toBinaryString() ist int) und bei dieser Konvertierung wandert das Vorzeichen weiter. Das folgende Beispiel zeigt das bei der Binärausgabe:

    byte b = (byte) 255;
    int  i = 255;
    System.out.printf( "%d %s%n", b, Integer.toBinaryString(b) );

    // –1  11111111111111111111111111111111
    System.out.printf( "%d %s%n", i, Integer.toBinaryString(i) );

    // 255                         11111111

    Die Belegung der unteren 8 Bit vom byte b und int i ist identisch. Aber während beim int die oberen 3 Byte wirklich null sind, füllt Java durch die automatische Anpassung des Vorzeichens bei der Konvertierung von byte nach int im Zweierkomplement auf die oberen drei Byte mit 255 auf. Soll ohne Vorzeichen weitergerechnet werden stört das. Diese Automatisch Anpassung nimmt Java immer vor, wenn mit byte/short gerechnet wird und nicht nur wie in unserem Beispiel, wenn eine Methoden den Datentyp int fordert.

    Um bei der Weiterverarbeitung einen Datenwert zwischen 0 und 255 zu bekommen, also das Byte eines int vorzeichenlos zu sehen, schneiden wir mit der Und-Verknüpfung die unteren 8 Bit heraus – alle anderen Bits bleiben also ausgenommen:

    byte b = (byte) 255;

    System.out.println( b ); // -1

    System.out.println( b & 0xff ); // 255

    Bibliotheksmethoden für vorzeichenlose Behandlung

    Immer ein & 0xff an einen Ausdruck zu setzen um die oberen Bytes auszublenden ist zwar nicht sonderlich aufwändig, aber schön ist das auch nicht. Hübscher sind Methoden wie toUnsignedInt(byte), die mit dem Namen deutlich dokumentieren, was hier eigentlich passiert. In Java 8 gibt es daher einige Neuerungen.

    Neue Methoden in Byte:

  • static int toUnsignedInt(byte x)
  • static long toUnsignedLong(byte x)
  • In Integer:

  • static long toUnsignedLong(int x)
  • static String toUnsignedString(int i, int radix)
  • static String toUnsignedString(int i)
  • static int parseUnsignedInt(String s, int radix)
  • static int compareUnsigned(int x, int y)
  • static int divideUnsigned(int dividend, int divisor)
  • static int remainderUnsigned(int dividend, int divisor)
  • In Long:

  • String toUnsignedString(long i, int radix)
  • static String toUnsignedString(long i)
  • static long parseUnsignedLong(String s, int radix)
  • static int compareUnsigned(long x, long y)
  • static long divideUnsigned(long dividend, long divisor)
  • static long remainderUnsigned(long dividend, long divisor)
  • In Short:

  • static int toUnsignedInt(short x)
  • static long toUnsignedLong(short x)
  • Neben den einfachen Methoden toUnsignedXXX()-Methoden in den Wrapper-Klassen gesellen sich Methoden hinzu, die auch die Konvertierung in einem String bzw. das Parsen eines Strings ermöglichen. Bei Integer und Long lassen sich ebenfalls neue Methoden ablesen, die Vergleiche, Division und Restwertbildung vorzeichenlos durchführen.

    Echte typsichere Container

    Verwenden Entwickler die Sammlungen untypisiert, also mit dem Raw-Typ, so lässt sich nicht verhindern, dass ein ungewünschter Typ die Sammlung betritt und beim Entnehmen zu einer Ausnahme führen kann. Der Grund liegt in der internen Umsetzung, dass nur der Compiler selbst die Typsicherheit sicherstellt, aber nicht die Laufzeitumgebung. Um die Typsicherheit zu erhöhen, bietet die Collections-Klasse ein paar Wrapper-Methoden, die eine Sammlung nehmen und Operationen nur eines gewissen Typs durchlassen – verstoßt ein Aufrufer dagegen, gibt es eine ClassCastException.

    class java.util.Collections

    § static <E> Collection<E> checkedCollection(Collection<E> c, Class<E> type)

    § static <E> List<E> checkedList(List<E> list, Class<E> type)

    § static <K,V> Map<K,V> checkedMap(Map<K,V> m, Class<K> keyType, Class<V> valueType)

    § static <E> Queue<E> checkedQueue(Queue<E> queue, Class<E> type)

    § static <E> Set<E> checkedSet(Set<E> s, Class<E> type)

    § static <K,V> SortedMap<K,V> checkedSortedMap(SortedMap<K,V> m, Class<K> keyType, Class<V> valueType)

    § static <E> SortedSet<E> checkedSortedSet(SortedSet<E> s, Class<E> type)

    Beispiel: Lasse in eine Menge nur Strings, aber nichts anderes.

    Set<String> set = Collections.checkedSet( new HashSet<String>(),

    String.class );

    set.add( "xyz" ); // Compiler OK

    Set rawset = set;

    rawset.add( "abc" ); // Compiler OK

    rawset.add( new Object() ); // Compiler OK,  N Laufzeitfehler

    Die Ausnahme ist: “Exception in thread "main" java.lang.ClassCastException: Attempt to insert class java.lang.Object element into collection with element type class java.lang.String”

    Auch wenn kein Programmierer freiwillig falsche Typen in eine Sammlung platziert, ist es doch besser, eine absolute Sicherheit zu bekommen, die nicht auf dem Compiler beruht. Wenn etwa ein Programm einer Skriptsprache die Möglichkeit eröffnet Elemente in eine Datenstruktur zu setzen, so lässt sich für die Skriptsprache eine geprüfte Sammlung zur Ablage nach außen geben. Achtet das Skript nicht auf den richtigen Typ, so knallt es im Skript. Das ist gewünscht, denn andernfalls würde viel später erst der falsche Typ bei der Bearbeitung auffallen und dann knallt es an der ganz falschen Stelle.

    JDK-Interna: Removes the use of shared character array buffers by String along with the two fields needed to support the use of shared buffers.

    Eine recht fette Änderung, die großen Einfluss auf die Performace von substring-Operationen hat. Denn die ist nun nicht mehr so schnell wir früher O(1), sondern O(length()). Dafür spart man Speicher bei jedem String-Objekt, und davon gibt es zur Laufzeit einer normalen Anwendung wirklich viele. Reduktion: zwei ints, also 2 x 4 Byte pro String-Objekt.

    Implementierung: http://hg.openjdk.java.net/jdk8/tl/jdk/rev/2c773daa825d

    File-Klassen auf NIO.2 umstellen

    Um sich von der Klasse java.io.File zu lösen und in Richtung Path zu migrieren, ist Handarbeit angesagt. Zunächst gilt es, alle Stellen zu finden, die im Workspace oder Projekt die Klasse File referenzieren. Am Einfachsten ist es, eine Anweisung wie File f; in den Code zu setzen, dann zum Beispiel in Eclipse Strg+Shift+G zu aktivieren. Es folgt eine Liste aller Vorkommen. Diese Liste muss nun abgearbeitet werden und der Code auf NIO.2 konvertiert werden. Nicht immer ist es so einfach

    Scanner s = new Scanner( new File(dateiname) );

    in

    Scanner s = new Scanner( Paths.get(dateiname) );

    umzusetzen oder

    new File( dateiname ).delete();

    in

    Files.delete( Paths.get( dateiname ) );

    Eine andere Stelle, an der Konvertierungen möglich sind, betreffen FileReader und FileWriter. Diese Klassen sind gefährlich, weil sie standardmäßig die im System voreingestellte Kodierung verwenden. Eine Kodierung wie UTF-8 im Konstruktor explizit vorzugeben ist jedoch nicht möglich. NIO.2 bietet eine bessere Methode, um an einen Reader/Writer aus einer Datei zu kommen und Entwickler werden gezwungen, die Kodierung immer sichtbar anzugeben.

    Deklaration

    Klassisch

    Mit NIO.2

    Reader r =

    new FileReader(f);

    Files.newBufferedReader(Paths.get(f),

    StandardCharsets.ISO_8859_1);

    Writer w =

    new FileWriter(f);

    Files.newBufferedWriter(Paths.get(f),

    StandardCharsets.ISO_8859_1);

    Beziehen eines Dateistroms klassisch und mit NIO.2

    Der Quellcode wird erst einmal länger, doch der Gewinn ist, dass die Kodierung nicht verschwindet und auch die Ein-/Ausgabe gleich gepuffert ist. Freunde der statischen Import-Anweisung komprimieren weiterhin zu Anweisungen wie

    Reader r = newBufferedReader( get( f ), ISO_8859_1 );

    Es ist auf den Fall Handarbeit angesagt. Eigene Blöcke, etwa zum Schreiben in Dateien können komplett zusammengestrichen werden auf einfache Methodenaufrufe wie

    Files.write( Paths.get( f ), bytes );

    List<String> lines = Files.readAllLines( Paths.get( f ), StandardCharsets.ISO_8859_1 );