Muuttujat ja tietotyypit
osaamistavoitteet
- Muistat, mitä ovat muuttujat ja vakiot
- Muistat, mitä ovat merkkijonot ja listat
- Tunnet Javan alkeistietotyypit
- Tunnet, miten merkkijonoja, taulukoita ja listoja käytetään Javassa
Ohjelmat käsittelevät muistiin tallennettua tietoa. Korkean tason kielissä, kuten Javassa käytetään selkokielisiä nimiä viitattaessa muistiin tallennettuun tietoon. Tällaista nimeä, joka viittaa muistissa olevaan tietoon, kutsutaan muuttujaksi (engl. variable). Ohjelmoijan tarvitsee muistaa vain nimi; käyttöjärjestelmä ja tietokoneen sisäinen logiikka huolehtivat tiedon todellisesta sijainnista muistissa.
Ennen käyttämistä muuttuja tulee määritellä.
tyyppi muuttujanNimi;
Muuttujan tyyppi määritellään muuttujan nimen edessä, ja se kertoo, millaista tietoa muuttuja voi sisältää, esimerkiksi kokonaisluvun, desimaaliluvun tai totuusarvon. Muuttujan nimi on ohjelmoijan valitsema tunniste, jonka avulla muuttujaan viitataan. Nimi voi sisältää kirjaimia ja alaviivoja. Muuttujan nimi ei kuitenkaan voi olla Java-kielessä varattu avainsana eikä muuttujan nimi saa alkaa numerolla.
Muuttujan määrittelyn jälkeen muuttujaan voi sijoittaa lausekkeiden arvoja:
muuttujanNimi = lauseke;
Yhtäsuuruusmerkin oikealla puolella olevan lausekkeen (engl. expression) arvo tallennetaan sen vasemmalla puolella nimettyyn muuttujaan. Jos muuttujissa oli aiemmin jotain muita arvoja, ne korvataan uusilla.
void main() {
double korkokerroin; // Muuttujan määrittely, double = desimaaliluku
double paaoma; // Muuttujan määrittely
korkokerroin = 0.05; // Arvon sijoitus muuttujaan
paaoma = 150.0; // Arvon sijoitus muuttujaan
IO.println("korkokerroin = " + korkokerroin);
IO.println("paaoma = " + korkokerroin);
}
Muuttujaa ei voi käyttää ennen kuin siihen sijoittaa arvon ainakin kerran. Tätä varten voi käyttää yhdistettyä määrittely- ja sijoituslausetta, joka yhdistää muuttujan määrittelyn ja alkuarvon sijoittamisen samalle riville:
double paaoma = 0.05;
// Yllä oleva on sama kuin:
double paaoma;
paaoma = 0.05;
Muuttuja voi olla myös lausekkeen osana, ja siten sen arvoa voidaan käyttää osana sijoitettavaa lauseketta.
void main() {
double korkokerroin = 0.05;
double paaoma = 150.0;
double paaomaKorolla = (1 + korkokerroin) * paaoma;
IO.println("korkokerroin = " + korkokerroin);
IO.println("paaoma = " + korkokerroin);
IO.println("paaomaKorolla = " + paaomaKorolla);
}
Ohjelmoinnissa sijoitus on lause, eli yksittäinen suoritettava käsky.
Esimerkiksi lausetta double paaoma = 150.0; voi ajatella tarkoittavan:
"tallenna luku 150.0 muistiin paikkaan, jota kutsutaan tästä eteenpäin nimellä
paaoma". Muuttujan arvo pysyy samana kunnes jokin toinen lause muokkaa
muuttujan arvoa.
Javan tietotyypit voidaan jakaa kahteen pääryhmään: alkeistietotyyppeihin (engl. primitive data types) ja viitetietotyyppeihin (engl. reference data types). Kaikki tieto tallennetaan tietokoneen muistiin binäärilukuina (nollien ja ykkösten sarjana), ja tietotyypit eroavat toisistaan siinä, kuinka paljon muistia ne varaavat, millaista dataa ne esittävät ja millä säännöillä dataa voi käsitellä. Alkeistietotyypit sisältävät yksinkertaisia arvoja, kuten kokonaislukuja ja totuusarvoja, kun taas viitetietotyypit sisältävät monimutkaisempia rakenteita, kuten olioita, taulukoita ja merkkijonoja.
Alkeistietotyypit
Javassa on kahdeksan sisäänrakennettua alkeistietotyyppiä, joita voi karkeasti jakaa neljään kategoriaan: kokonaisluvut, liukuluvut, merkit ja totuusarvot.
Kokonaisluvut
Kokonaisluvuille on olemassa neljä tyyppiä, jotka eroavat toisistaan
lukualueen ja muistinkulutuksen perusteella. Yleisimmin käytetty
kokonaislukutyyppi on int.
| Tyyppi | Koko (tavua /bittiä) | Lukualue (suuntaa antava) |
|---|---|---|
byte | 1 tavu (8 bittiä) | -128 ... 127 |
short | 2 tavua (16 bittiä) | -32 768 ... 32 767 |
int | 4 tavua (32 bittiä) | n. -2 miljardia ... 2 miljardia |
long | 8 tavua (64 bittiä) | n. +/- 9 * 10^18 |
Huomautus: Muuttujan tyyppi ei vaihdu lennosta
Useissa dynaamisissa ohjelmointikielissä, kuten Pythonissa tai JavaScriptissa, kokonaisluvuille ei välttämättä ole suurinta arvoa: suurille luvuille varataan joko lisää tilaa tai vähemmän merkitseviä numeroita pyöristetään. Tämä ei päde Javassa. Jos laskutoimituksen tuloksena kokonaisluku ylittää muuttujan tyypin lukualueen, luku vuotaa yli (engl. overflow) ja "pyörähtää lukualueen ympäri":
void main() {
int suuriLuku = 2000000000;
IO.println("suuriLuku = " + suuriLuku);
suuriLuku += 1000000000;
IO.println("suuriLuku = " + suuriLuku);
}
Siispä mahdollinen lukualue on otettava huomioon ohjelmaa kirjoittaessa. Jos on mahdollisuus, että laskutoimitus ylittää tyypin lukualueen, on syytä vaihtaa toiseen tietotyyppiin.
Javasta löytyy myös suuria lukuja käsittelevä
BigInteger
-tyyppi, jota ei tällä opintojaksolla käsitellä.
Liukuluvut
Desimaaliluvuille käytetään liukulukutyyppejä. Yleisin näistä on
double.
| Tyyppi | Koko (tavua) | Tarkkuus |
|---|---|---|
| float | 4 tavua (32 bittiä) | n. 7 merkitsevää numeroa |
| double | 8 tavua (64 bittiä) | n. 15 merkitsevää numeroa |
Huomautus: Liukuluvut ovat epätarkkoja!
Java käyttää liukulukuja desimaalilukujen double ja float
esittämiseen. Liukulukuja voidaan ajatella esittävän desimaalilukujen
likiarvoja. Liukulukujen tarkka toiminta on standardoitu (IEEE 754 -standardi);
vaikka ne on tarkoitettu desimaalilukujen esittämiseen, niillä on silti joitain
mielenkiintoisia ja kenties yllättäviä eroja tavallisiin desimaalilukuihin:
void main() {
// Laskutoimitukset voivat erota desimaaliluvuista pyöristysvirheiden takia
double pyoristysVirhe = 0.1 + 0.2;
IO.println("pyoristysVirhe = " + pyoristysVirhe);
// Jakaminen nollalla on määritelty eikä aiheuta virhettä
double negatiivinenAarettomyys = -1.0 / 0.0;
IO.println("negatiivinenAarettomyys = " + negatiivinenAarettomyys);
double positiivinenAarettomyys = 1.0 / 0.0;
IO.println("positiivinenAarettomyys = " + positiivinenAarettomyys);
// Jakolasku 0/0 ei aiheuta virhettä
double nan = 0.0 / 0.0;
IO.println("nan = " + nan);
}
Liukuluvuille on siten erikseen määritelty ja NaN =
Not A Number. Lisäksi liukulukujen väliset laskutoimitukset
voivat sisältää pieniä virheitä johtuen liukulukujen esitystavasta ja
pyöristysvirheistä.
Hyvin tarkkoja laskuja vaativille ohjelmille löytyy myös
BigDecimal
-tyyppi, jota ei tällä opintojaksolla käsitellä.
Merkit
Yksi merkki tallennetaan char-tyyppiseen muuttujaan, joka käyttää
2 tavua muistia.
Totuusarvot
Totuusarvoja varten on boolean-tyyppi, jolla on kaksi
mahdollista arvoa: true (tosi) tai false (epätosi).
Viitetietotyypit
Toisin kuin alkeistietotyypit,
viitetietotyyppinen muuttuja sisältää varsinaisen tiedon sijaan vain pienen,
kiinteän kokoisen arvon eli viitteen (engl. reference).
Viitteen avulla ohjelma pääsee käsiksi varsinaiseen dataan,
jonka kokoa tai sisältöä ei välttämättä tiedetä ennen kuin ohjelma ajetaan.
Javassa käytännössä kaikki muut tietotyypit kuin alkeistietotyypit ovat
viitetietotyyppejä. Esimerkiksi String on viitetietotyyppi, kuten myös kaikki
taulukot ja listat. Alkaen luvusta 2 tutustumme olio-ohjelmointiin; Javassa
kaikki oliot ovat viitetietotyypit.
Kaikkiin viitetyyppimuuttujiin on sallittua sijoittaa erikoisarvo null.
Tämä niin sanottu null-viite merkitsee, että muuttuja ei sisällä viitettä
mihinkään tietoon. Yritys muokata tai lukea muuttujaa, jonka arvo on null,
tuottaa yleensä virheen ohjelman ajon aikana:
String teksti = null;
String tekstiIsolla = teksti.toUpperCase();
IO.println(tekstiIsolla);
java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because "<local1>" is null
at main.main(main.java:3)
Koska virhe tapahtuu vasta ajon aikana, Javassa on yleensä ohjelmoijan vastuulla varmistaa, että muuttujan tai funktion parametrin arvona ei ole null-viite. Varmistus voidaan tehdä esimerkiksi ehtorakenteella, jotka esitetään seuraavassa alaluvussa.
Valinnaista lisätietoa: Miksi viitetietotyyppejä on olemassa?
On useita syitä sille, miksi nämä kaksi eri kategoriaa tietotyypeille on olemassa.
Ensimmäinen liittyy suorituskykyyn ja muistin hallintaan. Jos kaikki muuttujat
olisivat arvopohjaisia (kuten alkeistietotyypit), se aiheuttaisi valtavasti
turhaa muistin kulutusta ja hidastaisi ohjelman suorituskykyä, erityisesti
suurten tietorakenteiden kohdalla. Jos meillä olisi vaikkapa kirja, joka
sisältäisi 1000 sivua tekstiä, niin joka ikinen kerta kun haluamme käsitellä
kirja-muuttujaa, meidän pitäisi kopioida kaikki 1000 sivua muistissa. Tämä
olisi erittäin tehotonta. Sen sijaan viitetietotyypit mahdollistavat sen, että
vain viittaamme kirja-olioon, joka sijaitsee jossakin muualla muistissa,
ilman että tarvitsee kopioida koko kirjaa joka kerta.
Toinen syy liittyy jaettuun tilaan. Usein haluamme, että useampi ohjelman osa
muokkaa samaa tietoa. Esimerkiksi on järkevää, että pankkitili-olio on jaettu
useiden eri toimintojen kesken, kuten talletus, nosto ja tilin saldo.
Arvopohjaisessa maailmassa joutuisimme kopioimaan pankkitili-olion joka kerta,
kun tililtä halutaan nostaa rahaa, tehdä tilisiirto tai vaikkapa tarkistaa
saldo. Tämä johtaisi helposti siihen, että eri kopiot olisivat eri tilassa, mikä
saattaisi aiheuttaa virheitä.
Kolmas syy on dynaaminen koko. Viitetietotyypit mahdollistavat dynaamisesti kasvavien ja kutistuvien tietorakenteiden, kuten linkitettyjen listojen, pinojen ja jonojen, luomisen. Näitä rakenteita ei voida helposti toteuttaa arvopohjaisina, koska arvopohjaisten muuttujien koko on kiinteä käännösaikana.
Neljäs syy liittyy erityisesti olio-ohjelmointiin, ja liittyy osittain myös kolmanteen kohtaan. Javassa viitteet mahdollistavat polymorfismin. Koska muuttuja on vain viite, se voi osoittaa mihin tahansa, joka "näyttää" oikealta tyypiltä.
Elain lemmikki = new Koira();
lemmikki = new Kissa();
Jos nämä olisivat puhtaita arvotyyppejä, Elain-tyyppiselle muuttujalle pitäisi
varata kiinteä määrä muistia. Jos Kissa sitten tarvitsisikin enemmän muistia
kuin Elain on varannut, koodi hajoaisi. Viitteiden avulla muuttujan koko on
aina sama (viitteen koko), riippumatta siitä kuinka valtava olio viitteen päässä on.
Literaalit
Literaali (engl. literal) tarkoittaa ohjelmakoodiin kirjoitettua kiinteää arvoa. Eri tietotyypeillä on omat kirjoitussääntönsä literaaleille.
- Merkit (
char): Kirjoitetaan yksittäisen lainausmerkin sisään, esimerkiksi'A','*'ja'x'. Erikoismerkit alkavat kenoviivalla:'\n'(rivinvaihto),'\u03A9'(kreikkalainen iso omega) ja'\t'(tabulaattori). - Kokonaisluvut (
byte,short,int,long): Kirjoitetaan suoraan numerona, esimerkiksi42,-7ja0.long-luvun literaali päättyy isoon tai pieneen kirjaimeenLtail, esimerkiksi12345678901L. - Liukuluvut (
float,double): Kirjoitetaan desimaalipisteellä erotettuna, esimerkiksi3.14,-0.001ja2.0. Voidaan käyttää myös tieteellistä muotoa:1.5e3(eli 1.5 × 10³ = 1500) ja2.0E-4(eli 2.0 × 10⁻⁴ = 0.0002). Oletuksena desimaaliluvut ovatdouble-tyyppiä. Jos haluat luodafloat-luvun, literaalin tulee päättyä isoon tai pieneen kirjaimeenFtaif, esimerkiksi3.14f. - Totuusarvot (
boolean): Kirjoitetaan avainsanoinatruejafalse.
void main() {
char merkki = 'A';
IO.println("merkki = " + merkki);
int luku = 123;
IO.println("luku = " + luku);
long isoLuku = 12345678901L;
IO.println("isoLuku = " + isoLuku);
double liukuluku = -2.0;
IO.println("liukuluku = " + liukuluku);
float pieniLiukuluku = 2.0f;
IO.println("pieniLiukuluku = " + pieniLiukuluku);
double tieteellinenMuoto = 1.5e-2;
IO.println("tieteellinenMuoto = " + tieteellinenMuoto);
boolean totuusarvo = true;
IO.println("totuusarvo = " + totuusarvo);
}
Käärijäluokat
Javassa kullekin alkeistietotyypille on olemassa niin sanottu käärijäluokka
(engl. wrapper class). Käärijäluokka "käärii" alkeistietotyypin arvon olion
sisään, jolloin alkeistietotyypin arvoa voidaan käsitellä oliona. Esimerkiksi
int-tyypin käärijäluokka on Integer. Käärijäluokista löytyy hyödyllisiä
metodeja, kuten toString() sekä vakioita, kuten MAX_VALUE. Alkeistietotyypit
ja niitä vastaavat käärijäluokat on esitetty alla olevassa taulukossa.
| Alkeistietotyyppi | Käärijäluokka |
|---|---|
| byte | Byte |
| short | Short |
| int | Integer |
| long | Long |
| float | Float |
| double | Double |
| char | Character |
| boolean | Boolean |
Alla olevassa esimerkissä käytetään käärijäluokkien MAX_VALUE-vakioita,
tulostetaan primitiivityyppien käärittyjä arvoja, sekä havainnollistetaan
kokonaisluvun lukualueen ylitystä.
void main() {
byte tavu = Byte.MAX_VALUE;
short kaksiTavua = Short.MAX_VALUE;
IO.println(tavu);
IO.println(kaksiTavua);
IO.println(Short.toString(kaksiTavua).charAt(0));
int maksimi = Integer.MAX_VALUE;
IO.println(maksimi + " on suurin luku, jonka voi tallettaa int-tyyppiseen muuttujaan" );
int ylivuoto = Integer.MAX_VALUE + 1;
IO.println("Ylitetään lukualue:");
IO.println(ylivuoto);
}
Merkkijonot
Javassa merkkijonoja ei lasketa alkeistietotyypiksi.
Java-kieli tarjoaa kuitenkin merkkijonoille oman syntaksin: uuden merkkijonon
voi luoda kirjoittamalla merkkejä lainausmerkkien " väliin:
void main() {
String jono = "Opiskelen ohjelmointia!";
IO.println("jono = " + jono);
}
Javassa merkkijono on muuttumaton. Jos yrität suorittaa jonkin operaation merkkijonolle, saat tulokseksi uuden merkkijonon, eikä alkuperäinen merkkijono muutu. Katsotaan tästä esimerkki:
void main() {
String muuttumaton = "Tämä on muuttumaton.";
IO.println("muuttumaton = " + muuttumaton);
muuttumaton.concat("Vai onko sittenkään?");
IO.println("muuttumaton = " + muuttumaton);
}
Metodin concat() palauttamaa uutta merkkijonoa ei nyt tallenneta mihinkään,
ja alkuperäinen merkkijono pysyy ennallaan. Toisin sanoen, merkkijonomuuttujien
arvoa voi muuttaa vain sijoittamalla:
void main() {
String muuttumaton = "Tämä on muuttumaton.";
IO.println("muuttumaton = " + muuttumaton);
// HIGHLIGHT_GREEN_BEGIN
muuttumaton = muuttumaton.concat("Vai onko sittenkään?");
// HIGHLIGHT_GREEN_END
IO.println("muuttumaton = " + muuttumaton);
}
String-tyyppi sisältää lukuisia hyödyllisiä metodeja. Alla on lueteltu joitain
niistä.
| Metodi | Selitys |
|---|---|
jono.charAt(paikka) | Palauttaa jonossa olevan yksittäisen merkin indeksistä paikka. |
jono.length() | Palauttaa jonossa olevien merkkien määrän eli jonon pituuden. |
jono.trim() | Palauttaa kopion jonosta ilman alussa ja lopussa olevia ylimääräisiä tyhjiä merkkejä. |
jono.replace(mitä, millä) | Palauttaa kopion jonosta, jossa jonot/merkit mitä on korvattu jonolla/merkillä millä. |
jono.split(haku) | "Pilkkoo" jonon osiin haku-jonon esiintymien kohdalla ja palauttaa taulukon pilkotuista osajonoista. Huomaa, että haku on ns. säännöllinen lauseke. |
jono.contains(etsittävä) | Palauttaa true, jos etsittävä löytyy jonosta. |
jono.indexOf(etsittävä) | Palauttaa indeksin, jossa etsittävä esiintyy ensimmäistä kertaa. |
jono.substring(mistä, mihin) | Palauttaa osan jonosta alkaen indeksistä mistä päättyen indeksiin mihin. |
String.join(merkki, jonot) | Palauttaa jonon, jossa taulukossa jonot olevat jonot ovat peräkkäin yhdistettynä merkillä merkki). |
Kaikki metodit ja niiden tarkat selitykset löytyvät JavaDocs-sivulta (ks. Class
String).
Katsotaan vielä, miten yllä olevia esimerkkejä voi käyttää:
void main() {
String mjono = "Opiskelen ohjelmointia java-kielellä.";
IO.println("mjono = " + mjono);
IO.println("Ensimmäinen merkki: " + mjono.charAt(0));
IO.println("Jonon pituus: " + mjono.length());
IO.println(); // Lisää ylimääräisen rivivaihdon
// Merkkijonojen yhdistäminen onnistuu + operaattorilla
mjono = mjono + " Hei maailma!";
IO.println("mjono (lisäyksen jälkeen) = " + mjono);
// Tekstin korvaaminen merkkijonossa
mjono = mjono.replace("java", "Java");
IO.println("mjono (korvattu java -> Java) = " + mjono);
IO.println();
// "Pilkkoo" jonon kahteen osajonoon viivan kohdalla
// HUOM: split-metodissa merkit \^$.|?*+()[]{}
// vaativat, että niiden eteen laitetaan kaksi kenoviivaa \\
// Eli jos haluttaisiin pilkkoa pisteen kohdalla, tulisi kirjoittaa
// mjono.split("\\.") eikä mjono.split(".");
// jälkimmäinen versio on ns. säännöllinen lauseke (regular expression),
// joka pilkkoo jonon jokaisen merkin kohdalla
String[] lauseet = mjono.split("\\.");
IO.println("lauseet = " + Arrays.toString(lauseet));
IO.println();
// indexOf etsii indeksin, jossa annettu jono löytyy
int ohjelmointiaPaikka = mjono.indexOf("ohjelmointia");
IO.println("ohjelmointiaPaikka = " + ohjelmointiaPaikka);
// substring palauttaa osajonon annetusta jonosta indeksin perusteella
String osajono = mjono.substring(ohjelmointiaPaikka, ohjelmointiaPaikka + 12);
IO.println("osajono = " + osajono);
IO.println();
// Operaatio "String + lauseke" muuntaa lausekkeen arvon merkkijonoksi
String toinenJono = "1/2 = " + (1.0 / 2);
IO.println("toinenJono = \"" + toinenJono + "\"");
}
Luvun parsiminen merkkijonosta
Merkkijono voidaan muuntaa luvuksi käyttämällä käärijäluokkien parse-alkuisia
metodeja. Esimerkiksi Integer.parseInt muuntaa merkkijonon kokonaisluvuksi ja
Double.parseDouble muuntaa merkkijonon desimaaliluvuksi.
void main() {
String kokonaislukuJono = "42";
int kokonaisluku = Integer.parseInt(kokonaislukuJono);
IO.println("kokonaisluku = " + kokonaisluku);
String desimaalilukuJono = "3.14";
double desimaaliluku = Double.parseDouble(desimaalilukuJono);
IO.println("desimaaliluku = " + desimaaliluku);
}
StringBuilder
Käytä StringBuilder-luokkaa, kun tarvitset muunneltavan merkkijonon. Se
tarjoaa menetelmiä merkkijonon muokkaamiseen ilman uusien merkkijono-olioiden
luomista, mikä tehostaa muistin käyttöä.
Alla on lueteltu joitain StringBuilder-luokan hyödyllisiä metodeja.
| Metodi | Selitys |
|---|---|
sb.charAt(paikka) | Palauttaa jonossa olevan yksittäisen merkin indeksistä paikka. |
sb.length() | Palauttaa jonossa olevien merkkien määrän eli jonon pituuden. |
sb.append(arvo) | Lisää arvon nykyisen jonon loppuun. |
sb.toString() | Palauttaa kopion tästä jonosta String-merkkijonona. |
Kaikki toiminnot ja niiden tarkat selitykset löytyvät JavaDocs-sivulta (ks. Class
StringBuilder).
Alla on esimerkkejä metodien käytöstä.
void main() {
StringBuilder muuttuva = new StringBuilder("Tämä on muuttuva");
IO.println("muuttuva = " + muuttuva);
IO.println("muuttuva.length() = " + muuttuva.length());
IO.println();
muuttuva.append(" merkkijono.");
IO.println("muuttuva = " + muuttuva);
IO.println("muuttuva.length() = " + muuttuva.length());
IO.println();
String muuttumatonKopio = muuttuva.toString();
IO.println("muuttumatonKopio = " + muuttumatonKopio);
}
Taulukot
Taulukkoja (engl. array) käytetään tallentamaan joukkoa samantyyppisiä alkioita muuttujaan. Tämä helpottaa datan organisointia.
Uuden taulukon määrittely ja luominen Javassa onnistuu seuraavasti:
Tyyppi[] nimi = new Tyyppi[koko];
Tässä new Tyyppi[koko] luo taulukon, joka sisältää koko kappaletta alkioita,
joiden tyyppi on Tyyppi. Taulukon luomisen jälkeen alkioiden arvoja voi
asettaa käyttäen sijoituslausetta seuraavasti:
void main() {
int[] arvosanat = new int[4];
arvosanat[0] = 4;
arvosanat[1] = 2;
arvosanat[2] = 2;
arvosanat[3] = 5;
IO.println("arvosanat = " + Arrays.toString(arvosanat));
}
Sijoituslauseessa [numero] tarkoittaa alkion paikkaa eli indeksiä
taulukossa. Javassa indeksointi alkaa nollasta, eli ensimmäinen alkio on
indeksissä 0, toinen indeksissä 1, ja niin edelleen. Taulukon viimeisen alkion
indeksi on aina taulukko.length - 1.
Jos alkioiden arvot tunnetaan etukäteen, taulukon voi myös luoda seuraavasti.
void main () {
int[] arvosanat = new int[] {4, 2, 2, 5};
IO.println("arvosanat = " + Arrays.toString(arvosanat));
}
Yhdistetyssä muuttujan määrittely- ja sijoituslauseessa new Tyyppi[]-osa
on myös sallittua pudottaa pois, jolloin yllä olevaa voi vielä hieman tiivistää.
void main () {
int[] arvosanat = {4, 2, 2, 5};
IO.println("arvosanat = " + Arrays.toString(arvosanat));
}
Javassa taulukkojen kokoa ei voi muuttaa taulukon luomisen jälkeen. Arvon sijoittaminen indeksiin, jota ei ole taulukossa, aiheuttaa ajonaikaisen virheen.
int[] arvosanat = new int[] {4, 2, 2, 5};
arvosanat[5] = 3;
java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 4
at main.main(main.java:3)
Javassa taulukon pituuden voi aina tarkistaa length-attribuutilla.
Lisäksi taulukon voi tulostaa käyttämällä Arrays.toString-metodia (ks.
JavaDocs)
void main() {
int[] arvosanat = new int[] {4, 2, 2, 5};
IO.println("Taulukon pituus: " + arvosanat.length);
IO.println("Taulukon sisältö: " + Arrays.toString(arvosanat));
}
Moniulotteiset taulukot
Toisin kuin esimerkiksi C#-kielessä, Javassa ei ole erillisiä moniulotteisia
taulukkoja. Sen sijaan Javassa voi luoda taulukon, jonka alkioina
ovat taulukot, eli Tyyppi[][]:
void main() {
int[][] taulukko2D = new int[][] {
new int[] {1, 2, 3},
new int[] {4, 5, 6, 7},
new int[] {8, 9, 0},
};
// Taulukon pituus on tässä siten taulukkojen lukumäärä, eli "rivien" määrä.
IO.println("Taulukossa on " + taulukko2D.length + " taulukkoa.");
// Indeksointi toimii normaalisti; alkiona on nyt kokonainen taulukko
IO.println("taulukko2D[0] on taulukko: " + Arrays.toString(taulukko2D[0]));
IO.println("taulukko2D[1] on taulukko: " + Arrays.toString(taulukko2D[1]));
IO.println("taulukko2D[2] on taulukko: " + Arrays.toString(taulukko2D[2]));
// Myös yksittäisen taulukon indeksointi toimii normaalisti,
// mutta huomaa syntaksi!
int ensimmainenAlkio = taulukko2D[0][0];
IO.println("Ensimmäisen rivin ensimmäinen alkio: " + ensimmainenAlkio);
IO.println("Rivillä 2 (indeksi 1) kolmas alkio (indeksi 2) on: " + taulukko2D[1][2]);
}
Huomaa, että yllä olevassa esimerkissä taulukko2D[0][0] viittaa
ensimmäisen taulukon (taulukko2D[0]) ensimmäiseen alkioon
(taulukko2D[0])[0]. Yllä oleva taulukko ja siinä olevat alkiot voisi kuvata
siis seuraavasti:
Huomaa erityisesti, että "rivitaulukkojen" ei tarvitse olla välttämättä samanpituuisia.
Vakiot
Muuttuja, jolle voidaan sijoittaa arvo vain alustuksen yhteydessä, määritellään
käyttämällä final-avainsanaa. Javan koodauskäytänteisiin kuuluu, että
final-muuttujat kirjoitetaan suuraakkosin ja sanat erotellaan toisistaan
alaviivalla.
Javassa final-avainsanaa voi käyttää sekä alkeistietotyyppien että
viitetietotyyppien kanssa. On kuitenkin huomattava, että viitetietotyyppisen
muuttujan tapauksessa final-sana tarkoittaa, että viitettä ei voi muuttaa
osoittamaan uuteen dataan. Viitteen päässä olevaa dataa voi silti muuttaa, jos
tietotyyppi sallii sen.
final int PAIVIA_VIIKOSSA = 7;
final int[] PAIVIA_KUUKAUDESSA_KARKAUSVUOSI = new int[] {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
// PAIVIA_VIIKOSSA = 8; // Tämä aiheuttaa käännösvirheen
PAIVIA_KUUKAUDESSA_KARKAUSVUOSI[0] = 30; // Tämä on sallittu
Vakioita tarvitaan mm. koodin lukemisen helpottamiseksi, toisteisen koodin vähentämiseksi, luotettavuuden parantamiseksi ja suorituskyvyn parantamiseksi.
Listat
Lista on tietorakenne, joka voi kasvaa ja kutistua tarpeen mukaan. Kuten taulukko, lista voi sisältää vain yhden tyypin mukaisia alkioita. Listan koko ei ole kiinteä, mikä tekee siitä joustavamman tilanteisiin, joissa alkioiden määrä ei ole etukäteen tiedossa. Javan listat vastaavat siten JavaScriptin taulukkoja.
Javan yleisin listan tyyppi on ArrayList<T>, jossa T on listassa olevien
alkioiden tyyppi. Jotta listoja voi käyttää koodissa, tulee ne ensin tuoda
kääntäjän näkyville lisäämällä tiedoston ensimmäiselle riville import-määre:
import java.util.*;
// Varsinainen ohjelma
void main() ...
import-määre kertoo kääntäjälle, että ohjelmassa käytetään tyyppejä, jotka
löytyvät java.util-pakkauksesta. Lyhyesti, pakkaus on tapa järjestellä luokkia
yhteisen nimittäjän alle. Jos olet käyttänyt C#:a aiemmin, likimain vastaava käsite
siellä on nimiavaruus (namespace). Pakkauksiin palataan tarkemmin myöhemmissä
osissa; tässä vaiheessa riittää tiedostaa, että kääntäjä ei välttämättä tiedä
tyyppien olemassaolosta ellei ne tuo näkyviin import-määreellä.
Kun java.util-pakkauksen sisältö on tuotu ohjelmaan, voidaan listoja alustaa
seuraavasti:
import java.util.*;
void main() {
// Tapa 1: Tyhjä lista ilman alkioita
List<Integer> arvosanat = new ArrayList<Integer>();
// Lisätään alkiot yksi kerrallaan
arvosanat.add(4);
arvosanat.add(2);
arvosanat.add(2);
arvosanat.add(5);
IO.println("arvosanat = " + arvosanat);
// Tapa 2: Lista, jossa alkiot annettu valmiiksi
List<Integer> arvosanatValmis = new ArrayList<Integer>(List.of(4, 2, 2, 5));
IO.println("arvosanatValmis = " + arvosanatValmis);
}
Javan rajoituksista johtuen listan alkioiden tyypin on aina oltava viitetyyppi.
Niinpä esimerkiksi int-alkioita sisältävää listaa ei voi kirjoittaa muodossa
ArrayList<int>:
List<int> lista = new ArrayList<int>();
error: unexpected type
List<int> lista = new ArrayList<int>();
^
required: reference
found: int
Jos tarvitset listoja, jonka alkioina ovat alkeistietotyypit, käytä alkioiden
tyyppinä alkeistietotyyppien käärijäluokat, jotka ovat
viitetyyppejä, mutta toimivat kuten niitä vastaavat alkeistietotyypit. Toisin
sanoen, ArrayList<Integer> on sallittu, kun taas ArrayList<int> ei ole.
Puolestaan ArrayList<String> on sallittu, koska merkkijono on
viitetietotyyppi.
huomautus
Javan koodauskäytänteisiin kuuluu, että listamuuttujien tyyppinä käytetään
List<T>, kun taas muuttujien arvojen alustuksessa käytetään
tarkempaa tyyppiä, kuten ArrayList<T>.
Toisin sanoen, vaikka alla oleva on sallittu
void main() {
ArrayList<String> nimet = new ArrayList<String>(List.of("Matti", "Teppo"));
IO.println("nimet = " + nimet);
}
koodauskäytänteiden mukaisesti on yleisempää esittää muuttuja seuraavasti:
void main() {
List<String> nimet = new ArrayList<String>(List.of("Matti", "Teppo"));
IO.println("nimet = " + nimet);
}
Lisäksi, jos alkioiden tyyppi on muuttujan määrittelyrivin perusteella selvä, alkioiden tyyppi saatetaan usein jättää pois listan alustuksesta:
void main() {
// Kääntäjä päättelee, että ArrayList<> = ArrayList<String>
// muuttujan tyypin perusteella
List<String> nimet = new ArrayList<>(List.of("Matti", "Teppo"));
IO.println("nimet = " + nimet);
}
Palaamme tarkemmin List<T> ja ArrayList<T> -tyyppien välisiin eroihin
osassa 5. Voit tässä vaiheessa kuitenkin miettiä,
että List<T> on yleinen tyyppi listoille, kun taas ArrayList<T> on
(eräs) Javan tarjoama tapa esittää lista.
Katsotaan vielä listojen eräitä hyödyllisiä metodeja.
| Metodi | Selitys |
|---|---|
size() | Palauttaa listassa olevien alkioiden lukumäärän. |
add(lisättävä) | Lisää alkion listan loppuun. |
add(indeksi, lisättävä) | Lisää alkion listan indeksiin indeksi siirtäen loput alkiot yhden paikan eteenpäin. |
get(indeksi) | Palauttaa indeksissä indeksi olevan alkion. |
remove(poistettava) | Poistaa listasta poistettava:n ensimmäisen esiintymän siirtäen loput alkiot yhden paikan taaksepäin. |
remove(indeksi) | Poistaa listasta paikassa indeksi olevan alkion. |
Löydät lisää metodeja JavaDocs-sivustolla (ks. Class ArrayList<E>).
import java.util.*;
void main () {
// Luodaan tyhjä merkkijonolista
List<String> nimet = new ArrayList<>();
// Lisätään alkioita listaan
nimet.add("Matti");
nimet.add("Teppo");
nimet.add("Liisa");
// Tulostetaan listan koko
IO.println("Listan koko: " + nimet.size());
IO.println("------");
// Haetaan alkio indeksistä 1 (toinen alkio)
String toinen = nimet.get(1);
IO.println("Toinen alkio: " + toinen);
IO.println("------");
// Poistetaan alkio indeksistä 0 (ensimmäinen alkio)
nimet.remove(0);
IO.println("Poistettiin ensimmäinen alkio.");
IO.println("------");
// Tulostetaan kaikki alkiot
IO.println("nimet = " + nimet);
IO.println("------");
// Tulostetaan listan koko
IO.println("Listan koko: " + nimet.size());
// Kaksi esimerkkiä siitä, kuinka luoda listaan heti sisältöä
List<String> elaimet = new ArrayList<>(List.of("koira", "kissa", "kala"));
List<String> varit = Arrays.asList("punainen", "sininen", "keltainen");
IO.println("elaimet = " + elaimet);
IO.println("varit = " + elaimet);
}
Huomaa ainakin nämä erot Javan, C# ja Pythonin välillä listoja käytettäessä.
Muuttuja i viittaa listan indeksiin.
| Toiminto | Java | C# | Python |
|---|---|---|---|
| Lukeminen tietystä paikasta | list.get(i) | list[i] | list[i] |
| Listan koko | list.size() | list.Count | len(list) |
| Poistaminen | list.remove(i) | list.RemoveAt(i) | list.pop(i) |
| Onko lista tyhjä? | list.isEmpty() tai list.size() == 0 | list.Count == 0 | if not list: tai len(list) == 0 |
Javan tyyppijärjestelmä
Java on staattisesti tyypitetty kieli, mikä tarkoittaa, että muuttujien tyypit määräytyvät käännösaikana, ei ohjelman ajon aikana. Jos yrität sijoittaa muuttujaan väärän tyyppistä tietoa, ohjelma ei käänny, ja kääntäjä antaa virheilmoituksen.
Käytännössä Javassa eri tietotyyppejä ei voi käyttää toistensa sijaan, ellei
kieli nimenomaisesti salli sitä. Esimerkiksi totuusarvoa (boolean) ei voi
käyttää lukuarvona, eikä viitetyyppistä arvoa voi käsitellä kokonaislukuna. Jos
ohjelmoija yrittää rikkoa näitä sääntöjä, seurauksena on käännösvirhe.
void main() {
boolean totuusarvo = false;
totuusarvo = 1;
}
error: incompatible types: int cannot be converted to boolean
totuusarvo = 1;
^
1 error
error: compilation failed
Yllä oleva käännösvirhe kertoo, että kokonaislukua (int) ei voida muuntaa
totuusarvoksi (boolean). Tämä on selkeä ero dynaamisesti tyypitettyihin
kieliin, kuten Pythoniin tai JavaScriptiin, jossa tyyppi määräytyy
ohjelman ajon aikana, mahdollistaen erityyppisten arvojen sijoittamisen samaan
muuttujaan:
// Tämä on sallittu koodi JavaScriptissa
let totuusarvo = true;
console.log(`totuusarvo = ${totuusarvo}`);
totuusarvo = 1;
console.log(`totuusarvo = ${totuusarvo}`);
On kuitenkin väistämätöntä, että ohjelmassa tulee käsitellä useita erityyppisiä arvoja. Tätä varten Javassa on valmiiksi määritelty joitain automaattisia sääntöjä, joiden perusteella kääntäjä osaa tehdä implisiittisen tyyppimuunnoksen sijoituksissa ja lausekkeissa. Esimerkiksi
- kokonaislukuja (
int) voidaan automaattisesti muuntaa desimaaliluvuksi (double), - pienempiä kokonaislukuja (esim. 8-bittinen kokonaisluku
byte) voidaan "laajentaa" suurempiin kokonaislukuihin (esim. 32-bittinen kokonaislukuint).
Tyyppimuunnossääntöjä on paljon lisää; yleisperiaate on, että jos muunnos ei aiheuta tiedon menetystä, sille on todennäköisesti olemassa implisiittinen muunnos.
void main() {
int kokonaisluku = 23;
IO.println("kokonaisluku = " + kokonaisluku);
double desimaaliluku = kokonaisluku; // OK: int -> double muunnos on implisiittinen
IO.println("desimaaliluku = " + desimaaliluku);
// HUOM: jakolasku on int / int => desimaalit häviävät
double puoletVirhe = 1 / 2;
IO.println("puoletVirhe = " + puoletVirhe);
// OIKEIN: jakolasku on int / double -> double / double
double puoletOikein = 1 / 2.0;
IO.println("puoletOikein = " + puoletOikein);
}
Lisäksi ohjelmoija voi erikseen pakottaa ns. eksplisiittisen tyyppimuunnoksen
käyttämällä syntaksia (uusiTyyppi)muuttujanNimi. Tämä soveltuu tilanteisiin,
jossa muunnos ei olisi mahdollista implisiittisesti:
void main() {
long sairaanIsoLuku = 40000000000L; // long = 64-bittinen luku
IO.println("Iso, long-tyyppinen luku: " + sairaanIsoLuku);
// long -> int ei ole implisiittinen, mutta se onnistuu eksplisiittisesti
int katkaistu = (int)sairaanIsoLuku; // int = 32-bittinen luku
IO.println("int-luku eksplisiittisen tyyppimuunnoksen jälkeen: " + katkaistu);
}
Staattinen tyypitys tarkoittaa Javassa käytännössä sitä, että tyyppeihin liittyvät virheet pyritään estämään jo ennen ohjelman suorittamista. Kääntäjä toimii eräänlaisena turvaverkkona, joka varmistaa, että arvot, muuttujat ja operaatiot ovat keskenään yhteensopivia.