Java-ohjelmointi, syksy 2005, koe 12.12.2005 Tehtävän 3 arvosteluperusteet Jaakko Nenonen, 20.12.2005 --------------------------------------------------------------------------------------------------------- Tehtävänanto --------------------------------------------------------------------------------------------------------- Tee ohjelma, joka tarjoaa seuraavan kielenkäänöspalvelun: Ensin ohjelma lukee tekstitiedoston, jossa rivillä 1 olevan alkukielisen sanan käännös on rivillä 2, rivillä 3 olevan alkukielisen sanan käännös on rivillä 4, jne. Siis jokaisen parittoman rivin alkukielisen sanan käännös on seuraavalla parillisella rivillä. Jos tiedostossa on pariton määrä rivejä, viimeisen rivin sana jätetään ottamatta huomioon. Saat olettaa, että kullakin rivillä on vain yksi sana. Koko tiedoston puuttumiseen ja muihin virhetilanteisiin on kuitenkin varauduttava. Muodostettuaan yllä kuvatulla tavalla itselleen sanakirjan ohjelma tarjoaa käännöspalvelun: Kun käyttäjä kirjoittaa sanan alkukielellä, ohjelma joko kertoo sanan käännöksen tai ilmoittaa, että kysytty alkukielinen sana oli tuntematon. Suunnittele ja toteuta itse ohjelman loppuminen. [Koetilanteessa annettiin lisäohjeita suomeksi ja englanniksi: 1. tekstitiedoston nimi pyydetään käyttäjältä, 2. jos alkukielinen sana ei ole yksikäsitteinen, viimeisin jää voimaan, 3. kyseessä ei siis ole assosiaatio "sana-sanalista" vaan "sana-sana".] (17 pistettä) ------------------------------------------------------------------------------------------------------ Yleistä ------------------------------------------------------------------------------------------------------ Tehtävä oli mielestäni aika vaikea mutta siitä huolimatta se osattiin melko hyvin. Arvostelu on tehty melko lempeästi eikä pikkuvirheistä juuri sakotettu. Tällä tavoin saatiin pistejakaumasta melko tasainen, täysien pisteiden ja lähes täysien pisteiden vastauksia oli eniten. Ratkaisutapoja oli useita oikeita. Ratkaisu sai olla perinteinen main-metodiratkaisu (jollainen malliratkaisukin on) tai vaihtoehtoisesti oliotyylinen ratkaisu, jossa olion palveluita olivat sanakirjan muodostaminen tiedostosta, yksittäisen sanan kääntäminen jne. Myös sanakirjan toteutuksia sallittiin useita erilaisia. Helpoin ratkaisu oli hashtable, jossa avaimena ja arvona käytettiin sanaa ja sen käännöstä. Toinen tapa oli käyttää kahta vektoria, joista toiseen laitettiin alkukielen sanat ja toiseen käännökset. Myös yhdellä vektorilla pärjäsi jos oli taitava. Sen sijaan taulukkoon tiedoston sanoja ei voinut lukea, koska tiedoston rivien määrää ei tiedetä kun taulukko alustetaan. Taulukon koon asettaminen 100000:ksi ei ollut mikään ratkaisu. Eikä myöskään se että tiedosto yritettiin lukea läpi kahdesti: ekalla kierroksella selvittämällä rivien määrä ja seuraavalla kierroksella alustamalla taulukko. Taulukon käyttämisestä sakotettiin pisteitä roimasti. Taulukon käyttäjillä oli yleinen harhaluulo että tiedosto_olio.length() palauttaisi tiedoston rivien määrän vaikka se palauttaa tiedoston tavujen määrän. Tiedoston läpikäyntitapoja oli useita oikeanlaisia joita on lueteltu alla esimerkkiratkaisun yhteydessä. Rivimäärältään parittoman tiedoston erikoistapaukseen, jossa viimeisen rivin sana jää ilman käännöstä oli mielestäni keskitytty turhankin paljon. Jos viimeinen rivi käsiteltiin väärin, menetti vain pisteen. Joissakin ratkaisuissa keskityttiin viimeiseen riviin niin paljon että todellisuudessa kaikki rivit menivät jo väärin. Silloin menetti tietysti paljon enemmän pisteitä! Ne jotka käyttivät hashtableä, osasivat käyttää sitä keskimäärin hyvin. Jos sen aksessoreita (put, containskey, get) ei muistanut nimeltään oikein, ei sakotettu pisteitä. Sen sijaan poikkeuskäsittely oli ymmärretty heikommin. Tehtävässä ainut rivi joka aiheutti poikkeuksen oli skannerin alustus: try { Scanner syöttötiedosto = new Scanner(tiedosto_olio); } catch (FileNotFoundException e) { System.out.println("tiedostoa " + tiedosto_olio + " ei löytynyt. Lopetetaan"); return; // tai System.exit(0); } Arvostelun kannalta ei ollut tärkeää oliko ymmärtänyt laittaa try-catchiä juuri oikeaan paikkaan scannerin ympärille kunhan sen teki muuten oikeaoppisesti. Jos oli siis laittanut try-catchin lisäksi näppäimistöltä lukemisen tai tiedoston luonnin tai käsittelyn ympärille turhaan, siitä ei rokotettu pisteitä. Useammassa paperissa näytti siltä ettei kirjoittaja ollut ymmärtänyt että ohjelman suoritus vielä catch-lohkon jälkeenkin jos poikkeus tapahtuu. Siis return; oli unohdettu. Tästä sakotettiin erityisen paljon pisteitä. Toinen paha virhe oli aiheuttaa tarpeettomia poikkeuksia ohjelman normaalissa suorituksessa. Esimerkiksi seuraava on erittäin huonoa ohjelmointityyliä vaikka toimineekin oikein: try { while(true) { alkukieli = tiedosto.nextLine(); käännös = tiedosto.nextLine(); hashtable.put(alkukieli,käännös); } } catch (NoSuchElementException e) { System.out.println("sanakirja on valmis!"); } Kaikkein helpoimmalla ja täysillä pisteillä poikkeuskäsittelyn osalta pääsi kun laittoi main-metodin otsikkoon throws-määreen: public static void main(String[] args) throws Exception {... ------------------------------------------------------------------------------------------------------ Esimerkkiratkaisu ------------------------------------------------------------------------------------------------------ import java.io.*; import java.util.*; public class Translator { private static Scanner lukija = new Scanner(System.in); public static void main(String[] args) throws FileNotFoundException { System.out.println("Anna tiedosto, jossa on sanoja alkukielellä ja käännettynä"); String tiedostonNimi = lukija.nextLine(); File tiedosto_olio = new File(tiedostonNimi); if (!tiedosto_olio.exists()) { System.out.println("Tiedostoa " + tiedostonNimi + " ei löydy!"); return; // keskeytetään kaikki! } Scanner syöttötiedosto = new Scanner(tiedosto_olio); Hashtable sanakirja = new Hashtable(); String alkukielellä = null, käännettynä = null; int rivi = 0; while (syöttötiedosto.hasNextLine()) { rivi++; if (rivi % 2 == 1) // pariton rivi alkukielellä = syöttötiedosto.nextLine(); else { // parillinen rivi käännettynä = syöttötiedosto.nextLine(); sanakirja.put(alkukielellä, käännettynä); } } //käyttäjäystävällinen tulostus, ei vaadittu täysiin pisteisiin. if (rivi % 2 == 1) System.out .println("Viimeisellä sanalla \"" + alkukielellä + "\" ei ole käännöstä! \nSe jätetään pois sanakirjasta!"); System.out.println(sanakirja); // testaukseen syöttötiedosto.close(); // Huom.!! do { System.out .println("Minkä sanan käännöksen haluat? (Tyhjä rivi lopettaa.)"); String kysytty = lukija.nextLine(); if (kysytty.equals("")) break; // >>>>>>> lopetus <<<<<<< if (!sanakirja.containsKey(kysytty)) System.out.println("Sana \"" + kysytty + "\" on tuntematon!"); else System.out.println("\"" + kysytty + "\" on " + "\"" + sanakirja.get(kysytty) + "\""); } while (true); } } ------------------------------------------------------------------------------------------------------ Vaihtoehtoisia toistorakenteita tiedoston läpikäyntiin: ------------------------------------------------------------------------------------------------------ 2. tapa: ====== while (syöttötiedosto.hasNextLine()) { String alkukielellä = syöttötiedosto.nextLine(); String käännettynä; if (syöttötiedosto.hasNextLine()) { käännettynä = syöttötiedosto.nextLine(); sanakirja.put(alkukielellä, käännettynä); } } 3. tapa: ====== while(true) { if (!syöttötiedosto.hasNextLine()) break; alkukielellä = syöttötiedosto.nextLine(); if (!syöttötiedosto.hasNextLine()) break; käännettynä = syöttötiedosto.nextLine(); sanakirja.put(alkukielellä, käännettynä); } 4. tapa: ====== for (boolean pariton = true; syöttötiedosto.hasNextLine(); pariton = !pariton) { if (pariton) alkukielellä = syöttötiedosto.nextLine(); else { käännettynä = syöttötiedosto.nextLine(); sanakirja.put(alkukielellä, käännettynä); } } (muitakin tapoja ja muunnelmia oli!) --------------------------------------------------------------------------------------------------------- Pisteytyksen runko --------------------------------------------------------------------------------------------------------- Ratkaisu pisteytettiin seuraavasti: alustus ja poikkeuskäsittely (5,5 pistettä) ----------------------------------------------------------- * importit muistettu (0,25p) * private static Scanner lukija = new Scanner(System.in) muistettu (0,5p) * Poikkeuskäsittely, esim. throws FileNotFoundException tai try-catch (1p) * new File(...) konstruktorin kutsu oikein (0,5p) * file.exists() tarkistus tai hyvä try-catch (0,75p) * new Scanner (tiedosto_olio) luonti (0,5p) * Hashtablen luonti oikein. Huom! Geneerisyyttä ei välttämättä tarvittu. (2p) tiedoston läpikäynti (7p) ---------------------------------- * tiedoston oikeanlainen läpikäynti (4,5p) * avainparin lisääminen sanakirjaan put-aksessorilla (2p) * skannerin sulkeminen (0,5p) käännösten kysely (4,5p) ------------------------------------ * toistorakenne järkevä (1p) * tarkistetaan että käännös on olemassa (2p) * oikean käännöksen tulostaminen (1,5p) Yhteensä 17 pistettä Edelläolevaa pisteytystä käytettiin vain jos ratkaisu oli keskeneräinen tai muuten selvästi puutteellinen. Jos summaksi tuli esim. 8,5 pistettä, tapauskohtaisesti se pyöristettiin joka 8:aan tai 9:ään. Yleensä ylöspäin. Yleisesti ottaen käytettiin ns. "vähentävää" laskutapaa eli lähdettiin siitä että ratkaisu on 17 pisteen arvoinen ja katsotaan sitten mitä puutteita ratkaisussa on ja vähennettiin pisteitä. Samanlaisesta virheestä vähennettiin vain kerran vaikka niitä olisi ollut useita samassa ratkaisussa. Seuraavassa on jotain eniten esiintyneitä virheitä ja niistä tehtyjä vähennyksiä: ------------------------------------------------------------------------------------------------------ Vähennykset ------------------------------------------------------------------------------------------------------ * yritetään tallettaa tiedoston rivejä taulukkoon (maksimissaan 8 pistettä koko tehtävästä) * Kaikki sanat ja käännökset kysytään tiedoston sijaan käyttäjältä (max 8p) * tiedoston nimeä ei kysytä käyttäjältä vaan käytetään vakiotiedostoa (vähennetään 1piste) * ei poikkeuskäsittelyä lainkaan (-3p) * ei poikkeuskäsittelyä lainkaan mutta tarkistetaan file.exists() (-1p) * poikkeuskäsittely tehty mutta ohjelman suoritus jatkuu poikkeuksen tapahduttua (-3p) * aiheutetaan poikkeuksia ohjelman tavanomaisessa suorituksessa (-5p) (ks. esim yllä) * main-metodi puuttuu kokonaan, siis koodi kirjoitettu suoraan public class Sanakirja { ... } lohkon sisään (-1p) * tiedostoa käydään läpi ilman skanneria (-4p), siis esim. File tied = new File(...); while (tied.hasNextLine()) { ... } * tiedostoa läpikäydessä jokainen sanapari kirjataan väärin (-4p) * tiedostoa läpikäydessä viimeinen sanapari menee väärin jos pariton määrä rivejä (-1p) * kaikenlaista muutakin oli, mutta enempää ei muista :) ------------------------------------------------------------------------------------------------------ Pistejakauma ------------------------------------------------------------------------------------------------------ 0 ****** (5 kpl) 1 ******* (7) 2 ****** (6) 3 ** (2) 4 ****** (6) 5 * (1) 6 ***** (5) 7 * (1) 8 ** (2) 9 * (1) 10 ** (2) 11 **** (4) 12 *** (3) 13 *** (3) 14 ******** (8) 15 *** (3) 16 ************** (14) 17 ******************** (20) Yht: 93 ratkaisua Keskiarvo: 10,47 / 17 pistettä