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

Beteilige dich an der Unterhaltung

2 Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

*