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