Mehrfache Interfaces mit inneren Klassen

Die im Beitrag über Multidispatch angedeutete Möglichkeit, mit inneren Klassen oder Wrappern anderen „equals()“ und „hashCode()“-Methoden anzubieten, sollte vielleicht etwas genauer gezeigt werden. Als Beispiel dient hier einmal Java, aber die Thematik ist auch in Scala, Ruby, Perl, C# und anderen Sprachen vorhanden, weil es gängig ist, dass die Hashtabellen gerne mit einer einer bestimmten Kombination aus hashCode() und eqals() zusammenarbeiten. Das zwingt dazu, diese beiden Methoden nach der gängigsten Verwendung in Hashtabellen zu definieren.

Die Idee lässt sich aber auch anwenden, um verschiedene Interfaces mit derselben Klasse zu implementieren.

Das Prinzip sieht so aus:


public interface A {
    public int fa(int x);
}

public interface B {
    public int fb(int x);
}

public class X implements A {

    ....
    public int fa(int x) {
        ....
    }
    private class Y implements B {
        public int fb(int x) {
             return fa(x) % 999;
        }
    }
    public B getAsB() {
        return new Y()
    }
}

Man kann also die Implementierung von B in der inneren Klasse Y vorsehen und dabei auf alle Methoden und Attribute der umgebenden Klasse zugreifen. Die Referenz auf die umgebende Klasse ist bei allen Instanzen der (nicht-statischen) inneren Klasse implizit dabei.

Hier ein Beispiel für verschiedene hashCode() und equals-Methoden:


public interface StringPair {
    public String getLhs();
    public String getRhs();
}

public class StringPairImpl implements StringPair {
    private final String lhs;
    private final String rhs;

    public StringPairImpl(String lhs, String rhs) {
        assert lhs != null;
        assert rhs != null;
        this.lhs = lhs;
        this.rhs = rhs;
    }

    public String getLhs() {
        return lhs;
    }

    public String getRhs() {
        return rhs;
    }

    public int hashCode() {
        return lhs.hashCode() * 91 + rhs.hashCode();
    }

    public boolean equals(Object obj) {
         if (this == obj) {
             return true;
         } else if (! obj instanceof StringPairImpl) {
             return false;
         } else {
             StringPairImpl other = (StringPairImpl) obj;
             return this.lhs.equals(other.lhs) && this.rhs.equals(other.rhs);
         }
    }

    private class IgnoreCase implements StringPair {

        public String getLhs() {
            return lhs;
        }

        public String getRhs() {
            return rhs;
        }

        public int hashCode() {
            return lhs.toUpperCase().hashCode() * 91 + rhs.toUpperCase().hashCode();
        }

        public boolean equals(Object obj) {
             if (this == obj) {
                 return true;
             } else if (! obj instanceof StringPairImpl) {
                 return false;
             } else {
                 StringPairImpl other = (StringPairImpl) obj;
                 return this.lhs.toUpperCase().equals(other.lhs.toUpperCase())
                     && this.rhs.toUpperCase().equals(other.rhs.toUpperCase());
             }
        }
    }

    public StringPair getWithCaseIgnored() {
        return new IgnoreCase();
    }
}

Die innere Klasse bietet also eine Implementierung desselben Interfaces, aber hashCode() und equals() ignorieren diesmal Groß- und Kleinschreibung.

Eine andere Möglichkeit wäre es bei Collections, eine immutable-Variante über eine innere Klasse anzubieten und eine getImmutable()-Methode einzubauen. Das ist aber bekanntlich nicht der Weg, den die JDK-library gewählt hat.

Was man aber dabei auch sieht: Leider muss man sehr viel „unnötigen“ Code schreiben, der eigentlich offensichtlich ist, aber eben doch nicht fehlen darf. Das erschwert sowohl das Schreiben als auch das Lesen des Codes, vor allem aber die Wartbarkeit. So wird die an sich gute Idee der inneren Klassen teilweise wieder verwässert.

Share Button

Snapshot too old – Behandlung langlaufender SELECTs

Wer größere Datenbank-Applikationen entwickelt, wird sich mit dem Problem auseinandersetzen müssen, was bei langlaufenden Abfragen eigentlich passiert.

Man hat also ein „SELECT“ am laufen, dass mehrere Sekunden oder sogar Minuten dauert, vielleicht sogar eine Stunde. Das kann durchaus sinnvoll sein, aber es lohnt sich natürlich, dieses mit gutem Wissen über die Datenbank-Software genau anzuschauen und zu optimieren.

Trotz aller Optimierungen muss man aber prinzipiell davon ausgehen, dass sich die Daten während des Lesezugriffs ändern. Es finden dauernd Schreibzugriffe statt, die natürlich erst nach Abschluss der Transaktion mit einem „COMMIT“ wirklich sichtbar werden. Nun können aber während des Lesezugriffs jede Menge Transaktionen stattfinden. Würde man diese berücksichtigen, müsste der Lesezugriff dauernd von vorne anfangen oder er würde inkonsistente Daten zurückliefern, die von verschiedenen Ständen der Datenbank stammen.

Die Lösung ist, den Stand der Datenbank vom Beginn des Lesezugriffs quasi einzufrieren und für die Dauer des Lesezugriffs zur Verfügung zu stellen. Zugriffe, die später beginnen, bekommen einen anderen Stand zu sehen, auch wenn sie früher fertig werden. Im Fall von Oracle-Datenbanken werden hierfür sogenannte Snapshots verwendet. Es werden also ähnliche Mechanismen wie für Transaktionen verwendet, um mehrere Stände der Datenbank parallel zu speichern.

Das bedeutet aber, dass bei einer sehr aktiven Datenbank immer größere Unterschiede zwischen den beiden Ständen entstehen. Bei vielen Transaktionen oder vielen parallelen langlaufenden Lesezugriffen sind sogar oft viel mehr als zwei Stände. Dafür gibt es Bereiche in der Datenbank, sogenannte Rollback-Segmente. Diese muss man definieren und bereitstellen und wenn sie zu klein sind, laufen sie irgendwann über. Dann kommt es im Fall von Oracle zu dem Fehler „ORA-01555: Snapshot too old“ und der lange Lesezugriff scheitert. Wenn man Pech hat nach ein paar Stunden. Wenn es ein Zugriff durch eine Software ist, wird es interessant, ob die Fehlerbehandlung für diesen Fall funktioniert oder ob es eine obskure „Exception“ in der Log-Datei gibt, die niemand bemerkt.

Mehr dazu findet man unter anderem hier:

Grundsätzlich muss diese Fragestellung bei allen ernsthaften transaktionalen Datenbanken gelöst werden und es ist sicher interessant, wie das im Detail aussieht. Vielleicht ist bei anderen Datenbanken so etwas wie eine „Lese-Transaktion“ („read transaction“) die Antwort. Als Isolationslevel muss man mindestens „repeatable read“ oder „serializable“ wählen. Wenn man mit niedrigeren Isolationsleveln arbeitet, muss man genau wissen, was man tut, da es sonst überraschende Fehler gibt.

Bei PostgreSQL muss man das mit
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
oder
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
explizit einstellen, sonst wird „READ COMMITTED“ gewählt.

Bei mariaDB und mysql hängt es von der DB-Engine ab. InnoDB verwendet als Default „REPEATABLE READ“, DB2 scheint eher auf „READ COMMITTED“ zu setzen, MS-SQL-Server auch auf READ COMMITTED.

Höhere Isolation-Level bedingen natürlich auch das Risiko von Verklemmungen („Deadlocks“), was erkannt werden sollte und zum Rollback einer der beiden Transaktionen führen sollte.

Es lohnt sich also, bei anspruchsvollen DB-Applikationen genau zu schauen, wie man mit diesen Transaktion-Isolationen umgehen will und ob man mit den Default-Eintstellungen der verwendeten Datenbank-Software zufrieden ist.

Vor einigen Jahren habe ich übrigens feststellen müssen, dass man diese niedrigeren Isolationslevel bei Oracle wohl einstellen konnte, dass die Datenbank oder der JDBC-Treiber sich dann aber fehlerhaft verhalten haben. Man sollte also auch herausfinden, ob die entsprechende Eintstellung von der verwendeten Datenbanksoftware unterstützt wird und zwar nicht nur auf dem Papier, sondern real und absolut zuverlässig. Eine unzuverlässige transaktionale Datenbank kann man billig durch einen Zufallszahlengenerator ersetzen.

Share Button

2014

Καλή Χρονια — Feliz ano novo — Gelukkig nieuwjaar — Šťastný nový rok — Boldog új évet — عام سعيد — bun di bun an — Onnellista uutta vuotta — С новым годом — 新年好 — Godt nytt år — Feliĉan novan jaron — Hääd uut aastat — Ath bhliain faoi mhaise — Akemashite omedetô — Срећна нова година — Gullukkig niuw jaar — Szczęśliwego nowego roku — laimīgu jauno gadu — Srechno novo leto — Gott nytt år — Bonne année — Щасливого нового року — Godt nytår — سال نو مبارک — Un an nou fericit — Felix sit annus novus — Felice anno nuovo — Feliz año nuevo — Sretna nova godina — Laimingų naujųjų metų — Šťastný nový rok — Frohes neues Jahr — Gleðilegt nýtt ár

Share Button

OR-mappings: Hibernate & Co.

Die meisten Software-Projekte verwenden eine Datenbank. Es ist immer eine Herausforderung, die Datenbank und die Software zusammenzubringen, weil das zwei verschiedene Welten sind, die ihre eigene Entwicklung genommen haben.

Hierzu haben sich ganz verschiedene Ansätze etabliert.

  • DB-zentrisches Vorgehen: Ein starker DBA oder ein starkes DB-Team fängt zunächst an, das Datenmodell zu erstellen. Die Software muss sich daran anpassen. Für die performance-kritischen Zugriffe erhalten die Entwickler Unterstützung von einem DBA.
  • Eine Variante davon ist die Verwendung des Active-Record-Patterns, wo die Objekte aus der Datenbankstruktur generiert werden. Dieses Active-Record-Pattern verwendet man bei Ruby on Rails.
  • OR-zentrisches Vorgehen: Man verwendet objekt-relationale Mapper und definiert Klassen in seiner Software und ein paar XML-Dateien oder Annotationen dazu und hofft dann, dass der OR-Mapper nicht nur relationale Daten in Objekte und Objekte in relationale Daten wandelt, sondern dass auch noch gleich das Datenmodell und die Zugriffe automatisch generiert werden. Das ist der typische Ansatz bei Hibernate, JDO, Eclipselink etc.
  • Auch in der Rails-Welt versucht man es oft und gerne ohne DBA und lässt das DB-Schema generieren.
  • Man verzichtet auf die relationale SQL-Datenbank und arbeitet mit NoSQL-Datenbanken, die oft etwas näher an der Software liegen

Letztlich muss man sich aber die Fragen beantworten, ob man die Datenbank in den Mittelpunkt stellt und die Software passend zur Datenbank entwickelt oder ob man die Software in den Mittelpunkt stellt und die Datenbank nur als eine Art Persistenzmechanismus dafür verwendet.

Dann kommt die zweite Frage ins Spiel, wie man die objektorientierte, prozedurale und funktionale Welt der Software mit der relationalen Welt der SQL-Datenbanken oder der jeweiligen Struktur der NoSQL-Datenbanken zusammenbringt.

Eine dritte Frage taucht auf, ob man auf Softwareebene Caching-Mechanismen einbaut. Das muss man nicht, aber Hibernate verspricht einem so etwas gut zu können und auch bietet sogar Caching auf der Platte an, nicht nur im Memory.

Die erste Frage ist legitim und kennt wohl keine allgemeingültige Antwort. Man kann durchaus von einer Datenbank im Mittelpunkt ausgehen, wenn diese von mehreren Applikationen genutzt wird oder wenn man davon ausgeht, dass die Daten wesentlich langlebiger als die Software sind. Das könnte bei einer Kundendatenbank so sein. Oft behilft man sich mit dem Versprechen, dass es eine Export-Funktionalität gibt und wenn man dann irgendwann die Software umstellen will, kann man die Daten als XML exportieren und gleich in die neue Software übernehmen. Das kann Routine sein, ich wäre aber skeptisch und würde diese Migration sorgfältig planen und testen. Wahrscheinlich wird es nicht so einfach und so billig, wie man gehofft hat.

Die zweite Frage ist auch recht relevant. Man hat ja nicht nur Objekte in Relationen zu übersetzen, sondern muss sich auch mit Transaktionen auseinandersetzen und mit der Frage, wie tief man große Objekte mit vielen Unterobjekten wirklich lesen und schreiben will. Hier werden gute Antworten schwierig. Liest man sie nur in kleine Portionen, stößt man später auf Probleme, weil Unterobjekte fehlen, z.B. LazyLoadingException in Hibernate. Man muss hierfür eine Lösung finden und diese konsequent umsetzen. Ein radikaler Ansatz, der etwas umständlich ist, aber recht zuverlässig funktioniert, ist die aggregierten Objekte zu entkoppeln und stattdessen nur Schlüsselfelder zu speichern. Beim Zugriff muss man sie dann explizit mit dem Schlüsselfeld holen und stellt so sicher, dass sie aktuell sind. Hier stellen sich natürlich auch interessante Konsistenzfragen, wenn man Daten aus der Datenbank miteinander kombiniert, die von verschiedenen Zeitpunkten stammen und die deshalb nicht mehr notwendigerweise miteinander konsistent sind.

Ein beliebter Ansatz ist, recht viel in den Hauptspeicher zu laden und darauf zuzugreifen. Damit landen wir bei dem dritten Thema, dem Caching. Eines sollte zunächst einmal klargestellt werden: Wenn man eine relationale SQL-Datenbank benutzt und sich dabei auf das Caching des OR-Mapping-Frameworks verlässt, dann verlässt man die transaktionale Welt. Man handelt sich sehr viele Probleme ein, weil Fragen der Konsistenz nicht mehr einfach handhabbar sind, schon gar nicht auf DB-Ebene. Das lässt sich scheinbar in den Griff bekommen, wenn man sagt, alle Zugriffe laufen über die Applikation und die Datenbank ist nur noch eine Persistenzschicht. Nun laufen aber mehrere Threads gleichzeitig und man stößt dann auch noch in diesen heiklen Bereichen auf Hibernate-Fehler. Eclipselink, JDO, etc. sind da wohl nicht wesentlich anders. Kurz gesagt, das Caching ist für veränderliche Daten bestenfalls kaum handhabbar, aber ich gehe sogar soweit, dass das konzeptionell nicht korrekt ist. Nun kann man natürlich so etwas wie „Stammdaten“ haben, die sich selten verändern. Die lassen sich tatsächlich im Memory halten, wenn es einen Prozess gibt, diesen Cache aufzufrischen, wenn sich die Stammdaten ändern, vielleicht sogar mit einem Neustart der Applikation. Ja, das will niemand zugeben, dass man so etwas heute braucht, aber besser zweimal im Jahr die Applikation neu starten als zwei Monate im Jahr obskure Fehler suchen, die durch unsauberes Caching und Fehler im OR-Mapping-Framework entstehen.

Zusammenfassend kann man sagen, dass diese OR-Mapper durchaus zumindest in der Java-Welt ihren Bereich haben, wo sie nützlich sind. Aufpassen sollte man aber bei sehr großen Tabellen und bei sehr stark miteinander verknüpften Tabellen und vor allem beim Caching von veränderlichen Daten. Hier lohnt es sich, etwas mehr Aufwand zu treiben, denn der scheinbare Komfortgewinn durch den OR-Mapper existiert hier nicht oder nur zu einem unverhältnismäßig hohen Preis.

Share Button

Weihnachten — Christmas — Jul — Navidad 2013

καλά Χριστούγεννα — Feliz Natal — Zalig Kerstfeest! — Veselé Vánoce — Kellemes Karácsonyi Ünnepeket — ميلاد مجيد — Bella Festas daz Nadal! — Hyvää Joulua! — С Рождеством — 圣诞快乐 — God Jul! — Feliĉan Kristnaskon — Häid jõule — Nollaig Shona Dhuit! — クリスマスおめでとう ; メリークリスマス — Срећан Божић — Prettige Kerstdagen — Wesołych Świąt Bożego Narodzenia — Priecîgus Ziemassvçtkus — Vesele bozicne praznike! — God Jul — Joyeux Noël — З Рiздвом Христовим — Glædelig Jul — کريسمس مبارک — Crăciun fericit — Natale hilare — Buon Natale — Feliz Navidad — Sretan božić — Merry Christmas — Su Šventom Kalėdom — Vesele Vianoce! — Fröhliche Weihnachten — Gleðileg jól

Weihnachtsbaum bei Nacht
Quelle Wikimedia commons
Weihnachtsbaum bei Nacht 1981
Quelle: Wikimedia
© Karl Brodowsky 1981-2013
Share Button

Carry Bit: How does it work?

Deutsch

Most of us know from elementary school how to add multi-digit numbers on paper. Usage of the carry bit is the same concept, but not for base 10, not even for base 2, but for base 256 (in the old 8-bit-days), base 65536 (in the almost as old 16-bit-days), base 4294967296 (32 bit) or base 18446744073709551616 (64 bit), whatever is the word width of the CPU. Always using powers of two is common today, but it is quite possible that this will change from bits to trits (having three possible values, -1, 0 and 1) in the far future.

I do not think that application development should be dealing with low level stuff like bits and bytes, but currently common programming languages like Java, C, C++, C# and more would not let you get away with that, you have to be aware of the bits underlying their numeric types to some extent. So it is a good idea to spend some effort on understanding this. Unfortunately all of these languages are lacking the carry bit, but it is anyway useful to understand the concept.

I have been writing software since the beginning of the 80es. The computers available to me at that time where 8-bit with a 6502- or 6510-CPU and 1 MHz clock speed. Yes, it was 1 MHz, not 1 GHz. It was possible to program them in some BASIC-dialect, but that was quite useless for many purposes because it was simply too slow. Compiled languages existed, but were too clumsy and too big to be handled properly on those computers, at least the ones that I have seen. So assembly language was the way to go. In later years I have also learned to use the 680×0 assembly language and the 80×86 assembly language, but after the mid 90es that has not happened any more. An 8-bit CPU can add two 8-bit numbers and yield an 8-bit result. For this two variants need to be distinguished, namely signed and unsigned integers. For signed numbers it is common to use 2’s complement. That means that the highest bit encodes the sign. So all numbers from 0 to 127 are positive integers as expected. 127 has the 8-bit representation 01111111. Now it would be tempting to assume that 10000000 stands for the next number, which would be +128, but it does not. Having the highest bit 1 makes this a negative number, so this is -128. Those who are familiar with modular arithmetic should find this easily understandable, it is just a matter of choosing the representatives for the residue classes. But this should not disturb you, if you have no recent experience with modular arithmetic, just accept the fact that 10000000 stands for -128. Further increments of this number make it less negative, so 10000001 stands for -127 and 11111111 for -1. For unsigned numbers, the 8 bits are used to express any number from 0 to 255.

For introducing the carry bit let us start with unsigned integral numbers. The possible values of a word are 0 to 2^n-1 where n is the word width in bits, which would be 8 in our example. Current CPUs have off course 64-bit word width, but that does not change the principle, so we stick with 8-bit to make it more readable. Just use your imagination for getting this to 32, 64, 96 or 128 bits.

So now the bit sequence 11111111 stands for 255. Using an assembly language command that is often called ADD or something similar, it is possible to add two such numbers. This addition can typically be performed by the CPU within one or two clock cycles. The sum of two 8-bit numbers is in the range from 0 through 510 (111111110 in binary), which is a little bit too much for one byte. One bit more would be sufficient to express this result. The workaround is to accept the lower 8 bits as the result, but to retain the upper ninth bit, which can be 0 or 1, in the so called carry bit or carry flag. It is possible to query it and use a different program flow depending on it, for example for handling overflows, in case successive operation cannot handle more than 8 bit. But there is also an elegant solution for adding numbers that are several bytes (or several machine words) long. From the second addition onwards a so called ADC („add with carry“) is used. The carry bit is included as third summand. This can create results from 0 to 511 (111111111 in binary). Again we are getting a carry bit. This can be continued until all bytes from both summands have been processed, just using 0 if one summand is shorter than the other one. If the carry bit is not 0, one more addition with both summand 0 and the carry bit has to be performed, yielding a result that is longer than the longer summand. This can off course also be achieved by just assuming 1, but this is really an implementation detail.

So it is possible to write a simple long integer addition in assembly language. One of the most painful design mistakes of current programming languages, especially of C is not providing convenient facilities to access the carry bit, so a lot of weird coding is used to work around this when writing a long integer arithmetic. Usually 64-bit arithemetic is used to do 32-bit calculations and the upper 32 bits are used for the carry bit. Actually, it is not that hard to recover the carry bit, but it is anyway a bit annoying.

Subtraction of long integers can be done in a quite similar way, using something like SBC („subtract with carry“) or SBB („subtract with borrow“), depending on how the carry bit is interpreted when subtracting.

For signed integer special care has to be taken for the highest bit of the highest word of each summand, which is the sign. Often a so called overflow but comes in handy, which allows to recognize if an additional machine word is needed for the result.

Within the CPU of current 64 bit hardware it could theoretically be possible to do the 64-bit addition internally bit-wise or byte-wise one step after the other. I do not really know the implementation details of ARM, Intel and AMD, but I assume that much more parallelism is used for performing such operation within one CPU cycle for all 64 bits. It is possible to use algorithms for long integer addition that make use of parallel computations and that can run much faster than what has been described here. They work for the bits and bytes within the CPU, but they can also be used for very long numbers when having a large number of CPUs, most typically in a SIMD fashion that is available on graphics devices misused for doing calculations. I might be willing to write about this, if interest is indicated by readers.

It is quite interesting to look how multiplication, division, square roots, cube roots and more are calculated (or approximated). I have a lot of experience with that so it would be possible to write about hat. In short these operations can be done quite easily on modern CPUs, because they have already quite sophisticated multiplication and division functions in the assembly language level, but I have off course been able to write such operations even for 8-bit CPUs lacking multiplication and division commands. Even that I could cover, but that would be more for nostalgic reasons. Again there are much better algorithms than the naïve ones for multiplication of very long integers.

Links

Share Button

Kein kommerzieller Support mehr für Glassfish

Oracle stellt den kommerziellen Support für Glassfish ein und empfiehlt Kunden, die an solchem Support interessiert sind, auf WebLogic umzustellen:

Oracle stellt kommerziellen Support für GlassFish ein.

Die Frage ist natürlich, ob man dann nicht ein paar andere Umstellungen überdenkt:

  • Wenn es EJB sein muss, ist vielleicht auch der WildFly-Applikationsserver von JBoss eine Option
  • Wenn es Java sein muss, ist vielleicht auch Spring eine Alternative
  • Wenn es die JVM sein muss, sind Scala, Clojure oder JRuby einige weitere Möglichkeiten. Diese bringen in der Regel ihre eigenen Frameworks mit. Wer möchte noch EJB benutzen, wenn er schon bei JRuby, Scala oder Clojure angelangt ist? Ja, theoretisch ginge das wohl, EJBs in diesen Sprachen zu entwickeln.
  • Wenn nicht es nicht einmal die JVM sein muss, gibt es viele Möglichkeiten, die ich der Phantasie des Lesers überlasse.

Kurz gesagt, ich will hier nicht den Ruhm von EJB schmälern. Das tue ich vielleicht einmal in anderen Blog-Beiträgen. Aber niemand muss von Glassfish auf WebLogic umstellen. Und schon gar nicht muss man sich bei dem prall gefüllten Satz an Alternativen von Oracle bei den Verhandlungen über den Tisch ziehen lassen.

Nur benötigen diese Umstellungen immer viel Zeit und Geld und sie müssen gut geplant werden. Deshalb ist es sicher gut, längerfristig die Alternativen vorzubereiten und nicht erst wenn es schon zu spät ist. Sonst geht mit einem virtuellen Schild um den Hals in die Verhandlungen mit Oracle, auf dem steht, dass man gerne über den Tisch gezogen werden möchte.

Es sei dem Leser überlassen, seine Schlussfolgerungen für andere technisch interessante Softwareprodukte aus demselben Haus zu ziehen, z.B. mySQL oder Solaris. Und ja, man könnte theoretisch für diese Produkte ähnliche Listen anfertigen.

Share Button

Multidispatch (Multimethoden): verpassen wir etwas

Objektorientierte Programmiersprachen werden nach meiner Meinung eigentlich erst wirklich objektorientiert, wenn sie Polymorphie unterstützen. Das bedeutet, dass ein Stück Progammcode mit einem Objekt gewisse Manipulationen durchführt und dabei Methoden des Objekts aufruft. Diese tun etwas verschiedenes, je nach dem welches Objekt hier angesprochen wird, denn es ist eine Eigenschaft des Objekts, welche Methode wirklich aufgerufen wird. In vielen Objektorientierten Programmiersprachen, wie zum Beispiel Java oder C# oder C++ ist das so gelöst, dass das Objekt einer Klasse angehört und die Klasse die Methodenimplementierung enthält, aber es können natürlich auch Methoden direkt an einzelnen Objekten aufgehängt sein. Für die Freunde von C# und C++ sei daran erinnert, dass „virtual“ hier nicht fehlen darf.

Das ist alles wunderbar und man kann damit sehr viel anfangen. Nun wird die heile Welt gestört, wenn man sich irgendwelche Konzepte aus nie wirklich Mainstream gewordenen Programmiersprachen wie Common Lisp mit CLOS anschaut, wo es sogenanntes Multidispatch gibt. Die aufgerufene Methode hängt also nicht nur vom ersten speziellen Parameter, also dem Objekt, das oft vor dem „.“ steht, ab, sondern von allen Parametern. Das macht es unübersichtlich und niemand braucht so etwas, könnte man meinen. Das mag ja alles stimmen, aber wir werden doch mit Situationen konfrontiert, wo das Thema sehr relevant wird.

Nehmen wir an, man führt einen neuen numerischen Typ ein, z.B. Rational für rationale Zahlen \Bbb Q=\{ \frac{a}{b} : a, b \in \Bbb Z \wedge b > 0 \}. Nun braucht man Methoden für die Grundrechenarten, was sehr schön funktioniert, wenn man z.B. zwei rationale Zahlen addieren will. Nun soll es aber auch funktionieren, eine ganze Zahl zu einer rationalen Zahl zu addieren. Dafür kann man die „+“-Methode oder weniger schön die „add“-Methode noch einmal mehr überladen und einen ganzzahligen Parameter verstehen oder innerhalb der Methode diesen Fall zur Laufzeit erkennen. Der erste Ansatz greift leider zu kurz, weil wegen Vererbungshierarchie und gerade wegen der Polymorphie der genaue Type zur Compilezeit nicht bekannt ist und daher auf die allgemeine Methode, etwa so etwas wie

+(x : Numeric) ,

zurückgefallen wird, die wiederum abhängig vom realen Typ von x auf die richtige Methode oder den richtigen Methodenteil führen sollte.
Das ist alles noch gut machbar, man darf es nur nicht vergessen.

Aber was passiert nun, wenn q rational und n ganzzahlig ist und wir nicht q+n, sondern n+q schreiben? Nun müsste man Integer (oder BigInteger) oder was auch immer der Typ ist, hoffentlich ohne diesen absurden Überlauf bei 2^{31}-1 wie die java-ints, nachträglich beibrigen, rational zu verstehen. In Ruby kann man zumindest noch nachträglich die Methode + von Integer ändern und durch eine eigene ersetzen, die auch Rational versteht, aber der Ansatz stößt irgendwann an Grenzen. In Ruby behilft man sich oft damit, dass die +-Methode der linken Seite eine „coerce“-Methode der rechten Seite mit der linken Seite als Parameter aufruft, um Konvertierungen zu machen. Damit kommt man recht weit, aber man muss sehr genau hinschauen, was man tut.

Nun kommt sicher der Einwand, dass Operatorüberladen sowieso „böse“ ist. Das hilft aber hier nichts, die Probleme treten in identischer Form auf, wenn man es „add“ statt „+“ nennt.

Der zweite Einwand ist vielleicht, dass man nicht jeden Tag neue numerische Typen schreibt. Aber man schreibt doch hoffentlich ab und zu neue Klassen und nutzt Vererbung. Da muss man dann eine equals-Methode oder so etwas ähnliches implementieren. Und schon ist das Problem sehr relevant. In der Regel habe ich das dadurch gelöst, dass equals zuerst die Klassen von linker Seite (self oder this) und rechter Seite vergleicht und nur wenn die gleich sind, überhaupt weiter vergleicht. Das ist meistens ein guter Ansatz. Aber manchmal muss man wirklich Gleichheit zwischen Objekten zulassen, die nicht derselben Klasse angehören. Dann ist es schon eine Herausforderung, das korrekt und halbwegs solide zu lösen. Man sieht also, dass wir hier wirklich etwas verpassen… Aber solange man die Konzepte kennt, kann man sie grundsätzlich in jeder Programmiersprache anwenden, der Aufwand ist nur etwas größer.

Java ist hier tatsächlich etwas schlechter gestellt als z.B. Scala oder Ruby, weil man sehr schlechte Möglichkeiten hat, die eingebaute equals-Methode zu ersetzen. Während es bei SortedSet die Wahlmöglichkeit gibt, Comparable oder einen Comparator zu verwenden, setzen HashSet und HashMap fast immer auf die interne hashCode() und equals()-Methode und man muss sich mit Wrappern oder geeigneten inneren Klassen behelfen, um eine zweites Interface für dasselbe Objekt mit anderen equals()- und hashCode()-Methoden zu liefern.

Share Button

Scala Exchange 2013

Deutsch

I have visited the conference ScalaX 2013 in London on the beginning of December.

It was really fun. The talks were quite challenging, maybe even a little bit more than on other conferences I have visited, but that was really fun to listen to. About 400 participants were there and three tracks were available. So it was possible to learn a lot about advanced concepts in functional programming and software architecture with a focus on Scala, but also some Haskell and Clojure.

Some interesting talks were:

Share Button

Besuch bei Scala Exchange 2013

English

Gleich ein paar Wochen nach der Devoxx habe ich noch so eine Konferenz besucht, diesmal nur zwei Tage und unter dem Namen „Scala Exchange“ oder kurz #scalaX.
Es ging hauptsächlich um funktionale Programmierung und Architektur und das wiederum meistens recht „Scala-lastig“.
Die Vorträge waren um einiges anspruchsvoller als bei der Devoxx, aber das machte vielleicht auch den speziellen Reiz aus.

Interessante Vorträge waren unter anderem:

Share Button