Pád aplikace při řazení v tabulce

Před nějakou dobou jsem řešil zajímavý programátorský problém: aplikace přestala reagovat při opakovaném řazení tabulky s výsledky. Překvapila mne nejen chyba jako taková, ale hlavně její příčina. Vždyť tohle přece nemůže nastat!

Jednalo se o desktopovou aplikaci běžící pod MS Windows naprogramovanou v jazyce Java s využitím knihoven SWT.

Odhalení chyby

Při odhalování chyby se ukázalo jako výhoda, že tester prováděl při testování průběžné snímání obrazovky a jako popis chyby přiložil výsledné video. O co šlo: tester provedl nastavení a spustil měření. To je dlouhodobý proces, takže při čekání na výsledek "si tester začal hrát," přičemž víceméně náhodně klikal na řazení tabulky s výsledky. Tímto "hraním si" však chybu odhalil, ale mně nějakou dobu trvalo, než jsem stanovil jednoznačný postup pro reprodukci. Při standardním testování a při vývoji se totiž chyba neprojevila.

Při analýze jsem zjistil, že pád aplikace byl způsoben následující výjimkou:

Exception in thread "main" java.lang.IllegalArgumentException: 
Comparison method violates its general contract! at java.util.TimSort.mergeLo(Unknown Source) at java.util.TimSort.mergeAt(Unknown Source) at java.util.TimSort.mergeCollapse(Unknown Source) at java.util.TimSort.sort(Unknown Source) at java.util.TimSort.sort(Unknown Source) at java.util.Arrays.sort(Unknown Source) at org.eclipse.jface.viewers.ViewerComparator.sort(ViewerComparator.java:185) at org.eclipse.jface.viewers.StructuredViewer.getSortedChildren(StructuredViewer.java:1050) at org.eclipse.jface.viewers.AbstractTableViewer.internalRefreshAll(AbstractTableViewer.java:701) at org.eclipse.jface.viewers.AbstractTableViewer.internalRefresh(AbstractTableViewer.java:649) at org.eclipse.jface.viewers.StructuredViewer$8.run(StructuredViewer.java:1514) at org.eclipse.jface.viewers.StructuredViewer.preservingSelection(StructuredViewer.java:1422) at org.eclipse.jface.viewers.StructuredViewer.preservingSelection(StructuredViewer.java:1383) at org.eclipse.jface.viewers.StructuredViewer.refresh(StructuredViewer.java:1512) at org.eclipse.jface.viewers.ColumnViewer.refresh(ColumnViewer.java:548) at org.eclipse.jface.viewers.StructuredViewer.refresh(StructuredViewer.java:1469) ...

To znamená, že při definici metody pro porovnávání prvků tabulky byl někde porušen obecný kontrakt pro porovnávání, tj. a = a, a pokud a < b, musí zároveň platit, že b > a. Na první pohled samozřejmost, ale...

Analýza chyby

Původní myšlenku, že v průběhu řazení došlo zároveň k aktualizaci výsledku, jsem zamítnul, jednak na základě experimentů, jednak na základě faktu, že modifikaci GUI v SWT může provádět pouze GUI vlákno, které je dobře synchronizované, tudíž souběh požadavků na řazení a na aktualizaci tabulky je prakticky vyloučen.

Chyba tedy musela být v implementaci metody ViewerComparator.compare(...). Řazení mělo velmi specifické požadavky (referenční carrier vždy nahoře, pak měřené, pak neměřené atd.), což bylo nutno při implementaci porovnávání zohlednit. A protože ani code review chybu neodhalil, rozhodl jsem se provést následující experiment.

Při výskytu výjimky jsem si nechal všechny prvky tabulky vyexportovat do Excelu jako matici, kde řádky a sloupce odpovídaly jednotlivým prvkům a jednotlivá pole v tabulce zobrazovala výsledek vzájemného porovnávání. Výsledná matice vypadala přibližně takto:

  ABCDE
A 1 1 1 -1 1
B 1 1 -1 1 1
C 1 -1 -1 -1 1
D 1 -1 -1 1 -1
E -1 1 1 1 1

Původní motivace experimentu byla provést kontrolu správnosti výsledků jednotlivých porovnání. První pohled na matici však odhalil zásadní problém: matice by měla být antisymetrická, tj. v hlavní diagonále nuly, a pokud je v poli nad hlavní diagonálou hodnota 1, měla by být v odpovídajícím poli pod diagonálou hodnota -1, a naopak. Což na první pohled neplatí. Příčina chyby je tedy jednoznačná: špatná implementace ViewerComparator.compare(...).

Řešení

Aby bylo možno vyhovět specifickým požadavkům na řazení a zároveň aby mohl být snadno splnitelný obecný kontrakt pro porovnávání, rozhodl jsem se pro změnu implementace porovnávání následujícím způsobem: zavedl jsem "virtuální sloupec" kategorie. Prvky v tabulce byly na základě svých atributů zařazeny do kategorií. Při řazení pak byly prvky seřazeny nejprve podle kategorie a až v rámci jednotlivých kategorií byly seřazeny podle hodnoty.

Kromě vlastního řešení problému s pádem aplikace byla nová implementace i jednodušší a přehlednější, což se projevilo i snadnější implementací zavedení nové kategorie.

Inspirací pro řešení byla implementace ViewerComparator.compare(...), přestože v podobě, jak byla původně navržena a implementována, nešla úplně použít.

Tagy: Java, Programování

Tento web bude tebe a tvůj počítač krmit piškotkami, jelikož a protože je to slušný web a jako takový ví, že je potřeba návštěvu řádně pohostit, aby se u nás cítila dobře. Užíváním tohoto webu potvrzuješ, že netrpíš mentální anorexií, nedržíš žádnou obskurní dietu a že můžeš piškotki do sebe cpát kdykoli a v jakémkoli množství. Více informací...