JDK 1.5:n uudet ominaisuudet

Javan versio 1.5 toi uusia ominaisuuksia Javan kalustoon, tässä pyrimme esittelemään niistä tärkeimmät. Suurilta osin lisäykset ovat varsin C-henkisiä, mutta esimerkiksi for-each silmukka on tuttu Pythonin for -silmukkana. Osa kielen uudistuksista on syntaktista sokeria (Autoboxing), mutta osaa voisi pitää kauan odotettuina (Generics). Selvityksessä on myös pari API:n uudisusta, kuten JDK:n luokka Scanner sekä muotoiltu ulostulo (Formatted Output). Lähteenä on käytetty SUN:in dokumenttia Java 5:n uusista ominaisuuksista, joka löytyy täältä.

Scanner, luokka tyypitettyyn lukemiseen

Scanner-olion ideana on, että se voi paloitella ison syötteen tekstiä ns. tokeneihin(suomennos on poletti, joka on kömpelö), jotka voidaan välittää ohjelmalogiikalle. Scannerissa on tyypitykset valmiina, joten luokan käyttäjän ei tarvitse vaivautua tekemään koodia merkkijonon muuntamiseksi eri perustyypeille (int, long ...double). Scanneriin voidaan sijoittaa mikä tahansa vuo(stream), vaikkapa iso liuta rivinvaihdolla erotettuja lukuja tekstitiedostosta. Sijoituksen jälkeen merkkijonoja sekä lukuja voidaan kysellä yksi toisensa jälkeen sopivilla metodeilla. Scanner muistuttaa etäisesti luokkaa StreamTokenizer, joka on roikkunut 1.0 API:sta lähtien. Omakohtaiset kokemukseni StreamTokenizerista ovat kamalat.

Tässä esimerkki Scannerin käytöstä. Lähdekoodina on ScannerExample.java.
public static void main(String[] args)
{
  String syote = "jee 1 jee 2";
  Scanner s = new Scanner(syote);
  System.out.println(s.next());
  System.out.println(s.nextInt());
  System.out.println(s.next());
  System.out.println(s.nextInt());
}
Seuraavia seikkoja on hyvä ottaa huomioon Scanneria käytettäessä.

Lue - luokkaa käytetään ohjelmoinnin peruskursseilla syötteen lukemiseen. Sopisiko Scanner sitten luokan Lue - toiminnallisuuden korvaajaksi?

Scannerin puolesta: Scanneria vastaan:

Vaihtuvamääräiset parametrilistat (varargs)

Varargsit ovat näppärä tapa määritellä metodi vastaanottamaan mielivaltainen lukumäärä tietyntyyppisiä parametreja. Ennen vaihtelevamääräiset parametrit jouduttiin antamaan taulukossa, mutta nyt metodin nimessä voidaan määritellä vaihteleva määrä parametreja. Varagsien julistaminen tapahtuu "Olio... viittausnimi" - konstruktiolla. Metodin sisällä viittausnimi näkyy tavallisena taulukko-oliona.

Tässä esimerkki. Lähdekoodina on VarArgs.java.
public static void main(String [] args)
{
  // parametrien määrä voi olla mielivaltainen
  tee(2,3,5,6,76,253);

  //Tämä lause demonstroi käytttää JDK1.5:n autoboxing-ominaisuutta, josta lisää muualla tehtävässä.
  Integer[]params = {2,3,5,6,76,253};

  vanhaTee(params);

  //Huom! Varargs-metodille voi myös syöttää taulukon. 
  //Tämä takaa yhteensopivuuden vanhojen ohjelmien kanssa, vaikka rajapinta hieman muuttuisikin.
  tee(params);

}
//Vaihtuva määrä Integer -olioita 
public static void tee(Integer... params)
  {
   for(int i = 0; i < params.length; i++)
     {
	  System.out.println(params[i]);
	  }
  }

  // Vanhaan tyyliin, metodin runko on samanlainen tee():n kanssa.
  public static void vanhaTee(Integer[] params)
  {
   for(int i = 0; i < paramams.length; i++)
	 {
	   System.out.println(params[i]);
	 }
   }

Vain viimeinen parametri voi olla vaihtuvamääräinen! Arvojen assosioiminen parametrien nimiin olisi mahdotonta, jos vaikkapa void makeData(Object...a, Object b) olisi hyväksytty.

Kaksi sääntöä pätee:
  1. Jos teet luokan julkista rajapintaa, käytä varargsia säästeliäästi.
  2. Jos käytät luokkaa jossa on varargs-kutsu, käytä sitä estoitta.

Vaihtuvamääräisiä parametreja on mahdollista käyttää ylikuormitetusti, mutta se ei ole hyvä idea. Luokan käyttäjien on silloin hankala selvittää, mitä metodia kutsutaan. Katso (epä)selventävänä esimerkkinä VarArgsSotku.java. Esimerkissä ei ole mitään intuition vastaista, mutta ohjelmoijalle on työlästä selvittää mielessään kutsuttava metodi.

Muotoiltu tulostus (formatted output)

JDK 1.5:n myötä merkkijonoja voi myös muotoilla C-kielestä tuttuun tyyliin. Uusi luokka, Formatter toimii tulkkina printf-tyylisille merkkijonoille. Idea on se että syötemerkkijonoon upotetaan erityisiä merkkijonoja, jotka luokka tulkkaa, yleensä numeroiksi. Javan toteutuksessa C:hen on tiettyjä eroja. java heittää poikkeuksia herkästi huonosta syötteestä joka on ristiriitainen annettujen olioiden kanssa. C:ssä ei poikkeuksia ole, mutta siellä muotoiluntulkitsija on poikkeuksista hiljaa. Myöskään tulostuksessa käyttämättä jääneet olioviittaukset eivät aiheuta poikkeuksia. Esimerkeissä käytetään paljon myös JDK1.5:n muita ominaisuuksia, kuten autoboxingia ja varargsia, jotka tuovat lisää C-kielen kaltaisia konstrukteja javaan. Muotoilun etu on se että nyt vaikkapa konfiguraatiotiedostoon purkitetut merkkijonot ovat paremmin kustomisoitavissa. Myöskin omia olioita voi käyttää muotoillussa ulostulossa, jos ne toteuttavat Formattable - rajapinnan. Tietyissä asioissa, kuten viikonpäiviä tulostaessa, Formatter käyttää locale-tietoja lokalisoitua kieliasua varten.

Tässä muutama esimerkki tiedostosta FormatExample.java

Ensimmäinen parametri on muotoiltava merkkijono. %d -erikoismerkit viittaavat järjestyksessä niitä seuraaviin arvoihin, jotka ovat kokonaisluvut i sekä i*i. %d myöskin kertoo tulosteeseen tulevan desimaalimuotoinen kokonaisluku. Jos annamme vääräntyyppisen olion, jvm heittää poikkeuksen. %n tarkoittaa C:n tyylistä, laitteistoriippumatonta rivinvaihtoa, toki "\n"-yhdistelmää voi myös käyttää.

i = 5      
pw.format("Luku i on %d ja sen neliö on %d%n", i , i*i);	
Nyt tulostellaan liukulukuja. erikoismerkki pohjalla on %f, mutta siinä on käytetty viittausta $x parametriin, missä x on olion paikka parametrilistassa.
pw.format("%1$f on %1$f", i + 0.5);	
pw.format("Luvun i neliöjuuri on: %3$f , joten luku itse on %1$d ja sen kuutio on %2$d  %n",i,i*i*i,Math.sqrt(i));
Ennen tyyppimäärettä voi pistää myös kentän leveyden, sekä muita tulostukseen vaikuttavia lipukkeita(flags). Neliöjuuri tulostuu kolmen desimaalin tarkkuudella. itse luvun esitys vie kolme merkkiä ja sisältää nollia tyhjälle tilalle. Kuutio vie neljä merkkiä ja on tasattu oikealle.
pw.format("Luvun i neliöjuuri on: %3$.3f , joten luku itse on %1$03d ja sen kuutio on %2$4d %n",i,i*i*i,Math.sqrt(i));
Päivämääriä on mahdollista muotoilla paikallisten kieliasetusten mukaisesti. Allaoleva esimerkki puhuu suomea, jos JVM puhuu.
Calendar cal = new GregorianCalendar();	 
pw.format("Tänään on %1$tA, %1$td. %1$tBta  %1$tY %n", cal);

Autoboxing

Autoboxing -ominaisuuden ansiosta ei ohjelmoijan tarvitse huolehtia olioiden tai alkeistyyppien ns wrappauksesta (ns. käärimisestä, paketoinnista), joka mahdollistaa sen, että esim. int alkeistyypin muuttujia voidaan suoraan sijoittaa Integer -olioille tarkoitettuun tietorakenteeseen, siten ettei ohjelmoijan tarvitse tehdä tyyppimuunnosta Toisaalta, Integer -olioita voidaan myös suoraan sijoittaa int alkeistyypin arvoksi.

Autoboxing-esimerkki:
Object x;
Integer y;
int i;

x = 1; // Sallittu koska Integer-oliolle voidaan antaa numeerinen arvo ja Integer-luokka on Object-luokan aliluokka.
y = 1; // Sallittu koska Integer-oliolle voidaan antaa alkeistyypin arvo autoboxingin avulla.

System.out.println(x); // Sallittu koska Object-luokalla on toString -metodi.
System.out.println(y); // Sallittu koska Integer-luokalla on toString -metodi.

i = y; // Sallittu koska autoboxingilla voidaan antaa numeerisen arvon omaavan olion arvo alkeistyypille.
i = (int)y // Sallittu koska Integer-oliolla on numeerinen arvo joka voidaan tyyppimuuntaa int-alkeistyypin arvoksi.
i = y.intValue();//Sallittu koska Integer-oliolla on intValue()-metodi joka palauttaa Integer-olioon talletetun int-arvon.
x = y; // Sallittu koska Integer-aliluokka toteuttaa Object-yliluokan.

i = x; // Object-oliota ei voida sijoittaa int-alkeistyypin arvoksi. Object-luokalla ei ole välttämättä int-arvoa.
i = (int)x; // Object-oliota ei voida muuntaa int-alkeistyypiksi.
i = x.intValue(); // Object-olio ei toteuta metodia intValue()
y = x; // Object-olio ei toteuta Integer-oliota vaan päinvastoin.

Generics

Javan geneerisyyden ideana on, että sen avulla voidaan ohjelmoida metodeita ja olioita siten, että käytettävää tietotyyppiä ei tarvitse tietorakenteelle sitovasti ilmoittaa vaan se voi olla mitä tahansa. Tietotyyppi määritellään siis metodin kutsussa tai olion luonnin yhteydessä. Tämän etu on siis se, että esimerkiksi jono-tietorakennetta ei tarvitse erikseen ohjelmoida eri tietotyypeille esim Integer- tai String -olioille vaan molemmille voidaan käyttää samaa geneerisesti ohjelmoitua rakennetta. Tietorakenne-oliota kutsuttaessa vain ilmoitetaan mitä tietotyyppiä sinne voidaan tallettaa. Ennen javan 1.5 versiota ollaan voitu ohjelmoida vastaavasti, siten että ollaan luotu tietorakenne Object -tietotyypille. Tähän tietorakenteeseen on siis voitu tallettaa kaikkia Object-olion aliluokkien ilmentymiä. Ongelma tässä on kuitenkin se, että tämänkaltaiseen tietorakenteeseen on voitu tallettaa mitä tahansa Object -luokan aliluokkien ilmentymiä, jolloin mikään ei ole estänyt tallettamasta String-oliota Integer-olioille tarkoitettuun tietorakenteeseen Integer -olioiden sekaan. Tämä on siis mahdollistanut sellaisten virhetilanteiden syntymisen, joka on ilmennyt vasta myöhemmin tietorakennetta käytettäessä, jonka johdosta vian paikallistaminen ohjelmakoodissa on ollut hyvin vaikeaa.

Generics-esimerkki, katso myös Generics.java
	public class Generics {
		public static void main(String[] args) {		
			
			// Luodaan pino johon voidaan asettaa vain Integer-olioita.			
			Pino intPino = new Pino();
						
			intPino.push(new Integer(55));

			//Tämä kaataisi ohjelman väärän tyypin takia
			//pino.push(new String("kjshdjdn")); 
			
			//Vaikka a on perustietotyyppi, integerin voi sijoittaa siihen kiitos autoboxingin.			
			int a = intPino.pop();			
			System.out.println(a);		
			// Luodaan pino johon voidaan asettaa vain String-olioita.			
			Pino strPino = new Pino();					
			strPino.push(new String("abc"));						
			System.out.println(strPino.pop());
		}
	}

	public class Pino  {

	       private T[] pino;
	       private int huippu = -1;

	       public Pino() {
		       pino = (T[]) new Object[100];
	       }
	       public void push(T alkio){
		      huippu++;
		      pino[huippu] = alkio;
	       }
	       public T pop() {
		      huippu--;
		      return pino[huippu+1];
	       }
	       public boolean empty() {
		      if (huippu == -1)
			 return true;
		      else
			 return false;
  	       }
        } 

Static Importing

Yksittäisiä metodeita voidaan tuoda ohjelmaan import-määreellä:
import static pakkuksen_nimi.luokan_nimi.metodin_nimi
Kokonaisia luokkia voidaan importoida komennolla:
import static pakkuksen_nimi.luokan_nimi.*

Luokkakohtaisesta tuonnista on se hyöty, että ohjelmakoodissa ei tarvitse viitata metodin sijaintiin pakkauksen ja luokan nimillä vaan ne voidaan jättää pois ja käyttää viittaukseen pelkkää metodin nimeä. Tämän toiminnon käyttö tietyissä rajoissa selkeyttää ohjelmakoodia varsinkin jos ollaan importattu pelkkiä yksttäisiä metodeita. Importtaamalla kokonaisia luokkia tilanne voi muuttua päinvastaiseksi, jolloin ohjelmakoodista tulee sekavaa koska ei voida tietää missä metodin koodi itse asiassa sijaitsee. Luokkakohtaista tuontia on käytetty esimerkissä ForLoop.java

For-each-loop

For -loopin uudella ominaisuudella voidaan selkeyttää ohjelmakoodia tilanteissa, joissa sitä käytetään koko arvoalueen läpikäyntiin. Esimerkiksi taulukon läpikäynti voidaan tehdä seuraavasti: for(int i : taulukko) {...}. Tämä käy siis läpi koko taulukon ja suorittaa jokaiselle alkiolle halutut operaatiot. Tämä uusi For -loop tulisikin lukea For each eli jokaiselle. Tämä ei siis sovellu jos haluttaisiin käydä vain osa arvoalueesta läpi tai arvoaluetta haluttaisiin muokata. Tässä mukaelma tiedostosta ForLoop.java.

int[] taulukko = new int[10];
//Alustetaan taulukko vanhaan tyyliin, koska for-eachilla ei voi muuttaa taulukon arvoja
for (int i = 0; i < 10; i++) 
taulukko[i] = (int)(10 * Math.random());

for (int i : taulukko)
System.out.print(i+"\t");

Enums, eli luetellut tyypit

Jos Javassa on halunnut luetella mahdollisia parametreja, ohjelmoijan on pitänyt kirjoittaa paljon final-muuttujia tyyliin MONTH_JANUARY =1. Nyt C-mäisyys nimeltä enum muuttaa asiat paremmiksi. Ensinnäkään nimille ei tarvita etuliitettä (MONTH_), ja toiseksi luokkaa käyttävät koodit eivät voi kutsua metodeita tyyliin setMonth(1), joka tarkoittaisi samaa kuin setMonth(MONTH_JANUARY). Javan luetellut tyypit, toiseksi vaihtoehtojen lisäys ja muuttaminen vähemmän todennäköisesti hajottaa luokkaa käyttäviä luokkia. Enumit ovat myös semanttisesti paljon parempia kun enumin tulostaessa tuleekin itse enumin nimi eikä jokin kumma kokonaislukuvakio. Javan enumit ovat paljon hienompia kuin C:ssä kun ne saa toimimaan sisäluokan kaltaisesti.

Tässä Esimerkki: Sisäluokkamaista demonstraatiota on tiedostossa EnumExample.java
class EnumExample
{

    //Tässä on enum a'la C/C++. Vanha tyyli menisi LISUKE_RIISI = 1... etc
    public enum Lisuke
    {RIISI,RANSKALAISET,KASVISPETI,HIRSSIPUURO,PERUNAMUUSI}
   
    private Lisuke lisukkeella;

    public EnumExample(Lisuke l)
    {
	lisukkeella = l;
    }

    public String toString()
    {
	return("Lisukkeena on " +lisukkeella);
    }

    public static void main(String[] args)
    {
	//Tulostetaan kaikki annos-lisuke kombinaatiot
	
	for (Lisuke l : Lisuke.values())
	{
	     System.out.println(new EnumExample(l));
	}
    }
}