Потоки Java 23. ToArray

Терминальные операции либо возвращают значения других типов, либо вообще ничего не возвращают (вызывают только побочные эффекты). Они не позволяют применять другие операции и закрывают поток.

В этом посте мы рассмотрим две терминальные операции:

Объект [] toArray (). Возвращает массив значений, передаваемых потоком.

A [] toArray (генератор IntFunction ‹A []›). Возвращает массив значений, излучаемых потоком, используя предоставленную функцию генератора для выделения возвращаемого массива, а также любые дополнительные массивы, которые могут потребоваться для многораздельного выполнения или для изменения размера.

Это предложение «а также любые дополнительные массивы, которые могут потребоваться для секционированного выполнения или для изменения размера» в последнем определении выше, выглядит довольно загадочным. Я попытался привести пример, который показал бы необходимость в этом. Я тоже поискал в сети и не нашел такого примера. Однако было упомянуто, что более ранние реализации могут потребовать создания некоторых промежуточных буферов. Это все, что я могу сказать об этом.

Мы обсудим, что такое IntFinction, и рассмотрим некоторые его примеры в соответствующем разделе ниже.

toArray ()

Использовать toArray () просто:

Object[] res = Stream.of("a", "b", "c")
        .map(String::toUpperCase)
        .toArray();
  System.out.print(Arrays.toString(res));   //prints: [A, B, C]

Он просто собирает излучаемые значения потока в массив объектов класса Object, независимо от того, является ли поток параллельным или нет. Единственное ограничение - он должен быть конечным. В противном случае обработка никогда не закончится.

findAny (IntFunction)

toArray (IntFunction ‹A []›) удобен, когда вам нужно, чтобы результирующий массив имел определенный тип (а не только Object []).

Единственная цель IntFunction ‹A []› - получить размер результирующего массива и вернуть массив A [] запрошенного размера. Например:

IntFunction<String[]> f = i -> new String[i];
  System.out.print(Arrays.toString(f.apply(3)));  
                              //prints: [null, null, null]

Давайте подробнее рассмотрим определение IntFunction ‹R›. Это функциональный интерфейс, который имеет только один абстрактный метод R apply (int value). В случае метода A [] toArray ‹IntFunction‹ A [] ›generator) целое число, переданное в метод apply (), является размером результирующего массива. A []. Это означает, что переданная в IntFunction ‹A› должна возвращать массив A [значение]:

IntFunction<String[]> f = i -> new String[i];
  String[] res = Stream.of("a", "b", "c")
        .map(String::toUpperCase)
        .toArray(f);
  System.out.print(Arrays.toString(res));   //prints: [A, B, C]

В приведенном выше примере мы создали поток значений String, преобразовали каждое из них в верхний регистр и собрали все полученные значения в массив String []. Размер массива был определен автоматически (и передан в операцию toArray ()) реализацией Stream.

Мы можем встроить определение функции следующим образом:

String[] res = Stream.of("a", "b", "c")
        .map(String::toUpperCase)
        .toArray(i -> new String[i]);
  System.out.print(Arrays.toString(res));   //prints: [A, B, C]

Или мы можем использовать еще более компактную версию, используя ссылку на метод:

String[] res = Stream.of("a", "b", "c")
        .map(String::toUpperCase)
        .toArray(String[]::new);
  System.out.print(Arrays.toString(res));   //prints: [A, B, C]

Еще примеры

Ниже приведены несколько более сложные примеры, демонстрирующие использование toArray () и toArray (IntFunction ‹A []›) и, как мы надеемся, позволяют глубже понять их применимость. .

Мы собираемся использовать класс Человек:

class Person {
      private String name;
      public Person(String name) {
        this.name = name;
      }
      public String getName() { return name; }
      @Override
      public String toString() {
        return "Person{" + name + "}";
      }
  }

А ниже представлен фабричный метод, который создает объект класса Person, используя букву в качестве входных данных:

Person createPerson(String s){
      switch (s){
         case "a":
            return new Person("Alex");
         case "b":
            return new Person("Bernie");
         case "c":
            return new Person("Carol");
         default:
            return new Person("Zoi");
      }
  }

Используя класс Person и вышеуказанный фабричный метод, мы можем преобразовать поток объектов String в массив Person [] следующим образом:

Person[] res = Stream.of("a", "b", "c")
        .map(s -> createPerson(s))
        .toArray(Person[]::new);
  System.out.print(Arrays.toString(res));   
    //prints: [Person{Alex}, Person{Bernie}, Person{Carol}]

Чтобы сделать код более интересным, мы можем преобразовать (сопоставить) каждый объект Person с длиной значения свойства name, в результате чего получаем Integer [] массив:

Integer[] res = Stream.of("a", "b", "c")
        .map(s -> createPerson(s))
        .map(p -> p.getName().length())
        .toArray(Integer[]::new);
  System.out.print(Arrays.toString(res));   //prints: [4, 6, 5]

Два оператора map () в приведенном выше примере можно объединить в один следующим образом:

Integer[] res = Stream.of("a", "b", "c")
        .map(s -> createPerson(s).getName().length())
        .toArray(Integer[]::new);
  System.out.print(Arrays.toString(res));   //prints: [4, 6, 5]

Или, если вам нужен примитивный массив int [], можно добавить оператор mapToInt (), который преобразует каждое значение Integer в int :

int[] res = Stream.of("a", "b", "c")
        .map(s -> createPerson(s).getName().length())
        .mapToInt(i -> i)
        .toArray();
  System.out.print(Arrays.toString(res));   //prints: [4, 6, 5]

И, опять же, мы можем сделать конвейер обработки более компактным, объединив операторы map () и mapToInt () следующим образом:

int[] res = Stream.of("a", "b", "c")
        .mapToInt(s -> createPerson(s).getName().length())
        .toArray();
  System.out.print(Arrays.toString(res));   //prints: [4, 6, 5]

В следующем посте мы продолжим обсуждение операций терминала и обсудим самый мощный (и сложный) оператор reduce (), который имеет следующие перегруженные версии:

-- Дополнительное ‹T› сокращение (аккумулятор BinaryOperator ‹T›);

-- T reduce (T identity, BinaryOperator ‹T› аккумулятор);

-- U reduce (U-идентификатор, BiFunction ‹U, T, U› аккумулятор, BinaryOperator ‹U› объединитель);

Как видите, он предназначен для предоставления программисту полного контроля над параллельной обработкой и объединением результатов. Вы, вероятно, будете использовать его очень редко, потому что его специализированная версия (оператор collect ()) покрывает подавляющее большинство потребностей обработки. Мы рассмотрим оператор collect () в следующем посте.

Смотрите другие сообщения о потоках Java 8 и сообщения по другим темам.