Nu har det blivit dags att titta på ännu en av nyheterna i Java 1.5. Dagens ämne är annotations, eller metadata, som det också kallas. Annotations är en mekanism för att lägga till metainformation om program i källkoden. Alltså information om själva programmet. Vad för information man ska lägga till är upp till var och en att bestämma. Om du vill kan du lägga till information om hur fin du tycker varje metod är på en skala mellan 1 och 5. Vad informationen ska användas till är också upp till dig. Det som är nytt i Java 1.5 är ett formellt sätt att uttrycka informationen, vilket gör att det blir möjligt för verktyg att hitta den. Kanske längtar du efter ett verktyg som gör en topplista över dina 10 finaste metoder? Kanske inte.
Hur har vi överlevt utan annotations?
Det finns många sätt att klara sig utan annotations, det har vi ju gjort innan Java 1.5. Jag tänkte börja med att diskutera alternativen. Om du inte är intresserad kan du hoppa ner till nästa avsnitt där du kan läsa om hur man använder annotations.
Nästan alla programmeringsspråk har stöd för att ha kommentarer i källkoden. Vettiga kommentarer innehåller information om programkoden och man kan se dem som en enkel form av metadata. Med kommentarer har man till exempel en chans att tala om för någon som läser programmet att en viss metod förväntar sig positiva tal som indata. Även om sådana kommentarer kan vara nyttiga så har de ingen formell betydelse, det första kompilatorn gör är att hiva alla kommentarer. Det finns också speciella kommentarformat är populära att använda för att verktyg som känner till kommentarformatet ska kunna göra något listigt, till exempel skapa dokumentation eller andra specifikationsfiler automatiskt. JavaDoc och XDoclet är exempel på sådana verktyg. Kompilatorn ska ju strunta i alla sådana kommentarer, på samma sätt som den struntar i andra kommentarer, men i Java finns det ett undantag. Om man anger att en metod är deprecated i ett speciellt kommentarformat kommer kompilatorn ge ifrån sig en varning om någon försöker anropa metoden. Det är ett märkligt mellanting mellan en kommentar och en del av språket.
I Java har man alltid kunnat ange att ett fält inte ska strömmas med serialiseringsmekanismen genom att ange modifieraren transient för dem. Man skulle kunna se detta som metadata som ansetts viktig nog för att få ett alldeles eget nyckelord i språket. Vilka klasser som kan strömmas över huvud taget anges inte med något reserverat nyckelord utan genom att låta dessa klasser implementera interfacet Serializable. Serializable är ett tomt interface så det ställer inga extra krav på vad klassen ska innehålla. Interfacet används bara som en markering och det kan ses som ett sätt att uttrycka metadata. Att lägga till nya modifierare till språket är inte oss vanliga dödliga förunnat, men att använda tomma interface för att säga något om en klass kan vem som helst göra.
Ett annat sätt att få in metadata i sina program är att använda namnmagi. Det innebär att man tillskriver namn (identifierare) som har en viss form speciella egenskaper. Komponentmodellen JavaBeans använder namnmagi för olika saker. Exempelvis betyder metodnamn som börjar med get att det finns en egenskap med namnet av det som följer efter get. Vill man ha en egenskap som heter salary kan man alltså skapa en metod som heter getSalary. (Det här gör det bökigt om man vill programmera ett receptprogram på svenska och får metodnamnet getMat. Helt plötsligt har man djurfoder i programmet!) Alla som har skaffat sig den goda vanan att enhetstesta sin programkod med verktyget JUnit vet att det specialbehandlar metoder som börjar med test, som till exempel testDiagram. Jag är inte speciellt förtjust i namnmagi. Det är svårt att som läsare känna till alla namnmagiregler och det finns inte något i koden som hjälper till med att förklara vad de betyder. De bara råkar ha en speciell form på sitt namn. Om jag hittar på en egen komponentmodell, låt os kalla den JensBeans, så kan jag med en namnmagiregel säga att alla metoder som börjar på bokstaven B är speciella på så sätt att de aldrig exekveras. Det är inget som kompilatorn känner till och inte heller något som de flesta som läser koden kommer veta om. Jag kan dock göra en exekveringsmiljö för JensBeans-komponenter som gör att B-metoder aldrig exekveras. Korkat, men möjligt.
När man skapar Enterprise JavaBean-komponenter anger man viss metainformation i separata XML-filer. I praktiken blir det svårt att få en överblick och att hålla informationen i olika filer konsistent. För att förstå innebörden av källkoden måste man veta om vad som står i XML-filerna.
Annotations är en mekanism som gör det möjligt att specificera metainformation i källkod tydligare än vad interface, namnmagi och kommentarer kan erbjuda. Jämfört med kommentarer är annotations även formellare och informationen hos dem finns kvar och kan nyttjas vid exekveringen. Nu fortsätter vi med annotations i praktiken.
Att annotera
Nu har det blivit dags att titta på en annotering. Om man ogillar namnmagi och ändå vill tala om att en metod är en testmetod kan man göra så här:
@Test void startValues() {}
Snabel-a följt av namnet på en annoteringstyp kan man lägga på samma ställen som modifierare som private, static och final. Alltså framför typ-, metod-, och fältdeklarationer. Det rekommenderas att man lägger annoteringen framför modifierarna, men det är tillåtet att blanda dem. I exemplet ovan antar vi att det finns en annoteringstyp som heter Test. Annoteringstyper kan kräva en eller flera parametrar. Så här skulle man kunna tala om att en metod har snygghetsbetyget 3:
@Snygghet(3) public void foo() {}
Alla parametervärden måste vara konstanter vid kompileringstillfället. Om annoteringstypen kräver flera parametrar så anger man dem med namn och värde. Här talar vi om att en klass är en Enterprise JavaBean samtidigt som vi talar om en massa information om den:
@EnterpriseJavaBean(
name = "PlanningServer",
description = "Façade to the planning server",
jndiName = "ejb/PlanningServer",
type = EjbType.stateless,
viewType = ViewType.remote)
class PlanningServerBean {}
Sedan krävs det verktyg som vet vad det innebär att vara en Enterprise JavaBean och gör något vettigt av informationen. Men vi återkommer till hur annoteringsinformation kan användas. Först ska vi titta på några annoteringstyper i klassbiblioteket och hur man kan skapa sina egna annoteringstyper.
Annoteringstyper i klassbiblioteket
Det finns en handfull annoteringstyper i klassbiblioteket, färdiga att användas. Här är några av dem.
Override är en annotering som man kan sätta på metoder. Det talar om att metoden åsidosätter en metod från någon superklass. Det är ju egentligen inte något man måste specificera, det räcker att metodnamnet och parameterlistan är samma som för den metod man vill åsidosätta. Men om man explicit annoterar metoden med Override ger kompilatorn ett felmeddelande ifall det inte finns någon metod med samma namn och parameterlista i någon superklass.
Deprecated är samma sak som deprecated i JavaDoc-kommentarer. Det talar om att till exempel en metod inte bör användas. Oftast finns det bättre alternativ, men metoden finns kvar av kompatibilitetsskäl. Kompilatorn varnar om metoden används.
SuppressWarnings talar om för kompilatorn att den inte ska ge varningar av de sorter man specificerar.
Deklarera annoteringstyper
Till att börja med kan vi konstatera att det är ovanligt att applikationsprogrammerare skapar egna annoteringstyper. Det är snarare något som verktygstillverkare och liknande gör. Men det är inte för att det skulle vara speciellt svårt, snarare för att annotations främst är användbart tillsammans med verktyg som gör något baserat på vad som annoterats. Hursomhelst, så här kan annoteringstyperna från exemplen ovan deklareras:
@interface Test {}
@interface Snygghet {
int value();
}
@interface EnterpriseJavaBean {
String name();
String description() default "ännu en böna som kommer gå till världshistorien";
String jndiName();
EjbType type();
ViewType viewType() default ViewType.local;
}
Som synes är deklarationerna väldigt lika interface-deklarationer, bara ett litet snabel-a skiljer dem åt. Varje parameter anges som om den vore en metod och metodens returtyp anger parameterns typ. De typer som parametrarna får ha är byte, char, short, int, long, float, double, boolean, String, Class, enum-typer, och annoteringstyper. Dessutom tillåts arrayer av dessa. Det går att sätta default-värden och då blir det frivilligt att ange den parametern, annars måste alla parametrar anges. Ordningen på dem är valfri.
Att parametern i annoteringstypen Snygghet har namnet value är ingen slump. Om det bara finns en parameter så måste den heta just value för att den som använder annoteringstypen ska slippa skriva parameternamnet. Det räcker då att skriva en parentes med värdet när man annoterar något: @Snygghet(1).
Det går att annotera annoteringstyper. Det innebär helt enkelt att man anger metainformation om annoteringstypen. Och vad vill man kunna säga om annoteringstyper? Två vanliga saker finns som finns med i klassbiblioteket är target och retention:
Target talar om vilken sorts programelement annoteringstypen får användas för. Som parameter anger man något eller några av elementen i enumen ElementType. Där finns till exempel CONSTRUCTOR, FIELD, METHOD och TYPE.
Retention talar om hur länge annoteringsinformationen ska sparas. Som parameter anger man något av elementen i enumen RetentionPolicy, och det som finns att välja bland är: SOURCE, CLASS och RUNTIME. SOURCE innebär att annoteringen bara behövs i källkoden, kompilatorn kan kasta bort den. CLASS innebär att annoteringen ska finnas kvar i bytekoden efter kompileringen, däremot behövs den inte vid exekveringen och den kan därför ignoreras av den virtuella maskinen. RUNTIME innebär att annoteringen ska finnas kvar även när koden exekveras, man kan då komma åt den med reflection.
Så här kan det se ut om annoteringar om snygghet bara ska kunna göras för metoder och informationen ska finnas kvar när programmet körs:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Snygghet {
int value();
}
Att komma åt annoteringsinformationen
Nu har vi tittat på det mesta som behövs för att annotera Javakod. Den stora frågan som finns kvar är hur man använder informationen. Ett sätt är att skriva ett verktyg som tolkar källkod och gör något relevant med informationen och till sin hjälp har man då verktyget apt som följer med JDK. Det gör att man slipper skriva kod som tolkar källkoden själv. Ett annat sätt att använda informationen är att använda reflection och göra något med informationen när koden körs. Det är ganska rättframt. Så här skriver man ut hur snygg metoden foo är:
Method m = MinTestklass.class.getMethod("foo");
Snygghet s = m.getAnnotation(Snygghet.class);
System.out.println("Snygghet foo: " + s.value());
Slutsatser
Jag tycker att syntaxen för annotations i är ganska bedrövlig. Varför återanvända nyckelordet interface? Varför ska deklarationerna av annoteringsparametrar se ut som om de vore metoddeklarationer? Varför använda snabel-a? Det är också ironiskt att en konstruktion som syftar till att minska behovet av namnmagi själv använder namnmagi. Parameternamnet values har ju speciell betydelse. Förhoppningsvis finns det någon bra anledning till att det blev som det blev.
Är annotations bra då? Å ena sidan är jag misstänksam mot metaprogrammering eftersom det är extremt komplext. Betydelsen av det som står i programmet definieras inte längre bara av språkspecifikationen utan även av hur annoteringar tolkas. Hur kul är det att debugga? Å andra sidan har det visat sig i massor av situationer att man behöver uttrycka metadata. Som så många andra språkkonstruktioner är annotations en tveeggad motorsåg; rätt använd är den kraftfull och nyttig, fel använd kommer den ge programkod som är helt obegriplig. Det ska bli intressant att se vilka annoteringstyper som kommer att dyka upp framöver och vilka som kommer att bli vanliga. En tänkbar utveckling är att de viktigaste annoteringsbegreppen får plats som riktiga språkkonstruktioner i framtida programmeringsspråk. Men min slutsats är att annotations är en bra språkkonstruktion. Den låter oss uttrycka saker som vi behöver uttrycka.