Bij Java cursussen waarschuw ik de cursisten altijd om voorzichtig te zijn met de
toString() methode en ook bij code reviews komt het wel eens aan de orde. Sommige mensen
zijn geneigd "iets" te doen met het resultaat van een toString() aanroep (anders dan
afdrukken of loggen) en het kost me nog wel eens moeite om mensen te overtuigen dat dat nogal
riskant is.
Wat mij betreft is de redenering heel helder: weliswaar definieert de javadoc van
Object dat het resultaat een "concise but informative representation" van het object
moet zijn (en voor mensen goed leesbaar), maar dat laat nogal wat ruimte voor
variatie. Als programmeur kun je er dus niet op vertrouwen dat hetgeen je terugkrijgt
van toString bruikbaar is om er iets mee te doen of enige beslissing hierop te baseren.
Dit wordt nog wel eens afgedaan als een theoretische redenering. Immers, als je zelf de
toString implementeert weet je precies wat hij teruggeeft, en waarom zou je die kennis
niet kunnen gebruiken? Als je het heel netjes doet specificeer je in de javadoc heel
precies wat de return waarde is, en niemand kan je wat maken.
Waar dit aan voorbij gaat, is dat "derden" die specifieke semantiek niet verwachten. Er komt een dag dat
iemand anders de code aanpast of met een afgeleide class uitbreidt, en die gaat niet de
javadoc van toString() bestuderen – die kent hij al. Zonder zich er van bewust te zijn
breekt hij het (gewijzigde!) contract en is Leiden in last. En laten we wel wezen: op
de keper beschouwd was het preciezer definieren van het toString resultaat al een
wijziging in het contract. Hiermee wordt een belangrijk principe van "good coding
style" overtreden: goede code bevat geen verrassingen.
Aanroeper onbekend
Een andere reden om op te passen met de implementatie van de toString method, is dat
het onderdeel is van het Object contract en je nooit weet wie of wat je wanneer
aanroept. Hoe vreselijk dit uit de hand kan lopen, merkte ik een tijdje geleden toen ik
probeerde een performance probleem op te lossen.
Het ging om een applicatie die draaide op JBoss. De testers hadden het gevoel dat de
applicatie onder zware belasting steeds langzamer werd – meer dan ze op grond van de
belasting zouden verwachten. Met een profiler had ik van alles onderzocht, en ook wat
kleinere issues opgelost, maar de klacht bleef.
Nadat ik op een gegeven moment wat had zitten monitoren met JConsole en eigenlijk
gedachtenloos wat zat te klikken in de thread view, viel mijn oog op een stacktrace
waarin een toString method werd aangeroepen van een van andere Queue class. Dat was
verdacht, vooral omdat in die applicatie bij zware belasting nogal lange queues konden
ontstaan. Het zou toch niet zo zijn dat…
Het was wel zo. De queue bevatte enkele duizenden of tienduizenden elementen en de
toString leverde een string op van enkele megabytes groot (niet echt "concise"). De aanroep kwam uit een
toString van een worker-thread class; waarschijnlijk had iemand daarin ooit de toString van
de queue toegevoegd om beter te kunnen debuggen. En de verrassing was dat deze methode
niet vanuit applicatie code werd aangeroepen, maar vanuit JBoss.
Ergens diep in de transactie en lock management van JBoss, wordt van een thread die in
een wachtrij geplaatst wordt, de toString() aangeroepen; ook hier weer voor
debugging en/of monitoring. Hoe drukker de applicatie, hoe meer locks, hoe vaker de
toString werd aangeroepen en hoe drukker JBoss was met het uitrekenen van Strings die
eigenlijk nooit gebruikt werden, in plaats van met het uitvoeren van applicatie code.
Het was zo’n vondst waarop je altijd hoopt als je performance problemen onderzoekt. De
aanpassing was letterlijk in twee tellen gebeurd en de performance verbetering was echt
aanzienlijk.
De moraal van het verhaal is duidelijk: geen gekke dingen doen in de toString, want je
weet nooit wie het aanroept. En dat geldt niet alleen voor (afgeleide classes van)
infrastructurele elementen zoals Thread, maar juist omdat toString onderdeel uitmaakt
van het Object contract, voor alle classes.
