Objektorienterad programmering – bakgrund

Från början var filsystem bara en lista av filer, inte en träd-struktur med kataloger och underkataloger. När mjukvaruindustrin mognade stod man inför liknande problem som ledde till filsystem med trädstruktur.

Programmen började bli så stora att struktur och organisering var ett problem som drog ner produktiviteten. Objektorienterad programmering är ett sätt att råda bot på detta som ledde till fundamentala ändringar i programspråk. Objektorienterad programmering är ett dominerande paradigm för allt annat än små program i industrin.

Introducerades kring 1960, men började dominera på 90-talet, mycket tack vare C++ som dök upp 1983 och är en utveckling av det populära språket C.

Medan funktioner är utgångspunkten i många språk, såsom Haskell och C, är objekt det i de vanligaste objekt orienterade språken. För att skriva ett program måste man börja med att definiera ett objekt. Objekt är entiteter som samlar variabler och funktioner som hör samman. Objekt kallas vanligtvis klasser, class.

Java

I den kursen kommer ni lära er språket Java, som är objektorienterat. Java är ett populärt språk, se t.ex.:

Det är stor chans att stöta på Java i arbetslivet. Android-appar utvecklas mestadels i Java.

Java introducerades 1995.

Vad utmärker Java?

Rent funktionellt Imperativt Objektorienterat Aut. minneshantering

Haskell

X

X

Java

X

X

X

C#

X

X

X

C++

X

X

C

X

Det faktum att Java är ett imperativt språk, i kontrast till Haskell, kommer ha en central plats i kursen vid sidan om de objektorienterade aspekterna. Objektorienterad programmering kommer vi återkomma till senare i kursen, för att först fokusera på grundläggande saker i Java och sådant relaterat till imperativ programmering.

Java är ett statiskt typat språk, till skillnad från t.ex. Python, som är dynamiskt typat. Python är liksom Java imperativt.

Javas infrastruktur

Java är mycket plattformsoberoende i och med att kompilatorn producerar en slags högnivåmaskinkod som kallas Java Bytecode. När ett java-program exekveras så tolkas denna bytecode av ett run-time system, JVM (Java Virtual Machine). Bytecoden är oberoende av plattform och kan därför köras på alla datorer som har JRE (Java Runtime Environment) installerat. JRE består av JVM och Javas bibliotek kompilerat.

När det gäller Haskell får man en stor körbar fil med maskinkod som har runtime-systemet inbakat. För Java får man en liten fil med bytecode och för att köra programmet så startar man JVM som innehåller runtime-systemet och tolkar bytecoden.

Ett första Java-program

Hello-world i Haskell:

main = putStrLn "Hello world!"

Hello-world i Java:

class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello world!");
  }
}

Lägg denna kod i HelloWorld.java.

Kompilera programmet:

$ javac HelloWorld.java

Detta skapar HelloWorld.class.

Kör programmet:

$ java HelloWorld

Detta startar JVM för HelloWorld.class.

Installera JDK

Den dominerande Java-implementationen är Oracles. Java SE (Standard Edition). Denna är till för PC, Mac och Linux. JRE innehåller bara körningsmiljön, så den är bara användbar för att köra färdiga Java-program. JDK (Java Development Kit) är den ni ska installera. Denna innehåller kompilatorn och andra verkyg för att producera program. JDK inkluderar JRE. Aktuell version är 8 (1_8_x), men version 7 (1_7_x) fungerar också bra i denna kurs.

Utvecklingsmiljöer

Man kan få mycket hjälp och öka produktiviteten mycket genom att använda en IDE (Integrated Development Environment). Där kan nu editera text, men du får också hjälp med att:

Det finns flera stora IDE:er att välja mellan:

Labbhandledarna i kursen fokuserar på Eclipse när det gäller på att kunna svara på mer intrikata frågor.

Imperativ programmering

Till skillnad från rena funktionella språk så kan man säga att all beräkning i imperativa språk sker i IO, d.v.s. att man ger instruktioner om olika saker som ska ske i en viss ordning och att dessa saker har sidoeffekter.

Av denna anledning är också variabler om inget annat anges muterbara, likt IORefs i Haskell.

Imperativ programmering utgör på detta sätt en lägre grad av abstrahering från hur maskinkod och datorer fungerar.

Satser

I imperativa språk finns en syntaktisk och semantisk kategori (vid sidan om uttryck) som inte finns i Haskell, nämligen satser (statements). Satser innehåller i sin tur uttryck. De flesta satser i Java (och C, C++, C# ..) slutar med ett semi-kolon. Satser motsvarar uttryck av typen IO (). Satser har ingen typ. Definitionen av en funktion i Haskell är ett uttryck. Definitionen av en funktion i Java är en sekvens av satser.

Haskell:

do putStrLn $ "x = " ++ x
   putStrLn $ "y = " ++ y

eller

putStrLn ("x = " ++ x) >> putStrLn ("y = " ++ y)

Java:

System.out.println("x = " + x);
System.out.println("y = " + y);

Vi går nu igenom de grundläggande elementera satserna.

Uttryckssatser

Uttryckssatser (expression statements) är helt enkelt ett uttryck följt av semikolon. Det är den elementera formen av sats som innebär att det som beskrivs av uttrycket ska utföras, ovillkorligen och en gång. Notera att det inte bara är på sats-nivå som programkod har sidoeffekter utan att det mesta som sker beskrivs i uttryck. Satserna i exemplen ovan är alla uttryckssatser.

Beräkningen av ett uttryck kan resultera i ett värde, men detta ignoreras för uttryckssatser.

Variabeldeklarationer

För variabler i Java, liksom för IORefs i Haskell, finns det en skede när det går från att inte finnas till att finnas.

Variabelnamn har också ett scope, liksom i alla normala språk, inom vilket man kan referera till det.

Java är statiskt typat och till skillnad från i Haskell så måste man ange variablers (och funktioners) typ. Kompilatorn räknar inte ut det åt dig.

Typen int är heltal och motsvarar ungefär Int i Haskell.

En variabeldeklaration kan inkludera en initiering, d.v.s. man anger vad variabelns initiala värde ska vara.

int x = 5;

Utan initiering ser det ut så här:

int x;

Om initiering saknas så sätts värdet till ett, för typen, förvalt. För int är värdet 0.

Namn i Java ska, liksom i Haskell, börja med en bokstav och kan sedan innehålla siffror och underscore (_). Men de kan däremot inte innehålla apostrof (').

Typer

Vi går här igenom de primitiva typerna. Det finns även andra, som vi väntar med.

Notera att alla namn börjar med liten bokstav.

Heltalen har tecken (signed) d.v.s. kan vara negativa. De lagras på två-komplementformat.

Flyttalen representeras enligt IEEE 754.

Förvalt värde för alla heltal och flyttal är 0.

Förvalda initialvärden har dock 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;
x = 2;
System.out.println(x);

Se Oracles dokumentation för mer info.

Uttryck

Uttryck är uppbyggda på samma sätt som i Haskell (och i princip alla andra språk). Det finns unära och binära (ettställiga och tvåställiga) operationer som uttrycks med prefix-, infix- och suffix-operatorer. Operatorer har olika precedence och associativitet som avgör hur de grupperas. Paranteser kan användas för att explicit tala om hur grupperingen ska gå till.

Här är de grundläggande uttrycksformerna och operatorerna som har direkt motsvarighet i Haskell:

För precedence-ordning för operatorerna se Oracles dok. Ordningen känns igen från Haskell och andra språk.

Uttryck med sidoeffekter på variabelvärden

I Java och andra imperativa språk kan kan uttryck ha sidoeffekter.

Det är naturligt att tänka att tilldelningar alltid sker på topp-nivå i en uttryckssats:

x = 22;

Men de kan också förekomma i deluttryck. Resultatet av en tilldelning är värdet på uttrycket på höger sida av likhetstecknet.

int x = 2;
System.out.println(x = 5);

Något som är ganska användbart är tilldelning av flera variabler:

x = y = 0;

Annan användning av tilldelningar gör koden ofta svårtförståelig:

int x = 2, y = 30;
System.out.println((x = 5) * (32 - y) + x);

Vad blir resultatet? För att avgöra det måste man veta i vilken ordning olika delar av ett uttryck exekveras. Detta är väldefinierat. Det måste det vara eftersom uttrycksdelar kan ha sidoeffekter. Uttryck beräknas från vänster till höger.

Även om man vet det är det ofta ingen bra idé att skriva uttryck med lokala sidoeffekter. Det är lätt att glömma bort att en variabel har förändrats tidigare i uttrycket om det är stort. Bättre är att ge tilldelningar en mer framskjuten position, som en egen sats.

Förändringar kan också användas i deluttryck. Resultatet är värdet efter förändringen.

int x = 2;
System.out.println(x += 5);

Det finns också fyra ett-ställiga operationerna som har sidoeffekter och används mycket, även annat än som egna satser.

    int x = 10, y = 10;
    System.out.println(x++ + " " + ++y);
    System.out.println(x + " " + y);

Funktionsanrop

Funktioner kallas i Java metoder och syntaxen ser ut så här:

f(arg1, arg2)

I Haskell skriver man

f arg1 arg2

En metod som inte tar några argument anropar man så här:

f()

Metodanrop kan förstås också ha sidoeffekter, eftersom Java inte är ett rent språk. Och inte bara effekter på variabelvärden utan också IO.

Metoder kan returnera värdet av en viss typ precis som i Haskell. Resultatet av uttrycket som utgörs av metodanropet motsvarar då det värde som metoden returnerar. Metoder kan också var sådana att de inte returnerar något värde. Sådana kallas procedurer istället för funktioner i vissa språk.

Varje argument i anropet är ett uttryck.

Sammansatta satser

Satser som i sig består av satser. Dessa gör programkontroll möjlig, d.v.s. att utföra programdelar villkorligt och upprepa programdelar flera gånger.

if-satser

If-satser kan ha en else-del. Men else-delen kan också saknas, till skillnad från i Haskell. If med en else-del ser ut så här:

if (e) s1 else s2

där e är ett booleskt uttryck, villkorsuttrycket, s1 är den sats som exekveras om villkoret är sant och s2 den sats som exekveras om det är falskt. Liksom if-satser i IO i Haskell kan if-satsen i Java vara en sats i en sekvens av satser, så programexekveringen fortsätter i allmänhet efter att then- eller else-satsen utförts med nästa sats.

if (x > 0) x--; else x++;
y = x * x;

Eftersom språket är imperativt så är en if-sats utan else-del meningsfull. Om villkoret är sant så utför then-satsen och fortsätt sedan på nästa sats. Om villkoret är falskt så fortsätt på nästa sats direkt. Ett exempel:

if (x == 10) x = 0;

if-sats utan else-del motsvarar funktionen when i Haskell.

En form av sats som vi inte nämnt hittills är block som består av en sekvens av godtyckligt många satser omgivna av {}.

{
  s1
  s2
  s3
  ...
}

Detta är speciellt användbart i sammansatta satser såsom if-satser, så man i then- else else-delen kan utföra flera satser.

if (e) {
  s1_1;
  s1_2;
  ...
} else {
  s2_1;
  s2_2;
  ...
}