******************************** Ohjelmoinnin jatkokurssi Kurssikoe 8.12.2008/AW Tehtävä 3, arvosteluperusteita Sami Nikander ******************************** Tehtävässä piti laskea käyttäjän ilmoittamien sanojen esiintymisfrekvenssit tiedostossa. Keskeisiä aihepiirejä olivat syötteen lukeminen tiedostosta sekä tietorakenteen käyttö tuloskirjanpitoon. 1. Tiedoston lukeminen (9 p) 1.1 Tiedostonkäsittely ja poikkeukset (6 p) Käytetty Scanner- ja File-luokkia oikeaoppisesti tiedoston lukemiseen. Erityisesti tuli ymmärtää, että yhdellä Scanner-oliolla voi lukea tiedoston vain kerran alusta loppuun, eikä "lukupää" maagisesti siirry takaisin tiedoston alkuun millään ilveellä (ks. kohta Logiikkavirheet). Käsitelty tiedosto-Scannerin luontiin liittyvät tarkistetut poikkeukset(checked exception) ja reagoitu poikkeustilanteisiin havainnollisilla virheilmoituksilla. Virheistä toipumista ei vaadittu; ohjelma sai päättyä virhetilanteeseen kunhan tämä tapahtui "kauniisti" ja virheen syy tuli selväksi käyttäjälle. Erityisesti piti huomata, että poikkeuskäsittelyä tarvitaan nimenomaan Scanner-olion luonnissa, ei File-olion luonnissa. Tiedoston olemassaolon tarkistus File-luokan exists()-metodilla on kaunis tapa, mutta ei yksinään riitä: kääntäjä vaatii Scannerin luonnille poikkeuskäsittelyn, vaikka ohjelmoija itse olisikin varma tiedoston olemassaolosta. 1.2 Syötteen käsittely (3 p) Tutkittu syötteen loppuminen hasNext()-metodilla ennen lukuyritystä. Osattu pilkkoa tiedoston sisältö yksittäisiksi sanoiksi, jotta tiedoston sanoja voidaan vertailla tutkittaviin sanoihin. Tässä yksinkertaisin tapa on Scanner-luokan next()-metodi, joka palauttaa syötteestä (tiedostosta) suoraan seuraavan "sanan" (=ei-tyhjän String-merkkijonon). Monimutkaisemmistakaan tavoista ei toki sakotettu. Osattu lukea käyttäjän syötettä (tutkittavat sanat, tiedoston nimi) interaktiivisesti Scanner-luokan avulla (tiedoston nimen sai antaa myös esim. komentoriviparametrina). 2. Tietorakenteen käyttö frekvenssien laskennassa (9 p) 2.1 Esiintymien laskenta, toimintalogiikka (4 p) Kyselty etsittävät sanat käyttäjältä ja kirjattu ne talteen sopivaan tietorakenteeseen (HashMap tai vastaava) odottamaan käsittelyä. Laskettu sanojen kaikki esiintymät tiedostossa tehtävänannon mukaisesti ja tulostettu selkeä esitys (Hashmap.toString() riitti) kunkin sanan esiintymistä. Periaatteessa frekvenssit saattoi laskea myös sana kerrallaan ja tulostaa vastauksen kunkin sanan osalta erikseen (jolloin sanoja ei tarvitse tallettaa odottamaan käsittelyä), mutta tällöin seurauksena oli miinuspisteitä tiedostonkäsittelyn tehottomuudesta (ks. kohta Tyylivirheet). 2.2 Tietorakenteen käyttö (5 p) Käytetty HashMapia (tai vastaavaa itse laadittua tietorakennetta) sanojen ja esiintymien lukumäärien kirjaamiseen -pareina. Pisteet sai tietorakenteen asianmukaisesta käytöstä kaikissa prosessin vaiheissa: luonnista, alustuksesta (= sanojen lisääminen), aksessoinnista (= esiintymälukumäärien päivitys) ja läpikäynnistä (= sisällön tulostusta varten). HashMapin API-kuvauksen ymmärtämällä ja sitä soveltamalla pääsi helpoimmalla. Ilman HashMapiakin pääsi samaan lopputulokseen esim. parittamalla sanat ja lukumäärät kahteen rinnakkaiseen taulukkoon (String[] ja int[]) ja indeksoimalla niitä "yhteisellä" indeksimuuttujalla. Pisteitä vähennettiin, jos em. kohtien toteutuksessa oli puutteita. Lisäksi kokonaispisteitä vähennettiin seuraavista virheistä ja ongelmista: Logiikkavirheet -2..-4 p Nämä olivat virheitä, jotka aiheuttivat virheellisiä tuloksia tai muutoin estivät ohjelman oikeellisen toiminnan: - algoritmi löytää vain etsityn sanan 1. esiintymän rivillä (rivillähän voi olla useampiakin samoja sanoja!) - algoritmi tutkii vain kokonaisia rivejä, ei yksittäisiä sanoja - tiedosto luetaan Scannerilla loppuun, mutta sitä yritetään lukea hetken päästä uudestaan alusta (ainoa keino päästä takaisin alkuun on luoda uusi Scanner-olio samasta tiedostosta) - muita vastaavia virheitä tapauskohtaisesti Tyylivirheet -1..-2 p Nämä olivat virheitä, jotka eivät aiheuttaneet vääriä tuloksia, mutta vaikuttivat toteutuksen suorituskykyyn tai selkeyteen merkittävästi: - luetaan tiedosto uudestaan alusta loppuun jokaisen kyseltävän sanan kohdalla (tämä on hyvin tehoton ratkaisu, sillä tiedoston lukeminen on paljon hitaampi operaatio kuin tietorakenteen läpikäynti!) - lasketaan turhaan koko tiedoston *kaikkien* sanojen frekvenssit, kun riittäisi laskea käyttäjän syöttämät sanat - koko ohjelma (main-metodi) on yhden ison try-catch -lohkon sisällä, jolloin virhekohtaa ei voida paikallistaa eikä virheen tarkkaa syytä ilmoittaa käyttäjälle Sekalaiset huolimattomuusvirheet -1..-2 p - virheet merkkijonojen käsittelyssä, esim. samuusvertailu (s == "") equals-metodin sijaan, tai sijoitusoperaattori (=) vertailuoperaattorin (==) sijaan - muuttujien tyyppejä sekoitettu, esim. verrattu char-muuttujaa null-arvoon - virheet ehtolauseessa, esim. silmukasta ei ikinä tulla pois - yleisesti tarvittava muuttuja (esim. HashMap) esitelty ja alustettu silmukan sisällä, jolloin se alustetaan virheellisesti joka kierroksella uudestaan eikä ole käytettävissä silmukan ulkopuolella - muita vastaavia virheitä tapauskohtaisesti Seuraavilla seikoilla ei ollut vaikutusta arvosteluun: - triviaalit kirjoitus-/syntaksivirheet (satunnaiset puuttuvat puolipisteet, lenght/length jne.) - metodijako tai sen puuttuminen - muuttujien/metodien näkyvyysmääreet - toteutettu omia metodeja/algoritmeja merkkijonojen pilkkomiseen, etsimiseen tai vertailuun (löytyvät valmiina String-luokasta, esim. contains ja equals) ================================================================================ /* * Ohjelmoinnin jatkokurssi * Kurssikoe 8.12.2008/AW * Tehtävä 3 * Esimerkkiratkaisu, laatinut Sami Nikander */ import java.io.*; import java.util.*; public class LaskeSanojenMaara { public static void main (String args[]) { HashMap frekvenssit = new HashMap(); String tiedostoNimi; File tiedosto; Scanner tiedostonLukija; Scanner lukija = new Scanner(System.in); // ----------------------------------------------------------- // TIEDOSTON AVAAMINEN // kysy tiedostonimeä kunnes saadaan kelvollinen // ja avaa ko. tiedosto Scanner-olion luettavaksi while(true) { System.out.println("Mitä tiedostoa tutkitaan? " + "(tyhjä merkkijono keskeyttää ohjelman)"); tiedostoNimi = lukija.nextLine(); if (tiedostoNimi.equals("")) System.exit(0); // File-olion luonnissa ei voi tulla tiedostopoikkeusta... tiedosto = new File(tiedostoNimi); if (!tiedosto.exists()) { System.out.println("Tiedostoa " + tiedostoNimi + " ei löydy!"); } else { // ...mutta Scannerin luonnissa voi! // (HUOM: edes tiedoston olemassaolon tarkistus edellä // ei poista kääntäjän vaatimusta try-catchille!) try { tiedostonLukija = new Scanner(tiedosto); break; // tiedoston avaus onnistui, ulos silmukasta } catch (FileNotFoundException e) { System.out.println("Odottamaton virhe " + "tiedostojärjestelmässä, lopetetaan"); System.exit(-1); } } } // ----------------------------------------------------------- // SANOJEN KYSELY // kysele sanoja kunnes käyttäjä syöttää tyhjän, // lisää sanat frekvenssitauluun alkuarvolla 0 String etsittävä; do { System.out.println("Anna tutkittava sana " + "(tyhjä merkkijono lopettaa sanojen lisäämisen)"); etsittävä = lukija.nextLine(); if (!etsittävä.equals("")) frekvenssit.put(etsittävä, 0); } while(!etsittävä.equals("")); // ----------------------------------------------------------- // ANALYYSI // lue tiedostosta yksi sana kerrallaan, ja jos se on jokin // halutuista sanoista, päivitä ko. sanan laskuria int lkm; String sana; while(tiedostonLukija.hasNext()) { sana = tiedostonLukija.next(); // = seuraava sana! if (frekvenssit.containsKey(sana)) { lkm = frekvenssit.get(sana) + 1; frekvenssit.put(sana, lkm); } } // ----------------------------------------------------------- // RAPORTTI // Tulosta sanojen esiintymismäärät suoraan HashMapista System.out.println("Tiedoston " + tiedostoNimi + " analyysi:"); System.out.println(frekvenssit); } }