Använd konstanta typer för säkrare och renare kod

I denna handledning kommer vi att utvidga idén om uppräknade konstanter som beskrivs i Eric Armstrongs, "Skapa uppräknade konstanter i Java." Jag rekommenderar starkt att du läser den artikeln innan du fördjupar dig i den här, eftersom jag antar att du känner till begreppen relaterade till uppräknade konstanter, och jag kommer att utöka några av de exempelkoder som Eric presenterade.

Begreppet konstanter

När jag behandlar uppräknade konstanter ska jag diskutera den uppräknade delen av konceptet i slutet av artikeln. För närvarande fokuserar vi bara på den konstanta aspekten. Konstanter är i grunden variabler vars värde inte kan förändras. I C / C ++ används nyckelordet constför att deklarera dessa konstanta variabler. I Java använder du nyckelordet final. Emellertid är verktyget som introduceras här inte bara en primitiv variabel; det är en verklig objektinstans. Objektinstanserna är oföränderliga och oföränderliga - deras interna tillstånd kanske inte ändras. Detta liknar singletonmönstret, där en klass bara kan ha en enda instans; i det här fallet kan dock en klass endast ha en begränsad och fördefinierad uppsättning instanser.

De främsta anledningarna till att använda konstanter är tydlighet och säkerhet. Följande kod är till exempel inte självförklarande:

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

Från den här koden kan vi fastställa att en färg ställs in. Men vilken färg representerar 5? Om den här koden skrevs av en av de sällsynta programmerare som kommenterar hans eller hennes arbete kan vi hitta svaret högst upp i filen. Men mer troligt måste vi gräva efter några gamla designdokument (om de ens finns) för en förklaring.

En tydligare lösning är att tilldela värdet 5 till en variabel med ett meningsfullt namn. Till exempel:

offentlig statisk slutlig int RÖD = 5; public void someMethod () {setColor (RED); }

Nu kan vi berätta omedelbart vad som händer med koden. Färgen sätts till rött. Det här är mycket renare, men är det säkrare? Vad händer om en annan kodare blir förvirrad och förklarar olika värden så:

public static final int RED = 3; public static final int GRÖN = 5;

Nu har vi två problem. Först och främst REDär det inte längre satt till rätt värde. För det andra representeras värdet för rött av den variabel som heter GREEN. Kanske är det läskigaste att den här koden kommer att kompileras helt bra, och att fel kanske inte upptäcks förrän produkten har levererats.

Vi kan åtgärda problemet genom att skapa en definitiv färgklass:

public class Färg {public static final int RED = 5; offentlig statisk slutlig GRÖN = 7; }

Sedan, via dokumentation och kodgranskning, uppmuntrar vi programmerare att använda det så:

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

Jag säger uppmuntrar eftersom designen i den kodlistan inte tillåter oss att tvinga kodaren att följa; koden kommer fortfarande att kompileras även om allt inte är helt i ordning. Även om detta är lite säkrare är det alltså inte helt säkert. Även om programmerare ska använda Colorklassen, är de inte skyldiga att göra det. Programmerare kan mycket enkelt skriva och sammanställa följande kod:

 setColor (3498910); 

Känner setColormetoden igen att detta stora antal är en färg? Antagligen inte. Så hur kan vi skydda oss från dessa oseriösa programmerare? Det är där konstanterna kommer till undsättning.

Vi börjar med att omdefiniera metodens signatur:

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

Nu kan programmerare inte skicka in ett godtyckligt helvärde. De tvingas tillhandahålla ett giltigt Colorobjekt. Ett exempel på implementering av detta kan se ut så här:

offentlig ogiltig someMethod () {setColor (ny färg ("röd")); }

Vi arbetar fortfarande med ren, läsbar kod och vi är mycket närmare att uppnå absolut säkerhet. Men vi är inte riktigt där än. Programmeraren har fortfarande lite utrymme för att utlösa förödelse och kan godtyckligt skapa nya färger så:

public void someMethod () {setColor (new Color ("Hej, mitt namn är Ted.")); }

Vi förhindrar denna situation genom att göra Colorklassen oföränderlig och dölja instantiering från programmeraren. Vi gör varje typ av färg (röd, grön, blå) till en singleton. Detta åstadkoms genom att göra konstruktören privat och sedan utsätta offentliga handtag för en begränsad och väldefinierad lista med instanser:

offentlig klass Färg {privat Färg () {} offentlig statisk slutlig Färg RÖD = ny Färg (); offentlig statisk slutlig Färg GRÖN = ny färg (); offentlig statisk slutlig Färg BLÅ = ny färg (); }

I denna kod har vi äntligen uppnått absolut säkerhet. Programmeraren kan inte tillverka falska färger. Endast de definierade färgerna får användas; i annat fall kommer programmet inte att kompileras. Så här ser vårt genomförande ut:

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

Uthållighet

Okej, nu har vi ett rent och säkert sätt att hantera konstanta typer. Vi kan skapa ett objekt med ett färgattribut och vara säker på att färgvärdet alltid kommer att vara giltigt. Men vad händer om vi vill lagra det här objektet i en databas eller skriva det till en fil? Hur sparar vi färgvärdet? Vi måste kartlägga dessa typer till värden.

I JavaWorld- artikeln som nämnts ovan använde Eric Armstrong strängvärden. Att använda strängar ger den extra bonusen att ge dig något meningsfullt att returnera i toString()metoden, vilket gör felsökningsutdata mycket tydligt.

Strängar kan dock vara dyra att lagra. Ett heltal kräver 32 bitar för att lagra sitt värde medan en sträng kräver 16 bitar per tecken (på grund av Unicode-stöd). Exempelvis kan numret 49858712 lagras i 32 bitar, men strängen TURQUOISEkräver 144 bitar. Om du lagrar tusentals objekt med färgattribut kan denna relativt lilla skillnad i bitar (mellan 32 och 144 i det här fallet) lägga upp snabbt. Så låt oss använda heltalsvärden istället. Vad är lösningen på detta problem? Vi behåller strängvärdena eftersom de är viktiga för presentation, men vi kommer inte att lagra dem.

Versioner av Java från 1.1 och framåt kan automatisera serieobjekt så länge de implementerar Serializablegränssnittet. För att förhindra att Java lagrar främmande data måste du deklarera sådana variabler med transientnyckelordet. Så för att lagra heltalets värden utan att lagra strängrepresentationen, förklarar vi att strängattributet är övergående. Här är den nya klassen, tillsammans med accessorer till heltal och strängattribut:

offentlig klass Färg implementerar java.io.Serializable {privat int värde; privat övergående strängnamn; offentlig statisk slutlig Färg RÖD = ny färg (0, "röd"); offentlig statisk slutlig Färg BLÅ = ny färg (1, "Blå"); offentlig statisk slutlig Färg GRÖN = ny färg (2, "grön"); privat färg (int-värde, strängnamn) {this.value = värde; detta.namn = namn; } public int getValue () {returvärde; } public String toString () {return name; }}

Nu kan vi effektivt lagra instanser av konstant typ Color. Men hur är det med att återställa dem? Det kommer att bli lite knepigt. Innan vi går vidare, låt oss utvidga detta till ett ramverk som kommer att hantera alla de ovan nämnda fallgroparna för oss, så att vi kan fokusera på det enkla att definiera typer.

Det konstanta ramverket

Med vår fasta förståelse av konstanta typer kan jag nu hoppa in i den här månadens verktyg. Verktyget kallas Typeoch det är en enkel abstrakt klass. Allt du behöver göra är att skapa en mycket enkel underklass och du har ett komplett bibliotek med konstant typ. Här är hur vår Colorklass kommer att se ut nu:

offentlig klass Färg utökar Typ {skyddad färg (int-värde, strängbeskrivning) {super (värde, beskrivning); } offentlig statisk slutlig färg RÖD = ny färg (0, "röd"); offentlig statisk slutlig Färg BLÅ = ny färg (1, "Blå"); offentlig statisk slutlig Färg GRÖN = ny färg (2, "grön"); }

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

Tack vare vår hashtable-of-hashtables-organisation är det otroligt enkelt att avslöja den uppräkningsfunktionalitet som Erics implementering erbjuder. Den enda varningen är att sortering, som Erics design erbjuder, inte garanteras. Om du använder Java 2 kan du ersätta den sorterade kartan med de inre hashtabellerna. Men som jag sa i början av den här kolumnen är jag bara bekymrad över 1.1-versionen av JDK just nu.

Den enda logiken som krävs för att räkna upp typerna är att hämta den inre tabellen och returnera dess elementlista. Om det inre bordet inte finns returnerar vi helt enkelt null. Här är hela metoden: