[x^2 for x in lst]

Nyheter i Java 8 - del 6 - nyheter i klassen Arrays

2014-09-24

Nu är det dags för den sjätte delen i miniserien om nyheterna i Java 8. Förra delen handlade om parallell hantering av strömmar. Den här delen skall handla om de nya metoderna i klassen Arrays.

Nyheter i Arrays

I Java 8 kom det en hel del nyheter i klassen Arrays. Några av dessa underlättar parallell exekvering av operationer på arrayer. Vi skall kolla närmare på dessa nu. När vi ändå håller på, så passar vi på att ta en kik på de nya metoder som inte har med parallellism att göra också.

I korthet kan man säga att de nya metoderna i Arrays är följande:

  • <T extends Comparable<? super T>> void parallelSort(T[] a)
  • <T> void parallelPrefix(T[] array, BinaryOperator<T> op)
  • <T> void setAll(T[] array, IntFunction<? extends T> generator)
  • <T> void parallelSetAll(T[] array, IntFunction<? extends T> generator)
  • <T> Spliterator spliterator(T[] array)
  • <T> Stream stream(T[] array)
Många av metoderna finns även i överlagrade versioner som hanterar arrayer av de primitiva typerna. Det finns i många fall också överlagrade versioner som tar ett startindex och ett slutindex av arrayen och då bara opererar på den angivna delen av arrayen. I genomgången nedan kommer det att noteras vilka överlagrade varianter av respektive metod som finns.

parallelSort()

Vad gör metoden

parallelSort() är, inte helt oväntat, en parallell version av Arrays.sort(). Dvs den sorterar den inkommande arrayen "in place".

Notera att parallelSort() använder JVM:ens fork-join-pool, med de nackdelerar detta innebär. Se diskussionen angående fork-join-poolen i förra delen av serien, då avseende dess användande i samband med parallella strömmar.

Exempel

Som ett litet exempel på den variant av parallelSort() som tar en int[] som inparameter kan vi använda följande lilla klass:

import java.util.Arrays;
import java.util.Random;
import java.util.stream.IntStream;

public class MainClass {
  private final static int ARRAY_SIZE = 10;
  public static void main(String[] args) {
    // Generate array of intetegers.
    Random random = new Random();
    IntStream intStream = random.ints(ARRAY_SIZE);
    int[] randomNumbers = intStream.toArray();

    // Sort it an measure how long it takes.
    System.out.println("Before: " + Arrays.toString(randomNumbers)); 
    long startTime = System.currentTimeMillis();        
    Arrays.parallelSort(randomNumbers);
    long endTime = System.currentTimeMillis();    
    System.out.println("Execution time: " + (endTime - startTime) + "ms"); 
    System.out.println("After: " + Arrays.toString(randomNumbers));
  }
}

Exemplet ovan använder den nya metoden ints(int size) i Random för att generera en IntStream innehållande size slumptal. Sedan görs talen i IntStream:en om till en array med hjälp av metoden IntStream.toArray(). Ganska smidigt sätt att generera en array med slumptal eller hur?

För att göra det tydligt i vilken klass som ints(int) och toArray() fanns användes lokala variabler spara returvärdena från dessa metoder. Undviker man det blir genereringen av arrayen med slumptal mycket kort och koncis:

int[] randomNumbers = new Random().ints(ARRAY_SIZE).toArray();

Resultatet av en körning av ovanstående program kan se ut så här:

Before: [2128764741, 1109516321, -1918587090, -1349245711, 194244467, 2121767928, -434729300, -830024849, -14951130, 369065649]
Execution time: 1ms
After: [-1918587090, -1349245711, -830024849, -434729300, -14951130, 194244467, 369065649, 1109516321, 2121767928, 2128764741]

Nu är det ju ingen större bedrift att sortera tio stycken heltal. Vad vi egentligen vill testa är om Arrays.parallelSort() exekverar snabbare än Arrays.sort() då indatat är mycket större än så. För att prova det så sätter vi istället ARRAY_SIZE till 100 miljoner och kör igen. (Fast vi passar på att kommentera bort utskriften av arrayen före och efter...) Resultatet blir:

Execution time: 33707ms

Byter vi ut Arrays.parallelSort() mot Arrays.sort() och kör igen blir resultatet:

Execution time: 32719ms
Det var inte riktigt det resultat vi ville ha ;) Nu exekverades dock programmet på min ytterst slöa bussdator. Kör vi istället testerna på min jobbdator blir tiderna 10671ms respektive 4184ms. Nu ser det mer ut som förväntat.

Överlagrade varianter

parallelSort() finns i följande överlagrade varianter:

  • <T extends Comparable<? super T>> void parallelSort(T[] a)
  • <T extends Comparable<? super T>> void parallelSort(T[] a, int fromIndex, int toIndex)
  • <T> void parallelSort(T[] a, Comparator<? super T> cmp)
  • <T> void parallelSort(T[] a, int fromIndex, int toIndex, Comparator<? super T> cmp)
Dessutom finns det överlagrade versioner av parallelSort() för byte[], short[], int[], long[], float[], double[] och char[] i två varianter: dels den varianten som bara tar in själva arrayen och dels i den varianten som även tar in ett startindex och ett slutindex.

parallelPrefix()

Vad gör metoden

parallelPrefix() är en metod som går igenom indataarrayen element för element och applicerar en BinaryOperator på varje element. Inparametrar till anropen till BinaryOperator är elementet i fråga samt närmast föregående element.

I engelskan verkar en sådan operation beskrivas som att man cumulates varje element genom att applicera en funktion. Jag har inte hittat något bra svenskt ord för det. Är det någon som har koll på vad det heter på svenska så får ni gärna tipsa mig. Det är alltid roligt att utöka sitt ordförråd :)

parallelPrefix() arbetar, inte helt överraskande givet dess namn, parallellt så den BinaryOperator som man anger till metoden bör vara sidoeffektsfri.

Exempel

Som ett exempel på parallelPrefix() skall vi skriva ett litet program som adderar varje element i en int-array med närmast föregående element. Programmet ser ut så här:

import java.util.Arrays;

public class Prefix {
  public static void main(String[] args) {
    int[] array = new int[]{1,2,3,4,5,6,7,8,9};
    Arrays.parallelPrefix(array, (a, b) -> a + b);
    System.out.println(Arrays.toString(array));
  }
}
...
[1, 3, 6, 10, 15, 21, 28, 36, 45]

Vi har här använt lambdauttrycket (a, b) -> a + b som BinaryOperator.

Överlagrade varianter

parallelSort() finns i följande överlagrade varianter:

  • <T> void parallelPrefix(T[] array, BinaryOperator<T> op)
  • <T> void parallelPrefix(T[] array, int fromIndex, int toIndex, BinaryOperator<T> op)
Utöver dessa finns det versioner av för double[], int[] och long[] i både den varianten som enbart tar en array som inparameter och den varianten som utöver arrayen även tar in start- och slutindex.

setAll()

Vad gör metoden

setAll() initialiserar alla elementen i en array till värden som ges av en generatorfunktion. Generatorfunktionens typ är olika för de olika överlagrade varianterna av setAll(). Den är IntUnaryFunction när det gäller den varianten som operarar på en int[], IntToDouble när det gäller variantern för double[] och så vidare.

För varje element i arrayen som skall initialiseras anropas generatorfunktionen med elementets index som parameter (det är därför generatorfunktionerna har typen IntToSomething). Detta index kan sedan användas, om så önskas, till att bestämma vilket värde som elementet skall få.

Exempel

Ett exemelprogram som iniatialiserar en liten int[] så att varje element får ett värde som är dubbelt så stort som elementets index kan se ut så här:


import java.util.Arrays;

public class SetAll {
  public static void main(String[] args) {
    int[] array = new int[10];
    Arrays.setAll(array, a -> a * 2);
    System.out.println(Arrays.toString(array));
  }
}
...
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

Överlagrade varianter

setAll() finns i de här varianterna:

  • <T> void setAll(T[] array, IntFunction<? extends T> generator)
  • void setAll(double[] array, IntToDoubleFunction generator)
  • void setAll(int[] array, IntUnaryOperator generator)
  • void setAll(long[] array, IntToLongFunction generator)

parallelSetAll()

Vad gör metoden

parallelSetAll() gör samma sak som setAll(), dvs den initialiserar en array givet en generatorfunktion, men den gör det parallellt.

Exempel

Motsvarande exempel som ovan använde setAll() ser med parallelSetAll() ut så här:


import java.util.Arrays;

public class ParallelSetAll {
  public static void main(String[] args) {
    int[] array = new int[10];
    Arrays.parallelSetAll(array, a -> a * 2);
    System.out.println(Arrays.toString(array));
  }
}
...
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
Vi ser att resultatet blev detsamma som i det föregående exemplet, vilket ju känns bra.

Överlagrade varianter

parallelSetAll() har motsvarande varianter som setAll() det vill säga:

  • <T> void parallelSetAll(T[] array, IntFunction<? extends T> generator)
  • void parallelSetAll(double[] array, IntToDoubleFunction generator)
  • void parallelSetAll(int[] array, IntUnaryOperator generator)
  • void parallelSetAll(long[] array, IntToLongFunction generator)

spliterator()

Vad gör metoden

spliterator() returnerar en Spliterator för arrayen i fråga.

Vad är då en Spliterator? Ja, Spliterator är ett interface som enligt dokumentationen är tillför att, fritt översatt, "traversera och partionera elementen från en källa". Och vad betyder det då? Det betyder att Spliterator liknar Iterator på så sätt att Spliterator innehåller metoder för att stega igenom en uppsättning element på något sätt. Det finns dock två stora skillnader mellan Spliterator och Iterator.

Den första är att istället för att som Iterator ha separata metoder för att kolla om det finns fler element och för att hämta ut nästa element har Spliterator kombinerat dessa till samma metod som då tar en Consumer som appliceras på elementet om det finns något element kvar att applicera den på. Spliterator har två sådana metoder: void tryAdvance(Consumer<? super T> action) och void forEachRemaining(Consumer<? super T> action). Det som skiljer dessa två metoder åt är att tryAdvance() applicerar Consumer:n på nästa element medan forEachRemaining() applicerar den på alla resterande element.

Den andra och konceptuellt viktigaste skillnaden mellan Iterator och Spliterator är att det går att dela upp elementen i grupper och låta separata Spliterator:s hantera varje grupp. Det är detta som dokumentationen menar om när den talar om att partionera elementen. Uppdelningen i grupper görs med metoden Spliterator<T> trySplit() som returnerar en ny Spliterator som innehåller ett antal element från Spliterator:n på vilken trySplit() anropades. Notera att elementen efter anropet till trySplit() inte längre finns med i den ursprungliga Spliterator:n. Det är alltså frågan om en flytt av element, inte en kopierring. Notera också att man vid anropet till trySplilt() inte kan påverka vilka eller om ens några element som den Spliterator som returneras innehåller. Det är upp till implementationen av Spliterator:n att bestämma.

Vad är då poängen med att splitta? Den är att man på så sätt får möjlighet att parallellisera itereringen av elementen. Varje grupp kan itereras parallellt.

Den finns ett antal specialiseringar av Spliterator för de primitiva typerna double, int och long. Dessa är implementerade som inre-interface i Spliterator och de heter Spliterator.OfDouble, Spliterator.OfInt respektive Spliterator.OfLong.

Exempel

Det första exemplet visar hur forEachRemaining() kan användas för att behandla alla element i en array. I exemplet innebär hanteringen bara att elementet skrivs ut, men i verkliga livet kan givetvis valfria operationer göras i lambdauttrycket som ges som argument till forEachRemaining().

int[] oneToNine = new int[] {1,2,3,4,5,6,7,8,9};
Spliterator.OfInt spliterator = Arrays.spliterator(oneToNine);
spliterator.forEachRemaining((int i) -> System.out.println(i));
...
1
2
3
4
5
6
7
8
9

Det andra exemplet skriver först ut några av de inledande elementer mha tryAdvance() för att sedan anropa forEachRemaining() för att skriva ut resten.

System.out.println("Now we will run tryAdvance() a few times:"); 
int[] tenToNineteen = new int[] {10,11,12,13,14,15,16,17,18,19};
Spliterator.OfInt spliterator2 = Arrays.spliterator(tenToNineteen);
spliterator2.tryAdvance((int i) -> System.out.println(i));
spliterator2.tryAdvance((int i) -> System.out.println(i));
spliterator2.tryAdvance((int i) -> System.out.println(i));
spliterator2.tryAdvance((int i) -> System.out.println(i));
spliterator2.tryAdvance((int i) -> System.out.println(i));
System.out.println("-----------------------------"); 
System.out.println("Now we will run forEachRemaining():"); 
spliterator2.forEachRemaining((int i) -> System.out.println(i));

...
Now we will run tryAdvance() a few times:
10
11
12
13
14
-----------------------------
Now we will run forEachRemaining():
15
16
17
18
19

I det sista exemplet så splittar vi en Spliterator i två delar och behandlar sen varje Spliterator var för sig.

int[] twentyToTwentynine = new int[] {20,21,22,23,24,25,26,27,28,29};
Spliterator.OfInt firstSpliterator = Arrays.spliterator(twentyToTwentynine);
Spliterator.OfInt secondSpliterator = firstSpliterator.trySplit();
System.out.println("First spliterator contains these numbers: "); 
firstSpliterator.forEachRemaining((int i) -> System.out.println(i));
System.out.println("Second spliterator contains these numbers: "); 
secondSpliterator.forEachRemaining((int i) -> System.out.println(i));
...
First spliterator contains these numbers: 
25
26
27
28
29
Second spliterator contains these numbers: 
20
21
22
23
24
Vi ser att trySplit() har delat elementen så att de lägsta talen splittas av till den andra Spliterator:n medan de högsta talen blir kvar i den ursprungliga. Detta är dock inget vi direkt kan påverka.

Överlagrade varianter

Följande varianter finns av Spliterator:

  • <T> Spliterator<T> spliterator(T[] array)
  • <T> Spliterator<T> spliterator(T[] array, int startInclusive, int endExclusive)
  • Spliterator.OfDouble spliterator(double[] array)
  • Spliterator.OfDouble spliterator(double[] array, int startInclusive, int endExclusive)
  • Spliterator.OfInt spliterator(int[] array)
  • Spliterator.OfInt spliterator(int[] array, int startInclusive, int endExclusive)
  • Spliterator.OfLong spliterator(long[] array)
  • Spliterator.OfLong spliterator(long[] array, int startInclusive, int endExclusive)

stream()

Vad gör metoden

Metoden stream() returnerar en ström som backas av elementen i arrayen som ges som parameter till metoden. Den ger oss alltså möjlighet att på ett lätt sätt gå från en (gammal, tråkig) array till en (ny och skinande) Stream och därigenom kunna utnyttja alla de nya och roliga saker man kan göra med en sådan ;)

Exempel

Vi har ju använt Stream:s in rad exempel tidigare, så följande exempel kommer inte att vara så spännande. Det visar mest på hur man kan komma över till en Strean från en array. Vad vi sedan kan göra med strömmen i fråga har vi ju avhandlat i flera av de tidigare delearna i serien. I exemplet skriver vi bara ut elementen i strömmen till standard out.

int[] oneTonNine = new int[] {2,4,6,8};
IntStream stream = Arrays.stream(oneTonNine);
stream.forEach(System.out::println);
...
2
4
6
8

Överlagrade varianter

De här överlagrade versionerna finns av stream():
  • <T> Stream<T> stream(T[] array)
  • <T> Stream<T> stream(T[] array, int startInclusive, int endExclusive)
  • DoubleStream stream(double[] array)
  • DoubleStream stream(double[] array, int startInclusive, int endExclusive)
  • IntStream stream(int[] array)
  • IntStream stream(int[] array, int startInclusive, int endExclusive)
  • LongStream stream(long[] array)
  • LongStream stream(long[] array, int startInclusive, int endExclusive)

Avslutning del 6

I den här delen har vi gått igenom de metoder i Arrays som är nya i Java 8. De flesta metoder har något med parallellism att göra. Om jag får våga mig på en gissning så kommer stream() att vara den av de nya metoderna som man får mest nytta av. Med hjälp av den kan man behandla en arrays element som en ström och då få tillgång till allt godis som man kan göra med strömmar i Java 8.

En annan metod som jag kan tänka mig att man kan få nytta av är parallelSort(). Sortera gör man ju titt som tätt, så den kommer man säkert kunna använda.

Skall jag vara helt ärlig så är jag mer tveksam till hur ofta man kommer att använda de andra nya metoderna i Arrays. De känns inte lika användbara som de metoder jag nyss nämnde. Fast det är klart, kan spliterator() kombineras ihop på något bra sätt med ett ramverk som erbjuder parallell exekvering så kanske det blir användbart.

I nästa del, vilket antagligen blir den sista delen i serien, skall vi kolla lite översiktligt på övriga, icke-parallellism-relaterade nyheter i Java 8.

Tidigare delar i serien

Del 1: Interface
Del 2: Streams och lambda expressions
Del 3: Metoder i Stream, metod- och konstruktorreferenser
Del 4: Metoder klassen Collectors
Del 5: Parallella strömmar

Referenser

[1] API-dokumentationen för Arrays


Leave a reply

Your name as it will be displayed when the comment is posted on the page. Your email address will not be published.