TimerApplet Experiment

Dises Java Programm lässt sich online als Applet starten, aber auch downloaden und als Applikation starten.

Grund für dieses Experiment war die für mich bisher unerklärliche Entdeckung, dass ein Aufruf der Methode setLocation(x,y) auf Komponenten eines JPanel viel länger dauern, wenn dieses in einem JApplet liegt, als wenn es in einem JFrame liegt. Dies obwohl weder JApplet noch JPanel eine Methode wie setLocation, move, setBounds, oder reshape* überschreiben würden. Diese Methoden werden direkt von Component geerbt.

Die zur Zeitmessung verwendeten Komponenten sind von der selbsterstellen Klasse Ball, welche ganz normal von JComponent abgeleitet ist. Wird die Position eines Balls verändert, wird folgende Aufrufekette durchlaufen: setLocation(x,y) -> move(x,y) -> setBounds(x,y,w,h) -> reshape(x,y,w,h).

Wobei x und y naturgemäss für die X- und Y-Koordinaten stehen und w und h für die Breite (=Width) und Höhe (=Height). Die Breite und Höhe bleiben bei setLocation-Aufrufen natürlich konstant. Es ist zu betonen, dass innerhalb dieser Methoden nichts gezeichnet, sondern nur die Positionen der Objekte verändert wird.

Das TimerApplet ist ein Rechteck in dem sich die Bälle bewegen (siehe unten). In jedem Schritt wird für jeden Ball seine neue Position berechnet und mit setBounds gesetzt. Mit Hilfe der Funktion System.nanoTime() lässt sich sehr genau messen, wie lange es vom Aufruf von setBounds bis zur Rückkehr dauert. Einmal liegt das JPanel, das die Ball-Komponenten enthält in einem JFrame und einmal in einem JApplet.

Hier sollte ein Applet stehen. Möglicherweise haben Sie kein Java installiert. Java ist für eine Vielzahl an Software notwendig. Auf www.java.com können Sie überprüfen, ob Sie Java installiert haben und ansonsten Java herunterladen.

Hier TimerApplet downloaden

Für verschiedene Anzahl Bälle wird die Zeit gemessen, die in jedem Schritt für die setBounds-Aufrufe aller Bälle benötigt wird (Intel Core2 Duo T9400, 2.53GHz, 4GB RAM, Windows 7, 64-Bit, Java 1.6.0_17):

Messwerte für verschiedene Anzahl Bälle
Bälle JFrame (ms) JApplet (ms)
100 1 1
200 1 1
300 1 3
400 1 5
500 1 8
600 1 11
700 1 15
800 2 20
900 2 25
1000 2 30
Bälle JFrame (ms) JApplet (ms)
1500 4 66
2000 5 117
3000 8 256
4000 11 457
5000 14 710
6000 17 1068
7000 20 1439
8000 23 1879
9000 26 2403
10000 29 3051

Es sind gravierende Unterschiede zu beobachten. Während die setBounds-Aufrufe auf Komponenten in einem JApplet für 1000 Bälle bereits 15 Mal länger dauern, als wenn sie in einem JFrame liegen würden, dauert es für 10000 Bälle bereits über 100 Mal so lange. Tatsächlich macht es den Anschein, als würde die benötigte Zeit in JFrames erwartungsgemäss linear mit der Anzahl Bälle steigen, während die benötigte Zeit in JApplets quadratisch wächst! Man betrachte dazu den Sprung von 1000 Bällen auf 2000 Bälle, wo die Zeit in JFrames sich verdoppelt, sich die Zeit in einem JApplet jedoch vervierfacht. Entsprechend verhält es sich auch mit 5000 und 10000 Bällen.

Messwerte in einem Diagramm. Links die Messwerte in JApplets, rechts in JFrames
Werte in JApplets Werte in JFrames

Die Zeit, die für das Zeichnen der Komponenten benötigt wird, ist im übrigen fast gleich lange für JApplets und JFrames. Aber ein Befehl, der nur die Positionen von Komponenten verändert, scheint von der Applet bzw. Frame Umgebug zeitlich beeinflusst zu sein.

Um dem Rätsel näher auf den Grund zu gehen, habe ich den Zeitbedarf für verschiedene Methoden gemessen, die von reshape aufgerufen werden (Sourcecode siehe unten), welches unmittelbar von setBounds aufgerufen wird (von setLocation werden über move über setBounds alle Befehle direkt an reshape weiteregegeben, siehe Aufrufskette oben).

Für die Methoden getTreeLock, setBoundsOp, getBoundsOp, invalidate und invalidateIfValid konnte kein Unterschied in JApplets und JFrames festgestellt werden. Übrig bleiben noch vier Methoden, die private oder package-private, also (programmtechnisch) nicht sichtbar sind. Dies sind mixOnReshaping, reshapeNativePeer, notifyNewBounds und repaintParentIfNeeded. Da sie sich nicht aufrufen lassen, ist auch keine Zeitmessung möglich.

Eine dieser vier Methoden bzw. eine Methode, die von einer dieser vier aufgerufen wird, muss den grösseren Zeitbedarf der Komponenten innerhalb von JApplets verursachen. Vom reinen Anschauen der Funktionen, fällt nichts ins Auge, was das sein könnte. So bleibt die Entdeckung weiterhin rätselhaft. Falls ein Leser Genaueres weiss, bin ich natürlich interessiert.

Ausserdem sind auch nicht unterschiedliche Priorisierungen des Event-Dispatching-Thread (EDT) für die Zeitunterschiede verantwortlich. Auch wenn die Priorität des EDT in einem JFrame vom Standartwert von 6 auf 1 reduziert wird, verändert dies an der Zeitmessung so gut wie nichts. Dem EDT in JApplets eine andere Priorität verpassen zu wollen, ändert ebenfalls nichts, da sich hier der EDT partout nicht von seinem Standartwert von 4 abbringen lassen will.

Als Fazit bleibt die lapidare Feststellung, man sollte nicht zu viele bewegende Komponenten innerhalb von JApplets verwenden. Für ein einfaches Beispiel wie hier mit dem TimerApplet ist das Ableiten von JComponent sowieso nicht nötig, da ausser den Positions-Methoden keine weitere Methode verwendet wird.

Eine bessere Alternative ist es, sich eine komplett eigene Klasse (bzw. Interface) zu schreiben und das Panel, das die Objekte enthält eine ArrayList verwalten zu lassen, in welche diese Objekte gespeichert werden, oder das MVC-Prinzip anzuwenden und das Panel die zu zeichnenden Objekte aus dem Model abrufen zu lassen.

Über das Thema wird auch hier kurz diskutiert: www.java-forum.org

Klassendiagramm der verwendeten Swing Klassen und den selsbtgeschriebenen Komponenten Ball und TimerPanel.

Klassendiagramm der wichtigsten verwendeten Klassen

Sourcecode:
Alle java Files als zip-Datei
TimerApplet.java (inkl. TimerPanel.java und Ball.java)
Applet.java
Component.java
Container.java
Frame.java
JApplet.java
JComponent.java
JFrame.java
JPanel.java
Panel.java
Window.java

*JComponent überschreibt reshape folgendermassen (somit hat es keinen spürbaren Effekt):

@Deprecated
public void reshape(int x, int y, int w, int h) {
    super.reshape(x, y, w, h);
}

Die Methode reshape aus Component sieht folgendermassen aus (Auszug aus dem Sourcecode von Sun):

@Deprecated
public void reshape(int x, int y, int width, int height) {
    synchronized (getTreeLock()) {
        try {
            setBoundsOp(ComponentPeer.SET_BOUNDS);
            boolean resized = (this.width != width) || (this.height != height);
            boolean moved = (this.x != x) || (this.y != y);
            if (!resized && !moved) {
                return;
            }
            int oldX = this.x;
            int oldY = this.y;
            int oldWidth = this.width; 
            int oldHeight = this.height;
            this.x = x; 
            this.y = y;
            this.width = width; 
            this.height = height;

            if (resized) {
                isPacked = false;
            }
            
            boolean needNotify = true;
            mixOnReshaping();
            if (peer != null) {
                // LightwightPeer is an empty stub so can skip peer.reshape
                if (!(peer instanceof LightweightPeer)) {                        
                    reshapeNativePeer(x, y, width, height, getBoundsOp());
                    // Check peer actualy changed coordinates
                    resized = (oldWidth != this.width) || (oldHeight != this.height);
                    moved = (oldX != this.x) || (oldY != this.y);
                    // fix for 5025858: do not send ComponentEvents for toplevel
                    // windows here as it is done from peer or native code when
                    // the window is really resized or moved, otherwise some
                    // events may be sent twice
                    if (this instanceof Window) {
                        needNotify = false;
                    }
                }
                if (resized) {
                    invalidate();
                }
                if (parent != null) {
                    parent.invalidateIfValid();
                }
            }
            if (needNotify) {
                notifyNewBounds(resized, moved);
            }
            repaintParentIfNeeded(oldX, oldY, oldWidth, oldHeight);
        } finally {
            setBoundsOp(ComponentPeer.RESET_OPERATION);
        }
    }
}