Virtual machines

We all know that Java uses a „virtual machine“ that is it simulates a non-existing hardware which is the same independent of the real hardware, thus helping to achieve the well known platform independence of Java. Btw. this is not about virtualization like VMWare, VirtualBox, Qemu, Xen, Docker and similar tools, but about byte code interpreters like the Java-VM.

We tend to believe that this is the major innovation of Java, but actually the concept of virtual machines is very old. Lisp, UCSD-Pascal, Eumel/Elan, Perl and many other systems have used this concept long before Java. The Java guys have been good in selling this and it was possible to get this really to the mainstream when Java came out. The Java guys deserve the credit for bringing this in the right time and bringing it to the main stream.

Earlier implementations where kind of cool, but the virtual machine technology and the hardware were to slow, so that they were not really attractive, at least not for high performance applications, which are now actually a domain of Java and other JVM languages. Some suggest that Java or other efficient JVM languages like Scala would run even faster than C++. While it may be true to show this in examples, and the hotspot optimization gives some theoretical evidence how optimization that takes place during run time can be better than static optimization at compile time, I do not generally trust this. I doubt that well written C-code for an application that is adequate for both C and Java will be outperformed by Java. But we have to take two more aspects into account, which tend to be considered kind of unlimited for many such comparisons to make them possible at all.

The JVM has two weaknesses in terms of performance. The start-up time is relatively long. This is addressed in those comparisons, because the claim to be fast is only maintained for long running server applications, where start-up time is not relevant. The hotspot optimization requires anyway a long running application in order to show its advantages. Another aspect that is very relevant is that Java uses a lot of memory. I do not really know why, because more high level languages like Perl or Ruby get along with less memory, but experience shows that this is true. So if we have a budget X to buy hardware and then put software written in C on it, we can just afford to buy more CPUs because we save on the memory or we can make use of the memory that the JVM would otherwise just use up to make our application faster. When we view the achievable performance with a given hardware budget, I am quite sure that well written C outperforms well written Java.

The other aspect is in favor of Java. We have implicitly assumed until now that the budget for development is unlimited. In practice that is not the case. While we fight with interesting, but time consuming low level issues in C, we already get work done in Java. A useful application in Java is usually finished faster than in C, again if it is in a domain that can reasonably be addressed with either of the two languages and if we do not get lost in the framework world. So if the Java application is good enough in terms of performance, which it often is, even for very performance critical applications, then we might be better off using Java instead of C to get the job done faster and to have time for optimization, documentation, testing, unit testing.. Yes, I am in a perfect world now, but we should always aim for that. You could argue that the same argument is valid in terms of using a more high-level language than Java, like Ruby, Perl, Perl 6, Clojure, Scala, F#,… I’ll leave this argument to other articles in the future and in the past.

What Java has really been good at is bringing the VM technology to a level that allows real world high performance server application and bringing it to the main stream.
That is already a great achievement. Interestingly there have never been serious and successful efforts to actually build the JavaVM as hardware CPU and put that as a co-processor into common PCs or servers. It would have been an issue with the upgrade to Java8, because that was an incompatible change, but other than that the JavaVM remained pretty stable. As we see the hotspot optimization is now so good that the urge for such a hardware is not so strong.

Now the JVM has been built around the Java language, which was quite legitimate, because that was the only goal in the beginning. It is even started using the command line tool java (or sometimes javaw on MS-Windows 32/64 systems). The success of Java made the JVM wide spread and efficient, so it became attractive to run other languages on it. There are more than 100 languages on the JVM. Most of them are not very relevant. A couple of them are part of the Java world, because they are or used to be specific micro languages closely related to java to achieve certain goals in the JEE-world, like the now almost obsolete JSP, JavaFX, .

Relevant languages are Scala, Clojure, JRuby, Groovy and JavaScript. I am not sure about Jython, Ceylon and Kotlin. There are interesting ideas coming up here and there like running Haskell under the name Frege on the JVM. And I would love to see a language that just adds operator overloading and provides some preprocessor to achieve this by translating for example „(+)“ in infix syntax to „.add(..)“ mainstream, to allow seriously using numeric types in Java.

Now Perl 6 started its development around 2000. They were at that time assuming that the JVM is not a good target for a dynamic language to achieve good performance. So they started developing Parrot as their own VM. The goal was to share Parrot between many dynamic languages like Ruby, Python, Scheme and Perl 6, which would have allowed inter-language inter-operation to be more easily achievable and using libraries from one of these languages in one of the others. I would not have been trivial, because I am quite sure that we would have come across issues that each language has another set of basic types, so strings and numbers would have to be converted to the strings and numbers of the library language when calling, but it would have been interesting.

In the end parrot was a very interesting project, theoretically very sound and it looked like for example the Ruby guys went for it even faster than the the Perl guys, resulting in an implementation called cardinal. But the relevant Perl 6 implementation, rakudo, eventually went for their own VM, Moar. Ruby also did itself a new better VM- Many other language, including Ruby and JavaScript also went for the JVM, at least as one implementation variant. Eventually the JVM proved to be successful even in this area. The argument to start parrot in the first place was that the JVM is not good for dynamic languages. I believe that this was true around 2000. But the JVM has vastly improved since then, even resulting in Java being a serious alternative to C for many high performance server applications. And it has been improved for dynamic languages, mostly by adding the „invoke_dynamic“-feature, that also proved to be useful for implementing Java 8 lambdas. The experience in transforming and executing dynamic languages to the JVM has grown. So in the end parrot has become kind of obsolete and seems to be maintained, but hardly used for any mainstream projects. In the end we have Perl 6 now and Parrot was an important stepping stone on this path, even if it becomes obsolete. The question of interoperability between different scripting languages remains interesting…

Share Button

Scala, Ruby, Perl,… – wann nimmt man was?

Wer einen goldenen Hammer hat, für den sieht jede Schraube wie in Nagel aus. Aber wir haben einen riesigen Werkzeugkasten und wie man sieht, überschneiden sich tatsächlich manche Werkzeuge in ihren Einsatzbereichen, aber das Universalwerkzeug ist nicht wirklich in Sicht oder doch nicht wirklich in allen Bereichen mit den Spezialwerkzeugen konkurrenzfähig.

Oft hat man ja Altlasten, also vorhandene Applikationen, die man erweitern oder ergänzen soll oder es sollen sogar neue, „in die Landschaft passende“ Applikationen hinzugefügt werden. Dann landet man bei Cobol, Railo, Fortran, C, Java, C#, C++, PL/SQL, PL/1, (Visual)Basic u.s.w. Wobei diese zum Teil sogar ihre sehr große „Nische“ haben, in der sie noch aktuell sind. Dass C für Systemprogrammierung noch sehr aktuell und fast konkurrenzlos ist, sei unbenommen.

Wenn keine Vorgaben durch Bibliotheken, Altlasten, IT-Landschaft u.s.w. bestehen, ist es natürlich interessant, in der zur Verfügung stehenden Zeit möglichst viel machen zu können. Eine Technologie, die einen zwingt, viel Zeit mit trivialen Aufgaben zu verbringen und die eigentlich interessanten Dinge in einem Wust von trivialem Code zu verstecken, den man nun einmal schreiben muss, darf man dann auch schon einmal hinterfragen. Letztlich sind zwei Wege vielversprechend, um mit wenig Code viel auszudrücken und letzlich in wenig Zeit viel zu entwickeln: Die funktionalen Sprachen wie z.B. F#, Clojure, Erlang, Elixir und Haskell. Oder die Skriptsprachen wie Ruby, Perl, Perl6 (wenn es mal fertig wird), Python und PHP. Natürlich überschneidet sich das ein Stück weit, weil einige funktionale Sprachen auch ein bißchen wie Skriptsprachen einsetzbar sind und einige Skriptsprachen auch gewisse funktionale Konstruktionen unterstützen.

Letztlich erweisen sich die funktionalen Sprachen als vielversprechend, wenn es darum geht, sehr leistungsstarke Applikationen zu entwickeln, die einen hohen Durchsatz und eine hohe Parallelisierung der Verarbeitung verwirklichen. Twitter soll mit Scala diesen Weg gegangen sein. Grundsätzlich bieten alle ernsthaften funktionalen Sprachen hier einige Möglichkeiten. F# lässt sich übrigens auch mit Mono kombiniert unter Linux verwenden. Vielleicht hat Erlang noch einen kleinen Vorteil, weil es schon auf VM-Ebene für diesen Einsatzbereich optimiert ist. Diese VM lässt sich aber auch mit Elixir verwenden. Aber man hat die Wahl zwischen mehreren Wegen.

Für viele Web-Applikationen hat sich Ruby als gut geeignet erwiesen, man sieht aber, dass es einige sehr gute PHP-Applikationen gibt. z.B. MediaWiki, die Software, mit der Wikipedia läuft. Allerdings zeichnet sich im Moment ein Trend ab, mehr von der Logik in Javascript auf der Client-Seite zu implementieren und serverseitig (fast) nur noch Webservices mit REST anzubieten, die die eigentliche Businesslogik und die Zugriffe auf die Daten zur Verfügung stellen. Diese REST-Services kann man natürlich in Ruby entwickeln; aber da gibt es viele Wege…

Ruby und vor allem Perl sind aber auch sehr stark, wenn es darum geht, Textdateien zu verarbeiten. Man kann diese nach Mustern durchsuchen, umgruppieren und umbauen und damit schöne Auswertungen machen. Für sehr große Datenmengen sollte man sich natürlich noch etwas eingehendere Gedanken machen, weil dann der Durchsatz und die Parallelisierung plötzlich mehr Bedeutung bekommen als das eigentlich Parsen des Texts. Aber man kann noch viel mehr…

Eine große Applikation kann diese Dinge auch kombinieren. GNU-Emacs ist eine sehr altes Beispiel, dass hier einen noch heute sehr aktuellen Ansatz verfolgt. Die Grundfunktionalität ist in C entwickelt und die weitergehenden Funktionen und Erweiterungen alle in Emacs-Lisp. So läßt sich ohne viel Aufwand einen Erweiterung einbinden, was nicht so leicht geht, wenn man erst einmal neu kompliieren muss. Die Idee lässt sich auch heute noch aufgreifen, wenn man eine Applikation z.B. in Scala entwickelt und dann die Möglichkeit anbietet, diese über Ruby- oder Groovy-Scripte anzusprechen, zu erweitern und zu konfigurieren.

Wenn man einmal an Häuser denkt, ist die Idee nicht so abwegig. Das eigentliche Haus ist relativ stabil aus Beton, Stein und Holz gebaut und bleibt in der Regel jahrzehntelang unverändert. Die Möbel sind selten aus Beton gegossen und eher flexibler (auch wenn sie vielerorts jahrzehntelang gleich stehen).

Share Button

Closures I (Perl)

Alle „coolen“ Programmiersprachen haben sogenannte Closures. Java ist nicht cool, deshalb braucht man das dort nicht… 😉 Aber bitte bis zum Schluß weiterlesen…

Zunächst gibt es um diesen Begriff eine gewisse Verwirrung. Gemeint werden damit oft anonyme Funktionen, die man in einem Programm herumreichen kann, wie andere Werte und Objekte. Das ist so etwas, was man von funktionalen Programmiersprachen erwarten kann, auch wenn es noch nicht ausreicht, um eine Programmiersprache funktional zu machen. Die Funktionspointer in C sind etwas weniger als das, weil sie ja eine statisch definierte Funktion beschreiben, die erst im Kontext der funktion, der sie als Parameter oder Variable sichtbar gemacht werden, anonym wirken. Andere Programmiersprachen wie Ruby, Perl, JavaScript, die meisten Lisp-Dialekte einschließlich Clojure, Scala, und viele andere mehr, in neueren Versionen wohl sogar C# und Java, haben solche anonymen Funktionen.

Zur Closure werden sie aber erst durch die Einbindung von Variablen aus dem Kontext, in dem sie definiert werden.
Hier wird etwas darüber geschrieben, wie Closures in Ruby funktionieren:
Closures in Ruby

Doch nun zu Perl. In diesem Zusammenhang muß man darauf achten, ob die lokalen Variablen mit „my“ oder mit „local“ deklariert.
In der Regel ist „my“ das, was man verwenden sollte und in der Regel macht es keinen Unterschied. Aber in diesem Fall sieht man es: Variablen, die mit my deklariert wurden, werden im definierenden Kontext der anonymen Funktion eingebunden, solche, die mit local deklariert wurden, im aufrufenden Kontext. Es gibt auch noch „our“, wobei der Unterschied zu „my“ in diesem Blogbeitrag erklärt ist. In diesem Beispiel verhält sich „our“ genauso wie „my“.

Hier ein Beispiel für die Verwendung von „my“:

#!/usr/bin/perl

# 2nd-order function, returns a reference to a function
sub createAdder($) {
    # local variable, will be accessed by anonymous function (early binding)
    my $x = $_[0];

    # define anonymous function, obtain reference to it and assign it to result
    my $result = ( sub {
                       # @_ refers to parameters of anonymous function
                       my $y = $_[0];
                       # $x comes from createAdder, $y comes from context of caller and $z is parameter of function
                       return ($x, $y, $x+$y);
                   });
    return $result;
}

# local variable, will not be accessed by called anonymous function
my $x = 100;

my $px = 30;

# obtain reference to function
my $ff = createAdder($px);

# call function
for (my $i = 0 ; $i < 3; $i++) {     my @arr = $ff->($i);
    my $arr_str = "( " . join(", ", @arr) . " )";
    print "x=$x i=$i px=30 (px, y, px+i)=", $arr_str , "\n";
}

print "\n";

$x = 1000;
$px = 60;

my $ff = createAdder(60);

# call function
for (my $i = 0 ; $i < 3; $i++) {     my @arr = $ff->($i);
    my $arr_str = "( " . join(", ", @arr) . " )";
    print "x=$x i=$i px=60 (px, i, px+i)=", $arr_str , "\n";
}

Man sieht also, daß das $x aus createAdder() in die anonyme Funktion eingebaut wird und es kommt diese Ausgabe:

x=100 i=0 px=30 (px, y, px+i)=( 30, 0, 30 )
x=100 i=1 px=30 (px, y, px+i)=( 30, 1, 31 )
x=100 i=2 px=30 (px, y, px+i)=( 30, 2, 32 )

x=1000 i=0 px=60 (px, i, px+i)=( 60, 0, 60 )
x=1000 i=1 px=60 (px, i, px+i)=( 60, 1, 61 )
x=1000 i=2 px=60 (px, i, px+i)=( 60, 2, 62 )

Mit local sieht es so aus:

#!/usr/bin/perl

# 2nd-order function, returns a reference to a function
sub createAdder($) {
    # local variable, will be ignored by anonymous function (late binding)
    local $x = $_[1];

    # define anonymous function, obtain reference to it and assign it to result
    my $result = ( sub {
                       # @_ refers to parameters of anonymous function
                       my $y = $_[0];
                       # $x comes from createAdder, $y comes from context of caller and $z is parameter of function
                       return ($x, $y, $x+$y);
                   });
    return $result;
}

# local variable, will be accessed as $y by anonymous function
local $x = 100;

local $px = 30;

# obtain reference to function
my $ff = createAdder($px);

# call function
for (my $i = 0 ; $i < 3; $i++) {     my @arr = $ff->($i);
    my $arr_str = "( " . join(", ", @arr) . " )";
    print "x=$x i=$i px=30 (px, y, px+i)=", $arr_str , "\n";
}

print "\n";

$x = 1000;
$px = 60;

$ff = createAdder(60);

# call function
for (my $i = 0 ; $i < 3; $i++) {     my @arr = $ff->($i);
    my $arr_str = "( " . join(", ", @arr) . " )";
    print "x=$x i=$i px=60 (px, i, px+i)=", $arr_str , "\n";
}

Man sieht also, daß diesmal das $x aus dem aufrufenden Kontext in der anonyme Funktion verwendet wird und es kommt diese Ausgabe:

x=100 i=0 px=30 (px, y, px+i)=( 100, 0, 100 )
x=100 i=1 px=30 (px, y, px+i)=( 100, 1, 101 )
x=100 i=2 px=30 (px, y, px+i)=( 100, 2, 102 )

x=1000 i=0 px=60 (px, i, px+i)=( 1000, 0, 1000 )
x=1000 i=1 px=60 (px, i, px+i)=( 1000, 1, 1001 )
x=1000 i=2 px=60 (px, i, px+i)=( 1000, 2, 1002 )

Diese Programme haben jeweils eine Funktion 2. Ordnung, createAdder(), die selbst eine Funktion zurückgibt.

Nun stellt sich aber heraus, daß Java ein sehr ähnliches, natürlich einmal wieder etwas umständlicheres Konstrukt hat. Das sind anonyme innere Klassen, die ein Interface mit genau einer Methode implementieren. Und Java 8 hat die Closures jetzt auch ohne diesen Umweg. Aber das ist sicher einmal ein anderer Blog-Beitrag.

Share Button

Zeichenketten in Java, Ruby und Perl

Eigentlich sind die Zeichenketten in allen Programmiersprachen das gleiche, man kann sie als Literale angeben, irgend woher lesen oder zusammensetzen und dann vergleichen und ausgeben.

Aber es gibt einen großen Unterschied. In Java und in den JVM-Sprachen Scala und Clojure, die sich nicht speziell dagegen gewehrt haben, sind die normalen Zeichenketten unveränderlich. Das bedeutet, dass bei jeder Operation, die eine Zeichenkette zu verändern scheint, in Wirklichkeit jeweils eine neue Zeichenkette generiert wird. Das erschwert die Handhabung der Zeichenketten etwas, hat aber den Vorteil, dass man nicht durch Manipulationen an einer Zeichenkette anderen Programmteilen den Teppich wegziehen kann. Bei Multi-Thread-Programmen ist das besonders gefährlich, aber auch ohne mehrere Threads zu verwenden könnte das zu unerwarteten Fehlern führen, aber für Manipulationen kann es etwas unpraktischer sein, weil jedes Mal eine neue Zeichenkette generiert werden muss. Für diese Zwecke gibt es aber StringBuilder und StringBuffer, die explizit veränderliche Zeichenketten darstellen und für längere Manipulationen als Zwischenergebnis gut geeignet sind, aber man sollte diese am Schluss mit toString() in eine normale Zeichenkette umwandeln, die nicht mehr veränderlich ist. Da Scala und Clojure das Lied der unveränderlichen Objekte singen und als funktionale Sprachen oder teilweise funktionale Sprachen solche unveränderlichen Objekte der Normalfall sind, passt dieses Konzept dort sehr gut hinein.

In Perl und Ruby kann man sehr viele Manipulationen mit Zeichenketten machen, man könnte sogar fast sagen, dass diese Manipulationen von Zeichenketten mit regulären Ausdrücken die Stärke von Perl sind und deshalb auch sehr häufig vorkommen. Aber auch Perl und Ruby haben sich davor geschützt, dass man den Schlüssel (engl. key) einer Map verändert:

Beispiel in Ruby:

$ irb
irb(main):001:0> x="abc"
=> "abc"
irb(main):002:0> y="def"
=> "def"
irb(main):003:0> m={x=>3, y=>4}
=> {"abc"=>3, "def"=>4}
irb(main):004:0> x += "x"
=> "abcx"
irb(main):005:0> x
=> "abcx"
irb(main):006:0> m
=> {"abc"=>3, "def"=>4}
irb(main):007:0> y.gsub!(/d/, "x")
=> "xef"
irb(main):008:0> m
=> {"abc"=>3, "def"=>4}
irb(main):009:0>

Man sieht also, dass auch hier sichergestellt wird, dass das Verändern der Zeichenkette nicht die Schlüssel der Map verändert.

In Perl verhält es sich ähnlich:

$ perl
$x = "abc";
$y = "def";
%m = ($x, 3, $y, 4);
$x =~ s/a/A/;
print "x=$x y=$y\n";
print "m{x}=", $m{$x}, " m{'abc'}=", $m{'abc'}, " m(y)=", $m{$y},"\n";

Ctrl-D

x=Abc y=def
m{x}= m{'abc'}=3 m{y}=4

Die Zeichenketten werden beim Anlegen der Map %m kopiert und deshalb passiert nichts, wenn man nachträglich noch $x und $y ändert. Bei Ruby ist das entsprechend.

Man kann dies aber austricksen, zum Beispiel in Ruby:

$ irb
irb(main):001:0> class X
irb(main):002:1> attr_accessor :x
irb(main):003:1> def initialize(x)
irb(main):004:2> @x = x
irb(main):005:2> end
irb(main):006:1> end
=> nil
irb(main):007:0> x = X.new("abc")
=> #
irb(main):008:0> y = X.new("def")
=> #
irb(main):009:0> m={x=>3, y=>4}
=> {#=>3, #=>4}
irb(main):010:0> m
=> {#=>3, #=>4}
irb(main):011:0> x.x="abcd"
=> "abcd"
irb(main):012:0> x
=> #
irb(main):013:0> m
=> {#=>3, #=>4}
irb(main):014:0> y.x.gsub!(/d/, "D")
=> "Def"
irb(main):015:0> y
=> #
irb(main):016:0> m
=> {#=>3, #=>4}

Das lässt sich entsprechend in Perl und Java auch tun, führt eher zu überraschenden als erwünschten Ergebnissen und ist nicht zu empfehlen. In Ruby kann man die Schlüssel einer Map mit freeze() schützen:

irb(main):017:0> x.freeze
=> #
irb(main):018:0> x.x="u"
RuntimeError: can't modify frozen object
from (irb):18
from /usr/local/bin/irb:12:in `

'

Das ist ein recht eleganter Mechanismus, weil man ein Objekt mit einigen komplexeren Schritten initialisieren kann und dann durch freeze() schützen kann. Aber Vorsicht, es ist kein deepfreeze(), dies muss man explizit sicherstellen:

irb(main):019:0> x.x.gsub!(/a/, "u")
=> "ubcd"
irb(main):020:0> x
=> #
irb(main):021:0> x.x.freeze
=> "ubcd"
irb(main):022:0> x.x.gsub!(/a/, "u")
RuntimeError: can't modify frozen string
from (irb):22:in `gsub!'
from (irb):22
from /usr/local/bin/irb:12:in `
'

Eine interessante Frage ist oft, ob zwei Zeichenketten mit demselben Inhalt in Wirklichkeit dieselbe Zeichenkette sind oder ob es identische Kopien sind. Normalerweise ist das ja egal, aber es spielt eine Rolle bei Vergleichen. Diese sind billiger, wenn man sofort erkennt, ob es dasselbe Objekt ist und nicht erst zeichenweise vergleichen muss. Wenn große Datenmengen verarbeitet werden, ist es aber auch manchmal wegen des Speicherverbrauchs relevant.

Man kann in allen drei Sprachen beide Fälle haben. Mit zwei Variablen dieselbe Zeichenkette zu referenzieren ist in Java und Ruby einfach.

x = "abc";
y = x;

(in Ruby darf man die „;“ weglassen.)
In Perl sind die Variablen nicht Referenzen, sondern es werden wirklich Werte kopiert. Man muß dazu also explizit Referenzen verwenden:

$ perl
$x="abc";
$u=\$x;
$v=\$y;
print "x=$x u=$u v=$v\n";
$$u =~ s/a/A/;
print "x=$x u=$u v=$v\n";
print '$$u=', $$u, ' $$v=', $$v, ' $x=', $x,"\n";
Ctrl-D

x=abc u=SCALAR(0x8069820) v=SCALAR(0x80698ac)
x=Abc u=SCALAR(0x8069820) v=SCALAR(0x80698ac)
$$u=Abc $$v= $x=Abc

Umgekehrt kann man aber auch echte Kopien erzwingen, wenn man das will:
In Java geht das so:

String x = "abc";
String y = new String(x);

Oder in Ruby:

x = "abc"
y = String.new(x)

und in Perl ist es trivial:

$x = "abc";
$y = $x;

reicht schon aus.

Nun gewinnt man beim Vergleichen und beim Speicherverbrauch schon etwas, wenn man möglichst oft bei Vergleichen, die am Ende „true“ ergeben, schon an der Objektidentität und nicht erst am Vergleich aller Zeichen die Gleichheit erkennt. Aber wenn bei der Mehrheit der Vergleiche zwar die Länge gleich ist, aber die Zeichen sich irgendwo weit hinten unterscheiden, arbeitet das Programm vielleicht immer noch zu viel für diese Vergleiche. Wenn man also große Datenmengen verarbeiten will oder einfach nur meint, dass der Vergleich über die Objektidentität „richtiger“ ist, lässt sich das in Java und Ruby recht gut erzwingen.

In Java etwa mit

import java.util.IdentityHashMap;

public class MyMap extends IdentityHashMap {
public V put(String key, V value) {
assert key != null;
String uniqueKey = key.intern();
return super.put(uniqueKey, value);
}

public V get(Object key) {
if (key instanceof String) {
String str = (String) key;
return super.get(str.intern());
} else {
return null;
}
}
//..

public static void main(String args[]) {
MyMap map = new MyMap();
System.out.println(map.put("abc", "uvw"));
System.out.println(map.put(new String("abc"), "def"));
System.out.println(map.get("abc"));
System.out.println(map.get(new String("abc")));
}
}

ergibt die Ausgabe:

null
uvw
def
def

und verwendet intern nur die billigen Vergleiche mit == statt mit .equals(..).

In Ruby verwendet man dafür einfach „Symbole“ statt Zeichenketten, etwa

m = { :a => 3, :b => 4, :c => 5 }

Vorsicht ist aber geboten, sobald man Frameworks benutzt, die serialisieren, um Daten zu persistieren oder über das Netz zu schieben. Beim Deserialisieren geht diese Identität der Zeichenketten leider ganz schnell verloren, vor allem, wenn man „über das Netz“ vergleicht, was ja vorkommen kann.

Nun sei noch ein Grund erwähnt, warum man Java-Zeichenketten überhaupt mit so etwas wie y=new String(x) kopiert, wo sie doch unveränderbar (immutable) sind. Hier sollte man eine Optimierung in der Implementierung von String kennen. Wenn man substring() aufruft, wird ein neues String-Objekt für die Unterzeichenkette angelegt, dieses referenziert aber die Zeichensequenz der ursprünglichen Zeichenkette, nur mit anderen Anfangs- und Endzeigern. Dadurch wird in der Regel Speicher gespart und auch der Aufwand für das Allozieren von neuem Speicher vermieden. Wenn man nun aber von einer sehr langen, kurzlebigen Zeichenkette mit substring() eine sehr kurze, aber langlebige Zeichenkette extrahiert, verhindert man damit, dass der Speicher der eigentlichen Zeichensequenz der langen Zeichenkette freigegeben wird. So raffiniert und richtig es also in den allermeisten Fällen ist, die Optimierung der Library zu nutzen und zu schreiben

String y = x.substring(r, s);

so sollte man doch beachten, dass es in seltenen Fällen richtig und sogar wichtig ist, stattdessen

String y = new String(x.substring(r, s));

zu schreiben. Im Zweifelsfalls sollte man aber immer bei der ersten Schreibweise bleiben. Programme, die wegen ein paar Zeichenketten Memoryprobleme bekommen, die sich nicht durch Erhöhen der Speicherparameter leicht lösen lassen, sind zum Glück sehr selten.

Interessant ist sicher noch die Frage, in welcher Codierung die Zeichenketten gespeichert werden und wie man sie bei Ein- und Ausgabe richtig konvertiert. Das ist aber sicher genug Stoff für einen eigenen Artikel. Hier sei nur soviel gesagt: Java speichert die Zeichenketten mit utf-16, braucht also zwei Byte pro Zeichen, auch bei europäischen Sprachen.

Share Button