17. Containerklasser 17.7 Funktioner för listor och köer I avsnitt 17.6 såg vi hur man kunde använda iteratorer för att löpa igenom datasamlingar. Vi såg att man kunde använda iteratorer för alla datasamlingar som implementerar gränssnittet Iterable. Alla typer av listor och mängder som implementerar gränssnittet Collection i figur 17.1 på sidan 650 kan alltså genomlöpas med hjälp av iteratorer, eftersom Iterable är ett supergränssnitt till Collection. Ett annat sätt att löpa igenom elementen i en datasamling är att använda en s.k. ström. som hämtar sina data från datasamlingen. Det gör man genom att anropa metoden stream som är en default-metod i gränssnittet Collection. Strömmens typ beskrivs av gränssnittet Stream som erbjuder ett antal metoder för att hantera data i strömmen (se faktarutan). Stream är ett generiskt gränssnitt, men det finns också de icke-generiska specialversionerna IntStream, LongStream och DoubleStream. Dessa beskriver strömmar i vilka elementen är av typerna int, long resp. double. java.util.stream Parametrar markerade med p har typen Predicate, u Consumer, f Function, uop UnaryOperator, bop BinaryOperator och c Comparator. T står för Int, Long eller Double empty() (static) ger en tom ström generate(sup) (static) genererar en ström från en Supplier, sup of(e0,e1,e2...) (static) genererar en ström med elementen e0,e1,e2... iterate(e, uop) (static) genererar strömmen e, uop(e), uop(uop(e)... builder() (static) returnerar en Stream.Builder concat(s1, s2) (static) ger en sammanslagen ström av strömmarna s1 och s2 allmatch(p) anymatch(p) close() collect(col) count() distinct() filter(p) findany() findfirst() flatmap(f) flatmaptot(f) foreach(u) ger true om p blir true för alla element, false annars ger true om p blir true för något element, false annars stänger strömmen samlar ihop elementen med hjälp av en Collector, col ger antalet element i strömmen ger en ström där varje element bara förekommer en gång ger en ström med de element för vilka p blir true ger ett godtyckligt element i strömmen ger det första elementet i strömmen ger en ström där varje element kan avbildats till flera element ger en TStream där varje element kan avbildats till flera T utför u på varje element i strömmen, ger void 664 Författaren och Studentlitteratur kap17.fm 20 februari 2014 09.54:29 sida 664 av 690
17.7 Funktioner för listor och köer java.util.stream (fortsättning) foreachordered(u) utför u på varje element i strömmen, i tur och ordning isempty() ger true om strömmen är tom limit(n) ger strömmen trunkerad till högst n st element map(f) ger en ström där alla elementen avbildats med hjälp av f maptot(f) ger en TStream där alla elementen avbildats med hjälp av f max(c) ger det största elementet i strömmen min(c) ger det minsta elementet i strömmen nonematch(p) ger true om p inte blir true för något element, false annars reduce(bop) ger ett värde där varje nytt värde är resultatet av operationen bop mellan det förra värdet och det aktuella elementet reduce(v0, bop) som ovan, men v0 är startvärdet skip(n) hoppa över n st element i strömmen sorted() ger strömmen sorterad när elementen är naturligt jämförbara sorted(c) ger strömmen sorterad med jämföraren c toarray() skapar en array innehållande strömmens element parallel() returnerar en ekvivalent ström som är parallell sequential() returnerar en ekvivalent ström som inte är parallell isparallel() anger om strömmen är parallell iterator() returnerar en iterator som kan löpa igenom strömmen unordered() returnerar en ekvivalent ström som är oordnad Ytterligare metoder i IntStream, LongStream och DoubleStream (TStream) average() ger medelvärdet av elementen i denna ström boxed() ger en ström av typen Stream<T> maptoobj(f) ger en Stream där alla elementen avbildats med hjälp av f sum() ger summan av elementen i denna ström Följande finns bara i IntStream och LongStream asdoublestream() ger en DoubleStream med alla element från denna ström range(i,j) (static) genererar en ström med värdena i till j-1 rangeclosed(i,j) (static) genererar en ström med värdena i till j En av metoderna är foreach. Vi kan t.ex. använda denna för att skriva ut alla elementen i en lista li. (Jämför med hur vi gjorde i avsnitt 17.6.) li.stream().foreach(i -> System.out.println(i)); Gemensamt för metoderna i gränssnittet Stream är att de har parametrar som är referenser till funktionsgränssnitt av de typer som visades i faktarutan på sidan 401. Metoden foreach har t.ex. en parameter av typen Consumer. Vi kan alltså använda lambdauttryck som argument när vi anropar metoderna i gränssnittet Stream. En fördel är att Författaren och Studentlitteratur 665 kap17.fm 20 februari 2014 09.54:29 sida 665 av 690
17. Containerklasser man kan använda ett funktionellt skrivsätt och att metoderna kan kopplas ihop på ett naturligt vis. Antag t.ex. att vi har en lista words (av typen List<String>) med ord och att vi vill skriva ut alla ord i listan som är längre än 10 tecken. Då kan vi skriva words.stream().filter(s -> s.length() > 10).forEach(s -> System.out.println(s)); Här anropar vi först metoden filter. Denna får ett lambda-uttryck (av typen Predicate) som parameter. Denna ger villkoret för vilka element som skall väljas ut. Resultatet från metoden filter är en ström av data (av typen String) och på denna ström applicerar vi metoden foreach. Det går bra att koppla ihop hur många steg som helst. I följande sats kommer t.ex. de utvalda orden att skrivas ut med stora bokstäver. words.stream().filter(s -> s.length() > 10).forEach(s -> System.out.println(s)); Här används metoden map för att avbilda en text till motsvarande text bestående av stora bokstäver. Parametern är av typen Function. Vill vi att orden skall sorteras kan vi använda metoden sorted och vill vi spara dem i en ny samling istället för att skriva ut dem kan vi använda metoden collect. Denna skall som parameter ha en s.k. kollektor som anger hur data skall samlas ihop. I klassen Collectors finns ett antal färdiga sådana, t.ex. tolist som vi kan använda här. (En faktaruta med fler kollektorer finns på sidan 685.) List<String> longwords = words.stream().filter(s -> s.length() > 8).sorted(String::compareTo).collect(Collectors.toList()); Metoden sorted skall som parameter ha den funktion som skall användas när man jämför element parvis. Här har vi hänvisat till en befintlig metod. (Se sidan 403.) Vi visar några fler exempel på hur metoderna i faktarutan kan användas. Antag att listan li innehåller elementen {3,5,7,2}. Vi kan summera talen i listan genom att skriva int sum = li.stream().reduce(0, (v, e) -> v+e); // sum blir 17 Den första parametern till reduce är startvärdet för det hittills beräknade värdet. Eftersom vi skall summera är detta värde 0. I lambda-uttrycket betecknar den första parametern v det hittills beräknade värdet och den andra e det aktuella elementet. Alternativt kan man först göra om strömmen till en IntStream med hjälp av metoden maptoint och sedan på denna applicera funktionen sum: 666 Författaren och Studentlitteratur kap17.fm 20 februari 2014 09.54:29 sida 666 av 690
sum = li.stream().maptoint(e -> e).sum(); 17.7 Funktioner för listor och köer I nästa exempel börjar vi med att definiera en funktion f som omvandlar ett heltal till en ström med lika många element som heltalet anger och där varje element är lika med heltalet. För att skapa en ny ström använder vi ett objekt av klassen Stream.Builder. Function<Integer, Stream<Integer>> f = n -> { Stream.Builder<Integer> b = Stream.builder(); for (int i=0; i<n; i++) b.accept(n); // Ange vilka element som skall ingå return b.build(); // Skapa strömmen }; Vi kan sedan använda vår funktion f som argument till metoden flatmap som avbildar varje element i en ström till flera element: List<Integer> li2 = li.stream().flatmap(f).collect(collectors.tolist()); Om li som ovan innehåller elementen {3,5,7,2} så kommer listan li2 att innehålla {3,3,3,5,5,5,5,5,7,7,7,7,7,7,7,2,2}. En av fördelarna med gränssnittet Stream är att flera av metoderna kan utnyttja s.k. lat evaluering (lazy evaluation). Tag som exempel metoden filter. När ett uttryck som words.stream().filter(s -> s.length() > 10) skall beräknas kan man vänta med att göra något. Det finns ingen anledning att löpa igenom hela samlingen words direkt och filtrera ut de önskade elementen. Man kan vänta tills nästa led i beräkningen begär att få ett eller flera element. Man är med andra ord lat. Om man kopplar på ett led till, t.ex. words.stream().filter(s -> s.length() > 10) så kan även metoden map vara lat och vänta med att göra något till nästa led begär ett element. En fördel med att vara lat är att man inte behöver skapa någon ny mellanliggande samling av data. Inga onödiga kopieringar görs. Inga element har ännu lästs från samlingen word. Inget händer förrän vi kopplar på en metod som av naturen är ivrig (eager), t.ex. foreach eller collect. Vi kan t.ex.med hjälp av kollektorn joining slå ihop alla orden i strömmen till en String med kommetecken mellan orden: String w = words.stream().filter(s -> s.length() > 10).collect(Collectors.joining(", ")); Nu måste förstås alla elementen hämtas från samlingen word. Först nu utförs operationerna filter och map. Men endast en genomlöpning av samlingen word behövs. Författaren och Studentlitteratur 667 kap17.fm 20 februari 2014 09.54:29 sida 667 av 690
17. Containerklasser Det kan t.o.m. vara så att man aldrig behöver hämta alla elementen från den ursprungliga datasamlingen. Antag t.ex. att vi istället för att avsluta kedjan med en ivrig metod avslutar med metoden findfirst eller findany. Då räcker det att ett enda element kommer fram. Det är vanligt att en koppling av metoder inleds med en följd av lata metoder och avslutas med en ivrig. En annan möjlighet som öppnas när man kopplar samman strömmar på detta vis är att beräkningar i vissa fall kan göras parallellt. En del av metoderna skulle kunna utföra sitt jobb med hjälp av flera trådar som får exekvera samtidigt. (Sortering kan t.ex. göras parallellt för olika delar av en datasamling och de sorterade delarna kan fogas samman på slutet.) Programmeraren kan välja om han eller hon vill använda en parallell ström genom att anropa metoden parallelstream istället för stream. words.parallelstream().filter(s -> s.length() > 10) etc. I de exempel som visats hittills har vi utgått från listor, men det går också bra att generera strömmar från mängder eftersom dessa också implementerar gränssnittet Collection och har metoderna stream och parallelstream. Man kan också skapa strömmar från arrayer genom att anropa metoden Arrays.stream. Antag t.ex. att vi har en array ai Integer[] ai = {1, 2, 0, 9, 7}; och att vi vill beräkna det största talet i denna. Vi kan då skriva int m = Arrays.stream(ai).reduce(Integer.MIN_VALUE, (v, e) -> Math.max(v,e)); Alternativt kan man använda metoden max som ska ha en Comparator som argument: m = Arrays.stream(ai).max((v, i) -> v-i).get(); Man kan även generera strömmar från andra datakällor. Det går t.ex. att bilda en ström från en fil. I klassen BufferedReader finns metoden lines som ger en ström där elementen består av raderna i filen. Följande programrader kopplar en ström till kommandofönstret och räknar antalet rader man skriver där. BufferedReader in = new BufferedReader (new InputStreamReader(System.in)); System.out.println(in.lines().count()); Skulle vi istället vilja räkna antalet ord kan vi med hjälp av flatmap splittra upp varje rad i ett antal ord. Vi skapar ett objekt av klassen Pattern för vilket vi anger att orden skall avgränsas av ett godtyckligt antal vita tecken (se sidan 173). Vi kan då sedan anropa metoden splitasstream i den funktion som vi ger som argument till flatmap. Pattern p = Pattern.compile("\\s+"); // vita tecken är avgränsare System.out.println(in.lines().flatMap(s -> p.splitasstream(s)).count()); 668 Författaren och Studentlitteratur kap17.fm 20 februari 2014 09.54:29 sida 668 av 690
17.8 Avbildningstabeller Vi kan nu konstruera en alternativ version av programmet TextAnalys från sidan 659. import java.util.*; import java.util.regex.*; import java.text.*; import java.io.*; public class TextAnalys { // Alternativ version public static void main(string[] arg) throws IOException { Pattern p = Pattern.compile("\\s+"); Collator co = Collator.getInstance(); co.setstrength(collator.primary); new BufferedReader(new FileReader(arg[0])).lines().flatMap(s -> p.splitasstream(s)).map(s -> s.tolowercase()).distinct().sorted(co).foreach(s -> System.out.println(s)); } } 17.8 Avbildningstabeller 17.8.1 Gemensamma egenskaper Gränssnittet Map En avbildningstabell är en tabell där man använder en s.k. söknyckel för att komma åt information. Ett exempel är ett bilregister. Där utgör registreringsnumret söknyckeln och om man vet registreringsnumret kan man med hjälp av detta ta fram information om bilen, t.ex. bilmärke och årsmodell. Söknyckeln avbildas på ett värde (informationen). En söknyckel och tillhörande värde bildar ett par, en s.k. avbildning. På engelska används ofta termen key-value pair. Varje söknyckel kan bara avbildas på ett värde. (Samma bil kan inte ha flera registreringsnummer.) En viss söknyckel kan därför bara finnas en enda gång i en avbildningstabell. Däremot kan ett visst värde förekomma flera gånger. Om man t.ex. har en avbildningstabell där söknycklarna är namn på personer och värdena är åldrar, så kan flera personer ha samma ålder. I figur 17.3 visas de viktigaste av de standardgränssnitt och standardklasser som finns i Java för att beskriva avbildningstabeller. Gränssnitten visas i den skuggade delen av figuren. Gränssnittet Map beskriver egenskaper som är gemensamma för alla avbildningstabeller. Subgränssnittet ConcurrentMap beskriver avbildningstabeller som är lämpliga att använda i program som innehåller flera trådar och subgränssnittet NavigableMap beskriver avbildningstabeller där söknycklarna internt är sorterade. Till höger i figuren visas standardklasser som implementerar dessa gränssnitt. (Klassen Properties har satts inom parentes eftersom den är lite speciell. Se avsnitt 17.8.6.) Författaren och Studentlitteratur 669 kap17.fm 20 februari 2014 09.54:29 sida 669 av 690