Ganska vanliga lösningar på ganska vanliga uppgifter

Den här kursen har en central kärna med kunskaper och färdigheter som alla kommer att behöva, och en stor "periferi" där kunskaper och färdigheter bland annat beror på hur man utformar sitt projekt. Sett över flera års tid är det många uppgifter som återkommer gång på gång, och vi vill gärna hjälpa till med dem. Samtidigt har varje enskild grupp bara nytta av en liten del av detta under kursen – och vet inte i förväg vilken del som behövs.

Därför samlar vi lösningar på dessa ganska vanliga uppgifter här, istället för att alla ska delta i ett antal extra föreläsningar där inget är centralt för kursmålen och bara en bråkdel är nödvändigt för projekten. En del av materialet använder sig av tidigare föreläsningsbilder för att illustrera.

(Sedan kan ju en del av det ändå vara intressant för många, men skulle vi ta upp allt som är intressant skulle kursen ta flera år...)

Vi inkluderar även en del lösningar som många har nytta av, men som rör rena fakta snarare än begrepp att förstå, så att det är bättre att slå upp dem när man behöver dem.

Göra något med strängar

Som vi har sagt på en av de första föreläsningarna skiljer Java på tecken och strängar (0 eller flera tecken):

Python har en hel del specialsyntax för strängar som förenklar hanteringen och indexeringen. Java har mindre sådan syntax, och använder istället vanliga metodanrop för att manipulera strängarna. Därför blir stränghanteringen lite mindre koncis.

Om vi vill plocka ut tecken ur strängar, börjar index på 0, precis som i Python:

>>> s = "Spamalot"
>>> s[1]
'p'
>>> s[-2]
'o'
>>> len(s)
8
>>> s[len(s)-1]
't'
String s = "Spamalot";
s.charAt(1)

s.charAt(s.length() – 2)

s.length()

s.charAt(s.length() – 1)

Om vi vill plocka ut delar av strängar:

>>> s = 'Spamalot'
>>> s[1:]
'pamalot'
>>> s[1:3]
'pa'
>>> s[:5]
'Spama'
>>> s[:-1]
'Spamalo'
>>> s[:]
'Spamalot'
>>> " abc ".strip()
'abc'
>>> longstring.splitlines()
String s = "Spamalot";
s.substring(1)

s.substring(1,3)

s.substring(0,5)

s.substring(0, s.length() – 1)

s

s.strip()

s.lines()

Om vi vill jämföra strängar, använder vi inte operatorn "==". Den finns visserligen, men den testar om två variabler pekar på samma strängobjekt (samma minnesadress), medan equals() testar om två strängar innehåller samma tecken, vilket man oftare är intresserad av. Mer om detta på en föreläsning.

>>> "Aardvark" < "Abbot"
True
>>> "Queen" > "King"
True
>>> "a" * 4 == "aaaa"
True
>>> "a" in "The Holy Grail"
True
>>> "vers" in "universitet"
True
>>> "a" == "b"
False
"Aardvark".compareTo("Abbot") < 0

"Queen".compareTo("King") > 0

("a"+"a"+"a"+"a").equals("aaaa")

"The Holy Grail".contains("a")

"universitet".contains("vers")

"a".equals("b")

För att konvertera text till numeriska värden:

val = int("127")
val = float("218.52")
int	val = Integer.parseInt("127");
double	val = Double.parseDouble("218.52");

För att konkatenera (sätta ihop) strängar används "+". Detta fungerar även om man sätter ihop strängar med tal. I så fall konverteras talen automatiskt till strängar (kompilatorn sätter in extra anrop för konverteringen).

int height = 100;
int width = 200;
String description = "Height is " + height + " and width is " + width;

Man kan också "multiplicera" strängar, som i Python:

"xyz" * 3
"xyz".repeat(3)

För att skriva ut strängar (eller annat) används System.out.println():

int height = 100;
int width = 200;
System.out.println("Height is " + height + " and width is " + width);
System.out.println(description);
System.out.println(width);

För att skriva oskrivbara tecken, sådana som inte "syns" på skärmen eller som man inte hittar på tangentbordet, finns ett antal "escapekoder". En del av dessa har ärvts från C.

'\nnn':	Tecken med oktalt värde nnn ('\012' == tecken 10 = line feed)
'\unnnn': Tecken med hex-värde nnnn ('\u03D0' == grekiskt beta)
'\b':	Backspace	(ASCII 8)
'\t':	Tab		(ASCII 9)
'\n':	Newline		(ASCII 10)
'\f':	Form feed	(ASCII 12)
'\r':	Carriage return	(ASCII 13)
'\'':	Apostrof
'\\':	Backslash (\)

För att konvertera något till en sträng används den statiska hjälpmetoden String.valueOf(), som är överlagrad (overloaded) för olika typer.

// Finns redan
public class String {
    static String valueOf(char c) { … }
    static String valueOf(int i) { … }
    static String valueOf(boolean b) { … }
    ...
}

// I vår egen kod
String str = String.valueOf(127);	// "127"

Notera att detta inte behövs när vi konkatenerar strängar, enligt ovan.

Det finns också särskilda metoder för att formatera strängar, t.ex. lägga till inledande nollor:


    String str = String.format("%05d", 42)   // "00042"
	

För att konvertera strängar till numeriska värden:

val = int("127")
val = float("218.52")
int	val = Integer.parseInt("127");
double	val = Double.parseDouble("218.52");

Göra något med collections / listor

Grunderna i Javas hantering av listor diskuteras under en föreläsning. Här ska vi kortfattat visa mer saker man kan göra med dem. Tanken är främst att sortera ut det som är viktigast inom kursen så att ni inte missar det. Därför hänvisar vi till Javas API-dokumentation för mer detaljer!

Variabeln list står så klart för en lista. Variabeln coll står för en godtycklig collection, vilket betyder att en lista fungerar men det kan även vara t.ex. en mängd (Set). Många av metodnamnen antas vara uppenbara. Vissa metoder finns inte i själva listan, utan är statiska hjälpmetoder i Collections-klassen.

För att skapa en lista används dessa klasser:

ArrayList	– klart vanligast, implementationen liknar föreläsningsexempel
LinkedList	– om man har specifika krav på snabb insättning/borttagning mitt i en lista
Stack		– gammal klass, används normalt inte
Vector		– gammal klass, används normalt inte

För att lägga till eller ändra element:

coll.add(element)
list.add(int index, E value)	– lägg till på given position; flytta resten av elementen
list.set(int index, E value)	– ersätt elementet på given position; storlek ändras ej
coll.addAll(coll2)		– lägg till element från en annan Collection, t.ex. en lista
list.addAll(int index, Collection c)
Collections.fill(list, element) – ersätter existerande objekt
Collections.replaceAll(list, element1, element2)

För att plocka ut specifika element eller delar:

list.get(index)
list.sublist(fromInclusive, toExclusive) – en del av listan
coll.toArray()			– en Object[ ] med samma element

För att testa innehållet / leta efter element:

coll.isEmpty()
coll.size()
coll.contains(Object obj)
coll1.equals(Collection c2)		– exakt samma element?
coll1.containsAll(Collection c2)	– alla element i coll2 finns i coll1?
list.indexOf(Object needle)
list.lastIndexOf(Object needle)
Collections.binarySearch(list, needle, ...) – binärsökning i sorterad lista

För att ta bort element:

coll.clear()				– tar bort alla element
coll.remove(Object)
list.remove(index)

För att ändra listan på andra sätt:

list.sort()			– sortera, om elementen är Comparable (se föreläsning)
list.sort(comparator)		– sortera, annars
Collections.shuffle(list)	– flytta om alla elementen i listan
Collections.rotate(list, distance) – rotera listan (flytta element d positioner)

Skapa en mappning, som dict i Python

Python stödjer dicts, en förkortning till dictionaries, som associerar nycklar med värden. Detta kan ses som en generalisering av arrayer eller listor, så att man inte bara kan ha heltal som index utan godtyckliga värden. Att använda detta är överkurs, men vi vill ändå peka er rätt om ni skulle ha användning av den sortens funktionalitet.

Java har så klart en motsvarighet, men precis som för listor finns det ingen specialsyntax för detta. Istället används gränssnittet Map och dess implementationer, och alla operationer sker via metodanrop. Därför blir det som vanligt lite mer att skriva i Java.


>>> dict = {'a': 45, 'b': 39, 'c': 19}




>>> dict['a']
45
>>> dict['d'] = 4711
>>> dict
{'a': 45, 'c': 19, 'b': 39, 'd': 4711}
>>> len(dict)
4

public static void main(String[] args) {
   Map<Character,Integer> dict = 
		    new HashMap<>();
   dict.put('a', 45);
   dict.put('b', 39);
   dict.put('c', 19);
   System.out.println(dict.get('a'));
   // 45
   dict.put('d', 4711);
   System.out.println(dict);
   // {a=45, b=39, c=19, d=4711}
   System.out.println(dict.size());
   // 4
}

Viktigt: En simpel implementation av Map skulle visserligen kunna slå upp ett värde genom att iterera över en lista ända till den hittar något som matchar, men detta skulle vara enormt ineffektivt. I en Map med en miljon mappningar (nyckel/värde) skulle vi i genomsnitt behöva söka genom en halv miljon, och testa om det var rätt nyckel, innan vi hittade rätt.

Därför måste nyckelklassen uppfylla vissa krav som Map-klassen kan förlita sig på. String, wrappertyper (Character, Integer med mera), enumtyper och många andra klasser uppfyller redan detta, så vill ni ha dessa som nycklar ska det automatiskt fungera. Om en egen klass ska användas som nyckel (första parametern i put), måste ni däremot tänka lite längre. Detta kommer egentligen i TDDD86, så vi kommer inte att förklara alla detaljer här!

TreeMap förlitar sig på att man kan jämföra objekt med varandra, och se vilket som är "mindre" respektive "större". Det ställer samma krav som sortering, som kommer att diskuteras på föreläsningarna: Nyckeltypen ska implementera Comparable, eller så får man ge TreeMap-objektet en Comparator. Då kan TreeMap lagra informationen i ett röd-svart träd (en struktur som ni får lära er mer om i senare kurser), och behöver bara titta på ett logaritmiskt antal nycklar för att hitta rätt (kanske i storleksordningen 20 nycklar vid en miljon mappningar).

HashMap använder sig istället av hashkoder. Detta kan ni läsa mer om på nätet, eller få förklarat i TDDD86.

Skapa menyer

Med menyer menar vi den här typen av pulldown-menyer:

I Swing (Javas GUI-system) är menyer komponenter precis som knappar och textfält, men de hanteras på ett annat sätt än de flesta komponenter. De "vanliga" komponenterna läggs till med fönstrets egen add()-metod, men då hamnar de i själva verket i en speciell panel som kallas content pane. Ovanför detta finns en särskild plats för en menyrad. Det gör att man inte behöver tänka särskilt på layouthantering för menyerna:

För att lägga till menyer skapar man först en menyrad. Därefter lägger man till menyer i menyraden, och menyval i menyerna. Se följande exempel:

final JMenuBar bar = new JMenuBar();

final JMenu file = new JMenu("File");
file.add(new JMenuItem("Open", 'O'));		// Understruket O
file.add(new JMenuItem("Save As", 'A'));
file.addSeparator();       			// Separator
file.add(new JMenuItem("Print"));
bar.add(file);

final JMenu edit = new JMenu("Edit");
edit.add(new JMenuItem("Cut"));
edit.add(new JMenuItem("Copy"));
edit.add(new JMenuItem("Paste"));
bar.add(edit);

final JMenu help = new JMenu("Help");
help.add(new JMenuItem("About WordProcessor 1.0"));
bar.add(Box.createHorizontalGlue()); // "Glue" fungerar som en fjäder, trycker "help" till höger
bar.add(help);

frame.setJMenuBar(bar);

Se även Java Tutorial om menyer respektive separatorer.

Händelsehantering för menyer hanteras på samma sätt som för knappar eller andra komponenter: En ActionListener anropas när ett menyval aktiveras, oavsett hur det sker (mus/trackpad, tangentbord, ...).

final JMenuItem open = new JMenuItem("Open");
fileMenu.add(open);
open.addActionListener(...);

Göra en layout med flikar

Med hjälp av JTabbedPane kan du göra en layout med flikar där användaren själv kan välja flik, enligt nedan. Se även informationen i Java Tutorial.

Om du istället vill att programmet ska byta innehåll i fönstret, utan att användaren kan göra det, går du till nästa avsnitt.

Byta mellan olika komponenter

Med hjälp av CardLayout kan du göra en layout där du lägger in flera komponenter, men där bara en av dem i taget är synlig. Till skillnad från JTabbedPane ovan är det inte användaren utan programmet som byter mellan "korten" i CardLayout. Detta kan t.ex. användas om du vill byta mellan att fönstret visar en introskärm, en spelkomponent, en highscorelista, och så vidare – utan att du behöver ta bort den gamla komponenten och sedan lägga till en ny.

CardLayout är svår att illustrera eftersom den helt enkelt visar en komponent i taget, utan att ha några synliga kontroller för att byta komponent. Läs informationen i Java Tutorial för att förstå hur det fungerar.

Fråga användaren något

Om man vill fråga användaren något i ett GUI-program, har Java standardiserat stöd för dialogrutor.

Mer information finns på Java Tutorial om dialogrutor.

Reagera på tangenttryckningar

I Java finns det två huvudsakliga sätt att få något att hända när man trycker (eller släpper) en tangent i ett GUI-program.

Det första sättet kan vara ganska knepigt, bland annat för att det alltid är en specifik GUI-komponent som har tangentbordsfokus och man måste se till att just den komponenten har en lyssnare. InputMap plus ActionMap ser mer komplicerade ut, men brukar leda till färre problem i praktiken. Därför går vi genom den metoden, och använder föreläsningens "ordbehandlarexempel" som bas.

I ordbehandlaren kan vi definiera en handling som kan utföras. Detta är en speciell typ av ActionListener. Vi kan få hjälp att skapa den genom att ärva från hjälpklassen AbstractAction, så behöver vi bara implementera själva actionPerformed-metoden precis som "vanligt".

public class WordProcessor20 {
     ...

    private class QuitAction extends AbstractAction {
        @Override public void actionPerformed(final ActionEvent e) {
            // Gör vad som behövs -- öppna dialogruta och fråga, ...
            System.exit(0);
        }
    }
}

Nu vill vi se till att en QuitAction anropas när någon trycker Alt-F4. Tangentkombinationen kallas en KeyStroke, och med en InputMap kopplas den till ett namn på en handling. Sedan används en ActionMap för att koppla namnet till koden, det vill säga objektet av typen QuitAction.

Den kopplingen gör vi så här (förklaringar efter):

public class WordProcessor20 {
    private JFrame frame;

    public WordProcessor20() {
        this.frame = new JFrame("Word Processor 1.0");
        JComponent pane = frame.getRootPane();

        final InputMap in = pane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
        in.put(KeyStroke.getKeyStroke("Alt F4"), "quit");

        final ActionMap act = pane.getActionMap();
        act.put("quit", new QuitAction());
    }

    private class QuitAction extends AbstractAction {
        @Override public void actionPerformed(final ActionEvent e) {
            System.exit(0);
        }
    }
}

Bra att veta:

Stänga ett fönster

För kursens skull behöver vi oftast inte stänga fönster: Det går ju bra att stoppa ett helt program från utvecklingsmiljön. Ändå kanske man vill ha den möjligheten i en del projekt.

För att ifrån koden stänga fönstret frame anropar man frame.dispose().

För att bestämma vad stäng-knappen gör använder man (så snart fönstret har skapats) frame.setDefaultCloseOperation(x), där x är...

Ibland kanske man vill bestämma själv vad stäng-knappen ska göra, t.ex. öppna en dialogruta för att fråga om användaren verkligen är säker. Då använder man WindowConstants.DO_NOTHING_ON_CLOSE och lägger sedan till en WindowListener som blir informerad när någon trycker på knappen. WindowListener ser ut såhär:

package java.awt.event;
public interface WindowListener extends EventListener {
    public void windowOpened(WindowEvent e);
    public void windowClosing(WindowEvent e);  // När stängknappen trycks!
    public void windowClosed(WindowEvent e);
    public void windowIconified(WindowEvent e);
    public void windowDeiconified(WindowEvent e);
    public void windowActivated(WindowEvent e);
    public void windowDeactivated(WindowEvent e);
} 

Man får alltså skapa en lyssnare som implementerar detta gränssnitt och som ger de flesta metoderna, som man inte är intresserad av, helt tomma implementationer. Alternativt kan man låta lyssnaren ärva från klassen WindowAdapter, som är en trivial hjälpklass som helt enkelt har tomma implementationer av alla metoder från WindowListener:

package java.awt.event;
public class WindowAdapter implements WindowListener {
    public void windowOpened(WindowEvent e) { }
    public void windowClosing(WindowEvent e) { }
    public void windowClosed(WindowEvent e) { }
    public void windowIconified(WindowEvent e) { }
    public void windowDeiconified(WindowEvent e) { }
    public void windowActivated(WindowEvent e) { }
    public void windowDeactivated(WindowEvent e) { }
}
      
I så fall kan resultatet se ut så här:
public class AskBeforeClosing {
    private JFrame frame;

    public AskBeforeClosing() {
        this.frame = new JFrame("Try to close me");
        frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
        frame.addWindowListener(new CloseButtonHandler());
    }

    private class CloseButtonHandler extends WindowAdapter {
        public void windowClosing(final WindowEvent e) {
            int answer = JOptionPane.showConfirmDialog
                    (frame, "Do you really want to quit?", "Quit?", JOptionPane.YES_NO_OPTION);

            if (answer == JOptionPane.YES_OPTION) {
                System.exit(0);  // Avsluta hela Java-processen
            }
        }
    }
}
      

Se även info om dialogrutor.

Rita ut en bitmap-bild

Java har flera olika sätt att hantera bitmapbilder (JPEG, PNG med mera), beroende på hur avancerad funktionalitet man behöver.

När Java introducerades, i mitten av 90-talet, fanns klassen java.awt.Image. Det finns numera också varianter som BufferedImage som låter oss manipulera bilder i minnet. Men Image anpassades till användning i applets, Java-program som kör på websidor, och till dåtidens långsamma uppkopplingar. Därför kan den läsa in en bildfil från nätet, och den läser alltid asynkront och inkrementellt så man kan rita ut den del som har kommit fram hittills. Det gör också att de här klasserna är lite komplicerade att använda.

Istället fokuserar vi på javax.swing.ImageIcon, som implementerar Icon-gränssnittet. Den är enklare att hantera och klarar allt man behöver för typiska kursprojekt. En ImageIcon kan läsas in från minnet, från disk eller via HTTP när man anger en URL:

new ImageIcon(byte[] data)
new ImageIcon(String filename)
new ImageIcon(URL location)

Användbara metoder inkluderar:

int getIconHeight()
int getIconWidth()
void paintIcon(Component c, Graphics g, int x, int y)

Vi börjar med ett enkelt exempel. Notera att en bild läses in en gång för alla, och en pekare lagras i ett fält i klassen. Vi vill ju inte läsa in bilden varje gång den ska ritas ut! I paintComponent() väljer vi att slå på antialiasing och sedan rita ut bilden på position (50,50). Resten av koden, i main(), öppnar helt enkelt ett fönster och lägger in en IconPainter01-komponent där.

public class IconPainter01 extends JComponent {
    final ImageIcon icon = new ImageIcon(ClassLoader.getSystemResource("/javatut.jpg"));

    public void paintComponent(final Graphics g) {
       final Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint(	RenderingHints.KEY_ANTIALIASING,
			 RenderingHints.VALUE_ANTIALIAS_ON);

        icon.paintIcon(this, g, 50, 50);
    }
    
    public static void main(String[] args) {
       final JFrame frame = new JFrame("Graphics2D Test");
       frame.setLayout(new GridLayout(1,1));
       frame.add(new IconPainter());
       frame.setSize(300, 300);
       frame.setVisible(true);
    }
}
	

Notera att vi använder ClassLoader.getSystemResource() för att hitta filen. Detta fungerar tillsammans med Javas stöd för resurser, som i princip är datafiler som distribueras tillsammans med ett program. På det sättet undviker vi att behöva ange var filerna finns i en specifik installation, och filerna kan hämtas även om hela programmet råkar vara packat i en JAR-fil (liknande ZIP-fil).

Vad blir resultatet då?

Oj, bilden fick inte plats! Det kan ju vara bra att kunna ändra storlek på den när den ritas ut, men det fanns inget storleksargument i paintIcon(). Istället använder Java en generell transform, något som ändrar på koordinaterna för allt som ritas ut. Exempel på att skala allt med en faktor 0.5 i både x-led och y-led:

public class IconPainter02 extends JComponent {
    final ImageIcon icon = new ImageIcon(ClassLoader.getSystemResource("/javatut.jpg"));

    public void paintComponent(final Graphics g) {
       final Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint(	RenderingHints.KEY_ANTIALIASING,
			 RenderingHints.VALUE_ANTIALIAS_ON);

        final AffineTransform old = g2d.getTransform();
        final AffineTransform at = AffineTransform.getScaleInstance(0.5, 0.5);
        g2d.setTransform(at);

        icon.paintIcon(this, g, 50, 50);

        g2d.setTransform(old);
    }
}

Notera ovan att vi sparar undan den gamla transformen för att återställa den efter vi ritar!

Originalet till vänster, resultatet till höger:

Ja, transformen skalar verkligen allt -- även koordinaterna (50,50) för övre högra hörnet! Här kan man istället välja att transformera i flera steg. Först kod, sedan förklaring.

public class IconPainter02 extends JComponent {
    final ImageIcon icon = new ImageIcon(ClassLoader.getSystemResource("/javatut.jpg"));

    public void paintComponent(final Graphics g) {
       final Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint(	RenderingHints.KEY_ANTIALIASING,
			 RenderingHints.VALUE_ANTIALIAS_ON);

        final AffineTransform old = g2d.getTransform();

        // Steg 4: Gör ingenting (skala faktor 1)
        final AffineTransform at = AffineTransform.getScaleInstance(1,1);  

        // Steg 3:  Flytta (i "oskalade" koordinater)
        at.translate(50,50);  

        // Steg 2:  Skala ner alla koordinater
        at.scale(0.5, 0.5);
        
        g2d.setTransform(at);
        
        // Steg 1:  Starta på (0,0)
        icon.paintIcon(this, g, 0, 0);   
        
        g2d.setTransform(old);
    }
}

Detta ser ju väldigt skumt ut. Man måste i princip läsa det baklänges för att förstå. Anta att bildens storlek är 300x400 pixlar.

Originalet till vänster, resultatet till höger:

Transformer kan också göra annat än att flytta och skala bilder. Till exempel kan vi rotera dem. Då får vi tänka på samma sätt i fråga om vad man gör först. Exempel:

...

final AffineTransform old = g2d.getTransform();
final AffineTransform at = AffineTransform.getScaleInstance(1,1);
at.translate(50,50);
at.scale(0.5, 0.5);
at.rotate(Math.PI/4);

g2d.setTransform(at);
icon.paintIcon(this, g, 0, 0);
g2d.setTransform(old);

...

För att förstå vad som händer "fuskar" vi genom att visa allting steg för steg i ett större "fuskfönster" där man även ser det som egentligen faller utanför, där koordinaterna (0,0) finns i skärningspunkten mellan de röda linjerna. Rotationen står sist men sker först. Sedan skalas alla koordinater, och till slut förflyttas resultatet.

Originalet till vänster, det verkliga resultatet till höger:

Måla med texturer

En textur fungerar som den rutiga färgen i Kalle Anka på julafton (om nu någon fortfarande ser på det). Man anger en bild som ska användas, och den repeteras så många gånger som behövs för att fylla ytan med färg. Ytan kan ha godtycklig form. Exempel:

TexturePaint paint = new TexturePaint(smiley,
                new Rectangle2D.Double(0, 0, smiley.getWidth(), smiley.getHeight()));
g2.setPaint(paint);
g2.fill(...polygon...)   

Måla genomskinligt

Ibland kanske man vill måla halvgenomskinligt – till exempel för att en bakgrundsbild delvis ska synas bakom statusinformation:

Detta görs på följande sätt:

g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.7));

Förenklat kan man säga att vi just nu målar med 30% genomskinlighet (1.0 - 0.7), men den här funktionaliteten är egentligen mycket mer generell: Den talar om precis hur man ska kombinera ett existerande objekt med ett nytt när det målas ut. Mer information finns i dokumentationen för AlphaComposite respektive det ännu mer generella gränssnittet Composite.

Måla med kantutjämning

Antialiasing (kantutjämning) handlar om olika metoder att minska effekten av en relativt låg upplösning, för att få kanterna på linjer och kurvor att se jämnare ut. Ett exempel är att använda gråskalor istället för bara svart och vitt när text eller linjer ritas ut:

I Java kan man slå på antialiasing separat för text och för andra former än text. Detta görs med hjälp av så kallade Rendering Hints.

g2.setRenderingHint(
    RenderingHints.KEY_TEXT_ANTIALIASING,
    RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

g2.setRenderingHint(
    RenderingHints.KEY_ANTIALIASING,
    RenderingHints.VALUE_ANTIALIAS_ON);

Andra rendering hints kan användas för att påverka andra delar av "utritningen".

Hitta typsnitt

För nästan alla projekt räcker det säkert med Javas standardtypsnitt:

Men i vissa fall kan man vilja använda andra typsnitt, och dessutom ta reda på vilka typsnitt som finns. Det gör man så här:

public class MyClass {
   
    public void checkFonts() {
        GraphicsEnvironment ge = 	GraphicsEnvironment.getLocalGraphicsEnvironment();
        String[] names = ge.getAvailableFontFamilyNames();
        ...
    }
}

Upptäcka kollisioner på skärmen

Om man skriver ett spel kan det hända att man behöver testa om två objekt kolliderar på skärmen. Ett av många sätt bygger på Javas geometriska Shape-klasser, som kan representera ungefärlig eller exakt form för objekt på skärmen. Man kan till exempel skapa en bounding box som omsluter ett objekt:

Denna kan skapas med hjälp av Area-klassen, som låter oss kombinera flera former till en ny form, t.ex. en cirkel och en rektangel. Sedan kan man kontrollera om två shapes, som motsvarar stationära eller rörliga objekt och som har fått koordinater motsvarande var de är på skärmen, kolliderar. Detta fungerar även om man inte använder shapes för att rita objekten.

Shape s1	= ...;
Rectangle2D s2	= new Rectangle2D.Double(...);
if (s1.intersects(s2)) {
    // Collision!
}

Utöver detta finns många klassbibliotek som kan underlätta spelprogrammering, men vi har inget specifikt sådant bibliotek att rekommendera.

Spela upp ett ljud

Java har ett relativt avancerat ljudhanteringssystem med stöd för uppspelning, sampling, ljudbehandling, mixning av ljudkanaler, med mera. Detta kommer vi inte att ta upp i någon större detalj i den här kursen, inte minst för att det är objektorientering och inte ljudhantering som är fokus. Det vi vill visa är istället den enklaste möjliga ljudhanteringen, där man kan starta ett ljud eller loopa det ett visst antal gånger.

public class SoundPlayer2 {
    private final Clip beepSound;

    public SoundPlayer2() throws ... {
        URL url = SoundPlayer2.class.getResource("/sounds/beep-01a.mp3");
        if (url == null) { /* Felhantering eller kasta ett eget exception! */ }

        beepSound = AudioSystem.getClip();
        beepSound.open(AudioSystem.getAudioInputStream(url));
    }

    public void beepOnce() {	beepSound.start();  }
    public void beepTwice() {	beepSound.loop(1); }
}
Den intresserade kan se mer på Java Tutorial och efterföljande sidor.

Använda filer

Filhantering finns inte med på föreläsningarna, men i kursböcker (t.ex. Java Tutorial) och i gamla föreläsningsbilder.

Utöver detta vill vi också nämna klassen java.nio.file.Files, som innehåller en hel del smått och gott i fråga om filhantering. Bland annat finns readString() för att läsa in en hel fil som en sträng, readAllLines() för att få den som en lista av rader, och readAllBytes() för att få den som en byte-array.

Lagra banor, bräden och liknande

I många projekt används fördefinierade data av olika slag – inte bara bilder och ljud, utan banor, startpositioner för spelbräden, och många andra sorters information.

När det gäller bilder finns det ju färdiga lagringsformat, som .PNG-filer, och färdig kod som läser in dem. Där blir det enklast att lagra dem i filer och läsa in dem med hjälp av klasser som redan finns, t.ex. ImageIcon.

När det gäller andra data kan det istället vara enklast att göra fel – i alla fall i början. Vill man bygga upp olika banor kan man t.ex. hårdkoda dem:

	  world.place(10, 20, new Rock());
	  world.place(11, 20, new Water());
	  ...
	

Här rekommenderar vi istället att man använder sig av Gson för att lagra informationen i en fil. Då skiljer vi på data (som lätt kan ändras i en fil utan omkompilering) och kod (som definierar beteenden och som kan exekveras). Gson gör bland annat att vi inte själva behöver skriva koden som läser ett visst filformat, och beskrivs närmare i uppgiften "Spara på fil" i Tetris.

Spara eller skicka hela objekt

Java har grundläggande klasser för att skriva binärdata eller textsträngar till en fil och läsa tillbaka detta till minnet. Om vi vill använda samma grundläggande klasser och metoder för att spara mer komplex information, till exempel en lista med highscores i ett spel, måste vi själva iterera över listorna, se till att alla "delar" av en highscore skrivs respektive läses, och så vidare.

Java låter oss också skriva ner en representation av ett helt objekt till en fil (eller någon annan destination, till exempel en databas). Alla objekt det i sin tur pekar på skrivs också ner, eftersom objektet annars inte skulle "peka rätt" när det lästes tillbaka. Skriver man en lista till en fil skickar Java alltså även med objekten som listan innehåller, precis som man skulle vänta sig.

Detta kallas serialisering, eftersom man har ett antal objekt som ligger placerade på olika platser i minnet och pekar på varandra, och man vill spara tillståndet i alla dessa objekt som en serie av bytes som kan sparas undan någonstans. Detta kan också kallas marshalling (generellt), eller pickling i Python (sylta / marinera). Serialisering finns i många språk, även sådana som inte är objektorienterade: Ett generellt sätt att serialisera data är användbart för alla sorters sammansatta datatyper.

Serialisering i Java har en del problem. För denna kurs rekommenderar vi att man främst använder sig av andra tekniker, t.ex. Gson som sparar ner objekt i JSON-format (se Tetris-labben, delen för betyg 4.

I grunden är serialisering ett enkelt begrepp, men oavsett implementation eller språk finns det en hel del specialfall som man kan behöva känna till. Därför är serialisering överkurs. Vi rekommenderar att man bara använder detta om man tror vinsten är tillräckligt stor, dvs. om man ska skicka "tillräckligt" komplicerad information, och man förstår begreppen nedan. Annars kanske ett enkelt textprotokoll är enklare att förstå och debugga.

Tänk också på att serialisering normalt ger en låsning till ett specifikt språk. Som vi ska se i slutet kan det till och med vara så att en ny version av samma program inte kan läsa en fil som serialiserades med en äldre version.

Krav på objekt och klasser

För att man ska kunna spara ner ett objekt på detta sätt ställs flera krav, som kan summeras och approximeras till "objekten och deras fält måste implementera Serializable".

Cirkulära referenser; serialisera samma objekt flera gånger

Java klarar att hantera cirkulära referenser. Till exempel kan vi ha nodobjekt där varje nod pekar på sin förälder och varje förälder pekar på alla sina barn. Man kunde tänka sig att Java serialiserade föräldern, sedan första barnet, men då måste den serialisera föräldern, och sedan första barnet, som ju har en förälder... i all oändlighet. Istället håller den reda på alla objekt den har serialiserat och nästa gång skriver den bara ett ID som pekar tillbaka på det "gamla" objektet.

Så om man ändrar ett objekt och serialiserar det en gång till i samma ström, får man inte riktigt det väntade resultatet:

List list = new ArrayList();
oos.write(list);		// Writes object ID + entire list
list.add("Another element");
oos.write(list);		// Writes the object ID...

Mottagaren får då se två kopior av objektet som det såg ut första gången. Vill man skicka objektets nya tillstånd måste man nollställa strömmen först:

List list = new ArrayList();
oos.write(list);		// Writes object ID + entire list
list.add("Another element");
oos.reset();
oos.write(list);		// Writes the object ID...

Läsa gamla serialiserade data

Om man ändrar i en klass kan det hända att objekt som serialiserats med en äldre version av klassen inte längre kan läsas.

Vissa ändringar är helt förbjudna:

Andra kan vara tillåtna:

Men även dessa ändringar tillåts bara om man deklarerar samma serialiseringsversion. Per default beräknas detta med avseende på vissa egenskaper hos klassen på ett sätt som gör att nästan inga ändringar tillåts. Vill man själv tillåta framtida ändringar måste man vara förutseende och deklarera en egen version. Detta är ett statiskt fält i den klass som är serialiserbar, och heter exakt serialVersionUID:

public class Pair implements Serializable {
    private Object	first;
    private Object	second;
    private transient int	hashCodeCache;
    private final static long serialVersionUID = 1;  // for example

    Pair(Object first, Object second) {
        this.first = first;
        this.second = second;
    }
}

Om man i framtiden gör inkompatibla ändringar behöver man också ändra värdet på serialVersionUID, så att serialiseringen vägrar ta emot "gamla" objekt.

Felhantering

Många olika fel kan signaleras med hjälp av olika exception-typer. Ovan ignorerade vi allt detta för att förenkla exemple. Fel som kan uppstå inkluderar:

ClassNotFoundException		– received an object of a non-existing class
InvalidClassException
StreamCorruptedException	– bad control information in the stream
OptionalDataException		– primitive data found instead of objects
NotSerializableException	– an object was not Serializable
IOException			– the usual Input/Output related exceptions

Se även...

Slut på uppgifter och lösningar!

Föreslå gärna egna, om du tycker det finns information som saknas och som skulle hjälpa till i kursen.