Bei der Arbeit an Programmen trifft man auf immer wiederkehrende Aufgabenstellungen. Für diese bietet sich an, allgemeingültigen Code zu schreiben, der dann auch immer wieder verwendet werden kann. Diesen organisiert man am besten in Form von eigenen Bibliotheksklassen. Dazu möchte ich hier einige Beispiele zeigen.

Utility-Klassen

In Java gibt es einige Beispiel für Klassen, von denen keine Objekte erzeugt werden sollen, sondern deren Aufgabe es ist, nützliche Methoden zu einem ganz bestimmten Themenbereich bereit zu stellen. Ein typisches Beispiel dafür ist die Klasse java.lang.Math. Neben Klassenmethoden sind dort auch oft feste Werte als Konstanten mit hinterlegt. Analog dazu habe ich ebenfalls Methoden für kleinere, aber öfter benötigte Funktionalitäten in Klassen versammelt, die dem Namensschema XxxUtil folgen. Ich möchte hierzu Beispiele aus TextUtil und NumberUtil zeigen.

Bei der Arbeit mit Text steht man immer wieder vor der Aufgabe, bei einem »fremden« Wert (zum Beispiel aus einer Nutzereingabe oder beim Einlesen von Datendateien) zu prüfen, ob er überhaupt vorhanden, also nicht null oder nur ein leerer Text ist.

Dazu gibt es in TextUtil die Methode hasContent(...) der man den zu prüfenden Wert übergeben kann und die per boolean das Ergebnis zurück gibt. Oft will man, wenn das Ergebnis positiv ist, mit dem Text weiter arbeiten, und muss dann ggf. noch einmal die Methode trim() aufrufen, um eventuell vorhandene Leerzeichen an Anfang und Ende des Textes vor der Weiterarbeit zu entfernen. Um genau das zu vereinfachen, gibt es die Methode normalize(...).

public static String normalize(String str) {
  return (str == null) ? "" : str.trim(); } 

Sie garantiert, dass in jedem Fall ein String-Objekt zurück gegeben wird, wodurch dann folgendes Codemuster immer funktioniert:

String input = ...;
input = normalize(input);

if(input.isEmpty()) { // alternative Kodierung
  ... }
else {                // Verarbeitung der Eingabe
  ... } 

Wenn die alternative Kodierung bei einer fehlenden Eingabe nur darin besteht, dann mit einem festen Standardwert zu arbeiten, kann auch die Überladung von normalize(...) mit einem zweiten Parameter verwendet werden:

public static String normalize(
                     String str, String fallback) {
  if(fallback == null) {
    throw new NullPointerException("fallback");}
  str = normalize(str);
  return (str.length() == 0) ? fallback : str;
}

Aus der Methodenklasse NumberUtil möchte ich die Methode roundSmallValue(...) vorstellen. Ihre Aufgabe ist es, bei Gleitkomma-Werten im normal üblichen Bereich(small - im Vergleich zu dem Wertebereich von long) auf eine feste Anzahl von Nachkommastellen zu runden. Dadurch können zu »krumme« Werte, wie sie zum Beispiel beim Umrechnen von Maßeinheiten durch dabei ausgeführte Divisionen entstehen, auf ein sinnvolles Maß gerundet werden.

public static double roundSmallValue(double value, 
                                  int decimalCount) {

  if(decimalCount < -1 || 
            decimalCount > MAX_DECIMAL_COUNT) {
    String msg = "decimalCount must be within -1 ... " + 
        MAX_DECIMAL_COUNT + " but not: " + decimalCount;
    throw new IllegalArgumentException(msg);  }

  // else ...
  if(decimalCount == -1 || !Double.isFinite(value)) {
    return value; }

  // else ...
  // round to match required decimalCount
  double factor = Math.pow(10, decimalCount); 
	
  long rounded = Math.round(value * factor);
	
  return (rounded == Long.MAX_VALUE || 
          rounded == Long.MIN_VALUE) ? 
               value : rounded / factor;	

} 

Besondere Werte, wie Double.NaN, Double_POSITIVE_INFINITY und Double_NEGATIVE_INFINITY werden unverändert zurück gegeben. Das Gleiche gilt für Werte, bei denen die hier verwendete Math.round(...)-Methode einen der beiden Grenzwerte von Long zurück gibt (das sind dann Werte außerhalb des weiter oben genannten normal üblichen Bereichs).

Die im Code von round(...) verwendete Konstante MAX_PRECISION ist ebenfalls in NumberUtil kodiert und gibt den maximal möglichen Wert für den zweiten Aufrufparameter dieser Methode an. Im allgemeinen wird man hier mit Werten von 0 (keine Nachkommastelle) bis 3 arbeiten.

public static final int MAX_DECIMAL_COUNT = 6;
public static final long LONG_NULLMARKER = Long.MIN_VALUE;

Die hier auch mit gezeigte Konstante LONG_NULLMARKER wird von mir an einigen Stellen als Kennung dafür verwendet, dass hier gar kein long-Wert vorliegt bzw. ermittelt worden ist. Dadurch hat man an diesen Stellen für long einen gültigen Wert weniger zur Verfügung, andererseits dann aber einen zu 0 symmetrischen Wertebereich.

top ↑

Bibliothek für Maßeinheiten

Als Beispiel für aufeinander abgestimmte Interfaces und Klassen innerhalb von Bibliotheksklassen möchte ich das Interface Unit<T extends Unit<T>>, dessen Ableitungen und CommonAngleUnit als ein Beispiel für eine implementierende Klasse zeigen. Ursprünglich sind das Interface und die ersten Ableitungen und implementierenden Klassen bei der Arbeit zur Anzeige von Wetterdaten entstanden, um dem Nutzer bei der Anzeige der Werte die Möglichkeit zu geben, zwischen verschiedenen Maßeinheiten auswählen zu können. Ich möchte dies hier aber an dem einfachen Fall von Umrechnungen für Winkelmaßeinheiten zeigen.

Programmintern liegen die Werte jeweils für eine bestimmte Maßeinheit vor, sollen aber in einer anderen Maßeinheit angezeigt werden. Durch das Basisinterfaces wird die Methode valueOf(...) (in zwei Überladungen) gefordert, mit der ein gegebener Wert für eine beim Aufruf ebenfalls übergebene Maßeinheit (Unit) in den entsprechenden Wert der eigenen Maßeinheit umgerechnet werden kann.

public interface Unit<T extends Unit<T>> {

    /**
     * Marker for not given (unknown) double values 
     * - must bechecked by Double.isNaN(double).
     */
    double UNKNOWN_DOUBLE = Double.NaN;

    Class<? extends T> type();

    String symbol();

    String unitName();

    double valueOf(double value, T unit);

    double valueOf(double value, T unit, int precision);

}  

Die Parametrisierung mit T, dem Typ zur Kennzeichnung der (physikalischen) Größe, dient zur typsicheren Umwandlung, so dass auch syntaktisch erst gar nicht versucht werden kann, »Äpfel in Birnen« umzurechnen. Das wird an dem gleich noch gezeigten Beispiel deutlicher werden. Zuvor möchte ich aber kurz auf die Methoden eingehen, die das Basisinterface fordert.

Mit type() muss ein Class-Objekt für den Typ der Maßeinheit zurück gegeben werden, wie zum Beispiel AngleUnit - für Winkelmaßeinheiten. Außerdem muss eine Maßeinheit ihr Symbol und ihren Namen kennen. Mit den valueOf(...) - Methoden können schließlich Werte aus anderen Maßeinheiten (Bedingung: der gleiche Einheitentyp) in die Werte dieser Maßeinheit umgerechnet werden.

Ist zum Beispiel der Temperaturwert in Grad Celsius bekannt, soll aber als Grad Fahrenheit angezeigt werden, so wird valueOf(...) der Interface-Implementierung für Grad Fahrenheit aufgerufen. Dabei werden der Zahlenwert (in Grad Celsius) und, als die zugehörige Maßeinheit, die Interface-Implementierung für Grad Celsius übergeben. Der zurück gegebene Zahlenwert entspricht physikalisch der gleichen Temperatur, nur eben in Grad Fahrenheit.

Ich möchte das Prinzip zur Kodierung der Umrechnung an dem (einfacheren) Beispiel von Winkeleinheiten zeigen. Dazu gibt es die typisierte Ableitung AngleUnit des Basisinterfaces.

public interface AngleUnit extends Unit<AngleUnit> {

  double fullCircleValue();

}	

Diese fordert durch die weitere Methode fullCircleValue() die Angabe des Wertes, der einen vollen Kreiswinkel definiert. Gleichzeitig wird die Typangabe T auf AngleUnit eingeschränkt, so dass sich damit nur Objekte von Klassen, die ebenfalls dieses Interface implementieren, als zweiter Parameter der convert(...) - Methode übergeben lassen.

Die Kodierung von CommonAngleUnit zeigt, wie sich auf dieser Basis die Umrechnungen realisieren lassen. Es ist ein enum, mit dem aktuell die drei Winkelmaßeinheiten Altgrad, Neugrad und Bogenmaß realisiert werden.

public enum CommonAngleUnit implements AngleUnit {

  DEGREE (360, "°", "degree"),
  GON(400, "gon", "gon"),			
  RAD(2*Math.PI, "rad", "radians")
  ;

  // constants
  private static final Class<CommonAngleUnit> CL = 
                       CommonAngleUnit.class;	

  // fields
  private final double max;    // value of full circle
  private final String symbol, name;

  // constructor
  private CommonAngleUnit(
             double max, String s, String n) {
    this.max = max;    symbol = s;     name = n;  }

  // methods

  @Override
  public Class<CommonAngleUnit> type() { 
    return CL; }

  @Override
  public String symbol() { return symbol; }

  @Override
  public String unitName() {return name; }

  @Override
  public double fullCircleValue() { return max; }	


  @Override
  public double valueOf(
                  double value, AngleUnit unit) {

    if(unit == this) { return value; }

    // else ...
    return value * max / unit.fullCircleValue();

  } 


  // decimalCount: -1 no rounding
  @Override
  public double valueOf(double value, 
                  AngleUnit unit, int decimalCount) {

    // checks
    if(decimalCount < -1) {
      String msg = 
          "decimalCount must be -1 (no roundings)," +
          " 0, 1, ... - but not "+ decimalCount;
      throw new IllegalArgumentException(msg); }
    else if(decimalCount > 
            NumberUtil.MAX_DECIMAL_COUNT) {
      String msg = "decimalCount+" (> " + 
        NumberUtil.MAX_DECIMAL_COUNT+") is unsupported"
      throw new UnsupportedOperationException(msg); }

    if(unit != this) {
      value = valueOf(value, unit);  }

    if(decimalCount >= 0) {
      value = NumberUtil.roundSmallValue(
                              value, decimalCount); }

    return  value;

  } 

}	

Hierbei wird die Tatsache ausgenutzt, dass ein enum zwar eine besondere Klasse, mit einigen (zusätzlichen) besonderen Regeln ist, gleichzeitig aber die allgemeinen Regeln für eine Klasse ebenfalls gelten. Damit kann man hier auch mit internen eigenen Variablen arbeiten und auch weitere, zusätzliche Methoden kodieren. Dadurch lässt sich auch das Interface AngleUnit implementieren und alle Objekte dieses enum können dort eingesetzt werden, wo AngleUnit - Objekte syntaktisch zulässig sind.

Nach diesen »Vorarbeiten« kann nun auch, zum Abschluss, die praktische Kodierung von Werteumrechnungen gezeigt werden:

import static CommonAngleUnit.*;

double angleInDegree = 180;	// half circle

double angleInGon = 
   GON.valueOf(angleInDegree, DEGREE);
 
double angleInRadians = 
   RAD.valueOf(angleInDegree, DEGREE);
top ↑

Demo zum Ausprobieren

Die gerade gezeigte »feste« Umrechnung von Werten ist immer dann sinnvoll, wenn Werte in einer bestimmten Maßeinheit eingelesen, intern aber als Werte einer anderen Maßeinheit weiter verarbeitet werden sollen. Diese Umrechnung lässt sich auch als direkte Berechnung kodieren, der Einsatz der Bibliotheksklassen bietet aber den Vorteil der schnellen Änderung, wenn die Werte zum Beispiel zukünftig in einer anderen Maßeinheit abgespeichert bzw. eingelesen werden sollen.

Winkelumrechner

Auch bei einer flexiblen Eingabe, zum Beispiel per Eingabefeld für den Nutzer, oder der konfigurierbaren Anzeige von Werten je nach Nutzerwunsch kann gut mit den Bibliotheksklassen gearbeitet werden. Ein Beispiel, bei dem beide Möglichkeiten benötigt werden, ist ein kleines Programm zur Umrechnung von Maßeinheiten - hier gezeigt als Winkelumrechner. Die Bedienoberfläche ist weitgehend selbsterklärend, so dass ich nur den Einsatz der Bibliotheksklassen zeigen möchte.

Die Umrechnung ist innerhalb dieses kleinen Beispiels in der Methode convert() kodiert, die sowohl als Reaktion auf einen Buttonklick als auch bei einer Änderung der Auswahl in einem der beiden Felder für die Maßeinheiten aufgerufen wird (dabei wird auch der Wert für srcUnit bzw. targetUnit neu gesetzt).

private void convert() {

  String msg = "convert() called";

  try {
    double value = 
          Double.parseDouble(txfInput.getText());
    double convertedValue = 
          targetUnit.valueOf(value, srcUnit, 2);
    msg = value + " " + srcUnit.symbol() + " = " + 
          convertedValue + " " + targetUnit.symbol();
  }
  catch(Exception e) {
    msg = "Fehler: " + e.getClass().getSimpleName() + 
          "   "+e.getMessage(); 
  }

  lblMessages.setText(msg);

}

Die Kodierung des Winkelumrechner-s ist eine einfache Java Klasse für die Bedienoberfläche, die verwendeten Bibliotheksklassen können hier herunter geladen werden.