Tags: JEE | Java | Programmeren

Tien dingen die je altijd wilde weten over Java, maar niet durfde te vragen.

Door Marc Hartogs van Capgemini

Java zit vol prachtige technieken, maar het is lastig om ze allemaal paraat te hebben. Waarschijnlijk heeft elke Java-programmeur, hoe ervaren ook, wel onderwerpen die hij of zij niet helemaal scherp op het netvlies heeft. Dat is jammer, omdat krachtige oplossingen daarom soms onterecht vermeden worden, of verkeerd toegepast worden. Dit artikel wil het inzicht in tien onderwerpen vergroten, opdat elke ontwikkelaar er zonder reserves mee aan de slag kan. Aan de hand van compacte, volledige voorbeelden en hun uitvoer worden deze onderwerpen uitgelegd, waarbij de nadruk wordt gelegd op de betekenis van bepaaldebegrippen. De voorbeelden zijn getest op Java 6.

1- Listing: Overriding

Capgemini Listing 1 Overriding Uitvoer:
Schuldig.
Schuldig. 100 EURO boete.
Schuldig.
Hoger beroep: Vrijspraak.
Geen 100 EURO boete.

Iedereen gebruikt wel overriding en overloading, maar hoe zit het nu precies met instance variabelen, zichtbaarheid en exceptions? En is een classhiërarchie altijd wel zo’n logische afbeelding van de werkelijkheid? Een Raadsheer is een bijzondere vorm van Rechter, namelijk een rechter bij een Gerechtshof of Hoge Raad. Zo’n rechter kan een uitspraak doen in hoger beroep. In het voorbeeld is Raadsheer een subclass van Rechter, en er zijn twee variabelen, beide van type Rechter: r1 en r2. De eerste krijgt een object van class Rechter, de tweede van class Raadsheer. Aan de uitvoer kan je zien dat overriding alleen werkt op methodes. Bij instance variabelen werkt overriding niet (vonnis) maar overerving wel (straf). En je ziet dat een methode wel weer een variabele gebruikt die bij zijn eigen class hoort als die er is (vonnis). De compiler controleert op de zichtbaarheid van een methode (public, private etc.) en of de exceptions die een methode gooit allemaal afgehandeld worden. De compiler kijkt daarvoor naar het type van de variabele, en kan niet zien dat deze tijdens runtime verwijst naar een subclass object. Toch moet er de zekerheid zijn dat de subclass ookvoldoet.

De overriding methodes van de subclass mogen daarom niet een beperktere zichtbaarheid hebben, en mogen niet meer exceptions gooien. To override betekent terzijde schuiven, opheffen, en dat houdt bijvoorbeeld in dat de ene uitspraak een andere vervangt en ongeldig maakt, zoals ook in het voorbeeld. Bij overloading daarentegen wordt niets ongeldig gemaakt, er wordt slechts een naam dubbel gebruikt. Je kan het lezen als method name overloading. Dit voorbeeld illustreert ook dat een class-hiërarchie (superclass staat boven subclass) omgekeerd kan zijn aan de hiërarchie in de werkelijkheid: een subclass is een verbijzondering, waardoor het een “hogere” variant van de superclass is, met grotere verantwCapgemini-Listing-10-EqualHashCode.bmpoordelijkheid. Zo zou een superintendant een subclass van een intendant zijn.

2- Listing Protected: library/Pro, myapp/Sub, myapp/App

Uitvoer:
In Pro, method()... in helper().
In Sub, myMethod()... in helper(). Het ging net al even over zichtbaarheid. Een bijzonder geval is protected. Waar dient deze nou eigenlijk voor? Een van de meest logische toepassingen van protected is: voor gegevens of hulpmethoden die je binnen een eigen class gebruikt, maar waarvan je verwacht dat een subclass in een andere package die ook zou willen gebruiken. In het voorbeeld heeft een library class een helper() methode die wel door een subclass aangeroepen kan worden, maar niet direct via een instantie van die subclass zichtbaar is. Hetzelfde geldt voor de constante PI.

3- Listing: Threads

Capgemini Listing 3 Threads Uitvoer:
Count 1 10
Count 1 9
Count 1 8
Count 2 10
Count 2 9
Count 2 8
Yield 1 10
Yield 2 10
Yield 3 10
Yield 1 9
Yield 2 9
Yield 3 9
Yield 1 8
Yield 2 8
Yield 3 8
Count 3 10
Count 3 9
Count 3 8

Threads worden vaak ervaren als moeilijk te begrijpen. De reden is waarschijnlijk dat er een kleine gedachtensprong nodig is: de code van een applicatie wordt niet “zomaar” uitgevoerd, het zijn allemaal opdrachten aan een thread. Een thread rijgt zich een weg door de code, en voert de instructies uit die hij onderweg tegenkomt, zoals het creëren van een nieuw object, of het aanroepen van een methode op dat object. Letterlijk betekent thread onder andere: verloop, volgorde, levenslijn (the thread of life), verhaallijn (thread of a story). Een thread is ook daadwerkelijk een instantie van Thread: een object dat opdrachten moet gaan uitvoeren.

De eerste opdracht is om zichzelf te starten: start(). Vervolgens moet hij wachten of hij de volgende opdracht kan gaan uitvoeren, de methode run(). Het kan namelijk zijn dat er ook andere threads zijn, en er kan er normaal gesproken maar één tegelijk bezig zijn. Onderaan in het voorbeeld staat de opdracht Thread.yield(). Dit is een opdracht aan de thread om andere threads de ruimte te geven (to yield = voorrang verlenen, plaats maken.) Dit geeft geen garanties maar maakt hun kans wel groter. Merk op dat er geen expliciete verwijzing naar de huidige thread is. Thread geeft slechts aan dat de verdere instructies voor yield te vinden zijn in de class Thread. De verwijzing naar de huidige thread is impliciet. Dat kan ook, omdat die thread hoe dan ook degene is die alle opdrachten uitvoert.

Met de opdracht y3.join()worden twee threads aan elkaar gekoppeld. Dit zorgt ervoor dat de thread waarin main() draait pas verder gaat als thread y3 klaar is, hij wordt als het ware achter y3 vastgemaakt. Wederom is de verwijzing naar de huidige thread, die van main(), impliciet. Daardoor lijkt het alsof de opdracht join() alleen y3 betreft, maar dat is dus niet zo. Je zou het kunnen lezen als y3.join(thisthread).

4- Listing: Synchronization

Capgemini Listing 4 Synchronization Uitvoer met wait - notify
(starten met Java Synchronization j):
aftrap: p 1
winst: p 2 aftrap: p 1
winst: p 2 aftrap: p 1
winst: p 1 aftrap: p 2
winst: p 1 aftrap: p 2
winst: p 1 aftrap: p 2
winst: p 1

Bij threads speelt nog een ander lastig begrip een rol: synchronization. Synchronisatie heeft in het dagelijks leven te maken met het op elkaar afstemmen, coördineren van twee of meer (deel)systemen. Vooral in het Nederlands heeft synchronisatie echter sterk de betekenis van “gelijk laten lopen”, zoals bij horloges en synchroonzwemmen, en dat is nu juist wat bij synchronization niet aan de orde is. In de computerwereld heeft synchronization wel te maken met het coördineren van twee of meer deelsystemen (zoals threads of processen) maar het is niet gericht op het gelijk laten lopen van die deelsystemen. Synchronized in Java betekent dan ook niet “gelijk lopend met” maar slechts “gecoördineerd met” andere threads. Alle code zonder de markering synchronized wordt niet gecoördineerd. Dat wil zeggen dat die code op elk moment door willekeurig elke thread mag worden uitgevoerd. Code met markering synchronized wordt wel gecoördineerd: zolang een thread met deze code voor een bepaalde instantie bezig is, mag een andere thread deze code niet uitvoeren voor dezelfde instantie. In het spel van het voorbeeld willen we dat steeds de winst gaat naar de thread (speler) die als eerste 100.000 bereikt. We willen uitsluiten dat de andere thread er snel tussenkomt. De hele methode incAndCheck() is synchronized. (Achter de schermen wordt dit geregeld door op de betreffende instantie van Game een vlag te zetten die aangeeft dat een thread er mee bezig is. Dit heet een lock.)

Bovenop deze vorm van coördinatie van threads, waarbij de threads wel last van elkaar kunnen hebben, maar elkaar verder negeren, kunnen threads ook met elkaar communiceren, met behulp van wait(), notify() en notifyAll(). In het voorbeeld zeggen we met g.wait() dat de speler (thread) die zojuist gewonnen heeft aan de wachtrij van het spel (g) moet worden toegevoegd. Het betekent ook dat alleen de andere thread iets kan doen. Zodra die weer een aftrap gedaan heeft, komt hij in de code van Game het statement notify() tegen, wat er voor zorgt dat de thread in de wachtrij een bericht krijgt dat hij weer mee mag spelen. Bij de uitvoer met wait-notify kan je zien dat na winst van de één de ander de aftrap doet, bij de eerste uitvoer is dat niet zo. Hier komen we de impliciete verwijzing naar de huidige thread ook weer tegen, want g.wait() betekent eigenlijk iets als g.getWaitingList().add(thisthread). (Deze methoden horen bij Object, en niet bij Thread. Dat heeft ermee te maken dat ze gebruik maken van het lock-mechanisme op een instantie zelf.)

5- Listing: Serialize

Uivoer:
voor:
newTopStat, newTopMember,
newTopTrans
iniSubStat, newSubMember,
newSubTrans
na:
newTopStat, iniTopMember,
iniTopTrans
newSubStat, newSubMember, null Member variabelen kunnen na serialisatie in verschillende toestanden verkeren, die niet allen even voor de hand liggend zijn. Dit voorbeeld zet ze op een rijtje. Vlak voor het deserializen staat vr.subStat = "newSubStat". Afgezien van de misleidende, en af te raden, notatiewijze is deze toekenning bewust na het serializen gezet, om te laten zien dat static variabelen zich niets aantrekken van serialisatie: ze hebben altijd de waarde die als laatste is toegekend. De class Top is niet serializable dus worden zijn variabelen niet meegenomen, zelfs niet als een subclass wel serializable is. Bij het deserializen krijgen zijn non-static variabelen hun initiële waarde, of er nu transient voor staat of niet. De transient variabele van Sub tenslotte wordt ook niet meegenomen, en resulteert in een null bij het deserializen. Er is hier dus slechts één variabele die meegenomen wordt bij het serializen: subMember.

6- Listing: RegexFind

Capgemini Listing 6 RegexFind Uitvoer:
a => a_a_a_a__a
a.a => aba_ada___
a.?a => aba_ada___
.?+a => _bacada_ra
a.? => abacadab_a
b. => _ba____br_
[a-c] => abaca_ab_a
b.c =>_bac______
.* => abacadabra

Soms wordt ten onrechte geaarzeld om reguliere expressies in te zetten. De reden kan zijn dat de manier waarop Sun ze in Java heeft ingebouwd wel efficiënt maar niet heel doorzichtig is. De oorsprong van reguliere expressies (regular expressions) ligt bij de wiskundige taalmodellen, in het bijzonder die van reguliere talen. Elke reguliere expressie definieert een verzameling mogelijke strings. Vandaar dat ze ook goed gebruikt kunnen worden om de voorwaarden vast te leggen waar een string aan moet voldoen, en om strings die aan bepaalde voorwaarden voldoen terug te vinden in een groter geheel. Ze vormen daarmee een krachtig instrument in computertoepassingen, bijvoorbeeld voor invoercontrole en zoekopdrachten.

Wat Sun heeft gedaan volgt netjes het OO-principe van het scheiden van verantwoordelijkheden, maar daardoor moet je wel een paar stappen zetten. De eerste stap is het creëren van een Pattern, een gecompileerde regular expression. Ze hadden dit met een constructor kunnen doen, maar daarmee hadden ze de aanroepende code gedwongen om altijd new() te gebruiken. Met de gekozen oplossing (een factory method) staat het de Pattern class vrij om zelf te bepalen hoe hij aan een Pattern komt, bijvoorbeeld via caching. Het compileren van een regular expression kan namelijk een relatief zware taak zijn. Dat is ook een reden waarom het maken van een Pattern apart gezet is in deze compile-methode: het geeft de programmeur de mogelijkheid om een Pattern eenmaal te compileren, en vervolgens te hergebruiken. Vervolgens wordt aan de Pattern gevraagd om een Matcher te maken. Dat is niet heel intuïtief. Je zou eerder iets verwachten als Matcher. createMatcher(Pattern p, Strings). Kennelijk kan een pattern een Matcher maken en een referentie naar zichzelf daarin meegeven. De matcher is verantwoordelijk voor alles dat met het matchen te maken heeft: hij gebruikt de pattern om elke volgende match te vinden, hij houdt bij waar hij gebleven was, en hij houdt de laatste match en diens positie vast. Op die manier kan het voorbeeldprogramma precies laten zien waar in de string “abacadabra” de door de gebruiker ingevoerde expressie gevonden wordt.

7- Listing: RegexMatch

Capgemini Listing 7 RegexMatch Uitvoer bij aanroep met “Aap noot mies”:
Aap ja
noot nee
mies ja

Voor een eenmalige controle van een string met een regular expression kan een eenvoudige weg worden bewandeld: str.matches(controleRegEx); Maar in de meeste gevallen zullen we alle invoer van gebruikers willen controleren tegen een bepaalde expressie. Daartoe maken we weer een pattern aan en een matcher. We kunnen de matcher hergebruiken met de methode reset(). Het voorbeeld toont ook een bijkomend voordeel van het apart aanmaken van een pattern: je kan er nog bepaalde eigenschappen aan meegeven. Dit resulteert hier in een controle of de invoer begint met a t/m m, ongeacht hoofdletter.

8- Listing: ReflectEnum

Uitvoer:
final class Seizoen (Enum)
public static final Seizoen
Seizoen.LENTE
public static final Seizoen
Seizoen.ZOMER
public static final Seizoen
Seizoen.HERFST
public static final Seizoen
Seizoen.WINTER
public static Seizoen Seizoen.standaard
private static final Seizoen[]
Seizoen.$VALUES
public static Seizoen Seizoen.
valueOf(Java.lang.String)
public static Seizoen[] Seizoen.values()
LENTE
ZOMER
HERFST
WINTER
standaard: LENTE
Zelfde instantie De Enum is eigenlijk een class, maar een beetje een vreemde. En lang niet elke programmeur heeft gespeeld met reflection om classes te bestuderen. Een goede reden om beide eens te combineren. In de uitvoer van het voorbeeld kan je zien dat een Enum een final class is, met een speciaal kenmerk dat via isEnum() opgevraagd kan worden. Verder is duidelijk dat de waarden van een Enum instanties van diezelfde Enum zijn. Je kunt zo’n instantie toekennen aan een variabele, in dit geval standaard, en je kan hem opvragen via valueOf(). Deze wijzen allemaal naar dezelfde instantie.

9- Listing: GenericsWarning

Capgemini Listing 9 GenericsWarning Compileren met:
Javac GenericsWarning.
Java -Xlint:unchecked
Uitvoer:
GenericsWarning.Java:5: warning:
[unchecked] unchecked call
to add(int,E) as a member of
the raw type Java.util.List
list.add(0, val);
^

1 warning

Een mooie, maar enigszins gecompliceerde toevoeging aan Java 5 is generics. De reden om deze te introduceren was het verbeteren van de collections, zoals ArrayList, Hashmap etc. In collections kunnen objecten worden opgeslagen ongeacht hun type. Dit heeft onder andere tot gevolg dat de compiler geen type checking kan toepassen: het controleren of er correct met types wordt omgegaan. Wat we eigenlijk zouden willen is collections waarbij type checking wel toegepast kan worden, maar die tegelijk voor alle mogelijke types gebruikt kunnen worden. Want het zou erg onhandig zijn om voor elk mogelijk type weer een aparte collection te definiëren. De oplossing van generics is dat er voor elke collection een generieke definitie is die altijd van toepassing is op die collection, ongeacht het type objecten dat hij beheert, maar dat het type later wel nog gespecificeerd kan worden. Eigenlijk waren collections altijd al generiek, je kon immers elk type toepassen. Alleen het feit dat ze generiek zijn, wordt vanaf Java 5 expliciet aangegeven met een nieuwe notatie, zoals <E>, wat betekent: van een nog specifiek te maken type. Een deel van de generieke definitie van ArrayList zou dan zijn: public class ArrayList<E> { void add(int index, E element)

Dit geeft aan dat als er een ArrayList aangemaakt wordt, en hij wordt specifiek gemaakt door voor E een bepaald type in te vullen, dat dan alleen dat type toegevoegd mag worden met de methode add. Om code van voor Java 5 te kunnen compileren met een compiler van versie 5 of hoger, kunnen collections ook nog steeds zonder generics-notatie gebruikt worden, dus zonder een type te specificeren. In dat geval wordt een collection raw genoemd. En bij een methode die een element toevoegt zal de compiler dan waarschuwen dat hij niet kan checken of het correcte type toegevoegd wordt, terwijl je daar als programmeur mogelijk wel van uit gaat. Dat is precies wat bovenstaande warning aangeeft. Als je zeker weet dat je de warning kan negeren, kan je een annotation toevoegen: @SuppressWarnings("unchecked")

10- Listing: EqualHashCode

Uitvoer:
Janssen
al bekend Nu we toch met collections bezig zijn: ze vormen ook een veel voorkomende reden om de methoden equals() en hashCode() te implementeren, terwijl deze voor sommigen een no-go area zijn. Weliswaar defi - niëren deze methoden de gelijkheid van objecten, veel belangrijker is dat ze nodig zijn voor het terugvinden van objecten in een collection, en dat is weer nodig voor handelingen als toevoegen of raadplegen. Beoordelen of een object voorkomt kan op grond van het object zelf, of in het geval van bijvoorbeeld HashMap of Hashtable via een sleutel. In beide gevallen is het nodig dat objecten effi ciënt en correct vergeleken kunnen worden, en van beide is een voorbeeld in de listing te zien.

Het efficiënt terugvinden zowel op basis van sleutel als van objecten zelf is gebaseerd op twee stappen: eerst worden de hashcodes vergeleken, en pas als deze gelijk zijn worden de objecten zelf vergeleken. Daarom moeten de methodes aan bepaalde regels voldoen: objecten met gelijke hashcode hoeven zelf nog niet gelijk te zijn, maar als objecten gelijk zijn, dan moet de hashcode perse ook gelijk zijn. De equals() en hashCode() van EmplNr moeten geïmplementeerd worden, opdat deze correct als sleutel gebruikt kan worden. Als één van beide namelijk zou ontbreken zou in plaats daarvan de standaardimplementatie van Object gebruikt worden, en die is gebaseerd op een uniek id van de instantie. Daardoor zou showEmpl() nooit een element terug kunnen vinden. Verder wordt voor hashCode() de value door tien gedeeld om minder verschillende hashcodes te krijgen. Om precies te zijn: 1 op elke 10 werknemers. Dat is mogelijk nog niet optimaal maar al wel efficiënter. Bij equals() van Empl wordt de naam genegeerd en wordt gelijkheid gedefinieerd als het overeenkomen van landcode en socialeverzekeringsnummer (zoals BSN). In hashCode() worden dezelfde velden gebruikt, maar dan op een wat efficiëntere manier.

Afsluiting

Als bovenstaande voorbeelden bijdragen aan het begrijpen en correct toepassen van essentiële Java-technieken is een belangrijk doel van dit artikel al bereikt. Helemaal mooi zou het zijn als dit ook een aanleiding zou vormen om zelf met de code verder te experimenteren. Want voor de onderzoekende geest werpt elk antwoord zelf ook weer vragen op!

Lees meer over Capgemini
Ga terug naar We Love IT uitgave #2 - 2008
Advertentie