Programsspråks prestanda

Att göra jämförelser i prestanda mellan programspråk är inte lätt för språk kan vara olika effektiva på att lösa olika typer av uppgifter. Dessutom är ju prestanda inte det enda kriteriet för vad som är ett bra språk, inte ens det viktigaste nuförtiden när datorer är så snabba.

Men man kan ändå få en uppfattning hur det ligger, t.ex. här:

The Computer Language Benchmarks Game

Sammansatta satser, forts

if-satser, forts

Villkorliga exekveringar kan också användas inuti uttryck med den så kallade ternära operatorn (treställiga, ternary).

e1 ? e2 : e3

e1 är villkoret, e2 är then-villkoret som beräknas om villkoret är sant och e3 motsvarande för falskt. Notera att här måste en else-del finnas, för, i likhet med if-uttryck i Haskell, som måste i allmänhet beräkningen resultera i ett värde.

for-loopar

Loop har också en svensk motsvarighet, slinga, men kommer ej användas här.

for-loopar utför en sats eller ett block av satser upprepade gånger.

for (e1; e2; e3) s

for (e1; e2; e3) {
  s1
  s2
  s3
  ...
}

e1 är en initiering. Det är ett uttryck eller en variabeldeklaration. Variabler som deklareras i initieringen av en for-loop är bara i scope i denna.

e2 är ett villkor, ett boolskt uttryck. Detta beräknas efter initieringen. Är det sant så utförs satsen/satserna i loopen en gång.

e3 är ett uttryck som beräknas varje gång efter att satsen i loopen exekverats.

Därefter beräknas villkoret igen och är det sant fortfarande så utförs loopen en gång till. Så fortsätter det tills villkoret är falskt.

Exempel:

System.out.println("Jag kan räkna till 10:")
for (int i = 1; i <= 10; i++) {
  System.out.println(i);
}

Exempel:

int n = 0;
for (int i = 1; i <= 10; i++) {
  n += i;
}
System.out.println(n);

e3 används typiskt för att öka en variabel som utgör indexet för loopen, d.v.s. håller reda på hur många gånger den upprepats.

e1, e2 och e3 kan alla utelämnas. Om inget e2 finns så motsvarar villkoret implicit true, d.v.s. loopen börjar hela tiden om. Det finns andra sätt att avsluta loopen. Annars vore detta meningslöst.

Ett sätt att avsluta en loop annat än när villkoret beräknas till falskt är att använda satsen

break;

Denna sats innebär att loopen omedelbart avslutas och exekveringen fortsätter med satsen efter loopen.

while-loopar

while-loopar fungerar ungefär som for-loopar.

while (e) s

while (e) {
  s1
  s2
  s3
  ...
}

De innebär samma sak som

for (;e;) s

d.v.s en for-loop utan initierings- och inkrementeringsuttryck.

En variant på while-loopar är do-while-loopar:

do {
  s1
  s2
  s3
} while (e);

Den fungerar som while-loopar förutom att loopen exekveras en gång först utan att villkoret beräknas.

x = 0;
while (x > 0) {
  System.out.println("1");
}
do {
  System.out.println("2");
} while (x > 0);

break; kan även användas i båda typerna av while-loopar.

Exempel:

int talet = java.util.concurrent.ThreadLocalRandom.current().nextInt(1, 11);

System.out.println("Gissa på ett tal mellan 1 och 10.");
int gissning = Integer.parseInt(System.console().readLine());

while (gissning != talet) {
  System.out.println("Fel. Gissa igen.");
  gissning = Integer.parseInt(System.console().readLine());
}

System.out.println("Rätt!");

Det går att utföra samma sak på många sätt, t.ex. genom att använda for-loop, if-sats i loopen eller break; Följande variant eliminerar den ena förekomsten av raden som läser in ett tal, vilket är en fördel.

int talet = java.util.concurrent.ThreadLocalRandom.current().nextInt(1, 11);

System.out.println("Gissa på ett tal mellan 1 och 10.");

for (;;) {
  int gissning = Integer.parseInt(System.console().readLine());
  if (gissning == talet) break;    
  System.out.println("Fel. Gissa igen.");
}

System.out.println("Rätt!");

Istället för for (;;) kan man skriva while (true).

Nästlade loopar

Loopar kan vara nästlade.

Exempel:

for (int i  = 1; i <= 9; i++) {
  for (int j = 1; j <= 9; j++) {
    System.out.format("%3d", i * j);
  }
  System.out.println("");
}

Vilka värden en loops index ska anta kan bestämmas av aktuellt värde på en yttre loops index. Exempelvis kan man byta 9 mot i i villkoret för den inre loopen i exemplet ovan.

Förvalda initialvärden

Förvalda initialvärden har ingen betydelse vid deklarering av s.k. lokala variabler, vilket det har rört sig om i alla exempel hittills. För sådana kräver kompilatorn att tilldelning sker innan man läser värdet. Detta behöver dock inte ske vid deklarationen.

Följande ger kompileringsfel:

int x;
System.out.println(x);

Men detta går bra:

int x;
if (cond)
  x = 2;
else
  x = -2;
System.out.println(x);

Arrayer

Det svenska ordet för array är fält, men det kan också betyda andra saker. Här kommer en försvenskad variant av array användas.

Arrayer motsvarar på vissa sätt listor i Haskell, men de lagras annorlunda. Medan listor i Haskell lagras som en kedja av länkade noder så ligger elementen i en array precis efter varandra i minnet. Därför går det att komma åt ett element på en viss plats (med ett visst index) snabbt.

Liksom i Haskell-listor så består arrayer av en ordnad sekvens av element av samma typ. En deklaration för en array av int:s ser ut så här:

int[] xs;

En array av längd 25 skapas så här:

xs = new int[25];

Man kan också göra detta i ett steg med initiering:

int[] xs = new int[25];

Elementen i den nya arrayen får alla typens förvalda värde (0 för int) och dessa kan läsas utan att först explicit tilldelas ett värde.

Man kan ta reda på en arrays längd:

xs.length

Man kan läsa och ändra element i en array med samma syntax:

n = xs[12];
xs[9] = n;

Indexen i en array börjar på 0 och går därför till k − 1, där k är arrayens längd. Om man anger ett negativt eller för stort index så får man ett run-time fel.

Längden på en array kan inte förändras, men man kan ersätta en array med en annan.

int[] xs = new int[25];
xs[1] = 37;
xs = new int[2];
System.out.println(xs.length);
System.out.println(xs[1]);

Istället för att skapa en array med alla element 0 (för int) så kan man ange vilka element den ska innehålla.

int[] xs = {1,2,4,8,16};

Detta går bara att göra vid deklarationen, ej vid tilldelningar senare.

Exempel:

int[] xs = {1,2,4,8,16};

for (int i = 0; i < xs.length; i++) {
  System.out.print(xs[i] + " ");
}
System.out.println();

for (int i = 0; i < xs.length / 2; i++) {
  int tmp = xs[i];
  xs[i] = xs[xs.length - 1 - i];
  xs[xs.length - 1 - i] = tmp;
}

for (int i = 0; i < xs.length; i++) {
  System.out.print(xs[i] + " ");
}
System.out.println();

Flerdimensionella arrayer

Man kan skapa flerdimensionella arrayer:

int[][] x = {{1, 7}, {7, 6}, {0, 3}};
int[][] y = {{8, 3}, {2, 5}, {1, 7}};
int[][] z = new int[x.length][x[0].length];

System.out.println("Två " + x.length + "x" + x[0].length + " arrayer");

for (int i = 0; i < x.length; i++) {
 for (int j = 0; j < x[0].length; j++) {
  z[i][j] = x[i][j] + y[i][j];
 }
}    

Notera att x[i].length blir samma för alla index i, men att detta alltid är fallet. Man kan senare göra t.ex. så här:

x[1] = new int[10];

Rätt sätt att betrakta flerdimensionella arrayer är att se dem som arrayer av arrayer.

Definiera metoder

För att strukturera återanvända kod är det, liksom i alla programspråk, essentiellt att kunna definiera metoder.

Det gör man så här:

public static returtyp metodnamn(arg1typ paramnamn1, arg2typ paramnamn2) {
  lista av satser
}

Sedan anropar man metoden, som vi tidigare sett, så här:

metodnamn(arg1uttryck, arg2uttryck)

I listan av satser som definierar vad metoden ska göra använder man

return returVärdeUttryck;

för att ange vad metoden returnerar.

Exempel:

public static int pow(int x, int y) {
  int res = 1;
  for (; y > 0; y--) res *= x;
  return res;
}

Return-satser kan förekomma var som helst och det kan finnas flera return-satser i samma metod.

public static int abs(int x) {
  if (x < 0) {
    return -x;
  } else {
    return x;
  }
}

Metoder utan retur-värden

Metoden kan deklareras att inte returnera något värde. Man anger då retur-typen void

public static void printNumber(int x) {
  System.out.println(x);
}

För sådana metoden anges inget värde i return-satser, utan man skriver bara

return;

Man behöver inte heller ha något return-sats. Om ekveringen når slutet av sats-sekvensen så avslutas metoden helt enkelt.

Arrayer som argument

Man kan skicka arrayer som argument. Detta skiljer sig från primitiva typer (såsom int) genom att det inte är värdet (d.v.s. alla elementens värde) som skickas till metoden utan en referens. Det är bra för stora arrayer på det sättet att det går snabbare. Det har också den effekten att ändringar som metoden gör i arrayen finns kvar när exekveringen av den anropande metoden fortsätter. Här gäller det att ha tungan rätt i mun. Imperativ programmering kryllar av sidoeffekter av det här slaget, men det kan leda till svårhittade fel.

Exempel:

public static void reverse(int[] xs) {
  for (int i = 0; i < xs.length / 2; i++) {
    int tmp = xs[i];
    xs[i] = xs[xs.length - 1 - i];
    xs[xs.length - 1 - i] = tmp;
  }
}

public static void print(int[] xs) {
  for (int i = 0; i < xs.length; i++) {
    System.out.print(xs[i] + " ");
  }
  System.out.println();   
}

public static void main(String[] args) {
  int[] x = {1,2,3,4,5,6};
  print(x);
  reverse(x);
  print(x);
}

Objekt

Förutom primitiva värden och arrayer vid i Java en sorts värden som kallas objekt. Ett objekts typ är en klass, class.

Ett objekt innehåller data, en uppsättning värden av andra typer, lite som en tuple i Haskell. Ett objekt har också ett antal metoder kopplat till sig, metoder som använder och förändrar objektets värden. Det är klassen som bestämmer vilken data och vilka associerade metoder som ska finnas. Jämför Haskells klassystem där man kan associera funktioner till en klass av typer.

Klasser i Java är, liksom arrayer, referenstyper, d.v.s. hela objektets innehåll skickas inte med vid ett metodanrop utan bara en referens till objektet. Därför kan även objekt bli modifierade av den anropade metoden.

Liksom för arrayer anropar man new när man vill skapa ett nytt objekt i ett initialt tillstånd. Man använder punkt för att komma åt ett metod (eller ett värde) i ett objekt.

obj.metod(...)

Strängar

Strängar är en typ av objekt. Klassen heter String.

Strängar har literaler och är på det viset unika bland klasser:

"en sträng"

Skapa en ny, tom sträng:

String str = new String();

Slå ihop strängar.

String spc = " ";
System.out.println("en" + spc + "sträng");

Sträng-klassen har många metoder, t.ex.

String str = "åtta sju";
str.length()         --> 8
str.charAt(3)        --> 'a'
str.subString(2, 6)  --> "ta s"

Notera att length har parentes här, till skillnad från för arrayer.

För att testa likhet mellan strängar kan man inte använda ==. Man kan använda metoden equals.

str == "åtta sju"         --> false
str.equals("åtta sju")    --> true

Man kan använda reguljära uttryck för att till exempel dela upp en sträng i delar:

String str = "12,25,3,0,128";
String[] ss = str.split(",");
for (int i = 0; i < ss.length; i++) {
  System.out.println(ss[i]);
}

-->
12
25
3
0
128

Man kan skapa en sträng från en array av bytes. Låt bs vara en sådan array, d.v.s. av typen byte[]. Då kan man skriva

String str = new String(bs);

Detta omvandlar bytesekvensen till tecken enligt plattformens förvalda teckenuppsättning.

Läsa in en fil

Det finns en motsvarighet till Haskells readFile som är ett lätt sätt att läsa in hela innehållet i en fil. Om filnamn är sträng så gör

byte[] bs = java.nio.file.Files.readAllBytes(java.nio.file.Paths.get(filnamn));

att innehållet i filen med detta namn läses in till en array av bytes. java.nio.file.Paths.get omvandlar en sträng till en abstrakt representation av en sökväg i filsystemet, ett objekt av klassen Path. java.nio.file.Files.readAllBytes läser sedan in innehållet i filen med denna sökväg.

Om något går snett när detta ska utföras så kastas ett undantag (exception). Vi kommer behandla undantag senare och även gå igenom läsning och skrivning till filer och IO i allmänhet mer noggrant.

Programargument

Argumenten som man anger på kommandoraden när man startar ett program kommer man i Haskell åt med funktionen getArgs. I Java finns de med som ett argument till main-metoden:

public static void main(String[] args) {
  ...
}

Argumenten finns i varsin sträng. Första argumentet är args[0], andra args[1] o.s.v. Antalet argument är args.length.

Ställa in exekveringsmiljön i IDE

För laboration 1 behöver man kunna ange ett argument när programmet körs. Det är lätt när man kompilerar och kör program i kommandotolken. Om man kör programmet från en IDE så får man ställa in detta innan man startar körningen. I Eclipse finns “Run Configurations…” under menyn “Run”. Där kan man ställa in programargument under fliken “Arguments”. Motsvarande funktion finns i övriga IDEer.

I labb 1 är argumentet en fil som ska hittas. Antingen kan man ange den absoluta sökvägen till filen. Annars kan man chansa på att programmet när det körs har den katalogen som filen finns i som sin arbetskatalog (aktuella katalog). Om så inte är fallet går även detta att ställa in på samma flik som programargument.

Paket och import

För att kunna organisera stora projekt kan man utanpå klassystemet använda en trädstruktur av s.k. paket. Ovan förekommer metoden

java.nio.file.Files.readAllBytes

Denna är en del av klassen Files som ligger i paketet file som i sin tur finns i paketet nio som finns i toppnivåpaketet java.

En klass behöver inte tillhöra något paket.

Testprogrammet i labb 1 försöker anropa metoden sort i klassen Lab1, d.v.s. Lab1.sort. Inget paket anges så därför behöver klassen Lab1 befinna sig på rotnivå i pakethierarkin. IDEer kan automatiskt placera klasser i paket. Ha detta i åtanka om du stöter på problem när du ska köra testprogrammet.

För att slippa skriva ut vägen i paketstrukturen till metoder om och om igen i koden kan man i början av filen tala om att ett vissa pakets namespace ska vara tillgängligt. För att kunna hänvisa till metoden readAllBytes utan att ange paketvägen skriver man:

import java.nio.file.Files;

Det räcker då att skriva

Files.readAllBytes

Man kan också skriva

import java.nio.file.*;

så blir alla klasser i detta paket (dock ej klasser i eventuella underpaket) tillgängliga.

Javas API

Vi har redan använt ett par metoden i Javas API och sett att det är organiserat med en hierarki av paket och klasser.

Specifikationen för API:et finns online och är något man använder mycket.

Java 8 SE API specification

Där finns paketstrukturen och också alla klasser i bokstavsordning. Söker man däremot efter en metod som man känner till namnet på men har glömt vilken klass den finns i så kan man behöva använda en universell sökmotor. Bland sökmotorns resultat brukar träffar i Oracles API-dokumentation dyka upp.

Kommentarer och Javadoc

Kommenterar i Java:

x += 2;  // öka x med 2

// innebär kommentar till slutet av raden, som -- i Haskell.

/* Kommentar som löper
   över flera rader.
*/

/* inleder och */ avslutar en kommentar som löper över flera rader, som {- och -} i Haskell.

Javadoc är ett verktyg som låter dig automatiskt skapa html-dokumentation för din kod om du skriver kommenterarer till klasser och metoder på ett speciellt format.

Exempel:

/**
 * Returns x^y.
 * <p>
 * This method compute the power function for integers.
 *
 * @param  x  the base value
 * @param  y  the exponent
 * @return    x raised by y
 */
public static int pow(int x, int y) {
  ...
}

Kommentaren består av en beskrivning och en lista av block tags. Se How to Write Doc Comments for the Javadoc Tool för mer info.

Man kör t.ex.

javadoc MinKlass.java

för att skapa html-dokumentationen.