Java: Floating-Point-Quiz

von Hubert Schmid vom 2012-11-04

Auf viele Fachfragen in Vorstellungsgesprächen wird keine bestimmte Antwort erwartet. Der Fragesteller möchte viel mehr herausfinden, wie der Kandidat mit der Aufgabe umgeht und auf welche Weise er sich der Lösung nähert. Von dieser Art und diesem Niveau sind auch die folgenden Fragen. Und wie in Vorstellungsgesprächen geht es mir dabei ebenfalls nicht um die konkreten Antworten, sondern viel mehr um die Aussagen, die sich zwischen den Zeilen verbergen

Rahmen

Ich beginne mit dem Rahmenprogramm, das im Folgenden für die einzelnen Aufgaben um jeweils eine Methode erweitert wird. Dazu werden die drei einfachen Hilfsfunktionen parse, format und check verwendet, um den Blick später auf das Wesentliche zu fokussieren. Die eigentliche Aufgabe besteht jeweils darin, in der main-Methode die check-Aufrufe durch Aufrufe der jeweils neu eingeführten Methode zu ersetzen. Die Frage ist: Wie viele check-Aufrufe lassen sich auf diese Weise ersetzen?

public final strictfp class Quiz { static double parse(String value) { return Double.parseDouble(value); } static String format(double value) { return String.format("%.2f", value); } static void check(String a, String b, double c, double d) { System.out.println(a.equals(b) + " " + (c == d)); } // see below for methods: quiz1, quiz2, quiz3 and quiz4 public static void main(String... args) { check("1.0", "2.0", 1.0, 2.0); // "false false" check("1.0", "2.0", 1.0, 1.0); // "false true" check("1.0", "1.0", 1.0, 2.0); // "true false" check("1.0", "1.0", 1.0, 1.0); // "true true" } }

Das Programm gibt in dieser Form alle vier Kombinationen aus false und true aus. Das Schlüsselwort strictfp verwende ich lediglich, um den Interpretationsspielraum der Aufgabe zu verringern. Im Wesentlichen bewirkt es, dass sich Gleitkommazahlen auf allen Plattformen identisch verhalten. Abgesehen davon nehme ich an, dass der obige Code hinreichend verständlich ist.

Erste Methode

Die erste Methode sieht wie folgt aus, und ich wiederhole nochmals die Frage: Wie viele der check-Aufrufe aus obigem Programm lassen sich durch Aufrufe dieser Methode mit geeigneten Argumenten ersetzen, ohne die Ausgabe des Programms zu verändern?

static void quiz1(String lhs, String rhs) { check(lhs, rhs, parse(lhs), parse(rhs)); }

Tipp: Zwei der vier Aufrufe sind trivial ersetzbar. In einem der beiden verbleibenden Fälle stellt sich die Frage, ob der gleiche Wert unterschiedlich dargestellt werden kann. Diese Eigenschaft teilen sich Gleitkomma- und Ganzzahlen.

Zweite Methode

In der zweiten Aufgabe wird der umgekehrte Weg gesucht. Anstatt Zeichenketten zu übergeben und zu konvertieren, werden nun Gleitkommazahlen übergeben und in Zeichenketten formatiert.

static void quiz2(double lhs, double rhs) { check(format(lhs), format(rhs), lhs, rhs); }

Tipp: Man werfe nochmals einen Blick auf die Hilfsfunktion format: Offensichtlich kann bei der Formatierung in eine Zeichenkette Information verloren gehen, so dass zwei unterschiedliche Werte zum gleichen Ergebnis führen.

Dritte Methode

Die dritte Methode macht die Aufgabe einen Tick schwieriger, indem eine zusätzliche Indirektion eingeführt wird. Die Fragestellung bleibt die Gleiche: Wie viele der check-Aufrufe aus dem ursprünglichen Programm lassen sich durch Aufrufe dieser Methode mit geeigneten Argumenten ersetzen, ohne die Ausgabe zu verändern?

static void quiz3(String lhs, String rhs) { quiz2(parse(lhs), parse(rhs)); }

Tipp: Warum gibt es überhaupt Gleitkommazahlen in Java? Das ist eine merkwürdigere Frage und ich übertrage sie nochmals auf Ganzzahlen: Möchte man Ganzzahlen mit beschränktem Wertebereich haben – fehleranfälligen Code aufgrund von Überläufen sowie Arrays und Collections beschränkt auf zwei Milliarden Elemente? Eigentlich nicht. Python zeigt, dass es anders geht: Dort gibt es nur noch einen Ganzzahltyp, der einfach zu verwenden, unbeschränkt und effizient ist.

Vierte Methode

Vollständig wird das Ganze mit der letzten Umkehrung:

static void quiz4(double lhs, double rhs) { quiz1(format(lhs), format(rhs)); }

Tipp: double und float zeichnet aus, dass sie mit wenigen Bits einen großen Wertebereich mit hoher Genauigkeit abdecken und schnelle Operationen durch Hardware-Unterstützung anbieten. Doch gerade die Genauigkeit ist häufig ein Problem. In vielen Fällen sind die Fehler allerdings vernachlässigbar, beispielsweise bei der Darstellung eines Verhältnisses in Prozent: String.format("%.0f%%", 100.0 * 4 / 6)

Man sollte sich stets vor Auge führen, dass selbst einfache Formeln wie a + b - a == a und a / 100.0 * 100.0 == a in aller Regel mit Gleitkommazahlen nicht funktionieren. Wenn es auf jede Stelle ankommt, sollten sie einfach nicht verwendet werden. Das gilt insbesondere für Geldbeträge, für die java.math.BigDecimal die richtige Wahl ist.

Lösung

Es waren insgesamt vier Aufgaben, und in jeder Aufgabe ging es darum, wie viele Aufrufe sich durch die jeweils eingeführte Methode ersetzen lassen. In Summe ist 16 eine obere Schranke für die gesuchte Anzahl. Davon sind acht Aufrufe trivial zu ersetzen gewesen, und drei weitere Ersetzungen habe ich bereits verraten. Darüber hinaus habe ich eine wichtige Hinweise gegeben, die mehr oder weniger direkt zur Lösung führten.

Natürlich ist klar: Alle 16 Ersetzungen sind einfach möglich. Jede andere Lösung wäre mir auch zu umständlich gewesen. Denn es ist fast immer einfacher zu zeigen, was geht statt was nicht geht.