Exempel på gränssnitt och klass: Dynamisk array

Som en lite mer komplex och användbar klass ska vi implementera en lista med hjälp av en s.k. dynamisk array. Med en lista menar vi en sekvens av element, i detta exempel heltal, som kan växa och krympa.

Låt oss först bestämma vad vi vill att man ska kunna göra med en lista. Vi definierar ett gränssnitt:

public interface IntList {
  void add(int e);  // lägg till element e på slutet
  void add(int index, int e);  // sätt in element e på plats index
  void remove(int index);  // ta bort element på plats index
  void clear();  // töm listan
  int get(int index);  // läs element på plats index
  void set(int index, int e);  // uppdatera element på plats index till e
  int size();  // tala om listans längd
}

Detta interface går att implementera på olika sätt. Vi väljer att använda en dynamisk array. Det innebär att man har en array som kan vara större än aktuellt antal element i listan och att man förstorar arrayen vid behov. Från början allokeras en array med ett visst antal element. Arrayens verkliga storlek brukar kallas kapacitet.

Ett interface säger inget om vilka konstruerare som ska finnas i en klass som implementerar det. Låt oss göra en utan argument och en där man kan välja den initiala kapaciteten.

public class DynArrayIntList implements IntList {
  private final static int DEFAULT_CAPACITY = 10;

  private int[] a = null;
  private int size = 0;

  public DynArrayIntList() {
    a = new int[DEFAULT_CAPACITY];
  }

  public DynArrayIntList(int initialCapacity) {
    a = new int[initialCapacity];
  }

  public void add(int e) {
    add(size, e);
  }

  public void add(int index, int e) {
    if (index < 0 || index > size) {
      throw new IndexOutOfBoundsException();
    }
    if (size == a.length) growArray();

    for (int i = size; i > index; i--) {
      a[i] = a[i - 1];
    }
    a[index] = e;
    size++;
  }
  
  private void growArray() {
    int[] b = new int[a.length * 2];
    
    for (int i = 0; i < size; i++) {
      b[i] = a[i];
    }
    
    a = b;
  }

  public void remove(int index) {
    if (index < 0 || index >= size) {
      throw new IndexOutOfBoundsException();
    }
    
    size--;
    for (int i = index; i < size; i++) {
      a[i] = a[i + 1];
    }
  }

  public void clear() {
    size = 0;
  }

  public int get(int index) {
    if (index < 0 || index >= size) {
      throw new IndexOutOfBoundsException();
    }

    return a[index];    
  }
  public void set(int index, int e) {
    if (index < 0 || index >= size) {
      throw new IndexOutOfBoundsException();
    }

    a[index] = e;    
  }
  public int size() {
    return size;
  }
}

Grafisk gränssnitt

Det finns olika paket i Java:s API för att skriva program med grafiska gränssnitt. Det ursprungliga var awt. Sedan kom Swing med fler färdiga komponenter och ett utseende som var mer likt mellan olika plattformar. Den senaste varianten heter javafx och den har funnits några år. Den har vissa fördelar framför de äldre paketen när det gäller hårdvaruacceleration, användar-input på touchskärmar, animationer, 3D-grafik, html-innehåll. Men paketet är inte riktigt standard fullt ut. Man måste t.ex. installera särskilda tillägg för att få javafx att fungera i vissa IDEer. Eftersom det är ett modernare och mer kraftfullt system så är också abstraktionsnivån högre. I kursen fokuserar vi på Swing för att det är så etablerat och koncepten är konkreta. När man kan Swing är det lätt att gå vidare och lära sig javafx. Även om javafx används för mycket nyutveckling så finns många applikationer som använder Swing kvar, så därför är det nyttigt att känna till båda två.

Metodiken när man skriver program med GUI är dessutom densamma mellan javafx och Swing (och motsvarande system för andra programspråk). För det första så är interaktionen med användaren händelsestyrd, till skillnad från att vara programstyrd som den är i applikationer som interagerar via kommandoprompten Att den är händelsestyrd innebär att programmet har en initieringsfas där användargränssnittet byggs upp och att det sedan går in huvudfasen där den väntar på att händelser ska uppstå, ofta genom att användaren ger någon input, t.ex. klickar med musen.

Ett annat gemensamt drag är att kärnan i paketen består av en hierarki av klasser som representerar olika grafiska komponenter, t.ex. ett fönster med ram eller en knapp.

Swing bygger på awt så man behöver använda klasser även från detta paket här och var.

Hierarkin av de olika GUI-komponenterna i Swing

Basklassen är JComponent men denna ärver Container från awt.

Det finns också några klasser för toppnivå-element, bl.a.:

Starta ett program med GUI

I java-program kan koden, liksom i andra moderna programspråk såsom Haskell och C#, köras i olika trådar (threads). Om ett program har flera trådar igång så exekveras dessa ofta faktiskt eller virtuellt parallellt. När ett javaprogram börjar exekveras skapas en tråd som kör main-metoden. Ett java-program fortsätter exekveras tills alla trådar avslutats. Så även om huvudtråden blir klar med main-metoden så avslutas inte programmet om det skapats andra trådar som fortfarande är aktiva.

Trådar kan också användas för att utföra olika typer av uppgifter. I Swing och awt används en event dispatching thread som är den som väntar på händelser och utför det ska hända när en sådan uppstår. Det rekommenderas att man också utför initieringen av GUIt i denna tråd. Standardsättet att starta denna tråd är att anropa SwingUtilities.invokeLater. Detta innebär att event dispatching thread startas när huvudtråden är klar. Som argument tar denna metod en klass som implementerar interfacet Runnable. Detta deklarerar en metod

void run()

som körs i event dispatching thread. I denna metod (eller i hjälpmetoder) utför man initieringen av GUIt.

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

public class GUIDemo implements Runnable {
    public void run() {
      JFrame frame = new JFrame("GUIStart");
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

      // create frame contents here

      frame.pack();
      frame.setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new GUIStart());
    }
}

Här har vi valt att låta huvudklassen implementera Runnable. Det är vanligt men inte nödvändigt. Det kan göras av en hjälpklass istället.

Metoden setDefaultCloseOperation bestämmer vad som ska ske man använder stänger fönstret. Alternativet EXIT_ON_CLOSE innebär helt enkelt att hela programmet avslutas.

Ett första innehåll – JButton

Se kod för exempel på det som beskrivs i resten av dessa anteckningar.

Vi kan lägga in en knapp i ramen så här:

button = new JButton("Count");
frame.add(button);

En JFrame har en ram och titel. Själva ytan som innehållet befinner sig i kan man nå genom

frame.getContentPane()

Förr var man tvungen att explicit lägga till komponenter till denna, men nu kan man anropa add direkt för ramen.

Layout

Filosofin hos moderna system för GUI är att i möjligaste mån låta den som skriver programmet att uttrycka layout utan att ange faktiska positioner och storlekar i pixlar. Man gör detta genom att istället t.ex. säga att en viss komponent befinner sig ovanför eller till vänster om en annan.

Det finns några olika layout-modeller i java. Förvald är BoarderLayout. Den består av fem delar, en central och fyra stycken vid sidan om som bildar en ram. add(c) gör c till den centrala komponenten och den överlagrade varianten add(c, BorderLayout.SOUTH) (NORTH, WEST, EAST) ställer in komponenterna vid sidan om.

En annan är FlowLayout där man med add(c) kan lägga till godtyckligt många komponenter som placerar sig från vänster till höger, uppifrån och ner.

frame.setLayout(new FlowLayout());

En tredje är BoxLayout där man anger om komponenterna ska placeras horisontellt eller vertikalt efter varandra.

frame.setLayout(new BoxLayout(frame.getContentPane(), BoxLayout.Y_AXIS));

Oracles visualisering och förklaring av de olika layout-modellerna

JLabel

JLabel används för att visa en kort text. Den har en konstruktor JLabel(String s) som väljer vad det ska stå och en metod setText(String s) som man senare kan ändra texten med.

Lyssnare

Ett centralt begrepp i ett händelsestyrt användargränssnitt är lyssnare (listeners). De kan man se som metoder (som implementeras av någon klass) som blir anropade då en händelse har inträffat. Det kan vara att användaren har tryckt på en knapp, klickat eller flyttat på musen, minimerat fönstret, tryck på tangentbordet etc.

Många händelser gör man lyssnare till genom att implementera ActionListener och dess metod

void actionPerformed(ActionEvent e)

Ett ActionEvent är en komponent som innehåller information bl.a. om för vilken komponent händelsen inträffade och vilken typ av händelse det är.

För att lägga till en lyssnare (det kan finnas flera) använder main addActionListener(ActionListener l) för den komponent det gäller.

Man kan skapa en ActionListener för varje komponent (genom t.ex. lokala klasser), men man kan också ta hand om flera komponenters händelser på samma ställe genom att anropa getSource() och på så sätt avgöra vilken komponent det gäller.