Dienstag, 7. Juni 2011
Programmierrichtlinien haben doch Sinn (III)
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: allgemeines, Java
Fehlerbehandlung
RemoteExceptionsonst 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 ArrayListWenn man weiter sucht, findet man für Header das InterfacelockedRecNo; private int DataOffset; //166
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 ArrayListUnser Server sollte dann etwa so aussehen (ob das dann funktioniert, sei dahingestellt, aber die Exceptions sind geklärt):felder; public Fields() throws RemoteException { felder = new ArrayList (0); } ...
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: allgemeines, Fehler, Java
Montag, 6. Juni 2011
Listings - Source Code drucken
Labels: allgemeines, POS1-1, POS1-2, PR2, PR3, PR5
Autoboxing in Java
Folgendes Beispiel zeigt ein Problem beim Autoboxing in Java. Der Autor des Code-Fragments will int
-Werte in der LinkedList
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:
E remove(int index)
, welche das Element mit dem Index index entfernt und eineIndexOutOfBoundsException
wirft, wenn der Index nicht existiert.- boolean remove(Object o), welche das entsprechende Objekt entfernt, falls es existiert (und in diesem Fall
true
liefert).
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
Samstag, 4. Juni 2011
Programmierrichtlinien haben doch Sinn (II)
public int[] find(String[] criteria) throws RemoteException { ArrayListWo ist der Fehler? (Abgesehen von den schlecht formatierten schließenden Klammern in Zeile 11)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; }
Labels: allgemeines, Fehler, Java
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 ArrayListSoviel 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.(); 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"); } }
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: allgemeines, Fehler, Java
Mittwoch, 1. Juni 2011
Aufgaben zu verketteten Listen und binären Bäumen (POS1: 2A, 2C)
- Verkettete Listen
- Aufgaben zu verketteten Listen
- Aufgabe Verkettete Listen - Partner finden für Tanzkurs
- Beispielprojekt zu binären Bäumen
- Worthäufigkeiten mit binärem Baum ermitteln
Abonnieren Posts [Atom]