Inselupdate: Methode mit variabler Argumentanzahl (Vararg)

Bei vielen Methoden ist es klar, wie viele Argumente sie haben; eine Sinus-Methode bekommt nur ein Argument, equals(Object) ein Objekt, println(…) nichts oder genau ein Argument, usw. Es gibt jedoch Methoden, bei denen die Zahl mehr oder weniger frei ist. Ein paar Beispiele:

· Wenn der Aufruf System.out.printf(formatierungsstring, arg1, args2, arg3, …) etwas auf dem Bildschirm ausgibt, ist erst einmal nicht bekannt, wie viele Argumente die Methode besitzt, denn sie sind abhängig vom Formatierungsstring.

· Fügt Collections.addAll(sammlung, elem1, elem2, elem3, …) etwas einer Sammlung hin, ist frei, wie viele Elemente es sind.

· Wird ein Pfad für das Dateisystem zusammengebaut, ist vorher unbekannt, wie viele Segmente Paths.get( anfang, segment1, segment2, … ) besitzt.

· Startet new ProcessBuilder("kommando", "arg1", "arg1", …).start() ein Hintergrundprogramm, ist der Methode unbekannt, wie viele Argumente dem externen Programm übergeben werden.

Um die Anzahl der Parameter beliebig zu gestalten, sieht Java Methoden mit variabler Argumentanzahl, auch Varargs genannt, vor.

Eine Methode mit variabler Argumentanzahl nutzt die Ellipse (»…«) zur Verdeutlichung, dass eine beliebige Anzahl Argumente angegeben werden dürfen, dazu zählt auch die Angabe keines Elements. Der Typ fällt dabei aber nicht unter den Tisch; er wird ebenfalls angegeben.

System.out.printf() nimmt eine beliebige Anzahl von Argumenten an

Eine Methode mit Varargs haben wir schon einige Male verwendet: printf(…). Die Deklaration ist wie folgt:

class java.io.PrintStream extends FilterOutputStream implements Appendable, Closeable

PrintStream printf(String format, Object… args)
Nimmt eine beliebige Liste von Argumenten an und formatiert sie nach dem gegebenen Formatierungsstring format.

Gültige Aufrufe von printf(…) sind demnach:

Aufruf

Variable Argumentliste

System.out.printf("%n")

ist leer

System.out.printf("%s", "Eins")

besteht aus nur einem Element: "Eins"

System.out.printf("%s,%s,%s", "1", "2", "3")

besteht aus drei Elementen: "1", "2", "3"

Maximum eines Feldes finden

Die Klasse java.lang.Math sieht eine statische max(…)-Methode mit zwei Argumenten vor, doch grundsätzlich könnte die Methode auch beliebig viele Argumente entgegennehmen und von diesen Elementen das Maximum bilden. Die Methode könnte so aussehen:

static int max( int… array ) {
}

max(…) behandelt den Parameter array wie ein Feld. Da wir Argumente vom Typ int fordern, ist array vom Typ int[] und kann so zum Beispiel mit dem erweiterten for durchlaufen werden:

for ( int e : array )

Werden variable Argumentlisten in der Signatur definiert, so dürfen sie nur den letzten Parameter bilden; andernfalls könnte der Compiler bei den Parametern nicht unbedingt zuordnen, was nun ein Vararg und was schon der nächste gefüllte Parameter ist:

public class MaxVarArgs {

  static int max( int… array ) {
    if ( array == null || array.length == 0 )
      throw new IllegalArgumentException( "Array null oder leer" );
    int currentMax = Integer.MIN_VALUE;
    for ( int e : array )
      if ( e > currentMax )
        currentMax = e;
    return currentMax;
  }
  public static void main( String[] args ) {
    System.out.println( max(1, 2, 9, 3) );     // 9
  }
}

Tipp: Vararg-Design. Muss eine Mindestanzahl von Argumenten garantiert werden – bei max(…) sollten das mindestens zwei sein – ist es besser, eine Deklaration wie folgt zu nutzen: max(int first, int second, int… rest).

Compiler baut Array auf

Der Nutzer kann jetzt die Methode aufrufen, ohne ein Feld für die Argumente explizit zu definieren. Er bekommt auch gar nicht mit, dass der Compiler im Hintergrund ein Feld mit vier Elementen angelegt hat. So übergibt der Compiler:

System.out.println( max( new int[] { 1, 2, 9, 3 } ) );

An der Schreibweise lässt sich gut ablesen, dass wir ein Feld auch von Hand übergeben können:

int[] ints = { 1, 2, 9, 3 };
System.out.println( max( ints ) );

Hinweis. Da Varargs als Felder umgesetzt werden, sind überladene Varianten wie max(int… array) und max(int[] array), also einmal mit einem Vararg und einmal mit einem Feld, nicht möglich. Besser ist es hier, immer eine Variante mit Varargs zu nehmen, da diese mächtiger ist. Einige Autoren schreiben auch die Einstiegsmethode main(String[] args) mit variablen Argumenten, also main(String… args). Das ist gültig, denn im Bytecode steht ja ein Array.

Arrays.asList() Beispiel und Hinweis

Beispiel: Gib das größte Element eines Feldes aus.

Integer[] ints = { 3, 9, -1, 0 };

System.out.println( Collections.max( Arrays.asList( ints ) ) );

Zum Ermitteln des Maximums bietet die Utility-Klasse Arrays keine Methode, daher bietet sich die max(…)-Methode von Collections an. Auch etwa zum Ersetzen von Feldelementen bietet Arrays nichts, aber Collections. Sortieren und Füllen kann Arrays aber schon, hier muss asList() nicht einspringen.

Hinweis: Wegen der Generics ist der Parameter-Typ von asList() ein Objekt-Feld, aber niemals ein primitives Feld. In unserem Beispiel von eben würde so etwas wie

int[] ints = { 3, 9, -1, 0 };

Arrays.asList( ints );

zwar kompilieren, aber die Rückgabe von Arrays.asList(ints) ist vom Typ List<int[]>, was bedeutet, die gesamte Liste besteht aus genau einem Element und dieses Element ist das primitive Feld. Zum Glück führt Collections.max(Arrays.asList(ints)) zu einem Compilerfehler, denn von einer List<int[]>, also eine Liste von Feldern, kann max(Collection<? extends T>) kein Maximum ermitteln.

How to put files in a ZIP file with NIO.2

URI p = Paths.get( "c:/Users/Christian/Dropbox/jokes.zip" ).toUri();
URI uri = URI.create( "jar:" + p );

Map<String, String> env = new HashMap<>();
env.put( "create", "true" );
try ( FileSystem zipfs = FileSystems.newFileSystem( uri, env ) ) {
  Files.write( zipfs.getPath( "/j1.txt" ), "The truth is out there. Anybody got the URL?".getBytes() );
  Files.write( zipfs.getPath( "/j2.txt" ), "The more I C, the less I see.".getBytes() );
}

Designfrage: Wie nutzt man GWT-RPC-Aufrufe lokal etwa bei Offline-Anwendungen?

Standardmäßig sieht es ja im “normalen” entfernen RPC-Aufruf so aus: Als erstes die Schnittstelle:

public interface ContactRpcService extends RemoteService {

  Contact getContactById( long id );

}

Dann die zugehörige Async-Schnittstelle:

public interface ContactRpcServiceAsync {

  void getContactById( long id, AsyncCallback<Contact> callback );

}

Der Client hat nun so was wie

ContactRpcServiceAsync contactService = GWT.create( ContactRpcService.class );

contactService.getContactById( contactId, new DefaultCallback<Contact>() {

  @Override protected void handleResponse( Contact response ) {

    ….

  }

} );

DefaultCallback ist eine meine abstrakte Klasse, die AsyncCallback implementiert, aber das ist jetzt nicht so wichtig.

Auch etwa anders mache ich noch, damit ich weniger schreiben muss; ich habe mir eine Klasse Rpc deklariert, mit Konstanten für alle GWT-creates():

public class Rpc {

  private Rpc() {}

  public final static ContactRpcServiceAsync  contactService = GWT.create( ContactRpcService.class );

  // … un ddie Anderen

}

Normalerweise sieht es also bei mir so aus:

Rpc.contactService.getContactById( contactId, new DefaultCallback<Contact>() {

  @Override protected void handleResponse( Contact response ) {

    …

  }

} );

Damit nun Rpc.contactService.getContactById() im Offline-Modus etwas anderes macht, kann man zum Beispiel folgendes tun: Rpc.contactService stammt nicht von GWT.create(), sondern ist ein Proxy. Falls nun der Remote-Fall gewünscht ist, delegiert man an an die echte Rpc-Implementierung, andernfalls an eine lokale Variante.

public class Rpc {

  private Rpc() {}

  public final static ContactRpcServiceAsync  contactService = new ContactRpcServiceAsync() {

    private final ContactRpcServiceAsync delegate = GWT.create( ContactRpcService.class );

    private final ContactRpcServiceAsync local = new ContactRpcServiceAsync() {
      @Override public void getContactById( long id, AsyncCallback<Contact> callback ) {

        // hier alles lokale machen, also etwas aus dem Cache holen. Wenn alles gut geht, und die Daten vorhanden sind, dann aufrufen
        callback.onSuccess( result );
      }

    };

    @Override public void getContactById( long id, AsyncCallback<Contact> callback ) {

      wenn der remote Fall

        delegate.getContactById( id, callback );

      andernfalls

        local.getContactById( id, callback );

    }

  };

}

JPA-Beispiel in wenigen Minuten mit OpenJPA

  1. Beziehe unter http://mvnrepository.com/artifact/org.apache.openjpa/openjpa-all das openjpa-all-2.2.0.jar (etwas mehr als 6 MiB) und setzte es in den Klassenpfad.
  2. Habe einen Datentreiber im Klassenpfad (bei mir den der HSQLDB).
  3. Lege im Projekt einen Ordner META-INF an, platziere dort eine Datei persistence.xml:
  4. <?xml version="1.0" encoding="UTF-8"?>
    <persistence xmlns="http://java.sun.com/xml/ns/persistence"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://java.sun.com/xml/ns/persistence persistence_2_0.xsd"
      version="2.0">
      <persistence-unit name="traida" transaction-type="RESOURCE_LOCAL">
        <class>traida.shared.domain.Contact</class>
        <properties>
          <property name="openjpa.jdbc.DBDictionary" value="hsql"/>
          <property name="javax.persistence.jdbc.url" value="jdbc:hsqldb:file:hsqldbtest;user=sa" />
          <property name="javax.persistence.jdbc.driver" value="org.hsqldb.jdbcDriver" />
          <property name="openjpa.Log" value="DefaultLevel=ERROR, Tool=ERROR" />
          <property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema(ForeignKeys=true)"/>
        </properties>
      </persistence-unit>
    </persistence>

    Mehr zu den Properties unter http://openjpa.apache.org/builds/apache-openjpa-2.2.1-SNAPSHOT/docs/docbook/manual/main.html.

  5. Lege eine Klasse traida.shared.domain.Contact an:
  6. @Entity
    public class Contact {
      @Id
      @GeneratedValue( strategy = GenerationType.IDENTITY )
      public Long id;
      public String name;
      // Setter/Getter ausgelassen
    }

  7. Schreibe eine main(String[])-Methode mit:
  8. EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory( "traida" );
    EntityManager entityManager = entityManagerFactory.createEntityManager();
    entityManager.getTransaction().begin();
    Contact c = new Contact();
    c.name = "Hallo Willi";
    entityManager.persist( c );
    entityManager.getTransaction().commit();
    System.out.println( entityManager.createQuery( "select c from Contact c" ).getResultList() );
    entityManager.close();

  9. Fertig, jetzt freuen.

GunZip-Kommandozeilenprogramm in Java

package com.tutego.insel.io.zip;

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

public class gunzip {

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

    String filename = args[ 0 ];
    Path srcPath, destPath;

    if ( filename.toLowerCase().endsWith( ".gz" ) ) {
      srcPath  = Paths.get( filename );
      destPath = Paths.get( filename.replaceAll( "\\\\.gz$", "" ) );
    }
    else {
      srcPath  = Paths.get( filename + ".gz" );
      destPath = Paths.get( filename );
    }

    try ( InputStream is  = new GZIPInputStream( Files.newInputStream( srcPath ) ) ) {
      Files.copy( is, destPath );
    }
    catch ( IOException e ) {
      System.err.println( "Fehler: Kann nicht entpacken " + filename );
    }
  }
}

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 ] );
    }
  }
}

Frage: Sollte man überall this. verwenden?

Ich verwende es nicht, aus einem guten Grunde: Ein this macht mir explizit deutlich, dass es eine lokale Variable gibt, die vom Namen hier die Objektvariable überdeckt, siehe typisches Beispiel vom Getter:

public void setAge( int age )
{
  this.age = age;
}

Wenn ich this. sehe, erwarte ich einfach, dass es eine Überschattung gibt und ich würde mich wundern, wenn this. einfach "nur so" verwendet wird. Ich finde es wichtig, dass man durch solche Idiome etwas aussagt.

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.