Verwenden Sie konstante Typen für sichereren und saubereren Code

In diesem Tutorial wird die Idee der aufgezählten Konstanten erweitert, wie in Eric Armstrongs "Erstellen Sie aufgezählte Konstanten in Java" behandelt. Ich empfehle dringend, diesen Artikel zu lesen, bevor Sie sich mit diesem befassen, da ich davon ausgehen werde, dass Sie mit den Konzepten für aufgezählte Konstanten vertraut sind, und ich werde einige der von Eric vorgestellten Beispielcodes erweitern.

Das Konzept der Konstanten

Im Umgang mit aufgezählten Konstanten werde ich den aufgezählten Teil des Konzepts am Ende des Artikels diskutieren . Im Moment konzentrieren wir uns nur auf den konstanten Aspekt. Konstanten sind grundsätzlich Variablen, deren Wert sich nicht ändern kann. In C / C ++ wird das Schlüsselwort constverwendet, um diese konstanten Variablen zu deklarieren. In Java verwenden Sie das Schlüsselwort final. Das hier vorgestellte Werkzeug ist jedoch nicht einfach eine primitive Variable. Es ist eine tatsächliche Objektinstanz. Die Objektinstanzen sind unveränderlich und unveränderlich - ihr interner Status darf nicht geändert werden. Dies ähnelt dem Singleton-Muster, bei dem eine Klasse möglicherweise nur eine einzige Instanz haben kann. In diesem Fall kann eine Klasse jedoch nur eine begrenzte und vordefinierte Anzahl von Instanzen haben.

Die Hauptgründe für die Verwendung von Konstanten sind Klarheit und Sicherheit. Der folgende Code ist beispielsweise nicht selbsterklärend:

public void setColor (int x) {...} public void someMethod () {setColor (5); }}

Anhand dieses Codes können wir feststellen, dass eine Farbe festgelegt wird. Aber welche Farbe repräsentiert 5? Wenn dieser Code von einem dieser seltenen Programmierer geschrieben wurde, der seine Arbeit kommentiert, finden wir die Antwort möglicherweise oben in der Datei. Wahrscheinlicher ist jedoch, dass wir nach alten Designdokumenten (falls vorhanden) suchen müssen, um eine Erklärung zu erhalten.

Eine klarere Lösung besteht darin, einer Variablen mit einem aussagekräftigen Namen den Wert 5 zuzuweisen. Zum Beispiel:

public static final int RED = 5; public void someMethod () {setColor (RED); }}

Jetzt können wir sofort erkennen, was mit dem Code los ist. Die Farbe wird auf rot gesetzt. Das ist viel sauberer, aber ist es sicherer? Was ist, wenn ein anderer Codierer verwirrt ist und andere Werte wie folgt deklariert:

public static final int RED = 3; public static final int GREEN = 5;

Jetzt haben wir zwei Probleme. Zunächst REDwird nicht mehr auf den richtigen Wert gesetzt. Zweitens wird der Wert für Rot durch die benannte Variable dargestellt GREEN. Der vielleicht gruseligste Teil ist, dass dieser Code einwandfrei kompiliert wird und der Fehler möglicherweise erst erkannt wird, wenn das Produkt ausgeliefert wurde.

Wir können dieses Problem beheben, indem wir eine definitive Farbklasse erstellen:

öffentliche Klasse Farbe {public static final int RED = 5; public static final int GREEN = 7; }}

Dann ermutigen wir Programmierer über Dokumentation und Codeüberprüfung, es wie folgt zu verwenden:

public void someMethod () {setColor (Color.RED); }}

Ich sage ermutigen, weil das Design in dieser Codeliste es uns nicht erlaubt, den Codierer zur Einhaltung zu zwingen; Der Code wird auch dann kompiliert, wenn nicht alles in Ordnung ist. Dies ist zwar etwas sicherer, aber nicht ganz sicher. Obwohl Programmierer sollten die Verwendung der ColorKlasse, werden sie nicht benötigt wird . Programmierer könnten sehr leicht den folgenden Code schreiben und kompilieren:

 setColor (3498910); 

Erkennt die setColorMethode, dass diese große Zahl eine Farbe ist? Wahrscheinlich nicht. Wie können wir uns also vor diesen betrügerischen Programmierern schützen? Hier kommen Konstantentypen zur Rettung.

Wir definieren zunächst die Signatur der Methode neu:

 public void setColor (Farbe x) {...} 

Jetzt können Programmierer keinen beliebigen ganzzahligen Wert übergeben. Sie sind gezwungen, ein gültiges ColorObjekt anzugeben. Eine Beispielimplementierung könnte folgendermaßen aussehen:

public void someMethod () {setColor (neue Farbe ("Rot")); }}

Wir arbeiten immer noch mit sauberem, lesbarem Code und sind der absoluten Sicherheit viel näher gekommen. Aber wir sind noch nicht ganz da. Der Programmierer hat noch etwas Raum, um Chaos anzurichten, und kann willkürlich neue Farben wie folgt erzeugen:

public void someMethod () {setColor (neue Farbe ("Hallo, mein Name ist Ted.")); }}

Wir verhindern diese Situation, indem wir die ColorKlasse unveränderlich machen und die Instanziierung vor dem Programmierer verbergen. Wir machen jede Art von Farbe (rot, grün, blau) zu einem Singleton. Dies wird erreicht, indem der Konstruktor privat gemacht und dann öffentliche Handles einer eingeschränkten und genau definierten Liste von Instanzen ausgesetzt werden:

öffentliche Klasse Farbe {private Farbe () {} öffentliche statische endgültige Farbe ROT = neue Farbe (); öffentliche statische Endfarbe GRÜN = neue Farbe (); öffentliche statische endgültige Farbe BLAU = neue Farbe (); }}

In diesem Code haben wir endlich absolute Sicherheit erreicht. Der Programmierer kann keine falschen Farben herstellen. Es dürfen nur die definierten Farben verwendet werden. Andernfalls wird das Programm nicht kompiliert. So sieht unsere Implementierung jetzt aus:

public void someMethod () {setColor (Color.RED); }}

Beharrlichkeit

Okay, jetzt haben wir eine saubere und sichere Möglichkeit, mit konstanten Typen umzugehen. Wir können ein Objekt mit einem Farbattribut erstellen und sicherstellen, dass der Farbwert immer gültig ist. Was aber, wenn wir dieses Objekt in einer Datenbank speichern oder in eine Datei schreiben möchten? Wie speichern wir den Farbwert? Wir müssen diese Typen Werten zuordnen.

In dem oben erwähnten JavaWorld- Artikel verwendete Eric Armstrong Zeichenfolgenwerte. Die Verwendung von Zeichenfolgen bietet den zusätzlichen Vorteil, dass Sie in der toString()Methode etwas Sinnvolles zurückgeben können , wodurch die Debugging-Ausgabe sehr deutlich wird.

Die Aufbewahrung von Saiten kann jedoch teuer sein. Eine Ganzzahl benötigt 32 Bit, um ihren Wert zu speichern, während eine Zeichenfolge 16 Bit pro Zeichen benötigt (aufgrund der Unicode-Unterstützung). Beispielsweise kann die Nummer 49858712 in 32 Bit gespeichert werden, die Zeichenfolge TURQUOISEwürde jedoch 144 Bit erfordern. Wenn Sie Tausende von Objekten mit Farbattributen speichern, kann sich dieser relativ kleine Bitunterschied (in diesem Fall zwischen 32 und 144) schnell summieren. Verwenden wir stattdessen ganzzahlige Werte. Was ist die Lösung für dieses Problem? Wir behalten die Zeichenfolgenwerte bei, da sie für die Präsentation wichtig sind, aber wir werden sie nicht speichern.

Java-Versionen ab 1.1 können Objekte automatisch serialisieren, sofern sie die SerializableSchnittstelle implementieren . Um zu verhindern, dass Java fremde Daten speichert, müssen Sie solche Variablen mit dem transientSchlüsselwort deklarieren. Um die ganzzahligen Werte zu speichern, ohne die Zeichenfolgendarstellung zu speichern, deklarieren wir das Zeichenfolgenattribut als vorübergehend. Hier ist die neue Klasse zusammen mit den Zugriffsmethoden für die Integer- und String-Attribute:

öffentliche Klasse Color implementiert java.io.Serializable {private int value; privater vorübergehender Stringname; öffentliche statische Endfarbe ROT = neue Farbe (0, "Rot"); öffentliche statische Endfarbe BLAU = neue Farbe (1, "Blau"); öffentliche statische Endfarbe GRÜN = neue Farbe (2, "Grün"); private Farbe (int value, String name) {this.value = value; this.name = name; } public int getValue () {Rückgabewert; } public String toString () {return name; }}

Jetzt können wir Instanzen vom Typ Konstante effizient speichern Color. Aber was ist mit der Wiederherstellung? Das wird ein bisschen schwierig. Bevor wir weiter gehen, wollen wir dies zu einem Framework erweitern, das alle oben genannten Fallstricke für uns behandelt und es uns ermöglicht, uns auf die einfache Frage der Definition von Typen zu konzentrieren.

Das konstante Typ-Framework

With our firm understanding of constant types, I can now jump into this month's tool. The tool is called Type and it is a simple abstract class. All you have to do is create a very simple subclass and you've got a full-featured constant type library. Here's what our Color class will look like now:

public class Color extends Type { protected Color( int value, String desc ) { super( value, desc ); } public static final Color RED = new Color( 0, "Red" ); public static final Color BLUE = new Color( 1, "Blue" ); public static final Color GREEN = new Color( 2, "Green" ); } 

The Color class consists of nothing but a constructor and a few publicly accessible instances. All of the logic discussed to this point will be defined and implemented in the superclass Type; we'll be adding more as we go along. Here's what Type looks like so far:

public class Type implements java.io.Serializable { private int value; private transient String name; protected Type( int value, String name ) { this.value = value; this.name = name; } public int getValue() { return value; } public String toString() { return name; } } 

Back to persistence

With our new framework in hand, we can continue where we left off in the discussion of persistence. Remember, we can save our types by storing their integer values, but now we want to restore them. This is going to require a lookup -- a reverse calculation to locate the object instance based on its value. In order to perform a lookup, we need a way to enumerate all of the possible types.

In Eric's article, he implemented his own enumeration by implementing the constants as nodes in a linked list. I'm going to forego this complexity and use a simple hashtable instead. The key for the hash will be the integer values of the type (wrapped in an Integer object), and the value of the hash will be a reference to the type instance. For example, the GREEN instance of Color would be stored like so:

 hashtable.put( new Integer( GREEN.getValue() ), GREEN ); 

Of course, we don't want to type this out for each possible type. There could be hundreds of different values, thus creating a typing nightmare and opening the doors to some nasty problems -- you might forget to put one of the values in the hashtable and then not be able to look it up later, for instance. So we'll declare a global hashtable within Type and modify the constructor to store the mapping upon creation:

 private static final Hashtable types = new Hashtable(); protected Type( int value, String desc ) { this.value = value; this.desc = desc; types.put( new Integer( value ), this ); } 

But this creates a problem. If we have a subclass called Color, which has a type (that is, Green) with a value of 5, and then we create another subclass called Shade, which also has a type (that is Dark) with a value of 5, only one of them will be stored in the hashtable -- the last one to be instantiated.

In order to avoid this, we have to store a handle to the type based on not only its value, but also its class. Let's create a new method to store the type references. We'll use a hashtable of hashtables. The inner hashtable will be a mapping of values to types for each specific subclass (Color, Shade, and so on). The outer hashtable will be a mapping of subclasses to inner tables.

This routine will first attempt to acquire the inner table from the outer table. If it receives a null, the inner table doesn't exist yet. So, we create a new inner table and put it into the outer table. Next, we add the value/type mapping to the inner table and we're done. Here's the code:

 private void storeType( Type type ) { String className = type.getClass().getName(); Hashtable values; synchronized( types ) // avoid race condition for creating inner table { values = (Hashtable) types.get( className ); if( values == null ) { values = new Hashtable(); types.put( className, values ); } } values.put( new Integer( type.getValue() ), type ); } 

And here's the new version of the constructor:

 protected Type( int value, String desc ) { this.value = value; this.desc = desc; storeType( this ); } 

Now that we are storing a road map of types and values, we can perform lookups and thus restore an instance based on a value. The lookup requires two things: the target subclass identity and the integer value. Using this information, we can extract the inner table and find the handle to the matching type instance. Here's the code:

 public static Type getByValue( Class classRef, int value ) { Type type = null; String className = classRef.getName(); Hashtable values = (Hashtable) types.get( className ); if( values != null ) { type = (Type) values.get( new Integer( value ) ); } return( type ); } 

Thus, restoring a value is as simple as this (note that the return value must be casted):

 int value = // read from file, database, etc. Color background = (ColorType) Type.findByValue( ColorType.class, value ); 

Enumerating the types

Dank unserer Hashtable-of-Hashtables-Organisation ist es unglaublich einfach, die Aufzählungsfunktionalität von Erics Implementierung verfügbar zu machen. Die einzige Einschränkung ist, dass die Sortierung, die Erics Design bietet, nicht garantiert werden kann. Wenn Sie Java 2 verwenden, können Sie die inneren Hashtabellen durch die sortierte Zuordnung ersetzen. Wie ich am Anfang dieser Kolumne sagte, beschäftige ich mich derzeit nur mit der 1.1-Version des JDK.

Die einzige Logik, die zum Auflisten der Typen erforderlich ist, besteht darin, die innere Tabelle abzurufen und ihre Elementliste zurückzugeben. Wenn die innere Tabelle nicht existiert, geben wir einfach null zurück. Hier ist die gesamte Methode: