Når en lager enkle, interaktive programmer har en ofte behov for å lese inn linjer med tekst og konvertere deler av teksten til ulike typer tall, logiske verdier osv. Dette er et eksempel på det som kalles parsing, dvs. oppstykking av tekst i deler og tolkning av delene. For enkel input er Scanner-klassen (i java.util-pakken) veldig hendig, siden den håndterer både oppstykking av teksten i deler og konvertering til de vanligste datatypene.
Instansiering av Scanner
En Scanner instansieres ved å gi den en InputStream (f.eks. System.in), String eller File, som argument:
Scanner scanner = new Scanner(System.in); // antar her at en har lagt inn import java.util.Scanner; øverst i fila under pakke-deklarasjonen
Dette betyr at du kan bruke den til å lese tekst fra andre tekstkilder også, men fokuset her er input fra brukeren gjennom System.in.
Når Scanner'n er instansiert kan du gjøre essensielt to ting:
- En kan spørre om det er gitt inn input av en bestemt type som venter på å bli lest, f.eks. linje, tall eller logisk verdi. Dette gjøres med en av mange hasNext-metoder, f.eks. hasNextLine(), hasNextInt(), hasNextBoolean(), osv. Disse metodene returnerer alle true, dersom input'en som venter på å bli lest passer til typen verdi.
- En kan lese/konsumere input av en bestemt type., med tilsvarende next-metoder, f.eks. nextLine(), nextInt(), nextBoolean(), osv. Disse returnerer en verdi av angitt type, f.eks. vil nextInt() returnere int og nextLine() returnere String osv.
Innlesing av tekst-linjer
Det enkleste er å lese inn én og én linje, med nextLine()-metoden. Dette gjøres typisk i en løkke som går inntil datakilden er tom, dvs. så lenge hasNextLine() returnerer true:
// fortsett så lenge det er linjer i input-køen while (scanner.hasNextLine()) { String line = scanner.nextLine(); // vi hopper ut av løkka hvis linja er tom if (line == null || line.length() == 0) { break; } // gjør noe med line her }
Siden en får en hel linje om gangen, så må en selv tolke innholdet, f.eks. sjekk om formatet er riktig og dele det opp i mindre biter. Dette er nødvendig hvis innholdet i teksten er litt uforutsigbar, f.eks. tillater ulike kombinasjoner av ord, tall, skille tegn osv. Hvis formatet imidlertid er regulært, så en vet hva en kan forvente, så er det nyttig å bruke next-metodene som leser og returnerer spesifikke datatyper.
Innlesing av spesifikke datatyper
Anta f.eks. at du ønsker å lese inn et koordinat-par (int) og en logisk verdi (boolean). Da kan du bruke følgende kode:
// antar at scanner-variablen allerede er instansiert som over og at en har skrevet ut ledetekst til brukeren int x = scanner.nextInt(); int y = scanner.nextInt(); boolean b = scanner.nextBoolean();
Dersom brukeren så skriver inn en linje med tekst, f.eks. "0 42 true" (uten anførselstegnene), så vil x bli satt til 0, y bli satt til 42 og b til true. Hvis en derimot skriver inn "true 0 42" så vil programmet kræsje, fordi true ikke kan leses som en int. Det er derfor avgjørende at brukeren vet hva som forventes som input og gir det inn på riktig format.
NB! Scanner-en baserer seg på konvensjoner i din locale (altså lokaliteten din, dvs. den regionen du befinner deg i) når den tolker input. Det betyr bl.a. at hvis Java tror du er i Norge så vil den forvente ',' og ikke '.' som desimalkomma i desimaltall (typene double og float, som leses med nextDouble() og nextFloat()), og utløse InputMismatchException hvis du skriver inn noe annet. Hvis du vil tvinge Scanner-en til å bruke amerikanske konvensjoner, bl.a. '.' som desimalkomma, så må du si fra om det ved å kalle useLocale-metoden: scanner.useLocale(java.util.Locale.US).
Sjekk av type input
I noen tilfeller er det praktisk at koden som leser input er litt fleksibel, dvs. kan tillate og tolke input av med litt varierende type innhold. Da bruker en hasNext-metodene til å sjekke typen input, før en faktisk leser og tilordner. Anta f.eks. at en skal lese inn en blanding av tall og matematiske operatorer som +, -, * og /. Da kan en bruke følgende kode for sjekk og innlesing:
// fortsett så lenge det er tokens i input-køen (les om tokens i Oppstykking og tolkning av input, nedenfor) while (scanner.hasNext()) { // sjekk om input'en faktisk følger double-syntaksen if (scanner.hasNextDouble()) { double operand = scanner.nextDouble(); // gjør noe med operand her // ... } else { // hvis input'en ikke er en double, er det ihvertfall et ord/token String operator = scanner.next(); // gjør noe med operator-verdien her if ("+".equals(operator)) { // husk å sjekke String med equals! // +-operasjonen implementeres her ... } else if ... ... } }
Anta at brukeren har skrevet inn "1.0 + 2" (uten anførselstegn). I den første runden i while-løkka vil den første grenen av if'en bli kjørt og operand bli tilordnet 1.0, fordi "1.0" jo følger double-syntaks og hasNextDouble()-metoden derfor returnerer true. I den andre runden vil den andre grenen av if'en bli kjørt og operator blir tilordnet "+" fordi "+" ikke er gyldig iht. double-syntaks. I den tredje runden blir den første grenen kjørt igjen fordi "2" kan leses som en double. Merk imidlertid at det går galt dersom brukeren skriver inn "1.0+2", fordi reglene for oppstykking gjør at dette regnes som ett stykke input og ikke tre.
Oppstykking og tolkning av input
Tolkningen av input foregår egentlig i to trinn, oppstykking og konvertering.
Første trinn er å dele input'en opp i deler, såkalte tokens, ved at Scanner'n leser tegn for tegn inntil den kommer til det som er definert som skilletegn. Tegnene samles i ett token og legges i en input-kø. Når neste token skal leses, så spoles skilletegnene over, og en ny sekvens med tegn leses inntil neste skilletegn, osv. Denne delen er uavhengig av og foregår før tolkningen som int, double, boolean osv. Dette kan virke forvirrende, f.eks. skulle en tro at "1.0+2" burde kunne splittes opp i 1.0, + og 2 siden en tross alt ber om en double, et token og en double, men oppstykking forholder seg ikke til typen input, kun hva som er skilletegn og ikke.
Neste trinn er å konvertere hvert token til riktig type verdi og evt. fjerne det fra input-køen, i henhold til hva programmet spør om. F.eks. vil hasNextInt() forsøke å konvertere neste token i input-køen til en int (med Integer.valueOf-metoden) og returnere true hvis det går, men uten å fjerne den fra køen. Når en så kaller nextInt(), gjøres konverteringen på nytt, og tokenet brukes opp, dvs. fjernes fra input-køen. Kaller en derimot nextDouble(), så konverteres tokenet til en double-verdi i stedet (med Double.valueOf). En kan evt. bruke hasNext() og next(), som sjekker/returnerer tokenet som en ukonvertert String.
Egendefinerte skilletegn
I utgangspunktet brukes en eller flere mellomrom og linjeskift som skilletegn (egentlig det som kalles whitespace, som også omfatter tabulator-tegnet), men det er mulig å endre dette med useDelimiter-metoden. Argumentet er en String som angir et såkalt regulært uttrykk eller tekstmønster for hva som utgjør en gyldig skilletegn-sekvens. Hvis en f.eks. setter skilletegn-mønsteret til ett enkelt komma (,), så vil teksten "1,2,3," bli delt opp i tre tokens som alle er gyldige tall:
// use a single comma as delimiter scanner.useDelimiter(","); // parse a sequence of comma-separated ints, e.g. "1,2,3," int first = scanner.nextInt(), second = scanner.nextInt(), third = scanner.nextInt();
Det er viktig å merke seg to ting om skilletegn-mønsteret:
- Mønsteret angir hva som er en gyldig sekvens av skilletegn, så skal det være lov med f.eks. komme etterfulgt av ett eller flere stk. whitespace, så må dette angis eksplisitt med scanner.useDelimiter(",\\s+"). Her betyr ,\\s+ ett komma etterfulgt at ett eller flere stk. whitespace. Dersom en glemmer +'en så tillattes bare ett stk. whitespace. Les mer om reglene for slike mønstre her: http://docs.oracle.com/javase/6/docs/api/java/util/regex/Pattern.html
- Input fra konsollet krever at en trykker linjeskift og linjeskift-tegnet leses også av Scanner'n. Derfor blir det lett kluss og forvirring om ikke linjeskiftet tillates som enden av skilletegn-mønsteret. Hvis en f.eks. bruker koden over, med ett enkelt komma som skilletegnsekvens, og skriver inn 1,2,3\n (\n er her linjeskift-tegnet), så vil Scanner'n fortsatt vente på input! De to tegnene 3 og \n vil bli lest som ett uavsluttet token, siden det fortsatt ikke har dukket opp et komma (,). Hvis en så skriver kommaet (og et nytt linjeskift som trigger), vil programmet kræsje, fordi "3\n" ikke er en gyldig int,
Summa-summarum så kan setting av egendefinerte skilletegn med useDelimiter-metoden være nyttig, men er vanskelig å få til rett. Og en bør forsikre seg om at det fungerer ved å prøve det ut i praksis!
Ferdig |
---|
95 |