Vinkkejä Java-ohjelmointiin

Lukujen käsittely

Kokonaisluvut

Javan tavallisimmat kokonaislukutyypit ovat int ja long. Tyyppi int on 32-bittinen, ja siinä olevan luvun suuruus voi olla noin 2 · 109. Tyyppi long on puolestaan 64-bittinen, ja siinä olevan luvun suuruus voi olla noin 9 · 1018.

Yleensä hyvä valinta kokonaisluvun tyypiksi on int. Jos sen arvoalue on liian pieni, voi käyttää suurempaa tyyppiä long.

Jos käytetyn tyypin arvoalue loppuu kesken, tapahtuu ylivuoto (overflow), mikä näkyy usein siinä, että koodin tuloksena on negatiivinen luku, vaikka sellaista ei pitäisi tulla. Näin käy esimerkiksi seuraavassa koodissa:

int a = 123456789;
int b = 987654321;
System.out.println(a*b); // -67153019

Tässä on ongelmana, että laskun tulos ei mahdu int-tyyppiin. Ongelman voi korjata käyttämällä long-tyyppiä:

long a = 123456789;
long b = 987654321;
System.out.println(a*b); // 121932631112635269

Seuraavassa koodissa on vielä yksi sudenkuoppa:

long x = 123456789*987654321;
System.out.println(x); // -67153019

Nyt vaikka muuttujan x tyyppi on long, kertolasku lasketaan edelleen int-tyypillä. Yksi tapa korjata asia on muuttaa toinen luvuista long-tyyppiseksi näin:

long x = (long)123456789*987654321;
System.out.println(x); // 121932631112635269

Kuten tästä näkyy, long-tyypin käyttäminen vaatii tarkkuutta. Usein jos koodi antaa outoja tuloksia, jossain kohtaa on kuitenkin käytetty vahingossa int-tyyppiä.

Liukuluvut

Javan tavallisin liukulukutyyppi on 64-bittinen double. Liukulukujen etuna on, että niissä voi olla desimaaliosa:

double x = 12.527;

Liukuluvuissa on kuitenkin ongelmana, että niissä tapahtuu pyöristysvirheitä. Seuraava koodi havainnollistaa asiaa:

double x = 3*0.3+0.1;
double y = 1;
if (x == y) System.out.println("x ja y ovat samat");
if (x < y) System.out.println("x on pienempi kuin y");
if (x > y) System.out.println("x on suurempi kuin y");

Vaikka x:n ja y:n arvon pitäisi olla sama, koodi tulostaa x on pienempi kuin y. Syynä on, että laskua 3*0.3+0.1 ei pystytä laskemaan tarkasti, vaan x:n arvoksi tulee hieman alle 1.

Tämän vuoksi liukulukuja kannattaa välttää aina kun mahdollista. Yleensä löytyy jokin tapa, miten algoritmin voi toteuttaa tarkasti ilman liukulukuja.

Lause ja lauseke

Lause (statement) on ohjelmassa oleva komento, kun taas lauseke (expression) on jokin koodin osa, jolla on arvo. Esimerkiksi System.out.println(a+b) on lause, jonka osana on lauseke a+b.

Javassa melko moni lauseelta näyttävä ilmaisu on itse asiassa lauseke, jolla on arvo. Esimerkiksi sijoitus a = b on lauseke, jonka arvona on b. Seuraava koodi samaan aikaan sijoittaa muuttujan a arvoksi 5 ja tulostaa arvon.

int a = 3;
System.out.println(a = 5); // 5

Muutos yhdellä

Lausekkeet a++ ja ++a kasvattavat molemmat a:n arvoa yhdellä, mutta a++ on arvoltaan a, kun taas ++a on arvoltaan a+1. Seuraavat koodit havainnollistavat asiaa:

int a = 3;
System.out.println(a++); // 3
System.out.println(a); // 4
int a = 3;
System.out.println(++a); // 4
System.out.println(a); // 4

Ehdollinen lauseke

Ehdollinen lauseke a ? b : c on arvoltaan b, jos ehto a pätee, ja muuten c. Esimerkiksi seuraavassa koodissa ehdollisen lausekkeen arvo on "parillinen", jos ehto x%2 == 0 pätee (eli x on parillinen), ja muuten "pariton".

int x;
// ...
String s = x%2 == 0 ? "parillinen" : "pariton";

Taulukko

Javan perustietorakenne on taulukko, joka muodostuu peräkkäin olevista alkioista. Taulukon alkiot on numeroitu kokonaisluvuin 0, 1, 2, jne., ja niihin viitataan [] -merkinnän avulla.

Taulukko on hyvä valinta algoritmien toteuttamisessa, jos sen ominaisuudet riittävät, koska se on paljon kevyempi kuin esimerkiksi ArrayList-rakenne.

Taulukon käsittely

Seuraava koodi luo taulukon luvut, jossa on 5 alkiota. Jokainen arvo on aluksi 0.

int[] luvut = new int[5];

Toinen tapa luoda taulukko on antaa sen alkiot listana:

int[] luvut = {3,1,5,2,5};

Taulukon alkioita voi käsitellä samaan tapaan kuin tavallisia muuttujia:

luvut[0] = 2;
luvut[1] = 5;
luvut[2] = luvut[0]+luvut[1];

Taulukon tulostaminen

Taulukon tulostaminen vaatii hieman työtä, koska seuraava koodi ei toimi halutusti:

int[] luvut = {1,2,3};
System.out.println(luvut); // [I@3cd1a2f1

Taulukon pystyy kuitenkin tulostamaan näin:

int[] luvut = {1,2,3};
System.out.println(Arrays.toString(luvut)); // [1, 2, 3]

Viittaukset ja kopiointi

Javassa on kahdenlaisia muuttujia: alkeistyypin muuttujat (esim. int) ja oliomuuttujat (esim. taulukko). Näiden muuttujien käsittelyssä on tärkeä ero: alkeismuuttujien arvo kopioidaan sijoituksessa ja metodin parametrina, mutta oliomuuttujasta kopioidaan vain viittaus.

Esimerkiksi seuraavassa koodissa ei ole mitään yllättävää:

int a = 3;
int b = a;
b = 5;
System.out.println(a); // 3

Kuitenkin kun saman koodin toteuttaa taulukoilla, tulee yllättävämpi tulos:

int[] a = {1,2,3};
int[] b = a;
b[0] = 5;
System.out.println(a[0]); // 5

Tässä a ja b viittaavat samaan taulukkoon, eli kun taulukkoa b muuttaa, niin muutos heijastuu myös taulukkoon a. Tämä on yleinen syy bugeihin Java-ohjelmissa.

Jos taulukosta halutaan tehdä aito kopio, jonka muuttaminen ei vaikuta alkuperäiseen taulukkoon, ratkaisu on käyttää metodia clone:

int[] a = {1,2,3};
int[] b = a.clone();
b[0] = 5;
System.out.println(a[0]); // 1

Merkkijonot

Merkki vs. merkkijono

Merkki (char) on yksittäinen symboli, kuten kirjain tai numero. Merkki kirjoitetaan koodissa heittomerkkien sisään.

Merkkijono (String) on jono peräkkäin olevia merkkejä. Merkkijono kirjoitetaan koodissa lainausmerkkien sisään.

char c = 'a';
String s = "apina";

Metodi charAt antaa tietyssä kohtaa merkkijonoa olevan merkin:

String s = "apina";
System.out.println(s.charAt(1)); // p

Merkkiä voidaan käsitellä myös lukuarvon tavoin. Seuraava koodi näyttää merkkiä vastaavan merkkikoodin ja merkkikoodia vastaavan merkin:

System.out.println((int)'A'); // 65
System.out.println((char)65); // A

Merkkijonojen vertailu

Seuraava koodi ei toimi oikein, jos a ja b ovat merkkijonoja, koska ==-operaattori vertailee viittauksia eikä sisältöjä.

if (a == b) {
    // ...
}

Toimiva vertailu on käyttää metodia equals:

if (a.equals(b)) {
    // ...
}

Merkkijonon muuttaminen

Javassa merkkijonoa ei voi muuttaa sen luomisen jälkeen, vaan ainoa tapa on luoda uusi merkkijono. Esimerkiksi +=-operaattori luo uuden merkkijonon kopioimalla sen pohjaksi alkuperäisen merkkijonon sisällön. Seuraavan koodin seurauksena muistissa on kolme merkkijonoa: "apina", "x" ja "apinax".

String s = "apina";
s += "x";

Seuraava koodi on erittäin tehoton tapa luoda merkkijono, jossa on miljoona a-merkkiä:

int n = 1000000;
String s = "";
for (int i = 0; i < n; i++) {
    s = s+"a";
}

Ongelmana on, että silmukan jokainen askel luo uuden merkkijonon, eli koodi luo merkkijonot "a", "aa", "aaa", jne. Tässä kuluu paljon aikaa ja muistia. Tehokas tapa luoda tällainen merkkijono on sijoittaa merkit ensin char-taulukkoon ja muuttaa se sitten merkkijonoksi:

int n = 1000000;
char[] c = new char[n];
for (int i = 0; i < n; i++) {
    c[i] = 'a';
}
String s = new String(c);

int vs. Integer

Javassa alkeistyyppien (kuten int) rinnalla on vastaavia oliotyyppejä (kuten Integer), jotka ovat omiaan aiheuttamaan sekaannusta.

Oliotyypit ovat välttämätön paha Javan tietorakenteissa. Esimerkiksi kun luomme ArrayList-rakenteen, jonka sisältönä on int-lukuja, tyypiksi tulee antaa Integer:

ArrayList<Integer> luvut = new ArrayList<>();

Java tekee usein automaattisia muunnoksia tyyppien välillä. Esimerkiksi seuraava koodi toimii mainiosti, vaikka luvut sisältää Integer-lukuja ja koodi käyttää int-lukuja:

luvut.add(1);
luvut.add(2);
luvut.add(3);
int x = luvut.get(0);

Huomaa kuitenkin, että seuraava koodi ei ole toimiva:

if (luvut.get(0) == luvut.get(1)) {
    // ...
}

Tässä ei tapahdu muunnosta int-tyypiksi, vaan Java vertailee Integer-lukuja. Nyt ==-operaattori ei toimi halutusti, vaan vertailu tulee tehdä equals-metodilla:

if (luvut.get(0).equals(luvut.get(1))) {
    // ...
}

Asiaa mutkistaa vielä se, että ==-vertailu saattaa toimia näennäisesti, kun luvut ovat pieniä (esim. välillä –128…127), koska Java voi silloin vertailla niitä eri tavalla. Bugi tulee kuitenkin esille silloin, kun koodi käsittelee suurempia lukuja.

static-muuttujat

Luokassa oleva static-muuttuja on yhteinen kaikille luokasta luotaville olioille. Sen arvo säilyy tallessa olioiden välillä, toisin kuin tavallisen muuttujan arvo. Tarkastellaan esimerkkinä seuraavaa luokkaa:

public class Testi {
    static int a;
    int b;
    
    void tulosta() {
        a++; b++;
        System.out.println(a + " " + b);
    }
}

Kun luokasta luodaan kaksi oliota, muuttuja a on niille yhteinen, kun taas kummallakin oliolla on oma muuttuja b:

Testi x = new Testi();
x.tulosta(); // 1 1
Testi y = new Testi();
y.tulosta(); // 2 1

Moni outo bugi testatessa koodia johtuu siitä, että luokassa on käytetty static-muuttujaa. Tällöin tietoa säilyy muuttujissa testien välillä, vaikka testit eivät liity toisiinsa. Ratkaisu on kerrankin helppo: kun sanan static poistaa, niin koodi alkaa toimia moitteetta.

Komentorivin käyttäminen

On hyödyllinen taito osata kääntää ja suorittaa Java-koodi komentorivillä. Tällöin ohjelmoijalla on täysi kontrolli asioihin, toisin kuin IDEä (esim. NetBeans) käyttäessä.

Seuraavat esimerkit olettavat, että käytössä on Linux-ympäristö. Muissa ympäristöissä komentoriviä käytetään melko samalla tavalla.

Käännös ja suoritus

Seuraava komento kääntää tiedostossa Koodi.java olevan luokan:

$ javac Koodi.java

Tästä syntyy käännetty tiedosto Koodi.class, jonka voi suorittaa näin:

$ java Koodi

Komentoriviparametrit

Metodin main parametrina oleva taulukko args sisältää parametrit, jotka ohjelmalle on annettu komentorivillä. Seuraava ohjelma tulostaa kaikki parametrinsa:

public class Koodi {
    public static void main(String[] args) {
        for (int i = 0; i < args.length; i++) {
            System.out.println(i + " " + args[i]);
        }
    }
}

Voimme testata ohjelmaa suorittamalla se näin:

$ java Koodi apina banaani cembalo

Nyt ohjelman tulostus on seuraava:

0 apina
1 banaani
2 cembalo