Dienstag, 7. Juni 2011

 

Programmierrichtlinien haben doch Sinn (III)

Das Stück
                int recNo = 0;
                while((rf.length()-rf.getFilePointer())>recordlen)
                {
                int i = 0;
                String[] recordfields = new String[fields.size()];
                deleted = rf.readShort();
                for(Field field:fields)
                {
                    buf = new byte[field.fieldlen];
                    if(field.datentyp=='F' || field.datentyp=='C')
                    {
                        rf.read(buf, 0, field.fieldlen);
                        recordfields[i] = new String(buf,"ISO-8859-1").trim();
                    }
                    else if(field.datentyp == 'V')
                    {
                        rf.read(buf, 0, field.fieldlen);
                        recordfields[i] = new String(buf,"ISO-8859-1").trim();
                    }
                    i++;
                }
                records.add(new DataRecord(recNo,fields.size(),deleted));
                records.get(records.size()-1).setFields(recordfields);
                recNo++;
                }
wäre viel lesbarer, wenn man richtig einrückt. Es geht ganz einfach! Strg+Shift+F in Netbeans oder Eclipse.
                int recNo = 0;
                while ((rf.length() - rf.getFilePointer()) > recordlen) {
                    int i = 0;
                    String[] recordfields = new String[fields.size()];
                    deleted = rf.readShort();
                    for (Field field : fields) {
                        buf = new byte[field.fieldlen];
                        if (field.datentyp == 'F' || field.datentyp == 'C') {
                            rf.read(buf, 0, field.fieldlen);
                            recordfields[i] = new String(buf, "ISO-8859-1").trim();
                        } else if (field.datentyp == 'V') {
                            rf.read(buf, 0, field.fieldlen);
                            recordfields[i] = new String(buf, "ISO-8859-1").trim();
                        }
                        i++;
                    }
                    records.add(new DataRecord(recNo, fields.size(), deleted));
                    records.get(records.size() - 1).setFields(recordfields);
                    recNo++;
                }
Selbst wenn man in der Konsole mit vim arbeitet, kann man die Zeilen mit V und den Cursortasten markieren und anschließend = (ist-gleich) drücken.

Was ist da so schwierig dran?

Hilft beim Einarbeiten in den Source Code immens.

Labels: ,


 

Fehlerbehandlung

Bei folgendem Code-Stück sieht man nur die Ausgabe

RemoteException
sonst nichts. Keine weitere Information, um dem Problem auf die Schliche zu kommen.
public class Server {
    public static void main(String[] args) {
        try {
            LocateRegistry.createRegistry(1099);
            System.out.println("RMI-Registry erfolgreich gestartet");
        } catch (RemoteException ex) {
            System.out.println("Fehler beim Starten der Registry: " + ex);
        }
        try {
            DB db = new Data("fahrten.db");
            //System.out.println(((Data)db).records.size());
            DB d = (DB) UnicastRemoteObject.exportObject(db, 1099);
            System.out.println("1xx");
            Naming.rebind("Data", d);
            System.out.println("2xx");

            
            Header he = ((Data)db).getHeader();
            Header h = (Header) UnicastRemoteObject.exportObject(he, 1099);
            Naming.rebind("Header", h);


            System.out.println("Alles gebunden");


        } catch (RemoteException ex) {
            System.err.println("RemoteException");
        } catch (MalformedURLException ex) {
            System.err.println("MalformedURLException");
        }
    }
}
Ein einfaches System.err.println("RemoteException: " + ex.getMessage()); liefert schon etwas mehr hilfreiche Information;
RemoteException: remote object implements illegal remote interface; nested exception is: 
        java.lang.IllegalArgumentException: illegal remote method encountered: public abstract int data.Header.length()
In der Klasse Data findet man
public class Data implements DB, Serializable {

    private final int MAGIC_COOKIE = 4223;
    private Header header;
    private String fileName="";
    private ArrayList lockedRecNo;
    private int DataOffset; //166
Wenn man weiter sucht, findet man für Header das Interface
public interface Header extends Remote {
    public Field getField(int fID);

    public int length();

    public void addField(Field f);

}
Es fehlt schlicht und ergreifend die Implementierung zu Header. Das kann nicht funktionieren!

Die Implementierung muss UnicastRemoteObject erweitern, in Data sollte man statt private Header header; direkt die Implementierung verwenden: private Fields header;.

Die Implementierung von Fields beginnt etwa so:
public class Fields extends UnicastRemoteObject implements Header, Serializable {

    private ArrayList felder;

    public Fields() throws RemoteException {
        felder = new ArrayList(0);
    }
...
Unser Server sollte dann etwa so aussehen (ob das dann funktioniert, sei dahingestellt, aber die Exceptions sind geklärt):
public class Server {

    public static void main(String[] args) {
        DB db = null;
        try {
            db = new Data("fahrten.db");
        } catch (RemoteException ex) {
            System.err.println("RemoteException (new Data()): " + ex.getMessage());
        }
        try {
            LocateRegistry.createRegistry(1099);
            System.out.println("RMI-Registry erfolgreich gestartet");
        } catch (RemoteException ex) {
            System.out.println("Fehler beim Starten der Registry: " + ex);
        }
        try {
            DB d = (DB) UnicastRemoteObject.exportObject(db, 1099);
            Naming.rebind("Data", d);
        } catch (RemoteException ex) {
            System.err.println("RemoteException (rebind(Data)): " + ex.getMessage());
        } catch (MalformedURLException ex) {
            System.err.println("MalformedURLException: " + ex.getMessage());
        }
        try {
            Fields he = (Fields) ((Data) db).getHeader();
            Header h = (Header) UnicastRemoteObject.exportObject(he, 1099);
            try {
                Naming.rebind("Header", (Fields) h);
            } catch (MalformedURLException ex) {
                System.err.println("MalformedURLException (rebind(Header)): " + ex.getMessage());
            }
        } catch (RemoteException ex) {
            System.err.println("RemoteException: " + ex.getMessage());
        }
        System.out.println("Alles gebunden");
    }
}
Der langen Rede kurzer Sinn:

Für den Programmierer muss möglichst viel Information bereitgestellt werden. Für den Anwender hilft diese Information nichts und man muss einfache Fehlermeldungen anbieten (das ist hier auf dieser Ebene sowieso nicht möglich, denn die Software ist noch weit davon entfernt, einem Endanwender seine Dienste anzubieten).
Sinnvoll wäre es, die Exceptions zu loggen.
Logger.getLogger(Server.class.getName()).log(Level.SEVERE, null, ex);
Am schlimmsten sind jedoch solche Konstrukte:

try {
                    while(true) {
                        records.add(read(nr));
                        nr++;
                    }
                } catch (RemoteException ex) {
                } catch (RecordNotFoundException ex) {
                }
Fehler sind praktisch unauffindbar. Auch nicht mit dem Debugger, weil man nicht einmal einen Breakpoint setzen kann, um die Variable ex zu inspizieren (die enthält ja Infos zur Exception).
Nachsatz: das alles kostet mich Stunden, die ich sinnvoller verbringen könnte. Einfach den Kandidaten durchfallen lassen, denn die Unit-Tests sind zu 93% (einer von dreizehn hat funktioniert) schief gegangen.

Labels: , ,


Montag, 6. Juni 2011

 

Listings - Source Code drucken

Es sollte m.E. doch möglich sein, Listings in der folgenden - lesbaren - Form zu erzeugen:
Die Schrift ist leserlich, die Zeilen richtig eingerückt, Dateiname und Autor etc. sind ersichtlich (Ich gebe zu, dieses Bild ist schlecht...). Verwendet wurde recode und a2ps unter Linux. 
Wichtig ist auch, dass man allgemeine Programmierrichtlinien befolgt.
Aber mit Office kann man auch sehr schöne Listings erzeugen:
Man muss nur Kopf- und Fußzeilen einrichten, die Schrift auf Courier New (9 oder 10pt) oder eine andere mit fixer Breite und dann zwei Seiten auf eine drucken.
Ähnliches gibt es schon unter Listings (Source-Code drucken) zu lesen.

Stattdessen bekommt man so etwas.

Der Sourcecode ist unmöglich zu lesen, weil die Einrückung bald die dargestellte Zeilenlänge erreicht. Die Schriftgröße ist aber zumindest OK.

Schlimm ist aber das folgende Listing, da würden 4 Code-Seiten auf einem Blatt Platz haben. Trotzdem sind es aber nur 2 Seiten pro Blatt. Die Schrift ist unleserlich klein.

Das ist eine Zumutung!

Labels: , , , , ,


 

Autoboxing in Java

Folgendes Beispiel zeigt ein Problem beim Autoboxing in Java. Der Autor des Code-Fragments will int-Werte in der LinkedList lockedRecords speichern. Das funktioniert beim Speichern in Zeile 16 auch super, weil der int-Wert recNo automatisch in ein Integer-Objekt gepackt wird.

LinkedList<Integer> lockedRecords = new LinkedList<Integer>();

    public long lock(int recNo) throws RemoteException,
            RecordNotFoundException {
        long lockCookie = 0;
        if(existsRec(recNo)){
            synchronized(records.get(recNo)){
                while(lockedRecords.contains(recNo)){
                    try {
                        records.get(recNo).wait();
                    } catch (InterruptedException ex) {
                        System.err.println("Lock Interrupted!");
                    }
                }
                if(existsRec(recNo)){
                    lockedRecords.add(recNo);
                    boolean lock_getted = true;
                    lockCookie = lockedRecords.size() -1;
                    return lockCookie;
                }
            }
        }else{
            throw new RecordNotFoundException();
        }
        return lockCookie;
    }

    public void unlock(int recNo, long lockCookie) throws RemoteException,
            RecordNotFoundException, SecurityException {
        if(recNo > 0 && recNo < records.size()){
            if(lockCookie >= 0 && lockCookie < lockedRecords.size()
                    && lockedRecords.contains((int)lockCookie)){
                synchronized(records.get(recNo)){
                    lockedRecords.remove(recNo);
                    records.get(recNo).notifyAll();
                }
            }else{
                throw new SecurityException();
            }
        }else{
            throw new RecordNotFoundException();
        }
    }
unlock() stürzt (praktisch) immer in Zeile 34 ab! Und zwar mit einer IndexOutOfBoundsException.

Warum?

Die Klasse LinkedList (sowie viele andere Klassen des Java Collection Frameworks) besitzt zwei remove()-Methoden:

  1. E remove(int index), welche das Element mit dem Index index entfernt und eine IndexOutOfBoundsException wirft, wenn der Index nicht existiert.
  2. boolean remove(Object o), welche das entsprechende Objekt entfernt, falls es existiert (und in diesem Fall true liefert).
Ein int-Parameter wird nicht automatisch in ein Integer-Objekt verpackt, da zuerst die zu int passende Methode verwendet wird! E remove(int index) wirft aber eine IndexOutOfBoundsException, wenn der int recNo kein gültiger Index ist. Das wäre eher zufällig passend.

Autoboxing ist also im Allgemeinen sehr praktisch, kann aber zu unerwarteten Problemen führen.

Übrigens ist eine Lösung mit Verwendung von Integer nicht zielführend, da beim Autoboxing nur bei kleinen Werten tatsächlich dasselbe Objekt verwendet wird:

Integer i1 = 23;
Integer i2 = 23;
Integer i3 = 2132321423;
Integer i4 = 2132321423;
System.out.println(i1 == i2);
System.out.println(i1.equals(i2));
System.out.println(i3 == i4);
System.out.println(i3.equals(i4));
liefert:
true
true
false
true

Labels: ,


Samstag, 4. Juni 2011

 

Programmierrichtlinien haben doch Sinn (II)

Noch ein Bonmot, diesmal eine Endlosschleife:
    public int[] find(String[] criteria) throws RemoteException {
        ArrayList erg = new ArrayList();
        for(int i=0; i< records.size();i++){
            boolean match=true;
            if(criteria != null){
                for(int j=0; j< criteria.length;i++){
                    if(criteria[j] != null){
                        if(!records.get(i).getField(j).contains(criteria[j])){
                            match=false;
                            break;
                        }  }  }  }
            
            if(records.get(i).isDeleted()){
                match=false;
            }
            if(match){
                erg.add(i);
            }
        }
        int[] back = new int[erg.size()];
        for(int i=0; i< erg.size();i++){
            back[i]=(int)erg.get(i);
        }
        return back;
    }
Wo ist der Fehler? (Abgesehen von den schlecht formatierten schließenden Klammern in Zeile 11)

Labels: , ,


Donnerstag, 2. Juni 2011

 

Programmierrichtlinien haben doch Sinn

Eine Stunde Fehlersuche! Suche in einem fremden Code, der gewisse Anforderungen erfüllen muss. Der Unit-Test für eine Methode, die einen CSV-String liefern ist fehlgeschlagen. Das Feld an der Position 5 war immer leer. Der Test erwartete den String "42995";"47.01708,16.93225";"20.73708,29.96312";" 196";"201105132101";" 405";"9634090160";"O", die Methode lieferte jedoch 42995;"47.01708,16.93225";"20.73708,29.96312"; 196;201105132101;"";9634090160;"O"

OK, dieser "Fehler" war schnell gefunden. Die Methode hielt sich besser an das CSV-Format (nur Strings sind in Hochkomma eingeschlossen, Zahlen nicht) als mein Test. Das war schnell korrigiert und nun lieferte die Methode alles als String:
"42995";"47.01708,16.93225";"20.73708,29.96312";" 196";"201105132101";"";"9634090160";"O"

Fast richtig. Das ist ein wirklicher Fehler. Durchforsten und Debuggen hat ergeben, dass dieses Feld schon beim Lesen immer leer wird. Der Code dazu ist folgender (das Leerzeichen bei den Bedingungen "kleiner als" habe ich für den Blog hinzugefügt, damit da nicht ein ungültiges HTML-Tag ensteht, grundsätzlich erschwert das Fehlen solcher Leerzeichen das Lesen des Codes):

public Data(String fn) throws FileNotFoundException, IOException{
        fileName=fn;

        rf = new RandomAccessFile(fn, "rw");
            int mC=rf.readInt();
            if(mC==4223){
                int dataOffset=0;
                short count=0;

                dataOffset=rf.readInt();
                firstDataOffset=dataOffset;
                count=rf.readShort();
                information = new Header(count);
                for(int i=0;i< count;i++){
                    short fNameLength = rf.readShort();
                    byte[] roughData = new byte[fNameLength];
                    rf.read(roughData/*, (int)rf.getFilePointer(), fNameLength*/);

                    String fieldName = new String(roughData,"ISO-8859-1");
                    short fDescLength = rf.readShort();
                    roughData = new byte[fDescLength];
                    rf.read(roughData/*, (int)rf.getFilePointer(), fDescLength*/);
                    String fieldDescription = new String(roughData,"ISO-8859-1");

                    char type = (char)rf.readByte();

                    short fDataLength = rf.readShort();

                    Field f = new Field(fNameLength, fieldName, fDescLength, fieldDescription, type, fDataLength);

                    information.setHeaderField(i, f);
                }
                records = new ArrayList();
                while(dataOffset< rf.length()){
                    short flag = rf.readShort();
                    String[] data = new String[information.getLength()];
                    for(int i=0;i< data.length;i++){
                        int readLen = information.getField(i).length;
                        byte[] roughData = new byte[readLen];
                        rf.read(roughData/*, dataOffset, readLen*/);
                        switch(information.getField(i).type){
                            case'f':
                            case'F':
                                data[i] = new String(roughData,"ISO-8859-1");
                                break;
                            case'v':
                            case'V':
                                StringBuilder sb = new StringBuilder();
                                for(int j=0;i< readLen && (char)roughData[j]!='\0';j++){
                                    sb.append((char)roughData[j]);
                                }
                                data[i] = sb.toString();
                                break;
                            case'c':
                            case'C':
                                byte[] d = new byte[1];
                                d[0]=roughData[0];
                                data[i] = new String(d);
                                break;
                        }
                        dataOffset+=(2+readLen);
                    }
                    if(flag==0){
                        records.add(new RecordData(data, true, information));
                    }else{
                        records.add(new RecordData(data, false, information));
                    }

                }
                rf.close();
                information.recAnz = records.size();
            }else{
                throw new FileNotFoundException("Keine DB-Datei");
            } 
    }
Soviel konnte ich aufgrund der Rahmenbedingungen auch gleich herausfinden: das Feld mit Index 5 ist ein "V"-Feld, daher ist der Code ab Zeile 48 relevant. Der schaut eigentlich richtig aus. Erstaunlich ist jedoch, dass die "V"-Felder davor korrekt waren, das "F"-Feld auch. Aber das fehlerhafte "V"-Feld ist gleich nach dem ersten "F"-Feld. Daher war meine Hypothese: das "F"-Feld "erzeugt" den Fehler. Aber die Zeilen 44 und 45 sind korrekt.

Also nächste Hypothese: das Lesen der Daten ist fehlerhaft! Zeilen 38 bis 40. Aber die sind m.E. korrekt. Vielleicht wurde der Dateiheader falsch gelesen (dort sind die Feldlängen und Feldtypen spezifiziert).

Zeilen 5 bis 32. Aber auch dort ist nichts verdächtiges. Es ist schon zum Verzweifeln! Alles schaut richtig aus und trotzdem schlägt der Test fehl (zu Recht, denn das Ergebnis ist falsch).

Jetzt ist einmal Zurücklehnen angesagt. Das ganze mal von der Entfernung betrachten. Breakpoint auf Zeile 48 setzen und schauen, was sich tut.

Erst beim 5. Feld (0-basiert) tritt der Fehler auf. Die Schleife wird sofort verlassen, obwohl roughData tatsächlich mehr als 0 Bytes enthält, 5, um genau zu sein. readLen enthält sogar den richtigen Wert. Wie so zum Teufel ist dann die Bedingung j < readLen && roughData[j] != 0 nicht erfüllt (das Casten auf (char) hatte ich schon entfernt, weil '\0' tatsächlich den Wert 0 hat)?

Ich habe bei dem Ausdruck Leerzeichen eingebaut, die im Original nicht waren. Jetzt fällt es wie Schuppen aus den HaarenAugen! Im Original auf Zeile 49 steht i < roughData && roughData[j] != 0. i statt j - optisch fast nicht zu unterscheiden!

i zählt die Felder, j die einzelnen Bytes in einem Feld. Daher ist ab dem Feld 5, welches eine Länge von 5 hat, der erste Teil der Bedingung nicht mehr erfüllt und es wird ein leerer String erzeugt.

Ja, Programmieranfängern wird immer gepredigt: "sprechende" Namen verwenden!

Das hätte geholfen! Statt i könnte man z.B. fieldno verwenden und statt j wäre byteno angebracht. Damit wäre der Fehler nie passiert! Die Schleife hätte gleich falsch ausgesehen:

for(int byteno=0;fieldno< readLen && roughData[byteno]!=0;byteno++){
    sb.append((char)roughData[byteno]);
}
Wenn dann noch Leerzeichen dazwischen sind, springt der Fehler sofort ins Auge, denn warum wird in der Schleife einmal fieldno und einmal byteno verwendet?

Selbst wenn man in kurzen Schleifen i und j als Laufvariable erlaubt, dann müsste aber in der äußeren Schleife immer noch ein "sprechender" Name verwendet werden. Das stäche auch ins Auge (die Leerzeichen verbessern das auch noch!):

for (int i = 0; fieldno < readLen && roughData[i] != 0; i++){
    sb.append((char)roughData[byteno]);
}
Wenn schon kurze Laufvariable, dann besser nie i und j gemeinsam verwenden sondern "unterschiedlichere" Buchstaben wie i und k oder i und m. Die kann man nicht so leicht verwechseln!

Die Variante mit "sprechenden" Namen für Schleifen, die länger als ein paar Zeilen sind, und kurze Laufvariable für kurze Schleifen (Dreizeiler) ist die beste Möglichkeit, dann das Beispiel mit i und fieldno schaut gleich irgendwie falsch aus. Zumindest schaut man sich so etwas gleich näher an.

Leerzeichen und sprechende Namen bringen's!

Siehe auch Programmierrichtlinien allgemein. Dort steht leider nichts über die Verwendung von Leerzeichen.

Java Guidelines findet man z.B. hier: http://www.oracle.com/technetwork/java/codeconv-138413.html

Google findet auch etwas: www.google.com

Labels: , ,


Mittwoch, 1. Juni 2011

 

Aufgaben zu verketteten Listen und binären Bäumen (POS1: 2A, 2C)

Arbeiten Sie die folgenden Beiträge durch:
  1. Verkettete Listen
  2. Aufgaben zu verketteten Listen
  3. Aufgabe Verkettete Listen - Partner finden für Tanzkurs
  4. Beispielprojekt zu binären Bäumen
  5. Worthäufigkeiten mit binärem Baum ermitteln

    Labels: , ,


    This page is powered by Blogger. Isn't yours?

    Abonnieren Posts [Atom]