Java: Lösningar till tentamen 2003-12-18

Observera att detta är förslag på lösningar. Det kan finnas andra lösningar som också är korrekta, och det kan hända att en del av lösningarna är mer omfattande än vad som krävs för full poäng på uppgiften.

Uppgift 1 (5p)

a (1p)

Kod i "vanliga" programspråk brukar kompileras till maskinkod, som kan exekveras av en fysisk processor, till exempel en Pentium eller en Sparc-processor. Olika processortyper har olika maskinkod. Java-kod kompileras i stället till standardiserad bytekod, som kan köras av en virtuell javamaskin, det vill säga ett program som tolkar bytekoden.

b (2p)

Felhantering med undantag:

Exempel på felaktiga svar:

c (2p)

Abstrakta klasser och gränssnitt:

Uppgift 2 (5p)

Felen är rödmarkerade och understrukna. Saker som ska skrivas till står med kursiv stil.
public class UndersökArgument {
    int summan = 0;
    public static int Main(Strings[] args) {
        private int antalHeltal = 0;
        for (int i = 0; i < args.length(); ++i);
        {
            try {                                /* Om argumentet inte
                int talet =                       * är ett heltal kastas
                  Int.parseInt(args[i]);          * NumberFormatException,
                  antalHeltal = antalHeltal++;    * och antalHeltal
            }                                     * räknas inte upp.
            catch (NumberFormatException e) {     */

            }
            summan += talet;
        } // for
        System.out.println(antalHeltal +
                           " tillåtna heltal.");
        System.out.println("Summan är " + summan + ".");
    } // main
} // class UndersökArgument
Korrekt version av programmet:
public class UndersökArgument {
    public static void main(String[] args) {
        int antalHeltal = 0;
        int summan = 0;
        for (int i = 0; i < args.length; ++i)
        {
            /* Om argumentet inte är ett heltal kastas
             * NumberFormatException, och antalHeltal
             * räknas inte upp.
             */
            try {
                int talet =
                  Integer.parseInt(args[i]);
                antalHeltal++;
                summan += talet;
            }
            catch (NumberFormatException e) {

            }
        } // for
        System.out.println(antalHeltal +
                           " tillåtna heltal.");
        System.out.println("Summan är " + summan + ".");
    } // main
} // class UndersökArgument
Poängmall: 0 rätt = 0 poäng; 1-2 = 0.5; 3 = 1; 4-5 = 1.5; 6 = 2; 7-8 = 2.5; 9 = 3; 10 = 3.5; 11 = 4; 12-13 = 4.5; 14-15 = 5

Uppgift 3 (5p)

import javax.swing.*;
import java.awt.event.*;
import java.awt.*;

public class CalcApplet extends JApplet implements ActionListener {
    private JTextField text1 = new JTextField(10);
    private JTextField text2 = new JTextField(10);
    private JButton plusButton = new JButton("+");
    private JButton minusButton = new JButton("-");
    private JButton timesButton = new JButton("*");
    private JButton divideButton = new JButton("/");
    private JTextField svarstext = new JTextField(10);

    public void actionPerformed(ActionEvent event) {
        String s1 = text1.getText();
        String s2 = text2.getText();
        try {
            double num1 = Double.parseDouble(s1);
            double num2 = Double.parseDouble(s2);
            String op = ((JButton)event.getSource()).getText();
            double result = 0;
            switch (op.charAt(0)) {
            case '+': result = num1 + num2; break;
            case '-': result = num1 - num2; break;
            case '*': result = num1 * num2; break;
            case '/': result = num1 / num2; break;
            }
            svarstext.setText(result + "");
        }
        catch (NumberFormatException exc) {
            svarstext.setText("ERROR");
        }
    } // actionPerformed

    public void init() {
        plusButton.addActionListener(this);
        minusButton.addActionListener(this);
        timesButton.addActionListener(this);
        divideButton.addActionListener(this);
        Container cp = getContentPane();
        cp.setLayout(new FlowLayout());
        cp.add(text1);
        cp.add(text2);
        cp.add(plusButton);
        cp.add(minusButton);
        cp.add(timesButton);
        cp.add(divideButton);
        cp.add(svarstext);
    }
} // class CalcApplet
Här är en annan och lite mer rättfram lösning på actionPerformed:
    public void actionPerformed(ActionEvent event) {
        String s1 = text1.getText();
        String s2 = text2.getText();
        try {
            double num1 = Double.parseDouble(s1);
            double num2 = Double.parseDouble(s2);
            JButton button = (JButton)event.getSource();
            double result = 0;
            if (button == plusButton)
                result = num1 + num2;
            else if (button == minusButton)
                result = num1 - num2;
            else if (button == timesButton)
                result = num1 * num2;
            else if (button == divideButton)
                result = num1 / num2;
            svarstext.setText(result + "");
        }
        catch (NumberFormatException exc) {
            svarstext.setText("ERROR");
        }
    } // actionPerformed
Man kan också ha en actionPerformed-metod per knapp, men då måste man skapa fyra nya ActionListener-objekt, av fyra olika klasser, för varje klass kan bara ha en enda actionPerformed-metod.

Uppgift 4 (10p)

a (3p)
class UFOobservation {
    private final double tid, x, y;
    public UFOobservation(double tid, double x, double y) {
        this.tid = tid;
        this.x = x;
        this.y = y;
    }
    public boolean equals(Object o) {
        if (!(o instanceof UFOobservation))
            return false;
        UFOobservation u = (UFOobservation)o;
        return this.tid == u.tid &&
            this.x == u.x &&
            this.y == u.y;
    } // equals
} // class UFOobservation

En kommentar: När man programmerar bör man tänka på att programkod inte bara är något som datorn ska läsa och förstå, utan det är ännu viktigare att de människor som kommer att läsa koden förstår den. Därför bör man tänka på att skriva begriplig och vacker kod. Ett program är inte bra bara för att det är korrekt. Många har, såväl på den här uppgiften som på andra, skrivit onödigt omständlig kod. Till exempel kanske man ersätter en enkel return-sats som
return tid == u.tid && x == u.x && y == u.y;
med
if (tid == u.tid && x == u.x && y == u.y)
    return true;
else
    return false;
Just den omskrivningen är kanske inte så dålig, och det är inte omöjligt att det till och med är tydligare vad koden gör när man har två return-satser. Men se på det här:
if (tid != u.tid) {
    if (x != u.x) {
        if (y != u.y) {
            return false;
        }
    }
}
return true;
Här har man komplicerat koden ganska mycket, med tre onödiga if-satser, och det tar mycket längre tid att förstå vad som händer. Kanske beror det hela på att den som skrev koden inte kom ihåg &&-operatorn?

Man ska förstås inte skriva kod som är så kompakt att den är kryptisk, men man ska inte heller låta enkla saker svälla ut och bli oöverskådliga. Det finns en orsak till att matematikerna uppfunnit multiplikation, så att de kan skriva 12 * 5 i stället för 5 + 5 + 5 + 5 + 5 + 5 + 5 + 5 + 5 + 5 + 5 + 5 + 5. (Övning: Hitta felet. Vad illustrerar det?)

På den här uppgiften har jag inte dragit poäng för svårlästa eller onödigt komplicerade lösningar, men tänk på det i fortsättningen.

b (2p)

    static boolean liknande(UFOobservation o1, UFOobservation o2) {
        return Math.abs(o1.tid - o2.tid) < 1 &&
            Math.abs(o1.x - o2.x) < 1 &&
            Math.abs(o1.y - o2.y) < 1;
    }
c (2p)
    public static void main(String[] arg) {
        UFOobservation o1 = new UFOobservation(1, 2, 3);
        UFOobservation o2 = new UFOobservation(1, 2, 3);
        UFOobservation o3 = new UFOobservation(1.5, 2.9, 2.1);

        System.out.println("o1.equals(o2): " + o1.equals(o2));
        System.out.println("liknande(o1, o2)): " +
                           UFOobservation.liknande(o1, o2));
    } // main
d (2p)
class OmöjligUFOobservationException
    extends Exception { }
...
    public UFOobservation(double tid, double x, double y)
      throws OmöjligUFOobservationException {
        if (tid < 0 || x < 0 || y < 0)
            throw new OmöjligUFOobservationException();
        this.tid = tid;
        this.x = x;
        this.y = y;
    }
e (1p)
        try {
            UFOobservation o4 = new UFOobservation(1, 2, 3);
        }
        catch (OmöjligUFOobservationException e) {
            System.out.println("Fångat: " + e);
        }

Uppgift 5 (5p)

a (2p)
class AvanceradUFOobservation extends UFOobservation {
    private final double höjd;
    public AvanceradUFOobservation(double tid, double x,
                                   double y, double höjd)
      throws OmöjligUFOobservationException {
        super(tid, x, y);
        this.höjd = höjd;
    }

    public boolean equals(Object o) {
        if (o instanceof AvanceradUFOobservation) {
            AvanceradUFOobservation u = (AvanceradUFOobservation)o;
            return super.equals(o) && this.höjd == u.höjd;
        }
        else if (o instanceof UFOobservation)
            return super.equals(o);
        else
            return false;
    } // equals
} // class AvanceradUFOobservation
Följande equals-metod är sämre, eftersom den är osymmetrisk, men den ger inget poängavdrag.
    public boolean equals(Object o) {
        if (!(o instanceof AvanceradUFOobservation))
            return false;
        AvanceradUFOobservation u = (AvanceradUFOobservation)o;
        return super.equals(o) && this.höjd == u.höjd;
    } // equals
b (3p)

Likhet mellan objekt bör vara symmetrisk och transitiv. Om man ärver från en klass och utvidgar den med ytterliagre en egenskap, går det inte att åstadkomma en likhetsrelation som är både symmetrisk och transitiv.

Antag att vi har en UFO-observation, o1, och två avancerade UFO-observationer med höjd, ao1 och ao2:

UFOobservation o1 = new UFOobservation(1, 2, 3);
AvanceradUFOobservation ao1 = new AvanceradUFOobservation(1, 2, 3, 4);
AvanceradUFOobservation ao2 = new AvanceradUFOobservation(1, 2, 3, 5);
Att likheten mellan två objekt är symmetrisk innebär att o1 är lika med ao1 om och endast om ao1 är lika med o1. I Java betyder det att o1.equals(ao1) ska ha samma värde (true eller false) som ao1.equals(o1).

UFOobservation-klassens equals-metod vet inte något om höjder, utan jämför bara tid och x- och y-koordinater när den jämför med en annan UFOobservation-instans. AvanceradUFOobservation ärver UFOobservation, vilket betyder att en AvanceradUFOobservation-instans är en UFOobservation-instans. Därför kommer o1.equals(ao1) bara att jämföra tid, x och y, och den anser därför att objekten är lika. Den returnerar alltså värdet true.

För att bevara symmetrin måste AvanceradUFOobservation-klassens equals-metod skrivas så, att den, när den jämför med en UFOobservation-instans som inte är en AvanceradUFOobservation-instans, jämför på samma sätt som UFOobservation-klassens equals-metod. Det kan ske genom att anropa den metoden.

Alltså är o1 (symmetriskt) lika med ao1. På samma sätt är o1 (symmetriskt) lika med ao2.

Men samtidigt har ao1 och ao2 olika höjd, så de bör betraktas som olika!

De tre punkterna och deras likhet

Detta strider mot regeln att likhet mellan objekt bör vara transitiv. För våra tre objekt (ao1, o1 och ao2) gäller att om både ao1 = o1 och o1 = ao2, så ska ao1 = ao2, och tvärtom. I Java betyder det att ao1.equals(o1) and o1.equals(ao2) ska ha samma värde (true eller false) som ao1.equals(ao2).

Uppgift 6 (10p)

import java.net.*;
import java.io.*;

class ClientThread extends Thread {
    private final Socket socket;
    private final BufferedReader in;
    private final PrintWriter out;
    private final Storage storage;

    public ClientThread(Socket s, Storage m) throws IOException {
        socket = s;
        storage = m;
        in = new BufferedReader(
                     new InputStreamReader(
                             socket.getInputStream()));
        out = new PrintWriter(
                      new BufferedWriter(
                              new OutputStreamWriter(
                                      socket.getOutputStream())), true);
        start();
    } // ClientThread

    public void run() {
        try {
            while (true) {
                String inline = in.readLine();
                if (inline == null)
                    break;
                else if (inline.equals("LAGRA")) {
                    String newString = in.readLine();
                    storage.setString(newString);
                    out.println("OK");
                }
                else if (inline.equals("HÄMTA")) {
                    out.println(storage.getString());
                }
                else
                    out.println("NOK");
            } // while
        } // try
        catch(IOException e) {

        }
    } // run
} // class ClientThread

class Storage {
    private String s;
    public synchronized void setString(String s) {
        this.s = s;
    }
    public synchronized String getString() {
        return s;
    }
} // class Storage

public class MultiServer {
    public static final int PORT = 2000;
    public static void main(String[] args) throws IOException {
        Storage storage = new Storage();
        ServerSocket s = new ServerSocket(PORT);
        while(true) {
            Socket socket = s.accept();
            ClientThread t = new ClientThread(socket, storage);
        } // while
    } // main
} // MultiServer


Thomas Padron-McCarthy (Thomas.Padron-McCarthy@tech.oru.se) 25 december 2003