Java: Föreläsning 5

Av Thomas Padron-McCarthy (Thomas.Padron-McCarthy@tech.oru.se). Senaste ändring 8 november 2003.

Ungefär motsvarande föreläsningsanteckningar från förra året: Delar av java013.pdf

Innehåll i föreläsning 5

Flera trådar för I/O

I exemplet på nätverkskommunikation från föreläsning 4, med klasserna Client och Server, var både servern och klienten enkeltrådade: Lösningen är förstås att ha flera trådar: två trådar i klienten, och en tråd per klient i servern.

Trådad chat-server med trådad chat-klient

Filen MultiServer.java:
import java.net.ServerSocket;
import java.net.Socket;

import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.PrintWriter;
import java.io.IOException;

import java.util.ArrayList;
import java.util.Iterator;

class ClientThread extends Thread {
    private static int numberOfClients = 0;
    private static ArrayList allClients = new ArrayList();

    private final int clientNumber = ++numberOfClients;
    private final Socket socket;
    private final BufferedReader in;
    private final PrintWriter out;

    public ClientThread(Socket s) throws IOException {
        socket = s;
        in = new BufferedReader(
                 new InputStreamReader(
                     socket.getInputStream()));
        out = new PrintWriter(
                  new BufferedWriter(
                      new OutputStreamWriter(
                          socket.getOutputStream())), true);
        // true: PrintWriter is line buffered

        System.out.println("Klienttråd " + clientNumber +
                           " skapad.");
        out.println("Välkommen. Du är klient nummer " +
                    clientNumber + ".");

        allClients.add(this);

        // If any of the above calls throw an 
        // exception, the caller is responsible for
        // closing the socket. Otherwise the thread
        // will close it.
        start(); // Starts the thread, and calls run()
    }

    public void run() {
        try {
            while (true) {
                String inline = in.readLine();
                System.out.println("Klienttråd " + clientNumber +
                                   " tog emot: " + inline);
                // Not: inline == "quit"
                if (inline == null || inline.equals("quit"))
                    break;
                out.println("Du sa '" + inline + "'");
                Iterator i = allClients.iterator();
                while (i.hasNext()) {
                    ClientThread t = (ClientThread)i.next();
                    if (t != this)
                        t.out.println("Från klient " + clientNumber +
                                      ": " + inline);
                }
            }
            System.out.println("Klienttråd " + clientNumber +
                               ": Avslutar...");
        }
        catch(IOException e) {
            System.out.println("Klienttråd " + clientNumber +
                               ": I/O-fel");
        }
        finally {
            try {
                socket.close();
            }
            catch(IOException e) {
                System.out.println("Klienttråd " + clientNumber +
                                   ": Socketen ej stängd");
            }
            allClients.remove(allClients.indexOf(this));
        }
    } // run
} // class ClientThread

public class MultiServer {
    public static final int PORT = 2000;
    public static void main(String[] args) throws IOException {
        ServerSocket s = new ServerSocket(PORT);
        System.out.println("Server-socketen: " + s);
        System.out.println("Servern lyssnar...");

        try {
            while(true) {
                // Blocks until a connection occurs:
                Socket socket = s.accept();
                System.out.println("Uppkoppling accepterad.");
                System.out.println("Den nya socketen: " + socket);
                try {
                    ClientThread t = new ClientThread(socket);
                    System.out.println("Ny tråd skapad.");
                    System.out.println("Den nya tråden: " + t);
                }
                catch(IOException e) {
                    // If the constructor fails, close the socket,
                    // otherwise the thread will close it:
                    socket.close();
                }
            }
        }
        finally {
            s.close();
        }
    } // main
} // MultiServer
Filen Client2.java:
import java.net.Socket;
import java.net.InetAddress;

import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.PrintWriter;
import java.io.IOException;

final class ServerListener extends Thread {
    final private BufferedReader fromServer;

    public ServerListener(BufferedReader fromServer) {
        this.fromServer = fromServer;
    }

    public void run() {
        String lineFromServer;
        try {
            while ((lineFromServer = fromServer.readLine()) != null &&
                   !lineFromServer.equals("quit")) {
                System.out.println("Från servern: " + lineFromServer);
            }
        }
        catch (IOException e) {
            System.out.println("Undantag fångat: " + e);
        }
    }
} // class ServerListener

public class Client2 {
    public static final int PORT = 2000;
    public static void main(String[] args) throws IOException {
        InetAddress addr;
        if (args.length >= 1)
            addr = InetAddress.getByName(args[0]);
        else
            addr = InetAddress.getByName(null);

        Socket socket = new Socket(addr, PORT);
        System.out.println("Den nya socketen: " + socket);

        BufferedReader in = new BufferedReader(
            new InputStreamReader(socket.getInputStream()));
        PrintWriter out = new PrintWriter(
            new BufferedWriter(
                new OutputStreamWriter(
                    socket.getOutputStream())), true);
        // true: PrintWriter is line buffered

        BufferedReader kbd_reader = new BufferedReader(
            new InputStreamReader(System.in));

        ServerListener t = new ServerListener(in);
        t.start();

        String buf;
        while (true) {
            buf = kbd_reader.readLine();
            System.out.println("Från tangentbordet: " + buf);
            System.out.println("Till servern: " + buf);
            out.println(buf);
        }
    } // main
} // class Client2

Kort utvikning: Chatklient med fönster

Client3 är också en chatklient, likadan som Client2 ovan, men den har dessutom ett litet extra fönster med några knappar.

Som övning kan du flytta även utmatningen och inmatningen till det fönstret, så har du en grafisk chatklient!

Filen Client3.java:

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

final class ServerListener extends Thread {
    private final BufferedReader fromServer;

    public ServerListener(BufferedReader fromServer) {
        this.fromServer = fromServer;
    }

    public void run() {
        String lineFromServer;
        try {
            while ((lineFromServer = fromServer.readLine()) != null &&
                   !lineFromServer.equals("quit")) {
                System.out.println("Från servern: " + lineFromServer);
            }
        }
        catch (IOException e) {
            System.out.println("Undantag fångat: " + e);
        }
    }
} // class ServerListener

class ChatButtonWindow extends JFrame {
    private final PrintWriter toServer;

    public ChatButtonWindow(PrintWriter to) {
        super("Extra chat buttons");
        this.toServer = to;
        Container cp = getContentPane();
        cp.setLayout(new FlowLayout());
        JButton button1 = new JButton("Skicka förolämpning");
        cp.add(button1);
        button1.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent event) {
                    toServer.println("Ni, min herre, är en apa.");
                    System.out.println("Skickade en förolämpning.");
                }
            });

        JButton button2 = new JButton("Skicka beröm");
        cp.add(button2);
        button2.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent event) {
                    toServer.println("Du är bäst!");
                    System.out.println("Skickade beröm.");
                }
            });

        JButton button3 = new JButton("Avsluta");
        cp.add(button3);
        button3.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent event) {
                    System.out.println("Avslutar.");
                    System.exit(0);
                }
            });

        setSize(200, 200);
        setVisible(true);
    }
} // class ChatButtonWindow

public class Client3 {
    public static final int PORT = 2000;
    public static void main(String[] args) throws IOException {
        InetAddress addr;
        if (args.length >= 1)
            addr = InetAddress.getByName(args[0]);
        else
            addr = InetAddress.getByName(null);

        Socket socket = new Socket(addr, PORT);
        System.out.println("Den nya socketen: " + socket);

        BufferedReader in = new BufferedReader(
            new InputStreamReader(socket.getInputStream()));
        PrintWriter out = new PrintWriter(
            new BufferedWriter(
                new OutputStreamWriter(
                    socket.getOutputStream())), true);
        // true: PrintWriter is line buffered

        BufferedReader kbd_reader = new BufferedReader(
            new InputStreamReader(System.in));

        ServerListener t = new ServerListener(in);
        t.start();

        ChatButtonWindow w = new ChatButtonWindow(out);

        String buf;
        while (true) {
            buf = kbd_reader.readLine();
            System.out.println("Från tangentbordet: " + buf);
            System.out.println("Till servern: " + buf);
            out.println(buf);
        }
    } // main
} // class Client3

Kommunikationsprotokoll

Regler för hur de meddelanden som skickas mellan programmen ska se ut, och hur de ska tolkas.

Exempel 1:

Exempel 2:

Behållare

En behållare (engelska container) är ett objket som kan innehålla andra objekt, till exempel en array eller en mängd.

Java 2 har flera nya typer av behållare, som inte tas upp i Erikssons bok Programutveckling med Java. Läs mer i Kapitiel 11 i Bruce Eckels bok Thinking in Java, 3d Ed.

Notera att Programutveckling med Java kallar alla arrayer för "vektorer".

Klassdiagram med med de viktigaste typerna av behållare (ur Bruce Eckels Thinking in Java, 3d Ed):

Behållardiagram för Java

Behållarna finns i paketet java.util:

import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.HashSet;
import java.util.TreeSet;
import java.util.HashMap;
import java.util.TreeMap;

Alla behållare (utom de inbyggda arrayerna) innehåller enbart Object:

Ett testprogram med en massa behållare av olika slag: Datatest.java

Behållare: De inbyggda arrayerna

De inbyggda []-arrayerna är enklast, och ibland måste man använda dem, när de används av bibliotek och inbyggda mekanismer. Ett exempel är main-funktionens argument:
public static void main(String[] args) {
    for (int i = 0; i < args.length; ++i)
        System.out.println("Argument " + i + ": '" + args[i] + "'");
    }
}
Man måste skapa själva arrayen med new:
int[] intarray1 = new int[10];
int intarray2[] = new int[10];
String[] fruits = { "Äpple", "Päron", "Apelsin" };
int intarray[10]; // Ger kompileringsfel: ']' expected
fruits = new String[] { "Ananas", "Banan" };
Paketet java.util.Arrays innehåller några nyttiga funktioner, till exempel sort som sorterar en array:
java.util.Arrays.sort(fruits);
java.util.Arrays.fill(fruits, "Citron");
Man kan förstås också ha instanser av sina egna klasser i arrayerna. Antag att vi har en klass som heter Hamster:
Hamster[] ha1 =
    { new Hamster("Adam"), new Hamster("Bertil"), new Hamster("Cecar") };
for (int i = 0; i < ha1.length; ++i)
    System.out.println("ha1[" + i + "]: '" + ha1[i] + "'");
Utmatning, om vi har definierat en lämplig toString-metod i klassen Hamster:
ha1[0]: 'Hamster-Adam'
ha1[1]: 'Hamster-Bertil'
ha1[2]: 'Hamster-Cecar'
Man kan också allokera arrayen först, och sätta elementen efteråt:
Hamster[] ha2 = new Hamster[4];
ha2[0] = new Hamster("Anna");
ha2[1] = new Hamster("Beata");
ha2[2] = new Hamster("Cecilia");
ha2[3] = new Hamster("Dora");
Om man försöker adressera utanför arrayen, kastas ett ArrayIndexOutOfBoundsException:
try {
    ha2[4] = new Hamster("Eva");
}
catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("Undantag som fångades: '" + e + "'");
}
Arrayer är typsäkra. Det går inte att stoppa in fel sorts objekt:
ha2[2] = new Båt("Titanic"); // Ger kompileringsfel: "incompatible types"
Arrayerna har fix storlek, men man kan ändå "ändra storlek" på en array genom att skapa en ny:
System.out.println("ha2.length = " + ha2.length);
ha2 = new Hamster[7];
System.out.println("ha2.length = " + ha2.length);

En annan sorts "array": ArrayList

ArrayList är ett bra alternativ om man behöver en dynamisk array, dvs en array som kan växa och krympa. Från början är listan tom, och metoden add lägger till ett nytt element sist i listan. Metoden get hämtar elementet som finns på en viss position.
ArrayList båtar = new ArrayList();
Båt b1 = new Båt("Titanic");
Båt b2 = new Båt("Exxon Valdez");
Båt b3 = new Båt("Torrey Canyon");
båtar.add(b1);
båtar.add(b2);
båtar.add(b3);
båtar.add(b1);
båtar.add(b1);

System.out.println("Båtarna:");
for (int i = 0; i < båtar.size(); ++i)
    System.out.println("  båtar[" + i + "]: " + båtar.get(i));
Utmatning, om vi har definierat en lämplig toString-metod i klassen Båt:
Båtarna:
  båtar[0]: Båten Titanic
  båtar[1]: Båten Exxon Valdez
  båtar[2]: Båten Torrey Canyon
  båtar[3]: Båten Titanic
  båtar[4]: Båten Titanic
Vi la ju till båten Titanic flera gånger, så den förekommer förstås flera gånger i listan!

I stället för att stega fram ett heltal som anger vilken position vi vill titta på, och sen hämta elementet på den positionen med get, kan vi använda en Iterator. Metoden iterator returnerar en ny iterator, och i den finns metoderna hasNext och next.

System.out.println("Båtarna:");
Iterator i1 = båtar.iterator();
while (i1.hasNext())
    System.out.println("  En båt: " + i1.next());
Oftast är det mer praktiskt att byta while-loopen mot en for-loop. Då kan man definiera iterator-variabeln så att den bara finns inuti själva loopen.
System.out.println("Båtarna:");
for (Iterator i = båtar.iterator(); i1.hasNext(); )
    System.out.println("  En båt: " + i.next());
Såvitt Java-kompilatorn känner till, innehåller en ArrayList objekt av typen Object. Därför kommer get och next, såvitt kompilatorn vet, bara att returnera Object, och inte något mer specifikt:
System.out.println("Båtarna:");
for (Iterator i = båtar.iterator(); i.hasNext(); ) {
    Båt b;
    b = i.next(); // Ger kompileringsfel: "incompatible types"
    System.out.println("  En båt: " + b);
}
Man måste göra en explicit typkonvertering:
System.out.println("Båtarna:");
for (Iterator i = båtar.iterator(); i.hasNext(); ) {
    Båt b;
    b = (Båt)i.next();
    System.out.println("  En båt: " + b);
}
Observera att det är inget objekt som konverteras på något sätt. Vi bara talar om för Java-kompilatorn att "hördudu, det där Object som returneras av metoden next, det är faktiskt en Båt, så du kan lugnt lägga det i variabeln b".

Men dessutom kan man kan stoppa in objekt av helt fel sorts typ:

Hamster hasse = new Hamster("Hasse");
båtar.add(hasse); // Inget fel -- "båtar" innehåller Object, inte Båt!
Ojoj! Nu finns det en hamster bland båtarna.

Jämför med C++, och dess templates: vector<Boat>

När man försöker konvertera till den agivna typen, kan det kastas ett ClassCastException. Ett Object som verkligen är av typen Båt kan konverteras till Båt (jag talar ju bara om att den där saken där, det är faktiskt en båt), men en Hamster kan omöjligt konverteras till en Båt. (På vetenskapens nuvarande stadium.)

try {
    System.out.println("Båtarna:");
    for (Iterator i = båtar.iterator(); i.hasNext(); ) {
	Båt b;
	b = (Båt)i.next(); // Kommer att ge ClassCastException för hamstern Hasse
	System.out.println("  En båt: " + b);
    }
}
catch (ClassCastException e) {
    System.out.println("Undantag som fångades: '" + e + "'");
}
Metoden indexOf returnerar positionen för (den första förekomsten av) ett objekt i listan, eller -1 om det inte fanns i listan.

Tänk på att den gör en likhetsjämförelse. Är en ny hamster som heter Hasse lika med den gamla hamstern som heter Hasse? Eller räknas två hamstrar bara som lika om de verkligen är samma hamster, dvs om de två variablerna refererar till samma objekt? Det beror på hur vi definierat klassen Hamster, och dess equals-metod. Om vi inte skrivit någon egen equals-metod, räknas två objekt som lika bara om de är samma objekt.

System.out.println("Hasses position (1): " + båtar.indexOf(hasse));
System.out.println("Hasses position (2): " + båtar.indexOf(new Hamster("Hasse")));
Vi kan ta bort element ur listan med hjälp av metoden remove, både genom att ange en position och genom att ange vilket objekt vi vill ta bort:
båtar.remove(4);
båtar.remove(b1);

En annan sorts "array": LinkedList

Både ArrayList och LinkedList implementerar gränssnittet List, så de kan göra precis samma saker. Men LinkedList är internt representerad som en länkad lista, och har därför annorlunda prestanda.
LinkedList båtlista = new LinkedList();
for (Iterator i = båtar.iterator(); i.hasNext(); )
    båtlista.add(i.next());
båtlista.addAll(båtar); // Alla på en gång!

System.out.println("Båtarna i båtlistan:");
for (Iterator i = båtlista.iterator(); i.hasNext(); )
    System.out.println("  En båt: " + i.next());

Mängder: HashSet

En mängd tillåter inga dubletter, och elementen har ingen bestämd ordning. (Jo, i en behållare ligger elementen i någon ordning, men den ordningen beror på implementationsdetaljer i mängdens interna datastrukturer, inte på i vilken ordning man lagt dit dem.)

Både HashSet och TreeSet implementerar gränssnittet Set, så de kan göra precis samma saker, men de har olika prestanda.

HashSet båtmängd = new HashSet();
for (Iterator i = båtar.iterator(); i.hasNext(); )
    båtmängd.add(i.next());
båtmängd.addAll(båtar); // Alla på en gång!

System.out.println("Båtarna i båtmängden:");
for (Iterator i = båtmängd.iterator(); i.hasNext(); )
    System.out.println("  En båt: " + i.next());

båtmängd.remove(hasse);

System.out.println("Båtarna i båtmängden, nu utan hamstern Hasse:");
for (Iterator i = båtmängd.iterator(); i.hasNext(); )
    System.out.println("  En båt: " + i.next());
Utmatning:
Båtarna i båtmängden:
  En båt: Båten Exxon Valdez
  En båt: Båten Titanic
  En båt: Hamster-Hasse
  En båt: Båten Torrey Canyon
Båtarna i båtmängden, nu utan hamstern Hasse:
  En båt: Båten Exxon Valdez
  En båt: Båten Titanic
  En båt: Båten Torrey Canyon
Eftersom elementen lagras i en hashtabell, kan de komma i vilken ordning som helst.

En annan sorts mängd: TreeSet

Både HashSet och TreeSet implementerar gränssnittet Set, så de kan göra precis samma saker, men de har olika prestanda.

TreeSet lagrar internt sina data i form av ett träd, och den kräver därför att objekten implementerar gränssnittet Comparable, som säger att det ska finnas en compareTo-metod.

// TreeSet kräver att objekten implementerar Comparable
TreeSet telefonnummermängd2 = new TreeSet();
telefonnummermängd2.add(new Telefonnummer(116090));
telefonnummermängd2.add(new Telefonnummer(271010));
telefonnummermängd2.add(new Telefonnummer(271011));
telefonnummermängd2.add(new Telefonnummer(271012));
telefonnummermängd2.add(new Telefonnummer(111111));
telefonnummermängd2.add(new Telefonnummer(111111));
telefonnummermängd2.add(new Telefonnummer(111111));

System.out.println("Telefonnumren i telefonnummermängd 2:");
for (Iterator i = telefonnummermängd2.iterator(); i.hasNext(); )
    System.out.println("  Ett telefonnummer: " + i.next());
Utmatning:
Telefonnumren i telefonnummermängd 2:
  Ett telefonnummer: +46-(0)19-111111
  Ett telefonnummer: +46-(0)19-116090
  Ett telefonnummer: +46-(0)19-271010
  Ett telefonnummer: +46-(0)19-271011
  Ett telefonnummer: +46-(0)19-271012
Eftersom det är ett träd, kommer nycklarna i ordning.

Mappning: HashMap

En "map" eller "mapping" kallas ibland "dictionary" eller "table". Man lägger in par av objekt: en nyckel och ett värde. Om man vet nyckeln går det sen snabbt att slå upp värdet.

En HashMap och TreeMap implementerar gränssnittet Map, så de kan göra precis samma saker.

HashMap telefonbok1 = new HashMap();
telefonbok1.put("Anna", new Telefonnummer(116090));
telefonbok1.put("Bengt", new Telefonnummer(224000));
telefonbok1.put("Conny", new Telefonnummer(125566));
telefonbok1.put("Doris", new Telefonnummer(171045));
telefonbok1.put("Eberhart", new Telefonnummer(111111));
telefonbok1.put("Eberhart", new Telefonnummer(222222));
telefonbok1.put("Eberhart", new Telefonnummer(333333));

System.out.println("Conny har nummer " + telefonbok1.get("Conny"));

System.out.println("Telefonbok 1:");
for (Iterator i = telefonbok1.keySet().iterator(); i.hasNext(); ) {
    String namn = (String)i.next();
    System.out.println("  " + namn + " har nummer " + telefonbok1.get(namn));
}
Utmatning:
Conny har nummer +46-(0)19-125566
Telefonbok 1:
  Conny har nummer +46-(0)19-125566
  Doris har nummer +46-(0)19-171045
  Eberhart har nummer +46-(0)19-333333
  Bengt har nummer +46-(0)19-224000
  Anna har nummer +46-(0)19-116090
Eftersom det är en hashtabell, kan nycklarna komma i vilken ordning som helst.

En annan sorts mappning: TreeMap

En HashMap och TreeMap implementerar gränssnittet Map, så de kan göra precis samma saker, men de har olika prestanda.

TreeMap lagrar internt sina nycklar i form av ett träd, och den kräver därför att nyckelobjekten implementerar gränssnittet Comparable, som säger att det ska finnas en compareTo-metod.

// TreeMap kräver att nycklarna implementerar Comparable (och det gör String)
TreeMap telefonbok2 = new TreeMap();
telefonbok2.put("Anna", new Telefonnummer(116090));
telefonbok2.put("Bengt", new Telefonnummer(224000));
telefonbok2.put("Conny", new Telefonnummer(125566));
telefonbok2.put("Doris", new Telefonnummer(171045));
telefonbok2.put("Eberhart", new Telefonnummer(111111));
telefonbok2.put("Eberhart", new Telefonnummer(222222));
telefonbok2.put("Eberhart", new Telefonnummer(333333));

System.out.println("Telefonbok 2:");
for (Iterator i = telefonbok2.keySet().iterator(); i.hasNext(); ) {
    String namn = (String)i.next();
    System.out.println("  " + namn + " har nummer " + telefonbok2.get(namn));
}
Utmatning:
Telefonbok 2:
  Anna har nummer +46-(0)19-116090
  Bengt har nummer +46-(0)19-224000
  Conny har nummer +46-(0)19-125566
  Doris har nummer +46-(0)19-171045
  Eberhart har nummer +46-(0)19-333333
Eftersom det är ett träd, kommer nycklarna i ordning.

JDBC

...hinner vi nog inte med idag.