2 Visualisierung und Simulation mit Java
3 Lernmaterialien zur technischen Informatik

Carsten Kelling 1997 ([EMail senden]); letzte Änderung: 17. September 1997


4 Der Rechner-Baukasten

Dieses Kapitel wendet sich an den Programmierer, der mit der Klassenbibliothek "Rechner-Baukasten" eigene Applets entwerfen möchte. Die Komponenten des eigentlichen Rechner-Baukastens haben zwei hervorstechende Fähigkeiten: Sie simulieren die typischen Bestandteile eines auf der Registertransferebene betrachteten Computers, und sie visualisieren die dabei auftretenden Zeitabläufe. Zusätzlich enthält der Rechner-Baukasten mehrere Klassen, die features bereitstellen, wie sie in den Demonstrations-Applets in den "Lernmaterialien zur technischen Informatik" (Kapitel 3) zu sehen sind.

Die Grundannahme beim Verfassen der nachfolgenden Abschnitte war, daß der Programmierer noch keine Erfahrungen mit dem Rechner-Baukasten gemacht hat. Deswegen wird bottom up vorgegangen.

Dieser Annahme gemäß stellt 4.1 Komponenten des Rechner-Baukastens gleich zu Beginn die 13 Klassen des eigentlichen Rechner-Baukastens in ihrer Hierarchie vor. Aus der hier vorhandenen Übersicht kann man schnell diejenige Klasse auswählen, die einer zu simulierenden Rechnerkomponente entspricht.

Abschnitt 4.2 Das Package ckelling.baukasten legt dar, warum der Rechner-Baukasten als ein sogenanntes Package abgefaßt wurde, und welche Vorteile dieses für Codeverwaltung und Programmsicherheit bietet.

In 4.3 Die Wahl der Basisklasse des Applets erfährt der Baukasten-Neuling, daß ihm durch den Rechner-Baukasten eine mächtige Basisklasse für Applets zur Verfügung gestellt wird, die er ableiten und so beliebig erweitern kann. Diese Klasse Rechner enthält wichtige Konstanten und Variablen, die für eine einheitliche Darstellung der Komponenten sorgen. Viele für die Simulation wichtige Variablen sind ebenfalls bereits vordefiniert. Dazu kommen einige Methoden, die sich um häufig benötigte Verfahren wie die Erweiterung von Zahlen auf beliebig viele Stellen oder die Anzeige von Fehlermeldungen, die der Benutzer wirklich wahrnimmt, kümmern. Zu den weiteren Leistungen von Rechner gehört, daß diese Klasse dafür sorgt, daß auf kleinen Bildschirmen mit dem verfügbaren Platz sparsam umgegangen wird. Schließlich zwingt sie den Programmierer dazu, bestimmte Methoden zu implementieren, die von Hilfsklassen oder für die Simulation benötigt werden.

4.4 Erzeugung der Komponenten geht ausführlich darauf ein, wie man Rechnerkomponenten in eigenen Applets erzeugt. Dazu weist 4.4.1 zunächst auf den grafischen Editor hin, mit dem sich Rechneraufbauten in WYSIWYG-Manier erstellen lassen. Er unterstützt die Fähigkeit aller Komponenten des Rechner-Baukastens, ihre optischen Repräsentationen nicht an festen Bildschirmkoordinaten erscheinen zu lassen, sondern aneinander "anzuschließen". Ein Register an einer Abzweigung eines Busses bleibt so grafisch immer mit dem Bus verbunden, auch wenn die Abzweigung Länge und Position ändert. Ebenso sind alle mit dem Editor erstellten Aufbauten dagegen resistent, daß sich die Bildschirmrepräsentation einer Klasse hinsichtlich der Größe ändert, solange diese Klasse ihre neue Größe den an ihr "angeschlossenen" Komponenten korrekt mitteilt.

Im weiteren Verlauf von 4.4 wird aber auch illustriert, wie man Aufbauten von Hand programmieren kann. Darum führt 4.4.2 Koordinaten der Komponenten die notwendigen Methoden auf, um die Position von Komponenten zu ermitteln und Komponenten so zu verschieben, daß sie "angeschlossen" erscheinen. Im Rahmen des Eingehens auf die Erzeugung von Komponenten werden in 4.4.3 die nicht allgemeinen Fähigkeiten einiger Klassen besprochen, 4.4.4 expliziert die sinnvolle Aufteilung der Komponenten in zwei Gruppen, und 4.4.5 beschäftigt sich mit dem Entfernen von Komponenten inklusive ihres Abbildes.

Nachdem man gesehen hat, wie man Rechnerkomponenten erzeugen und plazieren kann, gehen die Abschnitte 4.5 und 4.6 auf die beiden oben genannten Fähigkeiten der Klassen des Rechner-Baukastens ein, also auf die Simulation respektive Visualisierung.

In 4.5 Wertzuweisung und Berechnung wird umfassend beschrieben, wie man Komponenten einen Wert zuweist, und wie man diesen wieder abfragt. Es wird erläutert, wie ein RAM oder ein Befehlsregister mit Befehls- und Datenanteil gegebenenfalls vorher adressiert werden müssen bzw. können. Schließlich kann man nachlesen, wie die Klassen ALU und Adder Werte auch verarbeiten können.

In 4.6 Visualisierung wird beschrieben, wie die Simulationsvorgänge in der Zeit didaktisch adäquat visualisiert werden können.

In 4.7 Zusätzliche Funktionalität kann man nachlesen, wie man Komponenten des Rechner-Baukastens mit teilweise mehrzeiligen Überschriften versehen kann und wie man mit dem Rechner-Baukasten auf einfache Weise kleine kontextbezogene Hilfetexte am Mauszeiger - info tips - erscheinen läßt.

Erst nachdem man mit dem Rechner-Baukasten sein Handwerkszeug kennt, kann man sich mit der Frage beschäftigen, wie man mit diesen konkreten Mitteln Mikrorechner oder Teile von diesen simuliert. Deswegen folgt erst in 4.8 Der Simulationsansatz die Beschreibung zweier Simulationsansätze, die für die Demonstrations-Applets entworfen und dort eingesetzt wurden. Der zweite, aus dem ersten entstandene Algorithmus eignet sich auch dazu, Simulationsschritte rückgängig zu machen und ist durch unvorhergesehene Benutzereingaben nicht zu erschüttern.

Der Abschnitt 4.9 Pfade und benötigte Dateien geht auf die notwendige logische Verzeichnistruktur der "Lernmaterialien" ein, weil der Rechner-Baukasten hauptsächlich ihrer Komplettierung dienen soll. Die Ausführungen über die Verzeichnisordnung bei Verwenden von Packages sind aber auch für eigenständige Applets von Belang; da Javaprogramme keine monolithischen Dateien sind, ist es stets notwendig, die Auffindbarkeit aller benötigten Klassendateien sicherzustellen.

4.10 Ausführen eines Applets erklärt dazu, welche Anweisungen in einer HTML-Datei notwendig sind, um ein Applet zu starten und damit dieses seinen Klassencode findet. Außerdem wird das Übergeben von Parametern an ein Applet beschrieben; diese Parametrisierung wurde bei den Applets in 3.3 Der Von-Neumann-Rechner verwendet, um mit einer Klasse auf bisher fünf verschiedene Themen abheben zu können. Es werden die 13 vom Rechner-Baukasten unterstützten Parameter mit ihrer Bedeutung und erlaubten Werten aufgeführt. Mit ihnen läßt sich beispielsweise festlegen, bei welchen Komponenten eines Aufbaus Werte durch den Benutzer eingetragen werden können, ob Zahlen im Hauptspeicher als Opcodes erscheinen oder ob das Applet bereits über die erweiterten Knöpfe "", "" und "" verfügen soll. Mit diesen kann der Benutzer statt kleiner Demonstrationsschritte ganze Maschinenbefehle auf einmal ausführen lassen oder den Rechner sogar bis zu einem breakpoint laufen lassen.

4.11 enthält eine kurze Aufstellung der mit dem Rechner-Baukasten erstellten sieben Demonstrations-Applets, von "Register" über "Adressierungsarten" bis zu "Pipeline" und "Cache", und der von ihnen verwendeten Klassen.

4.12 Unterstützungsklassen zur freien Verwendung behandelt neun Klassen, mit deren Hilfe Applets leistungsfähiger, einfacher und ansehnlicher werden können. Der Verwendungszweck dieser Klassen, die die Entwicklung eigener Demonstrations-Applets deutlich erleichtern können, wird jeweils kurz genannt. Anschließend folgen erläuterte und kommentierte Codeausrisse, mit denen die Erzeugung und Benutzung veranschaulicht wird. Dieses Kapitel kann dem fortgeschrittenen Programmierer auch als Nachschlagewerk dienen.

Um diese Aneinanderreihung von Fakten, Anleitungen und Hinweisen nicht zu trocken werden zu lassen und damit diese sich besser einprägen, wird im Laufe des Kapitels ein konkretes Beispiel verfolgt und Java-Code dazu angegeben. Die entsprechenden Stellen sind mit dem Text "Beispiel: RAM" markiert. Als Beispiel dient der aus 3.2.1.2 random access memory (RAM) bekannte Aufbau, anhand dessen dort erläutert wurde, wie ein RAM arbeitet, insbesondere wie man es adressiert und anschließend einen Wert hineinschreibt oder ausliest (Abbildung 31).



Abbildung 31: Das RAM-Applet - Simulation eines Lesevorganges, vor der Übernahme des vom RAM auf den Datenbus gelegten Wertes durch ein Register

Für die Programmcodebeispiele geht der Autor von grundlegenden Kenntnissen des Lesers in objektorientierter Programmierung aus - dieses umfaßt die Begriffe "Klasse", "Methode" und "Superklasse", die Prinzipien der Vererbung (mit dem Verbum "ableiten") und Instanziierung, sowie die allgemeineren Begriffe "Definition", "Deklaration" und "Initialisierung". Eine gute Einführung in das objektorientierte Paradigma bietet [JavaTutorial].


Klassenhierarchie der Komponenten des Rechner-Baukastens


Abbildung 32: Klassenhierarchie der Komponenten des Rechner-Baukastens

4.1 Komponenten des Rechner-Baukastens

Abbildung 32 zeigt die Klassenhierarchie der Komponenten des Rechner-Baukastens. Mit den in der Einleitung erwähnten zusätzlichen Klassen beschäftigt sich eingehend 4.12 Unterstützungsklassen zur freien Verwendung; dort findet sich auch eine Grafik zu deren Klassenhierarchie.

Eine kurze Beschreibung aller Klassen, die zur Simulation und Darstellung von Teilen eines Computers auf der Registertransferebene entworfen wurden, bietet Tabelle 9.

Klasse

Kurzbeschreibung

Adder Ein Addierer mit beliebig vielen Eingängen. Abgeleitet von ALU; kann im Gegensatz zu dieser nur Addieren und sieht anders aus, erzeugt aber auch flags.
ALU Führt diverse Arten von arithmetischen und logischen Berechnungen mit einem oder zwei Operanden durch und zeichnet das Symbol einer ALU inklusive Operanden, Operator, Resultat und flags.
Das Bild der ALU ist skalier- und in Schritten von 90 Grad rotierbar; Operanden, Operator, Resultat und flags werden aber nur bei ""-ähnlicher ALU angezeigt.
EditableLabel Zeigt einen ganzzahligen numerischen Wert ohne Rahmen an; ist editierbar.
EditableMemory Simuliert ein RAM und zeichnet dieses. Speicherzellen sind editierbar, falls nicht gesperrt.
Misc Ellipse mit beliebig vielen Zeilen Text als Symbol für "sonstige" Komponenten
Mux 2-auf-1-Multiplexer. Die Beschriftung der beiden Eingänge (Standard: "1"/"0") läßt sich ändern.
PipelineRegister Register für Pipelines; die linke und rechte Hälfte können getrennt eingefärbt werden, um Schreiben oder Lesen anzuzeigen. Beliebig viele mit Namen versehene Werte können gespeichert werden; diese werden erst auf Kommando in der nächsten Pipelinestufe sichtbar.
PipelineStageLabel Überschrift für Stufen einer Pipeline. Zeigt zwei Zeilen Text an: Opcode des in der Stufe enthaltenen Befehls und Name der Stufe.
RechnerKomponente Superklasse für folgende Elemente des Rechner-Baukastens: ALU, Adder, EditableMemory, TagMemory, SimpleBus, PipelineStageLabel, Misc, Mux.
Kann abgeleitet werden, um eigene Komponenten für den Rechner-Baukasten zu erzeugen.
RechnerKomponentePanel Superklasse für folgende Elemente des Rechner-Baukastens: Register16, Register8, Register16Split, PipelineRegister.
Leitet sich von java.awt.Panel ab und kann abgeleitet werden, um eigene Komponenten für den Rechner-Baukasten zu erzeugen, die auch AWT-Komponenten (wie TextField) benutzen.
Register16 16 Bit-Register (ohne Zwischenlinie); editierbar, falls nicht gesperrt.
Register16Split 16 Bit-Register (mit Zwischenlinie); Bytes einzeln editierbar, falls nicht gesperrt.
Register8 8 Bit-Register (ohne Zwischenlinie); editierbar, falls nicht gesperrt.
Achtung, speichert nur positive Werte!
SimpleBus Einfacher Bus; kann um beliebig viele Abzweigungen erweitert werden; ermittelt selbsttätig seinen Wert; kann Punkte von einem Startpunkt zu beliebig vielen Endpunkten laufen lassen; kann einen einzelnen Punkt über den Hauptstrang laufen lassen; Abzweigungen können dort, wo sie von dem Bus abgehen "Lötpunkte" oder Symbole für Tri-State-Gatter besitzen; der Hauptstrang des Busses kann farblich von den Abzweigungen abgehoben sein.
TagMemory

Simuliert ein RAM und zeichnet dieses. Speicherzellen sind editierbar, falls nicht gesperrt.
Bildet zusammen mit einem EditableMemory einen kompletten Lese- und Schreibcache; die Methoden readRam() und writeRam()"erkennen", ob sich ein Wert bereits im Cache befindet, ändern ggf. die Tag-Einträge und liefern beim Lesen den richtigen Wert zurück.

Bietet Einstellmöglichkeiten für

  • Größe des Caches,
  • Verhalten bei Schreibzugriffen (write through oder write back),
  • Assoziativität (direct mapped, zweifach-, vierfach-, voll-assoziativ),
  • linesize,
  • Ersetzungsstrategien (least recently used oder zufällig)
Tabelle 9: Die Klassen des Rechner-Baukastens, die Rechnerkomponenten simulieren

Beispiel: RAM

Für den oben genannten RAM-Aufbau benötigt man offenbar ein RAM, zwei Register (Quelle für Adresse, Quelle/Senke für Daten) und zwei Busse (Adreß- und Datenbus). Mit Hilfe von Tabelle 9 entscheidet man sich leicht, folgende Variablen im Java-Code zu verwenden:

4.2 Das Package ckelling.baukasten

Mit Javas package-Anweisung können "Pakete" (Packages) für zusammengehörige Klassen geschnürt werden; eine Klasse wird als zu einem Package gehörig angesehen, wenn ihre erste Anweisung etwa

package ckelling.baukasten;

lautet. Das sorgt nicht nur für mehr Ordnung in den üblicherweise großen Mengen von Klassen, die für verschiedene Projekte entstehen, sondern erlaubt auch differenziertere Steuerung von Zugriffsrechten: Methoden oder globale Variablen und Konstanten, die als protected deklariert sind, dürfen (nur) innerhalb des gesamten Package und von allen abgeleiteten Klassen benutzt werden. Das ist ein sinnvoller Mittelweg zwischen der Deklaration als public, die Zugriffe durch alle existierenden fremden Klassen erlaubt, und der als private, die gegenüber allen anderen Klassen abschottet.

Alle Klassen, die zu dem Rechner-Baukasten gehören, seien es Rechnerkomponenten oder nützliche Zusatzklassen, gehören zu dem Package ckelling.baukasten. Das gilt ebenso für die Demonstrations-Applets.

Wenn eine Klasse zu einem bestimmten Package gehört, reicht dort, um eine Klasse desselben Package anzusprechen, der Name der zweiten Klasse. Deswegen heißt es in den Demonstrations-Applets beispielsweise

register = new Register16(...);  // Eine Instanz von
                                 // ckelling.baukasten.Register16 erzeugen

Von außerhalb eines Package spricht man die Klassen innerhalb durch Davorstellen des Package-Namens an:

register = new ckelling.baukasten.Register16(...);

Wer auf Packages verzichtet, ordnet seine Klassen damit dem namenlosen Standard-Package zu, müßte also die aufwendigere Art der Klassenbezeichnung benutzen. Durch Zeilen der Art

import ckelling.baukasten.*;
import java.awt.Frame;

zu Beginn des Codes (außerhalb von Klassendefinitionen) lassen sich aber ganze Packages oder nur bestimmte ihrer Klassen "importieren", und die jeweiligen Klassen brauchen fürderhin lediglich durch ihren Namen angesprochen zu werden.

Noch ein Wort zu der Namenswahl für das Package des Rechner-Baukastens: Gemäß offiziellem Diktum von Sun [JavaWhitepaper] sind als Packagenamen nur noch solche zugelassen, die mit einer umgedrehten Internet-Adresse beginnen; auf diese Art kann die weltweite Einheitlichkeit der Packagebezeichner garantiert werden. Der folglich korrekte Name de.uni-hamburg.informatik.ckelling.baukasten ist aber für dauerhaften Einsatz einfach zu lang.

4.3 Die Wahl der Basisklasse des Applets

Alle Aufbauten aus Komponenten des Rechner-Baukastens sollten sich statt von java.applet.Applet von ckelling.baukasten.Rechner ableiten (welche Klasse wiederum von Applet abgeleitet ist). Zumindest erwarten alle Komponenten bis auf einige der in 4.12 Unterstützungsklassen zur freien Verwendung beschriebenen Klassen einen Verweis auf eine Instanz von Rechner als "gemeinsame Basis".

Die einheitliche Basisklasse Rechner bietet einige Vorteile:

Beispiel: RAM

Das Grundgerüst für den Java-Code der Hauptklasse des RAM-Applets könnte daher so aussehen:

package ckelling.baukasten; 	1.

import java.applet.*; 	2.
import java.awt.*;
import java.lang.*;
import java.util.*;


/**
 *  Komponenten_RAM.java
 *
 *  Demonstration der Rechner-Komponente "RAM" 	3.
 *
 *  @author   Carsten Kelling
 *  @version  0.8.1, 07.02.97
 */
public class Komponenten_RAM extends Rechner 	4.
{
  public final static String VERSIONSTRING = "Komponenten: RAM 0.8.1"; 	5.

  private EditableMemory           ram;

  private SimpleBus                abus;
  private SimpleBus                dbus;

  private Register8                address;
  private Register16               register;


  public String getVersionString() 	5.
  {
    return VERSIONSTRING;
  }

  public String getAppletInfo() 	5.
  {
    return "Demonstration der Rechner-Komponente 'RAM'";
  }

  public void init()  // wird nach dem Start des Applets einmal aufgerufen
  {
    System.out.println(VERSIONSTRING);
    System.out.println("Initialisierung - Anfang");  // zur Fehlersuche

    initialize(new Dimension(510, 400), new Dimension(510, 380)); 	6.

    ... (weitere Initialisierungen, z.B. der Rechnerkomponenten)

    System.out.println("Initialisierung - Ende");  // zur Fehlersuche
  }

Zu den Anmerkungen:

  1. Diese Klasse soll in das Package ckelling.baukasten gehören - siehe 4.2 Das Package ckelling.baukasten.
  2. Vier andere Packages werden komplett importiert, um die zu ihnen gehörenden Klassen mit kurzem Namen ansprechen zu können.
  3. Ein Kommentar, der nach diesem Schema (mit "/**" eingeleitet, Zeilen mit "*" beginnend, Ende mit "*/") geschrieben ist und direkt vor der ersten Zeile einer Methode oder der Deklaration einer Variable steht, kann von dem Programm javadoc ausgewertet werden. javadoc ist Bestandteil des JDK und erzeugt automatisch eine Dokumentation im HTML-Format zu Java-Klassen [JDK 1.0.2].
  4. Wie gesagt sollte sich die Applet-Klasse von ckelling.baukasten.Rechner ableiten; weil Komponenten_RAM in diesem Beispiel selbst in das Package ckelling.baukasten gehört, reicht als Bezeichnung der Superklasse ein einfaches "Rechner".
  5. Man erinnere sich, daß uns Rechner zwingt, via getVersionString() eine Versionsangabe unseres Applets bereitzuhalten - hier ist sie. Die Methode getAppletInfo() hingegen ist bereits leer in Applet vordefiniert und wird z.B. von appletviewer (dem Applet-Anzeigeprogramm aus dem JDK [JDK 1.0.2]) benutzt, um Informationen über ein Applet auszugeben (Menüpunkt "Info"). Zusätzlich zeigt appletviewer dort noch die Werte an, die von getParameterInfo() geliefert werden, welches wiederum in Rechner vordefiniert ist. Würde unser Applet initialize() erweitern, um neue Parameter zu unterstützen, so wäre es sinnvoll, auch getParameterInfo() anzupassen.
  6. Ruft das oben erläuterte initialize() auf. Der erste Parameter ist die Größe, auf die das Applet gebracht wird, falls der Bildschirm mindestens eine Größe von 800·600 Bildpunkten aufweist. Anderenfalls wird der zweite Parameter ebenso verwendet wie kleinere Schriften für alle Komponenten.

Für eine Aufzählung der standardmäßig von initialize() unterstützten Parameter, deren Wirkung und die Definition eines passenden Applet-Tags siehe 4.10 Ausführen eines Applets.

Eine Aufstellung aller Variablen und Konstanten sowie der Methoden mit benötigten Parametern findet sich in dem automatisch generierten, mit Kommentaren versehenen Dokument ckelling.baukasten.Rechner.html im Ordner Autodoku der "Lernmaterialien".

4.4 Erzeugung der Komponenten

Trotz des im vorigen Abschnitt aufgeführten Code-Beispiels ist es zu empfehlen, das notwendige Grundgerüst des Applets mit dem Baukasten-Editor (siehe 4.4.1 Der Baukasten-Editor) zu erzeugen. Dennoch wird die Erzeugung von Instanzen der Klassen des Rechner-Baukastens im folgenden detailliert erläutert.

Der vom Baukasten-Editor erzeugte Code enthält nicht nur alle notwendigen Methoden-Rümpfe, um das Applet sofort starten zu können, sondern natürlich auch die Deklarationen und Initialisierungen der benötigten Rechnerkomponenten.

4.4.1 Der Baukasten-Editor

Abbildung 33 zeigt eine typische Arbeitssituation mit dem Baukasten-Editor; einige der Parameter, die mit ihm an Komponenten eingestellt werden können, sind zu erkennen. Dieses nützliche Werkzeug benötigt kaum eine Beschreibung zu seiner Funktion - starten Sie das Applet über die Datei Editor.html aus dem Verzeichnis der "Lernmaterialien" und fangen Sie an, Komponenten zu erzeugen und abzuwandeln.

So viel sei aber noch gesagt: Nachdem man sich nach einem Klick auf "Neu" für einen Komponententyp entschieden hat, wird sofort eine Instanz dieses Typs erzeugt und kann modifiziert werden. Da diese Instanz sich genau so verhält, wie alle Instanzen der jeweiligen Klasse, arbeitet der Editor folglich mit echtem WYSIWYG (what you see is what you get). Änderungen der Größe der Komponente oder ihrer Koordinaten werden bereits nach Änderung eines Wertes oder spätestens einem Klick auf "Übernehmen" sichtbar, bei anderen Änderungen muß erst das Eigenschaftenfenster per "OK" geschlossen werden.

Der Editor arbeitet zwar mit WYSIWYG, das heißt aber nicht, daß sich Komponenten mit der Maus verschieben oder vergrößern lassen; Änderungen können nur im jeweiligen Eigenschaftenfenster vorgenommen werden.

Um die Eigenschaften einer Komponente nachträglich zu ändern, muß der entsprechende Eintrag in der Baumansicht mit der Maus markiert werden, bevor auf "Ändern" geklickt wird; auch Abzweigungen von Bussen können so nachträglich verändert werden. Analog funktioniert das Löschen von Komponenten.

Busse können erst mit Abzweigungen versehen werden, wenn ihr Eigenschaftenfenster ein erstes Mal per "OK" geschlossen worden ist.



Abbildung 33: Der WYSIWYG-Editor für den Rechner-Baukasten. Mit dem Editor arbeiten können Sie hier.

Der erzeugte Java-Code wird nach einem Klick auf "Code erzeugen" in der Java-Konsole angezeigt. Leider scheiden deswegen MS Internet Explorer und appletviewer als virtuelle Maschine für den Editor aus, denn ersterer zeigt keine Konsole an und letzterer benutzt ein MS-DOS-Fenster, das nur wenige Zeilen faßt. Obendrein zeigen beide sich durch die große Anzahl von AWT-Komponenten des Editor-Applets überfordert, was zu Darstellungsfehlern führt. In Netscape wird die Java-Konsole über "Show Java Console" aus dem "Options"-Menü angezeigt; es empfiehlt sich, ihren Inhalt direkt vor dem Klick auf "Code erzeugen" zu löschen. Leider können Aufbauten aus dem Editor (noch) nicht direkt abgespeichert werden, da die Dateizugriffsrechte für Applets (noch) uneinheitlich geregelt sind.

4.4.2 Koordinaten der Komponenten

Wenn man sich den von dem Editor erzeugten Initialisierungscode für die Komponenten ansieht, fällt auf, daß X- und Y-Koordinate, sowie Breite und Höhe aller Komponenten absolute Werte sind. Wer sich das Applet zur Speicherhierarchie angesehen hat, bei dem sich der Tag-Speicher automatisch verschiebt, wenn der Datenspeicher des Cache seine Breite ändert, hätte vielleicht erwartet, daß Komponenten des Baukastens statt reiner int-Werte Verweise auf andere Komponenten übergeben bekommen. In einem solchen Modell würde die Änderung von Größe oder Position einer Komponente zur Laufzeit Änderungen bei allen davon abhängigen Komponenten bewirken. Der Autor hat sich aber aus mehreren Gründen gegen ein solches Vorgehen entschieden:

Wie der Editor zeigt, ist es nicht so, daß man die Koordinaten und Breiten-/Höhenangaben seiner Komponenten stets und vollständig per Hand ausrechnen muß. Der Editor bietet es an, diese vier Angaben (bzw. fünf bei Busabzweigungen) entweder als absoluten Zahlenwert einzugeben, oder aber eine Komponente zu benennen, von der sich beispielsweise die X-Koordinate ableitet; zusätzlich muß man noch angeben, welche der X-Koordinaten, die sich an der anderen Komponente finden lassen, man benutzen möchte, beispielsweise die des linken oder rechten Randes. Außerdem läßt sich - kein großes Kunststück - optional ein fester Aufschlag angeben. Im RAM-Beispiel sorgen folgende Zeilen dafür, daß der Datenbus immer exakt in der Mitte des unteren Randes des RAMs beginnt:

Beispiel: RAM

//// DATENBUS ////
  Point coord = ram.getCoordinates("bottom");
  dbus = new SimpleBus("Datenbus", coord.x, coord.y, 0, 185, (Rechner) this);

Anmerkung: Ein java.awt.Point ist ein Objekt mit einer X- und einer Y-Koordinate, zwei int-Werten. In diesem Fall ist ein neuer Bus (Instanz von SimpleBus) erzeugt worden; hätte dbus bereits existiert, hätte der Code so lauten müssen:

Beispiel: RAM

Point coord = ram.getCoordinates("bottom");
dbus.setCoordinates(coord.x, coord.y,
                    "Wird bei Bussen als 'leftTop' angenommen");

Der Zeichenkettenparameter bei getCoordinates() beschreibt offenbar den Punkt an der Komponente, dessen Koordinaten man erfahren möchte; um die Wirkung des entsprechenden Parameters bei setTT>Coordinates() zu erläutern, noch etwas mehr Code:

Beispiel: RAM

coord = dbus.getCoordinates("register", "end");  1.
register = new Register16("Register", coord.x, coord.y,
                          "right", (Rechner) this);
register.setEditable(true);  2.
add(register);    3.
register.show();  3.

Zunächst sind zu diesem Codestück noch einige Anmerkungen nötig:

  1. Dieses ist eine Variante von getCoordinates(), die nur Busse kennen - zurückgeliefert werden die Koordinaten der Abzweigung des Datenbusses, die bei ihrer Initialisierung den Namen "register" bekam, und zwar nicht der Punkt, wo diese Abzweigung am Bus beginnt, sondern der, wo sie endet (deswegen "end"). Der zweite Parameter wird aber bei Fortlassen ebenfalls als "end" angenommen.
  2. Dieses ist eine "Spezialmethode", die nur Register und Speicher kennen, mit offensichtlicher Funktion - siehe hierzu 4.4.3 Besonderheiten der Komponenten.
  3. Einige Komponenten des Rechner-Baukastens wurden von java.awt.Panel abgeleitet, um innerhalb ihres Bildschirmbereiches AWT-Komponenten einsetzen zu können, wie z.B. ein TextField für die Werteingabe in Register16. AWT-Komponenten werden (theoretisch) immer dann (und nur dann) automatisch neu gezeichnet, wenn dieses notwendig geworden ist; dazu aber müssen sie per add() einem "Container" zugewiesen worden sein. Mit show() geht man sicher, daß die Komponente angezeigt und nicht im Hintergrund versteckt wird. Siehe hierzu 4.4.4 Die zwei Arten von Komponenten.

Die Abzweigung "register" des Busses geht von diesem nach links ab und liefert durch obigen Aufruf an getCoordinates() die Koordinaten des Punktes ganz links. Ohne den Parameter "right" in der Konstruktormethode müßte man, um etwa ein Register dort an den Bus anzuschließen, den Punkt coord erst soweit nach links verschieben, wie die neu zu erzeugende Komponente breit wäre. Diese Vorgehensweise ist bei Bussen notwendig, die den durch coord.x und coord.y beschriebenen Punkt stets als linke, obere Ecke ihrerselbst auffassen. Das neue Register oben jedoch benutzt (coord.x; coord.y) als den Punkt in der Mitte seiner rechten Seite, eben wegen des "right".

Hätte das Register bereits existiert, hätte der Aufruf

register.setCoordinates(coord.x, coord.y, "right");

es an dieselbe Stelle verschoben. Für die Plazierung von Komponenten existieren also die beiden in den folgenden Abschnitten aufgeführten Methoden.

4.4.2.1 getCoordinates()

Durch einen Aufruf der Methode getCoordinates(String qualifier) liefern Komponenten des Rechner-Baukastens die Koordinaten bestimmter "markanter" Punkte. Die Menge der unterstützten qualifier ist von der jeweiligen Klasse oder gar Instanz abhängig und läßt sich durch einen Aufruf von getPossibleQualifiers() ermitteln; alle Komponenten kennen den qualifier "leftTop" für die Koordinaten der linken, oberen Ecke (des kleinsten umfassenden Rechtecks bei Komponenten mit abgerundeten Ecken etc.). Tabelle 10 führt alle möglichen qualifier auf; man beachte, daß Busse eine Variante von getCoordinates() mit zwei Parametern kennen (in Klammern).

Sollte ein Parameter nicht unterstützt werden, wird er als "leftTop" angenommen und eventuell (bei Bussen) eine Fehlermeldung ausgegeben.

4.4.2.2 setCoordinates()

Durch einen Aufruf von setCoordinates(int x, int y, String grabMode) läßt sich jede Komponente des Rechner-Baukastens mit der durch grabMode bezeichneten "markanten" Stelle an den Punkt (x; y) verschieben. Für grabMode sind grundsätzlich alle Werte erlaubt, die auch qualifier bei getCoordinates()annehmen kann. Die Klassen EditableMemory, SimpleBus und TagMemory ignorieren diesen Parameter jedoch vollständig, d.h. sie nehmen grabMode als "leftTop" an.

4.4.3 Besonderheiten der Komponenten

Neben den Methoden, die von allen Komponenten des Rechner-Baukastens unterstützt werden und von denen im weiteren Verlauf des Kapitels die Rede sein wird, kennen die meisten Komponenten natürlich zusätzliche Methoden, um der Rolle der durch sie simulierten Rechnerkomponente gerecht zu werden.

So läßt sich das Bild einer ALU in Schritten von 90 Grad drehen oder ein RAM anweisen, ob es per Mausklick editierbar sein soll und ob es Opcodes statt Zahlenwerte anzeigen soll.

Einige dieser "Spezialitäten" lassen sich bereits über das Editor-Applet einstellen; in der nachstehenden Tabelle sind die Methoden aufgeführt, die dazu benötigt werden. Diese Aufstellung diene als kurze Übersicht der am häufigsten benötigten Modifikationsmöglichkeiten. Einen vollständigen Überblick liefert die Klassendokumentation in HTML-Form - Einstiegspunkt dazu ist die Datei packages.html aus dem Verzeichnis Autodoku im Verzeichnis der "Lernmaterialien".

"Qualifier"

unterstützt von Klasse(n)

liefert Koordinaten von welchem Punkt

"leftTop" (allen) linke, obere Ecke des kleinsten umfassenden Rechtecks
"left"

"right"
ALU
EditableLabel
EditableMemory
Misc
PipelineRegister
PipelineStageLabel
Register16
Register16Split
Register8
horizontaler SimpleBus
TagMemory

Mitte der linken/rechten Seite

Bei einem horizontalen SimpleBus Synonym für "start"/"end" (s.u.).

Bei ALU Abkürzung für "leftInput"/"rightInput" (s. u.).

"top" (oder "clock" bei Registern)

"bottom"
Adder
EditableMemory
Misc
Mux
PipelineRegister
PipelineStageLabel
Register16
Register16Split
Register8
vertikaler SimpleBus
TagMemory
Mitte der oberen/unteren Seite

Bei einem vertikalen SimpleBus Synonym für "start"/"end" (s.u.).
"rightTop"
"leftBottom"
"rightBottom"
EditableMemory
TagMemory
entsprechende Ecke des kleinsten umfassenden Rechtecks
"start"
"end"
SimpleBus Anfangspunkt des Busses (links/oben)
Endpunkt des Busses (rechts/unten)
z.B. ("ireg", "start")
z.B. ("ireg", "end")
SimpleBus Punkt, an dem die Abzweigung "ireg" am Bus beginnt/an dem sie endet.
z.B. "ireg" SimpleBus Abkürzung für ("ireg", "end")
"upper" / "upperInput"

"lower" / "lowerInput"

"output"
Adder
ALU
("" oder "")
Mux
oberer Eingang

unterer Eingang

Ausgang
"left" / "leftInput"

"right" / "rightInput"
ALU ("" oder "") linker Eingang

rechter Eingang
Tabelle 10: Varianten von getCoordinates()

4.4.4 Die zwei Arten von Komponenten

Spezifische Methode

Gegenstück

unterstützt von Klasse(n)

Wirkung

setEditable() isEditable() EditableLabel
EditableMemory
Register16
Register8
Register16Split
TagMemory
Zellen können angeklickt und editiert werden; erlaubt ggf. auch das Setzen von breakpoints und in Tag-Speichern das Einstellen von "valid" oder "dirty".
setOrientation() getOrientation() ALU Ausrichtung der ALU: "", "", "" oder ""
setMemorySize()
setBitWidth()
setLineSize()
initRam()
allowBreakpoints()
getMemorySize()
getBitWidth()
getLineSize()
getProgram()
breakpointsAllowed()
EditableMemory

TagMemory
Anzahl der Speicherzellen, Bits pro Speicherzelle, linesize, geladenes Programm einstellen; Setzen von breakpoints zulassen.
setOpcodesSupported()

showOpcodes()
getOpcodesSupported()

opcodesAreShowing()
EditableMemory Mitteilen, ob dieses RAM Opcodes anzeigen können soll; Opcodes statt Zahlen anzeigen (falls erlaubt).
setCacheSize()
setAssociativity()
setWriteMode()
setReplaceMode()
getCacheSize()
getAssociativity()
getWriteMode()
getReplaceMode()
TagMemory Größe, Assoziativität, Schreibverhalten und Ersetzungsstrategie eines Cache festlegen.
setFlags(int f)
setFlags(String
edgeName, int f)

und weitere
getFlags()
getFlags(String edgeName)

und weitere
SimpleBus Farbe des Hauptstranges (wie Abzweigungen in EDGE_COLOR oder aber in BUS_COLOR) wählen; sollen Abzweigungen "Lötpunkte" oder Symbole für Tri-State-Gatter besitzen?
Tabelle 11: Besonderheiten der einzelnen Rechnerkomponenten

Alle Komponenten des Rechner-Baukastens leiten sich von einer von zwei Klassen ab: RechnerKomponente oder RechnerKomponentePanel; deren gemeinsame Superklasse ist Object, also der kleinstmögliche gemeinsame Nenner zweier Klassen in Java. Wegen dieser unterschiedlichen "Herkunft" der Komponenten ist es ohne besondere Vorkehrungen nicht möglich, eine Methode zu schreiben, die beliebige Rechnerkomponenten, also z.B. eine Instanz von SimpleBus oder Register16, als Parameter übergeben bekommt. Abbildung 34 zeigt noch einmal die Klassenhierarchie des Rechner-Baukastens, um diese Aufteilung zu verdeutlichen.

Diese Unterscheidung der Rechnerkomponenten in Abkömmlinge von RechnerKomponente und solche von RechnerKomponentePanel ist natürlich nicht ohne Grund erfolgt. Historisch entstand zuerst RechnerKomponente unter folgenden Zielsetzungen:



Abbildung 34: Klassenhierarchie des Rechner-Baukastens - es ist deutlich zu erkennen, daß die Klassen von den Superklassen RechnerKomponente und RechnerKomponentePanel abstammen.

Schließlich aber wurde es opportun, auch für Rechnerkomponenten (statt nur für die Steuerfenster) einige AWT-Elemente zu benutzen, um das Rad, sprich einen zuverlässig funktionierenden Eingabebereich für Text oder einen Scrollbalken, nicht neu erfinden zu müssen. Auch lassen sich bei diesen schon existenten Komponenten viel leichter die Benutzereingaben, in Form von Mausklicks und Tastendrücken, abfragen (Stichwort events) und steuern (um z.B. die Eingabe unzulässiger Zeichen zu unterbinden). Damit die events aber auch in der Rechnerkomponentenklasse "landen", die etwas mit ihnen anzufangen weiß, muß diese java.awt.Panel ableiten - die Alternative wäre es gewesen, alle benötigten AWT-Komponenten an das Rechner-Objekt zu exportieren, wobei der Programmierer für jede neue Komponente aber dort die Methode handleEvent() hätte erweitern müssen.

Selbstverständlich gibt es auch hier eine Ausnahme: EditableMemory (und das von ihm abgeleitete TagMemory) sind "nur" Erben von RechnerKomponente, obwohl sie AWT-Komponenten benutzen. Das wurde möglich, weil EditableMemory seine aufwendige Grafikdarstellung von einer eigenen, von Panel abgeleiteten, Klasse EditableMemory_Ressource erledigen läßt. Bei einer Instanziierung von EditableMemory wird automatisch eine Instanz von EditableMemory_Ressource erzeugt und dem Rechner-Objekt hinzugefügt. Diese Bequemlichkeit erkauft man sich allerdings damit, daß man bei einem Entfernen des Speichers extra dessen remove() aufrufen muß (siehe 4.4.5 Entfernen von Komponenten), um auch das Bild verschwinden zu lassen. Für die wenigen von Register16 benutzten AWT-Komponenten hielt der Autor eine eigene, für das Bild zuständige Klasse jedoch für übertrieben aufwendig.

Beispiel: RAM

Die paint()-Methode dieses Applets, die dafür sorgen soll, daß alle verwendeten Komponenten neu gezeichnet werden, könnte vereinfacht lauten:

public synchronized void paint (Graphics onScreenGC)
{
  // Fläche des Applets (unsichtbar im Hintergrund) löschen
  offScreenGC.setColor(BACKGROUND);
  Rectangle size = bounds();
  offScreenGC.fillRect(0, 0, size.width, size.height);
 
  ram.     paint(offScreenGC);  // Komponenten
  abus.    paint(offScreenGC);  // zeichnen sich
  dbus.    paint(offScreenGC);  // im unsichtbaren
  address. paint(offScreenGC);  // Hintergrundbild
  register.paint(offScreenGC);  // neu
  
  // Hintergrundbild auf den sichtbaren Bildschirm kopieren
  onScreenGC.drawImage(offScreenImage, 0, 0, this);
}

Offenbar wird jede vorhandene Komponente explizit genannt und ihre paint()-Methode aufgerufen. Man beachte außerdem die Verwendung von double buffering (siehe hierzu und zu dem Neuzeichnen der Komponenten allgemein 4.6.4 Neuzeichnen der Komponenten).

Als Abhilfe entstand während der Arbeiten an dem Editor-Applet die Klasse RKWrapper. Sie ist ein sogenannter wrapper für RechnerKomponente und RechnerKomponentePanel, d.h. sie verbirgt in ihrem Inneren eine Instanz der einen oder der anderen Klasse, kennt alle Methoden, die RechnerKomponente und RechnerKomponentePanel gemeinsam sind, und ruft bei einem Aufruf einer Methode die korrespondierende Methode des Objektes in ihrem Inneren auf (und liefert gegebenenfalls den Wert zurück, den das "umfaßte" Objekt liefert). Mit RKWrapper ist es dann doch möglich, alle Komponenten eines Rechner-Applets in gleicher Form zu behandeln; eine allgemeine paint()-Methode, die auch für das RAM-Beispiel gälte, könnte dann so aussehen:

Beispiel: RAM

public synchronized void paint(onScreenGC)
{
  // Fläche des Applets löschen
  offScreenGC.setColor(BACKGROUND);
  Rectangle size = bounds();
  offScreenGC.fillRect(0, 0, size.width, size.height);

  // Von allen Komponenten wurde ein RKWrapper erzeugt
  // und im Vektor elements abgelegt.
  for (int i = 0; i < elements.size(); i++)
    ((RKWrapper) elements.elementAt(i)).paint(onScreenGC);

  // Hintergrundbild auf den sichtbaren Bildschirm kopieren
  onScreenGC.drawImage(offScreenImage, 0, 0, this);
}

In den Demonstrations-Applets wurde aus Gründen der Les- und Wartbarkeit die obere Variante benutzt. Dieses erlaubt, in zur Laufzeit unveränderlichen Methoden wie paint() oder updateAll() jede Komponente direkt im Code zu nennen und anzusprechen.

4.4.5 Entfernen von Komponenten

Manchmal kann es notwendig sein, Komponenten zur Laufzeit zu entfernen. Das Speicherhierarchie-Applet (Funktionsweise eines Cache) beispielsweise löscht die beiden den Cache bildenden Speicher, sobald bestimmte Parameter verändert werden, und erzeugt zwei neue.

Zunächst einmal läßt sich jede Komponente grundsätzlich durch Setzen auf null löschen. Beachten Sie dabei jedoch:


2 Visualisierung und Simulation mit Java
3 Lernmaterialien zur technischen Informatik

Carsten Kelling 1997 ([EMail senden]); letzte Änderung: 17. September 1997