Tarkempi johdanto Java-kieleen

Käymme tässä osiossa käydään lyhyesti tarkemmin läpi Javan perusteita auttamaan Java-kieltä aiemmin tuntemattomien kurssilaisten siirtymistä Java-kielen pariin. Taustaoletus on, että kurssin osallistujilla on jo jonkin verran ohjelmointikokemusta esimerkiksi Python- tai C++-kielillä.

Javan perussyntaksi periytyy suurelta osin C-kielestä, ja Java on C-kielen tavoin staattisesti tyypitetty kieli. Tämän ansiosta Java-koodin syntaksiin on varsin suoraviivaista sopeutua, jos on aiempaa kokemusta C- tai C++-kielestä.

Javan muuttujatyypit

Javan muuttujat jakautuvat korkealla tasolla kahteen kategoriaan: primitiivimuuttujiin ja viitemuuttujiin.

Primitiivimuuttujat tallettavat yksinkertaisia luku- tai totuusarvoja ja ovat samantapaisia arvomuuttujia kuin C/C++-kielen tavalliset muuttujat: primitiivimuuttuja itsessään tallettaa arvon.

Primitiivimuuttujien tyypit eli ns. primitiivityypit ovat:

  • Kokonaislukutyypit: byte, short, int, long ja char.

    • Neljä ensiksimainittua ovat järjestyksessä 8-, 16-, 32- ja 64-bittisiä etumerkillisiä kokonaislukutyyppejä, jotka käyttävät kahden komplementtiesitystä.

    • char on 16-bittinen etumerkitön kokonaislukutyyppi, joka on tarkoitettu esittämään yksittäisiä UTF-16 merkistökoodausta käyttäviä merkkejä.

    • Javassa kokonaislukuluvakiot (esim. 100) ovat oletuksena int-arvoja. Jos kokonaislukuvakion on tarkoitus olla long-arvo, lisää sen perään kirjain ‘L’ (esim. 3123456789L).

  • Liukulukutyypit: float, double

    • 32- ja 64-bittiset IEEE 745 -standardiin pohjautuvat liukulukutyypit.

    • Javassa liulukuluvakiot (esim. 3.14) ovat oletuksena double-arvoja. Jos liukulukuvakion on tarkoitus olla float-arvo, lisää sen perään kirjain ‘F’ (esim. 3.14F).

  • Totuusarvotyyppi: boolean

    • Arvo voi olla joko true tai false.

Tässä varsinkin C/C++-taustaisten on hyvä huomata, että Java-standardi määrittää primitiivityyppien koot ja esitysmuodot kiinteästi. Javassa ei siten ole C/C++-kielten tapaista järjestelmäkohtaista vaihtelua esim. sen suhteen, kuinka isoja arvoja jokin lukutyyppi voi esittää.

Seuraava koodinpätkä antaa muutaman esimerkin laillisista ja laittomistakin primitiivimuuttujien määrityksistä. Huomaa, että Java on esim. C/C++-kieliä tarkempi lukutyyppimuunnosten ja lukuvakioiden käytön suhteen. Jos haluat testata tämän koodinpätkän kääntämistä (missä tulisi kääntäjän virheitä), voit esimerkiksi kopioida koodinpätkän “Hello World”-esimerkin main-funktion sisään.

int i = 1000;        // int-muuttuja i = 1000.
int j;               // int-muuttuja j = 0 (lukutyypit oletusalustuvat nollalla).

long k = 3123456789;   // KÄÄNTÄJÄN VIRHE: 3123456789 on liian suuri ollakseen int!
long m = 3123456789L;  // OK: 3123456789L on long-vakio, ja long kykenee esittämään sen.

byte a = 100;        // byte-muuttuja a = 100. Int-vakio 100 --> byte-arvo 100.
byte b = i;          // KÄÄNTÄJÄN VIRHE: arvoa muuttava muunnos int --> byte.
                      // Java muuntaa int-vakion pienempään kokonaislukutyyppiin, JOS
                      // arvon voi esittää pienemmälläkin tyypillä. i = 1000 ei mahdu
                      // byteen vaan muuntuisi byte-arvoksi -24.
byte c = (byte) i;   // OK: pakotettu tyyppimuunnos int --> byte ja tuloksena c = -24.
                      // Javan tyyppimuunnoksen syntaksi on suoraan C-kielestä:
                      // annetaan kohdetyyppi suluissa muunnettavan arvon edessä.

boolean e;                // boolean-muuttuja e = false (oletusalustuu epätodeksi).
boolean f = true;         // boolean-muuttuja f = true.
boolean g = (boolean) i;  // KÄÄNTÄJÄN VIRHE: int-arvoa i ei voi muuntaa totuusarvoksi.
                          // Toisin kuin C/C++/Pythonissa, Javassa totuusarvoja ei voi
                          // sellaisenaan muuntaa lukuarvoiksi tai päinvastoin!
boolean h = i > j;        // h = true, koska ehtolauseen i > j eli 1000 > 0 arvo on true.

float x = 0.0;       // KÄÄNTÄJÄN VIRHE: arvo 0.0 on double-vakio, eikä Java muunna
                      // double-arvoa float-arvoksi ilman erillistä tyyppimuunnosta.

float y = 1.5F;      // OK: 1.5F on float-vakio.
float z = 0;         // OK: implisiittinen muunnos int 0 --> float 0.0F.

double p = 3.14;
float q = p;           // KÄÄNTÄJÄN VIRHE: double-arvon muunnos float-arvoksi!
float r = (float) p;  // OK: pakotettu tyyppimuunnos double --> float.

int s = p;           // KÄÄNTÄJÄN VIRHE: double-arvon muunnos int-arvoksi!
int t = y;           // KÄÄNTÄJÄN VIRHE: float-arvon muunnos int-arvoksi!
int u = (int) y;     // OK: pakotettu tyyppimuunnos float --> int. Tuloksena u = 1 eli
                      // arvo y = 1.5 ilman desimaaliosaa (ei hienoa pyöristystä tms.).

Viitemuuttujat tallettavat viitteitä olioihin. Viitemuuttujat muistuttavat käyttäytymiseltään C/C++:n osoittimia, mutta niiden yhteydessä ei käytetä mitään erityistä “osoitussyntaksia” eikä ole mahdollista tehdä muistiosoitteisiin pohjautuvia operaatioita. Pythonin muuttujat ovat samantapaisia kuin Javan viitemuuttujat, joskin Pythonin muuttujat ovat tyypittömiä.

Se, onko kyseessä primitiivimuuttuja vai viitemuuttuja, määräytyy implisiittisesti muuttujan määrityksen yhteydessä annetun tyypin perusteella. Esimerkiksi jos määritämme tyyppiä T olevan muuttujan x tapaan T x, on x primitiivimuuttuja jos ja vain jos tyyppi T on jokin alla luetelluista primitiivityypeistä. Muuten x on viitemuuttuja, joka voi viitata tyyppiä T oleviin olioihin.

Javassa olio on joko taulukko tai jonkin luokan olio. Eli huomaa, että Javassa taulukot (myös primitiiviarvoja tallettavat) ovat olioita. Javassa periaatteessa kaikki oliot luodaan dynaamisesti (eli yleensä new-operaattorilla). Joissain tapauksissa, kuten merkkijonojen ja taulukoiden yhteydessä, voi käyttää myös suorempia syntaktisesti C-kieltä muistuttavia alustustapoja.

Viitemuuttuja aina joko viittaa johonkin olemassaolevaan olioon tai sen arvo on ns. null-viite (joka ilmaisee, ettei viite viittaa mihinkään). Yksinkertaisuuden vuoksi käytämme usein viitemuuttujan x yhteydessä muotoa “x on …” olevaa ilmaisua, kun hiuksia halkoen pitäisi sanoa “x viittaa …”.

Edellisiin liittyen varsinkin C/C++-taustaisille oleellinen tieto on, että Java käyttää automaattista muistinhallintaa (roskienkeruuta, garbage collection). Ohjelmoija ei ole vastuussa dynaamisesti luotujen olioiden vapautuksesta, vaan Java-virtuaalikone voi muistin käydessä vähiin vapauttaa automaattisesti sellaisia olioita, joihin ei enää sillä hetkellä viitata.

Olion (sekä luokan/rajapinnan) jäseneen viitataan Javassa esim. C/C++/Python-kielistäkin tutulla pisteoperaattorilla. Esimerkiksi x.y tarkoittaa viittausta muuttujan x jäseneen y, ja tämä operaatio on laillinen ainoastaan, jos x on viitemuuttuja, jonka osoittamalla oliolla on jäsen y. Jos x olisi primitiivimuuttuja, tuottaisi operaation x.y yritys kääntäjän virheilmoituksen. Eräs hyvin yleinen ajonaikainen virhe on tilanne, jossa yritetään tehdä jäsenviittaus x.y, kun viitemuuttujan x arvo on null.

Java tarjoaa ison joukon valmiita luokkia. Näistä perustavanlaatuisia ovat esimerkiksi merkkijonoluokat String ja StringBuilder sekä primitiivityyppien byte, short, int, long, char, char, float, double ja boolean kääreluokat Byte, Short, Integer, Long, Character, Float, Double ja Boolean. Kääreluokkia käytetään kääreinä niitä vastaaville primitiivityypeille sellaisissa konteksteissa, joissa käsiteltävien arvojen oletetaan olevan olioviitteitä. Kääreluokat tarjoavat lisäksi apuvälineitä niitä vastaavien arvojen manipulointiin (esimerkiksi muunnokset merkkijonoesityksestä primitiiviarvoksi ja päinvastoin).

Huom! Kaikki edellämainitut luokat ovat muuttumattomia: olion esittämä arvo määritetään olion luonnin yhteydessä, eikä arvoa voi enää myöhemmin muuttaa.

Koska primitiivityyppien kääreluokkia käytetään varsin paljon, hoitaa Java monissa yhteyksissä automaattisesti muunnoksen primitiivityypistä sen kääreluokan olioksi tai päinvastoin (ns. boxing- ja unboxing-muunnokset). Nämä (un)boxing-muunnokset ovat Javassa samalla ainoa sallittu tyyppimuunnos primitiivityypistä viitetyyppiin tai päinvastoin. Yritys tehdä pakotettu tyyppimuunnos primitiivityypistä viitetyyppiin tai päinvastoin epäonnistuu, ellei kyseessä ole muunnos, jonka Java joka tapauksessa tekisi (un)boxing-muunnoksenakin.

Edellä kuvattuihin sääntöihin on yksi poikkeus: plus-operaattorin + yhteydessä tehtävä muunnos String-merkkijonoksi. Jos plus-operaattorin toinen operandi on String-merkkijono ja toinen mitä tahansa muuta tyyppiä, muuntaa Java toisenkin operandin automaattisesti String-merkkijonoksi. Tämä muunnos koskee niin primitiivi- kuin viitetyyppejäkin (mukaanlukien null-viitteet, jotka muuntuvat merkkijonoksi “null”). Viitetyyppien muunnos String-merkkijonoksi tehdään kutsumalla olion jäsenfunktiota toString(); kaikilla Javan viitetyypeillä on tällainen funktio (vähintään oletusversio, joka yleensä tulostaa tietoa olion identiteetistä).

Alla on kommentoitu esimerkkikoodinpätkä, joka havainnollistaa Javan viitemuuttujia, merkkijonoja, primitiivityyppien kääreluokkia sekä taulukoita.

int x = 100;       // int-arvo 100.
Integer y = 200;   // Autoboxingin avulla luotu Integer-olio, joka tallettaa int-arvon 200.
x + y;             // Käärittyihin lukuihin voi soveltaa aritmeettis-loogisia operaatioita
y / 3;             // tavalliseen tapaan. Tässä x + y = 300 ja y / 3 = 66.
x > y;             // false, koska ehto 100 > 200 ei päde.

String s1 = "One";                    // s1 viittaa String-vakio-olioon "One".
String s2 = new String(" to three:"); // s2 uusi String-olio, jonka sisältö on " to three:".
String n1 = " 1";
String n2 = " 2";
String n3 = " 3";

// Jos operaattoria + sovelletaan merkkijonoon, tulkitaan se merkkijonojen yhdistämiseksi:
// tuloksena on uusi String-olio, joka esittää merkkijonojen yhdisteen.
// Toinen yhdistettävä arvo voi olla muukin kuin merkkijono; tällöin Java automaattisesti
// tyyppimuuntaa arvon merkkijonoksi.
// Alla s3 lopulta viittaa uuteen String-olioon, jonka sisältö on "One to three: 1 2 3".
String s3 = s1 + s2 + n1 + n2 + n3;

// Yllä s3 muodostui tosiasiassa askeleittaisesta uusien ja uusien String-olioiden
// luontiketjusta: "One to three:", "One to three: 1", "One to three: 1 2" ja
// "One to three: 1 2 3". Tämä on selvästi tehotonta: luotuihin String-olioihin kopioitiin
// yhteensä 13 + 15 + 17 + 19 = 64 merkkiä, vaikka lopputuloksen pituus on ainoastaan 19.
// Tällaisessa tilanteessa kannattaisi harkita muuttuvan merkkijonon toteuttavan luokan
// StringBuilder käyttöä. StringBuilder-oliota voi muuttaa esim. jäsenfunktiolla append,
// joka lisää merkkejä perään. Alla tehdään samat lisäykset kuin yllä, mutta yhteensä 19
// merkin kopioinnilla: ensin StringBuilder-olion alkuarvoksi s1, ja sitten perään
// s2, n1, n2 ja n3.
StringBuilder b = new StringBuilder(s1);
b.append(s2).append(n1).append(n2).append(n3);

// StringBuilder-olion sisällön pohjalta voi halutessaan luoda tavallisen muuttumattoman
// String-olion kutsumalla jäsenfunktiota toString. Tässä luodaan 19-merkkinen uusi
// merkkijono. Merkkijonon s4 luonti vaati kaikkiaan 19 + 19 = 38 merkin kopioinnin,
// jos mukaan lasketaan ylempänä StringBuilder-olion b avulla tehty työ.
String s4 = b.toString();  // s4 alustuu b:n merkkijonolla "One to three: 1 2 3".

// On hyvä huomata, että kääntäjä voi tehokkaasti yhdistää vakiomerkkijonot yhteen jo
// käännösaikana. Merkkijonojen yhdistämisen mahdollinen tehottomuus koskee nimenomaan
// tilannetta, jossa ainakin toinen yhdistettävä osa on varsinainen String-olio. Esim. alla
// s5 alustuu suoraan merkkijonoksi "One to three: 1 2 3" ilman turhaa kopiointia: kääntäjä
// yhdistää vakiot ensin muotoon "One to three: 1 2 3" ja käyttää sitä String-olion alustukseen.
String s5 = "One" + " to three:" + " 1" + " 2" + " 3";

// HUOM! Jos tavallisia vertailuoperaattoreita != ja == soveltaa viitemuuttujiin,
// tulos kertoo sitä, viittaavatko viitemuuttujat keskenään samaan vai eri olioon.
// Javassa on sopimus, että jos luokan olioiden samanlaisuutta voi olla tarpeen verrata,
// tulisi luokan tarjota samanlaisuutta vertaileva jäsenfunktio nimeltä "equals".
s3 == s4;       // false, koska s3 ja s4 viittaavat eri String-olioihin.
s3 != s4;       // true, koska s3 ja s4 viittaavat eri String-olioihin.
s3.equals(s4);  // true, koska sekä s3 että s4 esittävät merkkijonoa "One to three: 1 2 3".

// Koska merkkijonovakiot ovat String-olioita, voi niidenkin kautta kutsua jäsenfunktioita.
"One two three: 1 2 3".equals(s3);    // true, koska s3:n sisältö on "One to three: 1 2 3".

// Jos vertaat merkkijonovakiota ja String-oliota, kannattaa käyttää yllä esitettyä tapaa,
// koska equals-funktio toimii oikein myös silloin, kun parametri on null-viite!
String s5 = null;
"One two three: 1 2 3".equals(s5);  // false, koska parametri s5 on null.
// Alla sinänsä sama vertailu kuin yllä, mutta nyt OHJELMA KAATUU ajonaikaiseen virheeseen!
s5.equals("One two three: 1 2 3");  // null-viitteen s5 kautta ei voi kutsua jäsenfunktiota...

String s6;  // Viitemuuttuja ilman alustusarvoa alustuu null-viitteeksi.
s5 == s6    // true, koska sekä s5 että s6 ovat null-viitteitä.

// Kunkin primitiivityypin "tyyppi" kääreluokka tarjoaa tapaan "parseTyyppi" nimetyn
// luokkafunktion, joka tulkitsee merkkijonon kyseisen primitiivityypin arvoksi. Merkkijonon
// voi siis esim. tulkita int-arvoksi funktiolla Integer.parseInt, float-arvoksi
// funktiolla Float.parseFloat ja boolean-arvoksi funktiolla Boolean.parseBoolean.
int i = Integer.parseInt("2021");                       // i = 2021.
float pi = Float.parseFloat("3.1415926535");            // pi = 3.1415926535F.
boolean t = Boolean.parseBoolean("tRuE");               // t = true (kirjainkoolla ei väliä).

// Kukin kääreluokka tarjoaa myös luokkafunktion toString, joka muuntaa sille parametrina
// annetun kääreluokan primitiivityyppisen arvon String-merkkijonoksi. Näiden avulla voi
// muuntaa primitiiviarvoja merkkijonoiksi. Jos arvo sen sijaan on jo kääreluokan oliossa,
// tee muunnos merkkijonoksi suoremmin kutsumalla olion toString-jäsenfunktiota.
String piStr = Float.toString(pi);           // piStr = "3.1415927"  (float on epätarkka!)
String weekHourStr = Integer.toString(24*7); // weekHourStr = "168"
String yStr = y.toString();                  // yStr = "200", tässä y oli Integer-olio.

// Javan taulukkomäärityksen syntaksi on varsin samanlainen, kuin esim. C-kielessä:
// muuttujamäärityksen yhteydessä annetaan yksi hakaslukupari per taulukon ulottuvuus.
// Tässä on kuitenkin kaksi selkeää eroa:
//   1) hakasulut tulee antaa muuttujan tyypin perässä eikä muuttujan nimen perässä.
//      (Java sinänsä sallii hakasulut nimenkin perässä, mutta sitä tapaa ei suositella.)
//   2) Muuttujan tyyppimäärityksen hakasuluissa ei anneta taulukon kokoa: taulukon koko
//      määräytyy joko implisiittisesti taulukon alustusarvojen mukaiseksi tai koko annetaan
//      hakasulkujen sisällä tehtäessä taulukon luovan new-operaattorin kutsu.

// Määritetään vals viitemuuttujaksi, joka voi viitata int-taulukkoon.
// Ellei anneta alustusarvoa, alustuu viite null-viitteeksi: tässä siis it = null.
int[] it;

// Asetetaan it viittaamaan uuteen new-operaattorilla luotuun 100-alkioiseen
// int-taulukkoon. Javassa kaikkien lukutyyppien alkiot alustuvat nolliksi.
it = new int[100];

// Taulukon voi luonnollisesti luoda suoraan taulukkomuuttujamäärityksen yhteydessä.
// Kaikkien viitetyyppien alkiot alustuvat null-viitteiksi, joten alla tuloksena
// 1000 null-viitettä sisältävä Double-taulukko.
Double[] dt = new Double[1000];

// Alla tuloksena dt[172] = 0.0, koska it[5] on 0. Tässä tarvitaan pakotettu tyyppi-
// muunnos int-arvosta double-arvoksi, jotta Java voi suorittaa autoboxing-muunnoksen
// double -> Double. Esimerkiksi yritys muuntaa int-arvo suoraan Double-arvoksi johtaisi
// kääntäjän virheilmoitukseen.
dt[172] = (double) it[5];

// Taulukon sisällön voi alustaa suoraan taulukkomuuttujamäärityksen yhteydessä
// samanlaisella notaatiolla kuin C-kielessä: alkiot aaltosulkeissa pilkuilla eroteltuina.
// Autoboxingia sovelletaan alustusalkioihinkin, joten esim. alla Integer-taulukon
// alkiot voi alustaa int-vakioilla, jotka Java automaattisesti muuntaa Integer-olioiksi.
Integer[] it2 = {1, 2, 3, 4};

// Alustuksen ja new-operaattorin voi halutessaan yhdistää. Tällöin new-operaattorille
// ei kuitenkaan anneta taulukon kokoa. Alla it3 on samanlainen kuin edellä it2.
Integer[] it3 = new Integer[]{1, 2, 3, 4};

// Javan taulukoilla on jäsenmuuttuja length, joka kertoo taulukon koon.
it2.length == 4;            // true, koska it2-taulukko alustettiin 4-alkioiseksi.

// Java ei salli viittausta laittomaan taulukon indeksiin. Esimerkiksi alla yritys
// kasvattaa arvoa it2[4] yhdellä johtaa ajonaikaiseen virheeseen, koska 4-alkioisen
// taulukon lailliset indeksit ovat 0...3.
it2[4] += 1;

Kuten edellisessä esimerkkikoodinpätkässä mainitaan, tulee viitemuuttujien välinen vertailu tavallisesti tehdä jäsenfunktiolla equals, joka vertaa kyseisen luokan oliota toiseen olioon ja palauttaa true, jos oliot ovat samanlaisia, ja muuten false. Tämänkin kanssa pitää kuitenkin olla tarkkana! Ellei luokka itse määritä equals-funktiota, on aina olemassa oletusversio, joka toimii samalla tavalla kuin ==-operaattori eli vertaa viitattujen olioiden identiteettiä eikä samanlaisuutta.

Javan perussyntaksit ja -operaatiot

Alla annettu kommentoitu esimerkkiohjelma havainnollistaa Javan perussyntaksia ja -operaatioita. Tässä ei vielä erityisemmin oteta kantaa luokkien ominaisuuksiin.

Ohjelma tutkii, onko komentoriviparametrina annettu päivämäärä laillinen. Tämän saisi tehtyä suoraviivaisemmin käyttämällä Javan luokkakirjaston valmiita apuvälineitä päivämäärien käsittelyyn.

// Jos haluat testata tätä koodia koneellasi, aseta se tiedostoon LegalDate.java,
// käännä tapaan "javac LegalDate.java" ja aja tapaan "java LegalDate pp.kk.vvvv",
// missä pp.kk.vvvv on jokin päivämäärä (esim. 29.2.2021).
public class LegalDate {
  // Huomaa, että kaikki luokan LegalDate jäsenet on tässä määritetty staattisiksi.

  // Funktiomääritys kuin C-kielessä: paluuarvon tyyppi funktion nimen eteen,
  // parametrien tyypit ja nimet suluissa pilkuilla eroteltuina funktion nimen perään.
  // Funktio isLeapYear palauttaa totuusarvon, joka kertoo, onko year karkausvuosi.
  static boolean isLeapYear(int year) {
    // Karkausvuosi: jaollinen 4:llä ja ei jaollinen 100:lla tai jaollinen 400:lla.
    // Javan loogisaritmeettiset operaatiot ja return-lause kuin C-kielessä.
    return (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0));
  }

  // Kuukausien päivien määrittämisen voi tehdä monella tavalla. Tässä käytetään
  // taulukkosyntaksin lisähavainnollistamiseksi kaksiulotteista esitäytettyä
  // int-taulukkoa: kullekin kuukaudelle 2-alkioinen alitaulukko, jossa päivien määrä
  // tavallisena ja karkausvuonna (ainoa ero helmikuussa eli toisessa alitaulukossa).
  static int[][] mDays = {{31, 31}, {28, 29}, {31, 31}, {30, 30}, {31, 31}, {30, 30},
                          {31, 31}, {31, 31}, {30, 30}, {31, 31}, {30, 30}, {31, 31}};

  // Funktio monthDays palauttaa tiedon, kuinka monta päivää kuukaudessa
  // month vuonna year on. Palautusarvo -1 vastaa virheellistä kuukautta.
  static int monthDays(int month, int year) {
    int days = -1;
    if(1 <= month && month <= 12) {
      // Ehdollinen operaattori kuin C-kielessä.
      days = isLeapYear(year) ? mDays[month-1][1] : mDays[month-1][0];
    }
    return days;
  }

  // Funktio isLegalDate tutkii, onko parametrien day, month ja year kuvaama
  // päivämäärä laillinen. Tässä vuosiluvun oletetaan olevan aina laillinen.
  static boolean isLegalDate(int day, int month, int year) {
    // Tuloksen laskenta on suoraviivaista, koska monthDays
    // palauttaa -1, jos kuukausi on laiton.
    return (1 <= day) && (day <= monthDays(month, year));
  }

  public static void main(String[] args) {
    // Javan String-luokan jäsenfunktio split palauttaa String-taulukon, jossa on
    // merkkijono ositettuna parametriksi annetun välimerkin perusteella. Tässä
    // pilkotaan komentoriviparametri args[0] osiin pisteen "." perusteella.
    // Pisteen edessä on kenoviivat teknisistä syistä: split tulkitsee parametrinsa
    // ns. säännölliseksi lausekkeeksi, jossa piste sellaisenaan olisi erikoismerkki.
    String[] dayMonthYear = args[0].split("\\.");
    boolean dateOk = false;
    if(dayMonthYear.length == 3) {
      // Tulkitaan merkkijonot kokonaisluvuiksi funktiolla Integer.parseInt.
      int day = Integer.parseInt(dayMonthYear[0]);
      int month = Integer.parseInt(dayMonthYear[1]);
      int year = Integer.parseInt(dayMonthYear[2]);
      dateOk = isLegalDate(day, month, year);
    }
    System.out.println("The date " + args[0] + (dateOk ? " is legal." : " is illegal!"));
  }
}

Non-static cannot be referenced from a static context

Aloittelevat Java-ohjelmoijat törmäävät helposti jokseenkin muotoa “non-static JÄSEN cannot be referenced from a static context” olevaan kääntäjän virheilmoitukseen yrittäessään viitata johonkin main-funktion rinnalle toteuttamaansa jäseneen. Virhe johtuu siitä, että kyseinen toinen jäsen on määritetty ilman static-määrettä. Kuten hieman myöhemmin vähän tarkemmin käydään läpi, ovat kaikki ilman static-määrettä määritetyt jäsenet oliokohtaisia jäseniä, joihin saa viitata vain jonkin olemassaolevan olion kautta. Staattisiin eli luokkakohtaisiin jäseniin voi viitata aina.

Tässä on huomattava, ettei Java-ohjelman käynnistäminen itsessään luo pääluokan oliota. Esimerkiksi kun edellä toteutettu esimerkkiohjelma aloittaa suorituksensa luokassa LegalDate määritetystä main-funktiosta, ei LegalDate-oliota ole olemassa. Siten esimerkiksi main-funktiosta voi suoraan sellaisenaan viitata vain luokan LegalDate staattisiin jäseniin. Toki ei-staattisiinkin jäseniin voisi viitata, jos ensin loisi luokan LegalDate olion ja tekisi viittauksen kyseisen olion kautta (myöhemmin vastaan tulevassa linkitetyn pinon toteuttavassa esimerkissä on mukana testausta varten main-funktio, jossa tehdäänkin näin).

Edellinen esimerkkiohjelma ei sisältänyt yhtään silmukkaa. Alla on vielä toinen esimerkki, joka havainnollistaa Javan silmukoita. Niidenkin syntaksi on pitkälti suoraan C-kielestä. Koodi etsii merkkijonon “tuni” esiintymä komentoriviparametrien sisältä käyttäen yksittäisten merkkien vertailuja. Tässäkin voisi hyödyntää myös valmiita apufunktioita.

// Jos kokeilet esim. suoritusta "java TuniSearch use your opportunities", pitäisi
// ohjelman löytää merkkijono "tuni" parametrin args[2] = "opportunities" sisältä
// ja tulostaa Found "tuni" at index 5 of opportunities.
public class TuniSearch {
  // Tässä esimerkissä kaikki toiminnallisuus on rumahkosti suoraan main-funktiossa.
  public static void main(String[] args) {
    // Muuttuja key on etsittävä merkkiono. Tässä aina "tuni". Lisämääre final on Javan
    // vastine C-kielen const-määreelle: muuttujaan key ei saa enää asettaa uutta arvoa.
    final String key = "tuni";

    // Iteroitavan alkiojoukon läpikäyvä for-silmukka on samanlainen kuin C++11:ssa
    // (ja se oli Javassa jo aikaisemmin kuin C++:ssa). C-kieli ei tällaista tunne.
    // Muuttuja word käy läpi kaikki komentoriviparametritaulukon args alkiot.
    // Tässä on lisäksi annettu uloimmalle silmukalle nimi "SEARCH_LOOP". Nimen
    // avulla on helppoa poistua sisäkkäisestä silmukasta nimetyllä break-lauseella.
    SEARCH_LOOP:
    for(String word : args) {
      // Tavanomainen indeksipohjainen for-silmukka, joka käy läpi kaikki
      // potentiaaliset key:n esiintymäkohdat merkkijonon word sisällä.
      // String-olion pituus saadaan kutsumalla sen jäsenfunktiota length.
      // Huomaa ero taulukoihin: taulukoiden length on jäsenmuuttuja eikä -funktio.
      for(int i = 0; i <= word.length() - key.length(); i++) {
        // Vaihtelun vuoksi while-silmukka, joka tutkii merkki kerrallaan,
        // löytyykö key:n esiintymä alkaen indeksistä i.
        // String-olion jäsenfunktiokutsu charAt(x) palauttaa indeksin x merkin.
        int j = 0;
        while(j < key.length() && word.charAt(i+j) == key.charAt(j)) {
          j += 1;
        }
        // Saatiinko täsmättyä kaikki key.length() merkkiä?
        if(j == key.length()) {
          // Tulostetaan vaihtelun vuoksi format-funktiolla. Se toimii
          // varsin samantapaisesti, kuin C-kielen printf-funktio. Yksi alla
          // näkyvä ero on, että format-funktiossa %n tarkoittaa rivinvaihtoa.
          System.out.format("Found \"tuni\" at index %d of %s%n", i, word);
          // Hypätään pois ulkosilmukasta käyttäen nimettyä break-lausetta.
          // Tavallinen break hyppäisi ulos vain lähimmästä ympäröivästä silmukasta,
          // mutta nimetty break annetun nimen mukaisesta ympäröivästä silmukasta.
          break SEARCH_LOOP;
        }
      }
    }
  }
}

Päätelty muuttujatyyppi var

Javassa voi käyttää funktioiden paikallisten muuttujamääritysten yhteydessä tyyppimäärettä var, jolloin Java päättelee muuttujan tyypin sen alustusarvon tyypin perusteella. Tämä mekanismi toimii ainoastaan ohjelmoijan työtä (mahdollisesti) helpottavana oikopolkuna. Tyyppimääreellä var määritetty muuttuja on ominaisuuksiltaan täysin samanlainen kuin jos muuttujan tyyppi olisi annettu eksplisiittisesti. Javan var toimii pitkälti samalla tavalla kuin C++-kielen tyyppimääre auto.

Koska muuttujan tyyppi päätellään alustusarvon perustella, on tyyppimääreellä var määritellylle muuttujalle luonnollisesti pakko antaa tyypin määrittävä alustusarvo. Tässä ei kelpaa esimerkiksi null tai taulukon pelkkä aaltosulkeiden sisällä annettu alustuslause (taulukon kanssa on käytettävä eksplisiittistä new-operaatiota).

Alla on pieni esimerkkikoodinpätkä.

// Alla new-operaatio auttaa päättelemään var → Integer[].
var it = new Integer[]{1, 2, 3, 4, 5};
for(var i : it) {   // var → Integer, koska it on Integer-taulukko.
  var d = 1.5 + i;  // var → double, koska arvo 1.5 + i on double.
}
// Alla var → String[], koska split palauttaa String-taulukon.
var parts = "1.1.2022".split("\\.");

Tyyppimäärettä var voinee suositella käytettäväksi lähinnä monimutkaisten ekslisiittisten tyyppimääreiden tilalla (jos silloinkaan). Tästä tulee myöhemmin ajoittaisia esimerkkejä.

Javassa voi määrittää vaihtelevan määrän parametreja ottavan funktion kirjoittamalla funktiomäärityksen parametrilistassa viimeisen (tai ainoan) parametrin tyypin perään kolme pistettä. Efekti on, että funktiota kutsuttaessa kyseisen parametrin tilalla voi antaa nollan tai useampia kyseisen tyyppisiä parametreja. Funktion sisällä vaihteleva parametrimuuttuja käyttäytyy aivan kuin se olisi alunperinkin ollut taulukko. Taulukko sisältää kutsussa annetut vaihtelevaa parametria vastaavat parametrit (ja taulukon pituus voi siis olla myös 0, jos kutsussa ei annettu yhtään vaihtelevaa parametria). Koska vaihteleva parametri tulkitaan pinnan alla lopulta taulukoksi, Java sallii vaihtelevien parametrien välittämisen funktiolla myös jo alunperinkin eksplisiittisesti taulukkona. Tämä tekee vaihtelevien parametrien käytöstä kohtalaisen joustavaa.

Alla on esimerkkinä kahden tai useamman int-arvon maksimin laskevan funktion toteutus. Tämä voisi periaatteessa olla joskus jopa hyödyllinen, koska Javan valmis maksimifunktio Math.max(int, int) hyväksyy vain täsmälleen kaksi parametria.

import java.util.Arrays;  // Testikoodi käyttää luokkaa java.util.Arrays.

public class Max {

  // Otetaan kaksi ensimmäistä arvoa tavallisina parametrina, jotta funktiolle
  // on pakko välittää vähintään kaksi parametria eli on jotain laskettavaa.
  public static int max(int first, int second, int... rest) {
    int maxVal = (first > second) ? first : second;
    for(int val : rest) {
      if(maxVal < val) {
        maxVal = val;
      }
    }
    return maxVal;
  }

  // Testausta varten main-funktio, joka muuntaa komentoriviparametrit
  // int-arvoiksi ja laskee niiden maksimeja.
  public static void main(String[] args) {
    int[] it = new int[args.length];
    for(int i = 0; i < args.length; i++) {
      it[i] = Integer.parseInt(args[i]);
    }

    // Ensin muutama testi, joissa maksimi lasketaan erillisiä parametreja
    // käyttäen: kutsut max(it[0], it[1]), max(it[0], it[1], it[2]) ja
    // max(it[0], it[1], it[2], it[3]).
    if(it.length >= 2) {
    System.out.format("Max(%d, %d) = %d%n", it[0], it[1],
                                        max(it[0], it[1]));
    }
    if(it.length >= 3) {
      System.out.format("Max(%d, %d, %d) = %d%n", it[0], it[1], it[2],
                                              max(it[0], it[1], it[2]));
    }
    if(it.length >= 4) {
      System.out.format("Max(%d, %d, %d, %d) = %d%n", it[0], it[1], it[2], it[3],
                                                  max(it[0], it[1], it[2], it[3]));
    }

    // Sitten vielä lopuksi testi, jossa lasketaan kaikkien parametrien
    // (jos niitä ainakin kaksi) maksimi yhdellä funktiokutsulla. Koska parametrien
    // määrää ei tiedetä etukäteen, ei tätä voi tehdä luettelemalla parametrit yksitellen.
    // Annetaan sen sijaan kahden ensimmäisen jälkeiset parametrit valmiina taulukkona.
    if(it.length >= 2) {
      // Arrays.copyOfRange(t, a, b) kopioi taulukon t osan a..b-1 uuteen taulukkoon.
      int[] rest = Arrays.copyOfRange(it, 2, it.length);
      // Arrays.toString luo primitiivitaulukosta järkevähkön merkkijonoesityksen.
      System.out.format("Max(%d, %d, %s) = %d%n", it[0], it[1], Arrays.toString(rest),
                                                              max(it[0], it[1], rest));
    }
  }
}

Jos suoritat edellisen testikoodin tapaan java Max 7 2 6 9 13 2 35 26, pitäisi tulostua:

Max(7, 2) = 7
Max(7, 2, 6) = 7
Max(7, 2, 6, 9) = 9
Max(7, 2, [6, 9, 13, 2, 35, 26]) = 35

Java-lähdekooditiedoston rakenne

Java-ohjelman lähdekoodi koostuu yhdestä tai useammasta lähdekooditiedostosta. Selvästi yleisimmin (ja myös tällä kurssilla) käytetty Java-kääntäjä javac vaatii, että lähdekooditiedostoilla on “.java”-tiedostopääte.

Edelliset esimerkit ovat jo antaneet jonkinlaisen käsityksen Javan lähdekooditiedoston rakenteesta. Tarkemmin ottaen Java-spesifikaatio määrää, että Java-lähdekooditiedoston on noudatettava seuraavaa rakennetta:

  1. Alussa voi olla yksi pakkausmääritys.

    • Pakkauksia käsitellään yksityiskohtaisemmin myöhemmin. Mutta todettakoon jo tässä, että niiden rooli muistuttaa C++:n nimiavaruuksia tai Pythonin moduleita.

  2. Seuraavaksi voi olla import-lauseita, jotka tuovat näkyviin muualla määritettyjä luokkia, rajapintoja tai niiden staattisia jäseniä.

    • Vrt. Pythonin import-lauseet ja C++:n include-direktiivit.

  3. Lopuksi voi tulla luokka- tai rajapintamäärityksiä.

    • Rajapintoja käsitellään myöhemmin. Tässä vaiheessa voit ajatella niitä eräänlaisina luokkina.

    • Suositus (esim. Googlen Java Style Guide): määritä vain yksi päätason luokka per kooditiedosto.

      • Päätason luokalla tai rajapinnalla tarkoitetaan luokkaa tai rajapintaa, joka on määritetty suoraan kooditiedoston rungossa (eikä muun luokan tai rajapinnan sisällä).

Java-kooditiedoston rakenne osaltaan ilmentää sitä, että Java on varsin vahvasti oliopohjainen ohjelmointikieli. Toiminnallista Java-koodia voi määrittää ainoastaan luokkien tai rajapintojen sisällä; niiden ulkopuolella saa olla ainoastaan pakkausmäärityksiä ja import-lauseita. Koska Javassa ei voi määrittää globaaleita muuttujia tai funktioita, on myös ohjelman suorituksen alkupisteen määrittävä main-funktio pakko toteuttaa jonkin luokan jäsenfunktiona, ja Java-ohjelmassa on aina oltava vähintään yksi luokka.

Vahva suositus, ja monin paikoin javac-kääntäjän ehdoton vaatimus: nimeä Java-lähdekooditiedosto sen päätason luokan tai rajapinnan mukaisesti. Esimerkiksi jos määrität päätason luokan TheBestClassEver, anna sen kooditiedoston nimeksi TheBestClassEver.java. Esim. theBestClassEver.java ei kelpaa, vaikka erona on ainoastaan ensimmäisen kirjaimen koko.

Alla on esimerkkiohjelma, joka havainnollistaa Java-kooditiedoston yleistä rakennetta. Esimerkin main-funktio tulostaa tämänhetkisen päivämäärän tiedot usealla eri kielellä hyödyntäen Javan luokkakirjaston päivämäärien ja paikallisten asetusten (“localet”) käsittelyyn tarjoamia apuvälineitä.

// Huomio: tämä koodi pitäisi asettaa tiedostoon WhatDayIsIt.java.

// Määritetään tiedoston sisältö kuulumaan
// pakkaukseen "fi.tuni.itc.programming3".
// Tiedostossa saa olla korkeintaan yksi pakkausmääritys.
package fi.tuni.itc.programming3;

// Kooditiedostossa käytetään Javan standardikirjaston pakkauksen
// java.text luokkaa DateFormat ja pakkauksen java.util luokkia Date ja
// Locale. Ne pitää tuoda erikseen näkyviin import-lauseilla.
import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;

// Päätason luokkamääritys.
// Suositus: vain yksi päätason luokka/rajapinta per tiedosto.
public class WhatDayIsIt {
  public static void main(String[] args) {
    Date now = new Date();  // Alustetaan Date-olio, joka vastaa nykyhetkeä.
    // Esimerkissä käytetättävät localet: Saksa, Japani, Korea ja USA
    Locale[] locales = {Locale.GERMANY, Locale.JAPAN,
                        Locale.KOREA, Locale.US};
    System.out.println("What day is it now in different locales?");
    // Käydään läpi edellä locales-taulukkoon asetetut neljä localea.
    for(Locale locale : locales) {
      // Tulostetaan rivin alkuun localen nimi amerikanenglanniksi (= Locale.US).
      System.out.print("In " + locale.getDisplayName(Locale.US) + ": ");
      // Alustetaan DateFormat-olio df, joka muotoilee Date-olion esittämän
      // päivämäärän sille parametrina annetun localen mukaisesti
      DateFormat df = DateFormat.getDateInstance(DateFormat.FULL, locale);
      // Kutsu df.format(now) muotoilee Date-olion now esittämän päivämäärän.
      System.out.println(df.format(now));
    }
  }
}

Tämä esimerkkiohjelma olisi esimerkiksi perjantaina 19.11.2021 suoritettuna tuottanut tulostuksen:

What day is it now in different locales?
In German (Germany): Freitag, 19. November 2021
In Japanese (Japan): 2021年11月19日金曜日
In Korean (South Korea): 2021년 11월 19일 금요일
In English (United States): Friday, November 19, 2021

Välihuomautus

Jos haluat kääntää ja ajaa edeltävän esimerkkiohjelman omalla koneellasi, kopioi koodi tiedostoon WhatDayIsIt.java. Tässä vaiheessa kurssia kannattaa poistaa koodin alussa oleva pakkausmääritys, jolloin kääntäminen ja ajo onnistuu suoraviivaisesti tapaan javac WhatDayIsIt.java ja java WhatDayIsIt. Pakkausten käytön mukanaan tuomia lisävaatimuksia käsitellään myöhemmin.

Javac-kääntäjä tuottaa jokaista eri luokkaa ja rajapintaa kohden erillisen “.class”-tiedostopäätettä käyttävän luokkatiedoston, jonka nimi vastaa asianomaisen luokan tai rajapinnan nimeä. Esimerkiksi HelloWorld-luokan kääntäminen tuottaa luokkatiedoston HelloWorld.class riippumatta siitä, minkä nimisessä lähdekooditiedostossa HelloWorld-luokka on määritetty. Samoin jos yksittäinen kooditiedosto sisältää useita eri luokkia tai rajapintoja, tuottaa kääntäjä jokaista luokkaa ja rajapintaa kohden oman erillisen luokkatiedoston. Tämäkin osaltaan kuvastaa Javan oliokeskeisyyttä: ajettava ohjelma koostuu joukosta käännettyjä luokkatiedostoja.

Luokkatiedostot sisältävät Java-virtuaalikoneen ymmärtämää tavukoodia. Jotta ohjelman voisi suorittaa, täytyy ainakin yhden sen luokan määrittää muotoa void main(String[] args) oleva staattinen julkinen jäsenfunktio. Ohjelma käynnistetään antamalla Java-virtuaalikoneelle parametriksi sen luokan nimi, jonka main-funktiosta ohjelman suoritus halutaan aloittaa. Javassa, kuten Pythonissakaan, ei ole esimerkiksi C++-kielen tapaista rajoitusta, että ohjelmaan voisi sisältyä vain yksi main-funktio.

Välihuomautus

Käännettyjä luokkatiedostoja voi suorittaa sellaisenaan missä tahansa käännöksessä käytettyä Java-versiota tukevassa Java-virtuaalikoneessa. Jos esim. olet kääntänyt Hello World -ohjelman luokkatiedostoon HelloWorld.class, voit kokeilla kopioida tiedoston sellaisenaan esimerkiksi niin Java-virtuaalikoneen omaavalle Windows-, Linux- kuin Mac-koneellekin ja ajaa sen kussakin tavanomaiseen tapaan komennolla java HelloWorld.

Hieman isompi esimerkki Java-ohjelmien siirrettävyydestä eri järjestelmien välillä voisi olla esimerkiksi Netbeans IDE, joka on toteutettu Javalla. Netbeansista on ladattavissa versio, joka vain puretaan johonkin tiedostohakemistoon ja ajetaan ilman erityisempää “asennusta”. Jos tämä hakemisto sijaitsisi esimerkiksi ulkoisella USB-kiintolevyllä, voisi kyseistä Netbeans-ympäristöä ainakin periaatteessa käyttää helposti sellaisenaan millä tahansa koneella, jossa on Java asennettuna ja joka on yhteensopiva kyseisen USB-kiintolevyn tiedostojärjestelmän kanssa.