Tiedostojen käsittely
osaamistavoitteet
- Osaat lukea ja kirjoittaa tekstitiedostoja Javan
Files-luokan avulla. - Osaat käyttää
Scanner-luokkaa tiedon lukemiseen ja parsimiseen tiedostosta. - Osaat käsitellä tiedostoja riveittäin hyödyntäen Stream-rajapintaa.
- Tunnet JSON-tiedostomuodon perusteet.
- Osaat käyttää Jackson-kirjastoa JSON-datan lukemiseen ja kirjoittamiseen.
- Ymmärrät, miten Javan
record-tietueet soveltuvat datan mallintamiseen.
Tiedoston käsittelyssä on aina sama peruskaari: avaat resurssin, luet tai kirjoitat dataa tietyssä muodossa, ja suljet resurssin. Java tarjoaa tähän useita valmiita vaihtoehtoja. Valinta riippuu siitä, luetko dataa vain riveittäin vai tarvitsetko rivien pilkkomista ja parsimista arvoiksi (esim. luvut), haluatko käsitellä suurta tiedostoa suorituskykyisesti, ja missä muodossa data on.
Oman tiedoston lisääminen projektiin
Oletetaan, että meillä on oheisen kaltainen tekstitiedosto.
nimi,ika
Maija,25
Matti,30
Tiedosto sisältää henkilöiden tietoja. Ensimmäisellä rivillä on sarakkeiden nimet, ja seuraavilla riveillä on tietoja henkilöistä. Tiedot on erotettu toisistaan pilkuilla. Tällaista tiedostomuotoa kutsutaan CSV-tiedostoksi (engl. comma-separated values), ja se on varsin yleinen tapa tallentaa taulukkomuotoista dataa tekstitiedostoon.
Tallennetaan tällainen tiedosto nimellä data.csv projektin juurikansioon.
Jotta IDEA osaa ohjelman ajon aikana käyttää tätä tiedostoa, määritellään, että
ohjelman työskentelykansio on projektin juurikansio. Tämän voi tehdä Run Edit Configurations. Valitse vasemmalta luokka,
johon main-metodi on kirjoitettu. Oikealla "Working directory" -kohdassa
varmista, että kansioksi on määritetty projektin lähdekoodin juurihakemisto,
joka päättyy yleensä src tai src/main/java.
Nyt voimme käyttää data.csv-tiedostoa ohjelmassamme.
Tiedoston käsittely Files API:lla
Files-luokka (tai oikeammin sanottuna java.nio.file-paketin API) tarjoaa
suoraviivaisen tavan lukea koko tiedosto kerralla sellaisissa tilanteissa,
joissa tiedoston koko on kohtuullinen. Voit esimerkiksi lukea koko tiedoston
muistiin rivilistana Files.readAllLines()-metodilla tai merkkijonona
Files.readString()-metodilla. Jos datan sisältää vaikkapa lukuja, päivämääriä
tai muuta erikoisempaa, tulee ne käsitellä erikseen.
Tehdään yllä oleva esimerkki käyttäen Files-luokan readAllLines()-metodia.
Tämä metodi lukee koko tiedoston muistiin listana merkkijonoja, joissa jokainen
merkkijono vastaa yhtä riviä tiedostossa. Tämän jälkeen voimme käydä listan läpi
ja pilkkoa jokaisen rivin sarakkeiksi.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
public class TiedostonLukija {
public static void main(String[] args) {
try {
List<String> lines = Files.readAllLines(Paths.get("data.csv"));
for (int i = 1; i < lines.size(); i++) {
String line = lines.get(i);
String[] parts = line.split(",");
String nimi = parts[0];
int ika = Integer.parseInt(parts[1]);
IO.println("Nimi: " + nimi + ", Ikä: " + ika);
}
} catch (IOException e) {
IO.println("Tiedostoa ei löydy tai sitä ei voi lukea: " + e.getMessage());
} finally {
// Ei tarvitse erikseen sulkea mitään, koska Files API hoitaa sen puolestamme
}
}
}
Samalla idealla – eli kokonainen tiedosto kerrallaan – voit myös
kirjoittaa tiedostoon. Kun koko sisältö on yhtenä merkkijonona, voit käyttää
Files.writeString()-metodia. Ennen kirjoittamista tulee varmistaa, että
kansio, johon tiedosto kirjoitetaan, pitää olla olemassa. Tämä voidaan tehdä
seuraavasti:
Path polku = Path.of("data", "tulos.txt"); // Polku-olio, joka sisältää tiedon kansiosta ja tiedostosta
Files.createDirectories(polku.getParent()); // Varmistetaan, että data-kansio on olemassa
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class KirjoitaTiedostoWriteString {
public static void main(String[] args) {
Path polku = Path.of("data", "tulos.txt");
try {
Files.createDirectories(polku.getParent()); // varmistetaan, että data-kansio on olemassa
String sisalto = "Hei!\nTämä on uusi tiedosto.\n";
Files.writeString(polku, sisalto, StandardCharsets.UTF_8);
IO.println("Kirjoitettiin: " + polku.toAbsolutePath());
} catch (IOException e) {
IO.println("Kirjoittaminen epäonnistui: " + e.getMessage());
}
}
}
Kun data on riveinä, esimerkiksi listana, on usein luontevaa kirjoittaa se
riveittäin käyttäen Files.write()-metodia.
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class KirjoitaTiedostoRiveina {
public static void main(String[] args) {
Path polku = Path.of("data", "data.csv");
List<String> rivit = List.of(
"nimi,ika",
"Maija,25",
"Matti,30"
);
try {
Files.createDirectories(polku.getParent());
Files.write(polku, rivit, StandardCharsets.UTF_8);
IO.println("Kirjoitettiin: " + polku.toAbsolutePath());
} catch (IOException e) {
IO.println("Kirjoittaminen epäonnistui: " + e.getMessage());
}
}
}
On myös mahdollista lisätä tekstiä olemassa olevan tiedoston loppuun. Se vaatii
parin lisäargumentin antamista. Alla lyhyt esimerkki Files.write()-metodin
käytöstä, jossa teksti lisätään olemassa olevan tiedoston loppuun.
// ...
Path polku = Path.of("data", "tulos.txt");
String rivi = "Uusi rivi, joka lisätään tiedoston loppuun.\n";
Files.writeString(
polku,
rivi,
StandardCharsets.UTF_8, // Käytetään UTF-8-koodausta
StandardOpenOption.CREATE, // Luo tiedosto, jos sitä ei ole
StandardOpenOption.APPEND // Lisää tekstiä olemassa olevan tiedoston loppuun
);
// ...
Lukeminen Scanner-oliolla
Scanner sopii tilanteisiin, joissa haluat lukea tekstiä ikään kuin palasissa:
esimerkiksi kokonainen rivi kerrallaan, seuraavaan välilyöntiin asti tai jopa
seuraavan luvun. Voit ajatella, että Scanner-olio on kuin lukupää, "kursori",
joka etenee sitä mukaa kun kutsut kursoria eteenpäin liikuttavia metodeja, kuten
nextLine() tai next(). Kun tiedostossa ei ole enää luettavaa, saat
hasNext()-metodilta paluuarvon false.
Scanner osaa myös lukea lukuja ja muita primitiivityyppejä suoraan (nextInt(),
nextDouble(), jne.), mikä vähentää käsin parsimista.
Alla olevassa mallikoodissa luetaan tiedosto Scanner-oliolla. Ensin luodaan
Tiedoston lukeminen tapahtuu siis rivi kerrallaan, ja jokainen rivi pilkotaan
sarakkeiksi.
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class TiedostonLukija {
public static void main(String[] args) {
Scanner scanner = null;
try {
scanner = new Scanner(new File("data.csv"));
// Ohitetaan otsikkorivi
if (scanner.hasNextLine()) {
scanner.nextLine();
}
// Luetaan rivejä, kunnes tiedoston loppu saavutetaan
while (scanner.hasNextLine()) {
String line = scanner.nextLine(); // Esim. "Maija,25"
String[] parts = line.split(","); // Pilkotaan rivi sarakkeiksi
String nimi = parts[0]; // Ensimmäinen sarake on nimi
int ika = Integer.parseInt(parts[1]); // Toinen sarake on ikä, parsitaan intiksi
IO.println("Nimi: " + nimi + ", Ikä: " + ika);
}
} catch (FileNotFoundException e) {
IO.println("Tiedostoa ei löydy: " + e.getMessage());
} finally {
// Suljetaan scanner
if (scanner != null) {
scanner.close();
}
}
}
}
Aina käsiteltävä aineisto ei ole näin "nättiä". Tiedostossa voi olla lukuja, joiden erottimina voi olla vaikkapa välilyönti, pilkku, puolipiste tai rivinvaihto.
Oletetaan tiedosto mittaukset.txt:
12 8, 5
-3; 10 7
virhe 2 1.5 3
Haluat lukea kaikki numerot riippumatta siitä, millä tavalla ne on eroteltu.
Tämä on vaikeampi tehdä siististi rivi kerrallaan Files-luokan avulla, mutta
Scanner-olion avulla asia hoituu kätevämmin. Scanner-olion
useDelimiter()-metodilla voit määritellä, mitkä merkit toimivat erottimina.
Esimerkiksi useDelimiter("[\\s,;]+") määrittelee, että välilyönti, pilkku ja
puolipiste ovat erottimia. Kaikki ennen erotinmerkkiä olevat merkit muodostavat
niin sanotun tokenin, joka voidaan lukea next()-metodilla.
import java.io.File;
import java.io.IOException;
import java.util.Locale;
import java.util.Scanner;
public class LueNumerotScannerilla {
public static void main(String[] args) throws IOException {
double summa = 0.0;
int maara = 0;
Scanner sc = new Scanner(new File("mittaukset.txt"));
try {
sc.useLocale(Locale.US); // desimaalierottimena piste "."
sc.useDelimiter("[\\s,;]+"); // erottimina välilyönti, rivinvaihto, pilkku tai puolipiste
while (sc.hasNext()) { // onko vielä luettavia tokeneja
if (sc.hasNextDouble()) { // onko seuraava palanen kelvollinen luku
summa += sc.nextDouble(); // lue luku ja lisää summaan
maara++;
} else {
sc.next(); // ohita token, joka ei ole kelvollinen luku
}
}
} finally {
sc.close();
}
IO.println("Lukuja: " + maara);
IO.println("Summa: " + summa);
IO.println("Keskiarvo: " + (maara == 0 ? 0 : summa / maara));
}
}
Scanner-oliolla ei voi kirjoittaa tiedostoon, se on vain lukutyökalu.
Tietovirrat (Stream)
Kokoelmien ohella (ks. osa 6.2) myös tiedostoja (ja muitakin ulkoisia resursseja) voidaan käsitellä Stream-rajapinnan avulla. Streamit ovat hyödyllisiä silloin, kun dataan halutaan tehdä useita peräkkäisiä operaatioita, kuten muunnoksia (map), suodatuksia (filter) ja keräilyä (esim. toList, collect). Tällöin käsittely kuvataan ketjuna, joka kertoo selkeästi mitä datalle tehdään vaihe vaiheelta.
Luettaessa tiedostoa virtana tyypillinen aloitus on Files.lines(polku). Se
tuottaa rivit laiskasti: rivejä ei lueta etukäteen kokonaan muistiin, vaan niitä
luetaan sitä mukaa kuin streamiä kulutetaan. Tämä on keskeinen ero
readAllLines-metodiin: Files.lines sopii myös suurille tiedostoille, koska
se ei vaadi koko tiedoston lataamista muistiin. Koska tiedostoa luetaan
taustalla, stream täytyy sulkea.
Tehdään aluksi yksinkertainen esimerkki, jossa toistetaan aiempi kuvio, mutta
nyt käytetään Files.lines-metodia ja Stream-käsittelyä. Käytämme aiemmin
opittua map-operaatiota muuntamaan jokaisen rivin taulukkomuotoon. Käytämme
kerääjäfunktiona forEach-metodia, joka suorittaa annetun lambda-lausekkeen
jokaiselle riville. Tässä parsimme rivit samalla tavalla kuin aiemmissa
esimerkeissä, ja lopuksi tulostamme nimet ja iät.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class TiedostonLukijaStream {
static void main() {
try {
Files.lines(Paths.get("data.csv"))
.skip(1) // Ohitetaan otsikkorivi
.map(line -> line.split(",")) // Pilkotaan rivi sarakkeiksi
.forEach(parts -> {
String nimi = parts[0]; // Ensimmäinen sarake on nimi
int ika = Integer.parseInt(parts[1]); // Toinen sarake on ikä, parsitaan intiksi
IO.println("Nimi: " + nimi + ", Ikä: " + ika);
});
} catch (IOException e) {
IO.println("Tiedostoa ei löydy tai sitä ei voi lukea: " + e.getMessage());
}
}
}
Jatketaan esimerkkiä hieman. Suodatetaan sellaiset henkilöt pois, joiden ikä on alle 18 vuotta, ja lopuksi tulostetaan nimet aakkosjärjestyksessä.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
public class TiedostonLukijaStream {
static void main() {
try {
List<String> nimet = Files.lines(Paths.get("data.csv"))
.skip(1) // Ohitetaan otsikkorivi
.map(line -> line.split(",")) // Pilkotaan rivi sarakkeiksi
// HIGHLIGHT_GREEN_BEGIN
.filter(parts -> Integer.parseInt(parts[1]) >= 18) // Suodatetaan alle 18-vuotiaat
.map(parts -> parts[0]) // Otetaan vain nimi
.sorted() // Järjestetään nimet aakkosjärjestykseen
.toList(); // Kerätään tulokset listaksi
nimet.forEach(IO::println); // Tulostetaan nimet
// HIGHLIGHT_GREEN_END
} catch (IOException e) {
IO.println("Tiedostoa ei löydy tai sitä ei voi lukea: " + e.getMessage());
}
}
}
Virtapohjaisessa käsittely on varsin näppärää, kun käsittely on suhteellisen yksinkertaista ja lineaarisesti etenevää. Virtapohjainen käsittely voi kuitenkin merkittävästi hankaloittaa esimerkiksi debuggaamista, joka on hyvä tiedostaa.
Valinnaista lisätietoa: Stream-käsittelyn haasteista tarkemmin
- kertakäyttöisyys: Stream-olion voi käyttää vain kerran, minkä jälkeen se on suljettava. Jos haluat käsitellä samaa dataa uudestaan, sinun täytyy luoda uusi Stream-olio.
- debuggaaminen: Ketjutus piilottaa välitulokset. Jos jokin map/filter-vaihe
heittää poikkeuksen, pinoloki kertoo kyllä missä lambdassa oltiin, mutta
"mikä rivi" ja "millä välituloksella" ei näy ilman erillisiä tulostuksia tai
erillisen
peek()-metodin kutsumista. Lambda-lausekkeita ei voi askeltaa yhtä suoraviivaisesti kuin perinteistäfor-silmukkaa. - virheiden käsittely: lambda-lausekkeiden sisällä tapahtuvat tarkistamattomat
poikkeukset (esim.
NumberFormatExceptionInteger.parseInt()-kutsussa) on käsiteltävä erikseen, koska lambda-lausekkeet eivät salli tarkistamattomien poikkeusten heittämistä suoraan. Tämä voi tehdä virheiden käsittelystä hieman monimutkaisempaa verrattuna perinteiseen silmukkaan. - laiskuus voi yllättää: Stream ei tee mitään ennen keräysoperaatiota
(
forEach,toList,collect,count, …). Tämä voi aiheuttaa yllätyksiä, kuten että koodi näyttää lukevan tiedoston, mutta mitään ei tapahdu, jos keräysvaihe puuttuu. Myöskään poikkeukset eivät synny siinä kohdassa, missä tiedosto avataan, vaan vasta keräysvaiheessa.
BufferedReader ja BufferedWriter
BufferedReader ja BufferedWriter ovat "perinteisiä" työkalut tekstitiedostojen käsittelyyn silloin, kun haluat lukea ja kirjoittaa rivi kerrallaan ja hallita käsittelysilmukkaa tarkasti. Ne puskuroivat I/O:ta, eli eivät tee järjestelmäkutsua jokaisesta yksittäisestä merkistä, vaan lukevat ja kirjoittavat suuremmissa paloissa. Tämä parantaa suorituskykyä erityisesti suurilla tiedostoilla ja tekee käsittelystä ennustettavaa. Jätämme näiden opiskelun omatoimiseksi, valinnaiseksi harjoitukseksi.
JSON-muotoinen tiedosto
JSON (JavaScript Object Notation) on suosittu tiedonvaihtomuoto, joka on paljon käytetty erityisesti web-kehityksessä. JSON-tiedostot ovat avain-arvo-pareja sisältäviä tekstitiedostoja, jotka voivat sisältää monimutkaisia tietorakenteita, kuten taulukkoja ja olioita.
JSON voi sisältää seuraavan tyyppisiä arvoja:
- merkkijono (
"Maija") - luku (
25) - totuusarvo (
true/false) - tyhjä arvo (
null) - taulukko (
[...]) - olio (
{...})
Esimerkiksi tiedosto henkilot.json voi näyttää tältä:
[
{
"nimi": "Maija",
"ika": 25,
"kaupunki": "Jyväskylä"
},
{
"nimi": "Matti",
"ika": 30,
"kaupunki": "Tampere"
}
]
CSV:hen verrattuna JSONin etu on se, että kentät voivat olla sisäkkäisiä, eli vaikkapa "kaupunki" voisi olla olio, jossa on "aikaisemmat_kaupungit" ja "nykyinen_kaupunki". Näin ollen rivit eivät ole sidottuja yhteen taulukkomalliin. Haittapuolena rakenne on usein hieman raskaampi lukea silmämääräisesti verrattuna CSV:hen. Lisäksi niissä on hieman enemmän syntaksia, mikä tekee niistä hieman monimutkaisempia käsitellä "käsin" ilman erillistä kirjastoa.
JSON-tiedostojen käsittely Jackson-kirjastolla
JSONin käsittely onnistuu toki käsin merkkijonoja pilkkomalla, mutta käytännössä tämä on virhealtista. Siksi JSONia kannattaa käsitellä siihen tarkoitetulla kirjastolla. Yksi yleisimmistä Java-kirjastoista on Jackson.
Lisää pom.xml-tiedostoon Jackson-riippuvuus:
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>3.0.4</version>
</dependency>
Riippuvuuden lisäämisen jälkeen virkistä Maven-projektisi. Alla oleva esimerkki
lukee tiedoston henkilot.json listaksi Henkilo-olioita. Selitämme koodin
tarkemmin seuraavaksi.
import tools.jackson.core.JacksonException;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
import java.nio.file.Path;
import java.util.List;
public class LueJson {
public static void main(String[] args) {
ObjectMapper mapper = new ObjectMapper();
Path polku = Path.of("data", "henkilot.json");
try {
List<Henkilo> henkilot = mapper.readValue(
polku.toFile(),
new TypeReference<List<Henkilo>>() {}
);
henkilot.forEach(h ->
IO.println(h.nimi() + " (" + h.ika() + "), " + h.kaupunki())
);
} catch (JacksonException je) {
IO.println("JSONin lukeminen epäonnistui: " + e.getMessage());
}
}
}
JSONin lukeminen tiedostosta: Lukeminen aloitetaan luomalla
ObjectMapper-olio, joka on Jackson-kirjaston keskeinen työkalu JSONin
muuntamiseen Java-olioiksi ja päinvastoin. Tämän jälkeen käytetään
readValue()-metodia, joka ottaa JSON-tiedoston ja kertoo, minkä tyyppiseksi
JSONin pitäisi muuntaa.
Jotta muunnos olisi mahdollinen, meidän täytyy mallintaa JSON-tieto
Java-olioiksi. Tehdään Java-luokka Henkilo, jossa on kentät nimi, ika ja
kaupunki, eli saman nimiset kentät kuin JSON-tiedostossa. Tehdään myös niitä
vastaavat getterit ja setterit, sekä tyhjä konstruktori – tämän kaltainen
luokka on Jackson-kirjaston vaatimus, jotta se osaa luoda olioita JSONista. Alla
esimerkki.
public class Henkilo {
private String nimi;
private int ika;
private String kaupunki;
public Henkilo() {
}
public void setNimi(String nimi) {
this.nimi = nimi;
}
public String getNimi() {
return nimi;
}
public int getIka() {
return ika;
}
public void setIka(int ika) {
this.ika = ika;
}
public void setKaupunki(String kaupunki) {
this.kaupunki = kaupunki;
}
public String getKaupunki() {
return kaupunki;
}
}
Tiedoston lukeminen voi epäonnistua, joten readValue()-metodi on syytä kääriä
try-catch-rakenteeseen. Jackson-kirjasto heittää
JacksonException-poikkeuksen, mikä on IOException-poikkeuksen aliluokka.
JSONin kirjoittaminen tiedostoon on aavistuksen lukemista helpompaa.
Kirjoittaminen tapahtuu writeValue()-metodilla, joka ottaa tiedoston ja
tallennettavan olion, ja muuntaa sen JSON-muotoon. Tässä on mahdollista, että
- kansion luominen epäonnistuu;
createDirectoriesheittääIOException-poikkeuksen, tai - JSON-tiedoston lukeminen epäonnistuu, jos tiedosto ei löydy, JSON on
virheellistä tai tyyppimuunnos epäonnistuu;
writeValueheittääJacksonException-poikkeuksen.
Kumpikin näistä poikkeuksista tulee käsitellä erikseen.
Seuraava esimerkki kirjoittaa listan henkilöitä tiedostoon output/henkilot-uusi.json:
import tools.jackson.databind.ObjectMapper;
import tools.jackson.core.JacksonException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class KirjoitaJson {
static void main() {
ObjectMapper mapper = new ObjectMapper();
List<Henkilo> henkilot = List.of(
new Henkilo("Aino", 22, "Turku"),
new Henkilo("Pekka", 41, "Oulu")
);
Path polku = Path.of("data", "henkilot-uusi.json");
try {
Files.createDirectories(polku.getParent());
mapper.writeValue(polku.toFile(), henkilot);
IO.println("Kirjoitettiin JSON: " + polku.toAbsolutePath());
} catch (IOException e) {
IO.println("Kansion luominen epäonnistui: " + e.getMessage());
} catch (JacksonException je) {
IO.println("JSON-prosessointi epäonnistui: " + je.getMessage());
}
}
}
Record-luokka JSONin mallintamiseen
Esitellään tässä kohtaa lyhyesti Javan record-käsite. Javan record on erityinen luokka, joka on suunniteltu pienten, suoraviivaisten datarakenteiden kuten JSONin kaltaisen rakenteisen datan mallintamiseen. Kun määrittelet recordin, Java muodostaa automaattisesti joitain rutiineja sinulle valmiiksi taustalla. Saat valmiina:
- automaattisen konstruktorin, joka ottaa kaikki kentät argumentteina,
- kenttiä vastaavat "getterit", joiden nimet ovat suoraan kenttien nimet, esim.
nimi()eikägetNimi(), equals()- jahashCode()-toteutukset,- selkeän
toString()-tulostuksen.
Record-olion komponentit (eli attribuutit) ovat käytännössä final-kenttiä, mikä tarkoittaa, että recordit ovat pääosin muuttumattomia olioita.
Näin recordit ovat luonnollinen pari JSON-kirjastoille: JSON-olio vastaa usein suoraan yhtä "datakimppua", jonka voi mallintaa recordilla ilman ylimääräistä koodia. Siinä missä perinteinen luokka kirjoitetaan usein niin, että määritellään kentät, konstruktorit ja getterit erikseen, recordissa sama asia ilmaistaan tiiviisti yhdellä rivillä.
On hyvä huomata, että recordiin saa kyllä kirjoittaa metodeja ja tarkistuksia, mutta perusajatus on pitää se pienenä ja keskittyä datan esittämiseen. Jos luokkaan alkaa kertyä paljon muuttuvaa tilaa tai monimutkaista toiminnallisuutta, tavallinen luokka on yleensä parempi valinta.
Määritellään datalle nyt tietotyyppi käyttäen record-rakennetta:
public record Henkilo(String nimi, int ika, String kaupunki) {}
Tällä tavalla pitkähkö Henkilo-luokka saadaan korvattua yhdellä rivillä. Tässä
tapauksessa record-luokka on toiminnallisuudeltaan täysin samanlainen kuin
aiemmin määritetty perinteinen luokka.
Tehtävät
Lataa aineisto: sanat.txt
Tallenna tiedosto projektisi työskentelyhakemistoon nimellä sanat.txt.
Tiedostossa on yksi sana per rivi. Mukana on tarkoituksella tyhjiä rivejä, välilyöntejä sanojen alussa/lopussa, samoja sanoja useaan kertaan, eri kirjainkokoja (esim. Java, java, JAVA).
Esimerkki aineiston alusta:
Java
python
JAVA
CSharp
java
Tee ohjelma, joka:
- Lukee kaikki rivit.
- Poistaa sanoista ylimääräiset välilyönnit ja muuttaa sanat pieniksi
kirjaimiksi. Vinkki:
String.trim()jaString.toLowerCase(). - Poistaa tyhjät rivit.
- Poistaa duplikaatit. (Vinkki:
distinct()-metodi Stream API:lla, taiSet-kokoelma.) - Järjestää sanat aakkosjärjestykseen. (Vinkki:
sorted()-metodi Stream API:lla, taiCollections.sort()-metodi Listalla.) - Kirjoittaa uuden tiedoston
output/sanat-siisti.txt, jossa on siistitty sanalista (yksi sana per rivi). - Kirjoittaa lisäksi tiedoston
output/raportti.txt, jossa on:- alkuperäisten rivien määrä
- uniikkien sanojen määrä sen jälkeen, kun tyhjät rivit on poistettu ja sanat on siistitty
- pisin sana (jos useita, mikä tahansa kelpaa). Vinkki: Jos ratkaiset
tehtävän Stream API:lla, voit käyttää
Stream.max(Comparator)-metodia pisimmän sanan löytämiseen.
Vinkki: tee ensin List<String> siistit = ..., ja kirjoita lopuksi
Files.write(...) kahteen eri tiedostoon.
raportti.txt:n pitäisi näyttää tältä:
Alkuperäisiä rivejä: 1074
Siistittyjä sanoja: 59
Pisin sana: binarytree
EDIT 23.2.2026: Jackson-kirjaston riippuvuuksia ja esimerkkejä päivitetty materiaalissa. Pahoittelut virheistä.
- Tee uusi Maven-projekti, joka käyttää Jackson-kirjastoa JSON-tiedostojen käsittelyyn.
- Lisää
pom.xml-tiedostoosi tarvittava riippuvuus. - Lataa henkilot.json ja tallenna se projektiisi samaan kansioon kuin missä koodisi on.
Henkilo-luokka tai vastaava record, jolla on kentätString nimi,int ika,String kaupunki.- Lue tiedosto
henkilot.jsonja muuta se listaksiHenkilo-olioita. - Suodata mukaan vain vähintään 18-vuotiaat.
- Tulosta heidän nimensä, ikänsä ja kaupunkinsa.
Muista varmistaa, että tallennat henkilot.json-tiedoston samaan kansioon kuin
missä koodisi sijaitsee. Tarkista sitten ajokonfiguraatiostasi, että
työskentelyhakemisto on sama kuin koodisi kansio, jotta tiedosto löytyy.
Saat henkilöiden tiedot tarvittaessa auki tästä:
henkilot.json
[ { "nimi": "Maija Laine", "ika": 25, "kaupunki": "Jyväskylä" }, { "nimi": "Matti Virtanen", "ika": 30, "kaupunki": "Tampere" }, { "nimi": "Liisa Niemi", "ika": 17, "kaupunki": "Helsinki" }, { "nimi": "Pekka Korhonen", "ika": 41, "kaupunki": "Oulu" }, { "nimi": "Aino Salmi", "ika": 22, "kaupunki": "Turku" }, { "nimi": "Jari Heikkinen", "ika": 19, "kaupunki": "Kuopio" }, { "nimi": "Sari Lehto", "ika": 16, "kaupunki": "Lahti" }, { "nimi": "Oskari Mäkinen", "ika": 28, "kaupunki": "Espoo" }, { "nimi": "Emilia Ranta", "ika": 33, "kaupunki": "Vantaa" }, { "nimi": "Teemu Koski", "ika": 45, "kaupunki": "Pori" }, { "nimi": "Noora Aalto", "ika": 18, "kaupunki": "Joensuu" }, { "nimi": "Kalle Hämäläinen", "ika": 52, "kaupunki": "Rovaniemi" } ]
EDIT 23.2.2026: Jackson-kirjaston riippuvuuksia ja esimerkkejä päivitetty materiaalissa. Pahoittelut virheistä.
Tee ohjelma, joka lukee tiedoston
henkilot.csv
(muoto nimi,ika,kaupunki) ja kirjoittaa siitä saman tapainen JSON-tiedoston
henkilot.json kuin edellisessä tehtävässä oli annettu valmiiksi. Jos rivi on
virheellinen (esim. ikä ei ole numero), ohita rivi ja jatka käsittelyä.
Tulostiedoston pitäisi näyttää tältä. Ei haittaa, jos sisennykset tai rivinvaihdot eivät ole täsmälleen samanlaisia.
[
{
"nimi": "Maija Laine",
"ika": 25,
"kaupunki": "Jyväskylä"
},
{
"nimi": "Matti Virtanen",
"ika": 30,
"kaupunki": "Tampere"
},
...
]
Tee yksinkertainen laskinohjelma, joka kysyy käyttäjältä toistuvasti kaksi lukua sekä laskutoimituksen ja tulostaa laskutoimituksen tuloksen.
Ohjelman tulisi toimia suunnilleen seuraavasti:
Anna laskutoimitus muodossa <luku> <operaattori> <luku>.
Kirjoita "sulje" sulkeaksesi ohjelman.
> 1 + 1
2.0
> 10 - 1
9.0
> 0.5 * 100
50.0
> 10 / 2
5.0
> 10
Anna laskutoimitus muodossa <luku> <operaattori> <luku>.
> kissa
Anna laskutoimitus muodossa <luku> <operaattori> <luku>.
> sulje
Ohjelma sulkeutuu.
Ohjelman on käsiteltävä käyttäjän syötteessä olevat virheet siten, ettei ohjelma kaadu virheellisen syötteen vuoksi.
Toteuta peruslaskutoimituksista summa (+), erotus (-), tulo (*) ja
osamäärä (/). Keksi lisäksi vähintään kaksi omaa vapaavalintaista
laskutoimitusta ja toteuta ne.
Älä käytä ehtorakenteita varsinaisten laskutoimitusten valitsemiseen. Voit
kuitenkin käyttää ehtorakenteita sekä try/catch-rakenteita syötteen
oikeellisuuden tarkistamiseen.
Vinkki 1
Voit toteuttaa operaatiot lambdalausekkeina. Käytä lambdalausekkeiden tyyppinä
BiFunction<Double, Double, Double>
(JavaDoc)
tai DoubleBinaryOperator
(JavaDoc).
Vinkki 2
Voit käyttää Scanner-luokkaa käyttäjän syötteen lukemiseen:
Scanner lukija = new Scanner(kayttajanSyote);
double luku1 = lukija.nextDouble();
String laskutoimitus = lukija.next();
double luku2 = lukija.nextDouble();
Saatat joutua lisäämään tarvittavat poikkeustenkäsittelyt.