Пожалуйста, учтите, что спецификации и другая информация, содержащаяся здесь, не являются заключительными и могут быть изменены. Информация доступна для вас исключительно ради ознакомления.
 Платформа Java™
Стандарт Эд. 8

Проект сборка-b92

Пакет java.util.stream

java.util.stream

См.: Описание

Пакет java.util.stream Описание

java.util.stream

Классы, чтобы поддерживать операции функционального стиля на потоках значений, как в следующем:

     int sumOfWeights = blocks.stream().filter(b -> b.getColor() == RED)
                                       .mapToInt(b -> b.getWeight())
                                       .sum();
 

Здесь мы используем blocks, который мог бы быть a Collection, как источник для потока, и затем выполняют "карту фильтра, уменьшают" (sum() пример работы сокращения) на потоке, чтобы получить сумму весов красных блоков.

Ключевая абстракция, используемая в этом подходе, Stream, так же как его примитивные специализации IntStream, LongStream, и DoubleStream. Потоки отличаются от Наборов несколькими способами:

Потоковые конвейеры

Потоки используются, чтобы создать конвейеры операций. У полного потокового конвейера есть несколько компонентов: источник (который может быть a Collection, массив, функция генератора, или канал IO); нуль или больше промежуточных операций такой как Stream.filter или Stream.map; и терминальная работа такой как Stream.forEach или java.util.stream.Stream.reduce. Операции с потоками могут взять в качестве значений функции параметров (которые часто являются лямбда-выражениями, но могли быть ссылками метода или объектами), которые параметризовали поведение работы, такой как a Predicate переданный к Stream#filter метод.

Промежуточные операции возвращают новое Stream. Они ленивы; выполнение промежуточной работы такой как Stream.filter фактически не выполняет фильтрации, вместо этого создавая новое Stream это, когда пересечено, содержит элементы начальной буквы Stream то соответствие данный Predicate. Потребление элементов из потокового источника не начинается, пока терминальная работа не выполняется.

Терминальные операции используют Stream и приведите к результату или побочному эффекту. После того, как терминальная работа выполняется, поток больше не может использоваться, и следует возвратиться к источнику данных, или выбрать новый источник данных, чтобы получить новый поток. Например, получение суммы весов всех красных блоков, и затем всех синих блоков, требует, чтобы "карта фильтра уменьшила" на двух различных потоках:


     int sumOfRedWeights  = blocks.stream().filter(b -> b.getColor() == RED)
                                           .mapToInt(b -> b.getWeight())
                                           .sum();
     int sumOfBlueWeights = blocks.stream().filter(b -> b.getColor() == BLUE)
                                           .mapToInt(b -> b.getWeight())
                                           .sum();
 

Однако, есть другие методы, которые позволяют Вам получать оба результата в единственной передаче, если многократный обход непрактичен или неэффективен. TODO обеспечивают ссылку

Операции с потоками

Промежуточная операция с потоками (такой как filter или sorted) всегда производите новое Stream, и alwayslazy. Выполнение ленивые операции не инициировало обработку потокового содержания; вся обработка задерживается, пока терминальная работа не начинается. Обработка потоков лениво учитывает существенные полезные действия; в конвейере, таком как пример суммы карты фильтра выше, фильтрация, отображение, и дополнение могут быть сплавлены в единственную передачу с минимальным промежуточным состоянием. Лень также позволяет нам избежать исследовать все данные, когда это не необходимо; для операций тех, которые "находят первую строку дольше чем 1000 символов", один не должен исследовать все строки ввода, как раз чтобы найти тот, у которого есть требуемые характеристики. (Это поведение становится еще более важным, когда входной поток является бесконечным и не просто большим.)

Промежуточные операции далее делятся на и stateful операции не сохраняющие состояние. Операции не сохраняющие состояние не сохраняют состояния от ранее замеченных значений, обрабатывая новое значение; примеры промежуточных операций не сохраняющих состояние включают filter и map. Операции Stateful могут включить состояние от ранее замеченных элементов в обработке новых значений; примеры stateful промежуточных операций включают distinct и sorted. Операции Stateful, возможно, должны обработать весь ввод прежде, чем привести к результату; например, нельзя произвести следствия сортировки потока, пока каждый не видел все элементы потока. В результате при параллельном вычислении, некоторые конвейеры, содержащие stateful промежуточные операции, должны быть выполнены в многократных передачах. Конвейеры, содержащие промежуточные операции исключительно не сохраняющие состояние, могут быть обработаны в единственной передаче, или последовательный или параллельный.

Далее, некоторые операции считают, закорачивая операции. Промежуточная работа закорачивает, если, когда подарено бесконечный ввод, она может произвести конечный поток в результате. Терминальная работа закорачивает, если, когда подарено бесконечный ввод, она может завершиться в конечный промежуток времени. (Наличие работы замыкания накоротко является необходимым, но не достаточное, условие для обработки бесконечного потока, чтобы обычно завершаться в конечный промежуток времени.) Терминальные операции (такой как forEach или findFirst) всегда нетерпеливы (они выполняются полностью прежде, чем возвратиться), и произведите не -Stream результат, такой как примитивное значение или a Collection, или имейте побочные эффекты.

Параллелизм

Переделывая совокупные операции как конвейер операций на потоке значений, много совокупных операций могут быть более легко параллелизированы. A Stream может выполниться или в последовательном или параллельно. Когда потоки создаются, они или создаются как последовательные или параллельные потоки; параллельные из потоков могут также быть переключены Stream#sequential() и BaseStream.parallel() операции. Stream реализации в JDK создают последовательные потоки, если параллелизм явно не требуют. Например, Collection имеет методы Collection.stream() и Collection.parallelStream(), которые производят последовательные и параллельные потоки соответственно; другие переносящие поток методы такой как java.util.stream.Streams#intRange(int, int) произведите последовательные потоки, но они могут быть эффективно параллелизированы, вызывая parallel() на результате. Набор операций на последовательных и параллельных потоках идентичен. Чтобы выполнить "сумму весов блоков" запрашивают параллельно, мы сделали бы:


     int sumOfWeights = blocks.parallelStream().filter(b -> b.getColor() == RED)
                                               .mapToInt(b -> b.getWeight())
                                               .sum();
 

Единственной разницей между последовательными и параллельными версиями этого примера кода является создание начальной буквы Stream. Ли a Stream выполнится в последовательном, или параллельное может быть определено Stream#isParallel метод. Когда терминальная работа инициируется, весь потоковый конвейер или выполняется последовательно или параллельно, определяется последней работой, которая влияла на последовательно-параллельную ориентацию потока (который мог быть потоковым источником, или sequential() или parallel() методы.)

Для результатов параллельных операций быть детерминированными и непротиворечивыми с их последовательным эквивалентом, значения функции, которые передают в различные операции с потоками, должны быть не сохраняющими состояние.

Упорядочивание

Потоки могут или, возможно, не имеют встретиться порядка. Встретиться порядок определяет порядок, в котором элементы обеспечиваются потоком для конвейера операций. Есть ли встретиться порядок, зависит от источника, промежуточных операций, и терминальной работы. Определенные потоковые источники (такой как List или массивы), свойственно упорядочиваются, тогда как другие (такой как HashSet) не. Некоторые промежуточные операции могут наложить встретиться порядок на иначе неупорядоченный поток, такой как Stream.sorted(), и другие могут представить упорядоченный неупорядоченный поток (такой как BaseStream.unordered()). Некоторые терминальные операции могут проигнорировать, встречаются с порядком, такой как Stream.forEach(java.util.function.Consumer<? super T>).

Если Поток упорядочивается, большинство операций ограничивается работать на элементах в их встречающееся с порядком; если источник потока является a List содержа [1, 2, 3], тогда результат выполнения map(x -> x*2) должен быть [2, 4, 6]. Однако, если источник имеет не определенный, встречаются с порядком, чем любая из шести перестановок значений [2, 4, 6] был бы допустимый результат. Много операций могут все еще быть эффективно параллелизированы даже под упорядочиванием ограничений.

Для последовательных потоков упорядочивание только относится к детерминизму операций, выполняемых неоднократно на том же самом источнике. ( ArrayList ограничивается выполнить итерации элементов в порядке; a HashSet не, и повторенная итерация могла бы произвести различный порядок.)

Для параллельных потоков, ослабляя ограничение упорядочивания может включить оптимизированной реализации для некоторых операций. Например, двойная фильтрация на упорядоченном потоке должна полностью обработать первый раздел прежде, чем это сможет возвратить любые элементы из последующего раздела, даже если те элементы доступны ранее. С другой стороны, без ограничения упорядочивания, двойная фильтрация может быть сделана более эффективно при использовании совместно используемого ConcurrentHashSet. Будут случаи, где поток структурно упорядочивается (источник упорядочивается, и промежуточные операции являются сохранением порядка), но пользователь особенно не заботится о встретиться порядке. В некоторых случаях, явно де-упорядочивая поток с BaseStream.unordered() метод может привести к улучшенной параллельной производительности для некоторого stateful или терминальных операций.

Невмешательство

java.util.stream пакет позволяет Вам выполнить возможно параллельные операции объемных данных по множеству источников данных, включая даже неориентированные на многопотоковое исполнение наборы такой как ArrayList. Это возможно, только если мы можем предотвратить интерференцию с источником данных во время выполнения потокового конвейера. (Выполнение начинается, когда терминальная работа вызывается, и заканчивается, когда терминальная работа завершается.) Для большинства источников данных, предотвращая интерференцию означает гарантировать, что источник данных не изменяется вообще во время выполнения потокового конвейера. (Некоторые источники данных, такие как параллельные наборы, специально предназначены, чтобы обработать параллельную модификацию.)

Соответственно, лямбда-выражения (или другие объекты, реализовывая соответствующий функциональный интерфейс) передали, чтобы передать методы потоком, никогда не должен изменять источник данных потока. Реализация, как говорят, вмешивается в источник данных, если это изменяет, или вызывает, чтобы быть измененным, источник данных потока. Потребность в невмешательстве применяется ко всем конвейерам, не только параллельным. Если потоковый источник не параллелен, изменяя источник данных потока во время выполнения потокового конвейера может вызвать исключения, неправильные ответы, или несовместимые результаты.

Далее, результаты могут быть недетерминированными или неправильными, если лямбда-выражения, которые передают к операциям с потоками, являются stateful. stateful лямбда (или другой объект, реализовывая соответствующий функциональный интерфейс) является той, результат которой зависит от любого состояния, которое могло бы измениться во время выполнения потокового конвейера. Пример stateful лямбды:


     Set<Integer> seen = Collections.synchronizedSet(new HashSet<>());
     stream.parallel().map(e -> { if (seen.add(e)) return 0; else return e; })...
 
Здесь, если отображающаяся работа выполняется параллельно, результаты для того же самого ввода могли бы измениться от выполненного, чтобы работать, из-за различий в планировании потоков, тогда как, с лямбда-выражением не сохраняющим состояние результатами всегда будет то же самое.

Побочные эффекты

Операции сокращения

Работа сокращения берет поток элементов и обрабатывает их в пути, который уменьшает до единственного значения или сводного описания, такого как обнаружение суммы или максимума ряда чисел. (В более сложных сценариях работа сокращения, возможно, должна была бы извлечь данные из элементов прежде, чем уменьшить те данные до единственного значения, такого как обнаружение суммы весов ряда блоков. Это потребовало бы извлечения веса от каждого блока прежде, чем подвести итог весов.)

Конечно, такие операции могут быть с готовностью реализованы как простые последовательные циклы, как в:


    int sum = 0;
    for (int x : numbers) {
       sum += x;
    }
 
Однако, может быть существенное преимущество для предпочтения a reduce operation по изменчивому накоплению такой, поскольку вышеупомянутые - должным образом созданный уменьшают работу, по сути parallelizable пока reduction operaterator имеет правильные характеристики. Определенно оператор должен быть ассоциативным. Например, учитывая поток чисел, для которых мы хотим найти сумму, мы можем записать:

    int sum = numbers.reduce(0, (x,y) -> x+y);
 
или более кратко:

    int sum = numbers.reduce(0, Integer::sum);
 

(Примитивные специализации Stream, такой как IntStream, даже имейте методы удобства для общих сокращений, такой как sum и max, которые реализуются, поскольку простые обертки вокруг уменьшают.)

Сокращение parallellizes хорошо начиная с реализации reduce может работать на подмножествах потока параллельно, и затем объединить промежуточные результаты получить заключительный корректный ответ. Даже если Вы должны были использовать форму parallelizable forEach() метод вместо исходного цикла foreach выше, необходимо бы все еще обеспечить ориентированные на многопотоковое исполнение обновления для совместно используемой переменной накопления sum, и необходимая синхронизация, вероятно, устранила бы любое увеличение производительности из параллелизма. Используя a reduce метод вместо этого удаляет все бремя параллелизации работы сокращения, и библиотека может обеспечить эффективную параллельную реализацию без дополнительной необходимой синхронизации.

"Блочные" примеры, показанные более ранние шоу, как сокращение объединяется с другими операциями, чтобы заменить для циклов объемными операциями. Если blocks набор Block объекты, у которых есть a getWeight метод, мы можем найти самый тяжелый блок с:


     OptionalInt heaviest = blocks.stream()
                                  .mapToInt(Block::getWeight)
                                  .reduce(Integer::max);
 

В его более общей форме, a reduce работа на элементах типа <T> приведение к результату типа <U> требует трех параметров:


 <U> U reduce(U identity,
              BiFunction<U, ? super T, U> accumlator,
              BinaryOperator<U> combiner);
 
Здесь, нейтральный элемент является и начальным семенем для сокращения, и результатом значения по умолчанию, если нет никаких элементов. Функция аккумулятора берет частичный результат и следующий элемент, и приведите к новому частичному результату. Функция объединителя комбинирует частичные результаты двух аккумуляторов привести к новому частичному результату, и в конечном счете окончательному результату.

Эта форма является обобщением формы с двумя параметрами, и является также обобщением карты - уменьшают конструкцию, иллюстрированную выше. Если мы хотели переделать простое sum пример используя более общую форму, 0 был бы нейтральный элемент, в то время как Integer::sum был бы и аккумулятор и объединитель. Для примера суммы весов это могло быть переделано как:


     int sumOfWeights = blocks.stream().reduce(0,
                                               (sum, b) -> sum + b.getWeight())
                                               Integer::sum);
 
хотя карта - уменьшает форму, более читаемо и обычно предпочтителен. Обобщенная форма обеспечивается для случаев, где существенная работа может быть оптимизирована далеко, комбинируя отображение и сокращение в единственную функцию.

Более формально, identity значение должно быть идентификационными данными для функции объединителя. Это означает это для всех u, combiner.apply(identity, u) равно u. Дополнительно, combiner функция должна быть ассоциативной и должна быть совместимой с accumulator функция; для всех u и t, следующее должно содержать:


     combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)
 

Изменчивое Сокращение

Изменчивая работа сокращения подобна обычному сокращению, в котором она уменьшает поток значений к единственному значению, но вместо того, чтобы привести к отличному однозначному результату, она видоизменяет контейнер общего результата, такой как a Collection или StringBuilder, поскольку это обрабатывает элементы в потоке.

Например, если бы мы хотели взять поток строк и связать их в единственную длинную строку, то мы могли достигнуть этого с обычным сокращением:


     String concatenated = strings.reduce("", String::concat)
 
Мы получили бы требуемый результат, и он будет даже работать параллельно. Однако, мы не могли бы быть довольными производительностью! Такая реализация сделала бы большое строковое копирование, и время выполнения будет O (n^2) в числе элементов. Более производительный подход должен был бы накопить результаты в a StringBuilder, который является изменчивым контейнером для того, чтобы накопить строки. Мы можем использовать тот же самый метод, чтобы параллелизировать изменчивое сокращение, как мы делаем с обычным сокращением.

Изменчивую работу сокращения вызывают collect(), поскольку это собирает вместе требуемые результаты в контейнер результата такой как StringBuilder. A collect работа требует трех вещей: функция фабрики, которая создаст новые экземпляры контейнера результата, накапливающаяся функция, которая обновит контейнер результата, включая новый элемент, и объединяющуюся функцию, которая может взять два контейнера результата и объединить их содержание. Форма этого очень подобна общей форме обычного сокращения:


 <R> R collect(Supplier<R> resultFactory,
               BiConsumer<R, ? super T> accumulator,
               BiConsumer<R, R> combiner);
 
Как с reduce(), преимущество выражения collect этим абстрактным способом то, что это непосредственно поддается parallelization: мы можем накопить частичные результаты параллельно и затем объединить их. Например, чтобы собрать Строковые представления элементов в потоке в ArrayList, мы могли записать очевидное последовательное для - каждая форма:

     ArrayList<String> strings = new ArrayList<>();
     for (T element : stream) {
         strings.add(element.toString());
     }
 
Или мы могли использовать parallelizable, собирают форму:

     ArrayList<String> strings = stream.collect(() -> new ArrayList<>(),
                                                (c, e) -> c.add(e.toString()),
                                                (c1, c2) -> c1.addAll(c2));
 
или, замечание, что мы проложили отображающуюся работу под землей в функции аккумулятора, более кратко как:

     ArrayList<String> strings = stream.map(Object::toString)
                                       .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
 
Здесь, наш поставщик только ArrayList constructor, аккумулятор добавляет stringified элемент к ArrayList, и объединитель просто использует addAll скопировать строки с одного контейнера в другой.

Как с регулярной работой сокращения, только прибывает возможность параллелизировать, если условие ассоциативности соблюдают. combiner ассоциативно если для контейнеров результата r1, r2, и r3:


    combiner.accept(r1, r2);
    combiner.accept(r1, r3);
 
эквивалентно

    combiner.accept(r2, r3);
    combiner.accept(r1, r2);
 
где эквивалентность означает это r1 оставляется в том же самом состоянии (согласно значению equals для типов элемента). Точно так же resultFactory должен действовать как идентификационные данные относительно combiner так, чтобы для любого контейнера результата r:

     combiner.accept(r, resultFactory.get());
 
не изменяет состояние r (снова согласно значению equals). Наконец, accumulator и combiner должно быть совместимым так, что для контейнера результата r и элемент t:

    r2 = resultFactory.get();
    accumulator.accept(r2, t);
    combiner.accept(r, r2);
 
эквивалентно:

    accumulator.accept(r,t);
 
где эквивалентность означает это r оставляется в том же самом состоянии (снова согласно значению equals).

Три аспекта collect: поставщик, аккумулятор, и объединитель, часто очень сильно связан, и удобно представить понятие a Collector как являющийся объектом, который воплощает все три аспекта. Есть a collect метод, который просто берет a Collector и возвращает получающийся контейнер. Вышеупомянутый пример для того, чтобы собрать строки в a List может быть переписан, используя стандарт Collector как:


     ArrayList<String> strings = stream.map(Object::toString)
                                       .collect(Collectors.toList());
 

Сокращение, Параллелизм, и Упорядочивание

С некоторыми сложными операциями сокращения, например собирать, которое производит a Map, такой как:

     Map<Buyer, List<Transaction>> salesByBuyer
         = txns.parallelStream()
               .collect(Collectors.groupingBy(Transaction::getBuyer));
 
(где Collectors.groupingBy(java.util.function.Function<? super T, ? extends K>) служебная функция, которая возвращает a Collector для того, чтобы сгруппировать наборы элементов, основанных на некотором ключе), может фактически быть контрпроизводительно выполнить работу параллельно. Это то, потому что объединяющийся шаг (объединяющийся один Map в другого ключом), может быть дорогим для некоторых Map реализации.

Предположите, однако, что контейнер результата, используемый в этом сокращении, был одновременно поддающимся изменению набором - таким как a ConcurrentHashMap. В этом случае параллельные вызовы аккумулятора могли фактически внести свои результаты одновременно в тот же самый совместно используемый контейнер результата, избавляя от необходимости объединитель объединить отличные контейнеры результата. Это потенциально обеспечивает усиление до параллельной производительности выполнения. Мы вызываем это параллельное сокращение.

A Collector это поддерживает параллельное сокращение, отмечается с Collector.Characteristics.CONCURRENT характеристика. Наличие параллельного коллектора является необходимым условием для того, чтобы выполнить параллельное сокращение, но что один не достаточно. Если Вы воображаете многократные аккумуляторы, вносящие результаты в совместно используемый контейнер, порядок, в котором депонируются результаты, недетерминирован. Следовательно, параллельное сокращение только возможно, если упорядочивание не важно для обрабатываемого потока. Stream.collect(Collector) реализация только выполнит параллельное сокращение если

Например:

     Map<Buyer, List<Transaction>> salesByBuyer
         = txns.parallelStream()
               .unordered()
               .collect(groupingByConcurrent(Transaction::getBuyer));
 
(где Collectors.groupingByConcurrent(java.util.function.Function<? super T, ? extends K>) параллельный компаньон к groupingBy).

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

Ассоциативность

Оператор или функция op ассоциативно, если следующее содержит:

     (a op b) op c == a op (b op c)
 
Важность этого, чтобы быть параллельной оценке может быть замечена, если мы разворачиваем это до четырех сроков:

     a op b op c op d == (a op b) op (c op d)
 
Таким образом, мы можем оценить (a op b) параллельно с (c op d) и затем вызовите op на результатах. TODO, что делает ассоциативное среднее значение для изменчивых функций объединения? FIXME: мы описали изменчивую ассоциативность выше.

Потоковые источники

TODO, куда этот раздел идет? XXX - изменяются на раздел, чтобы передать потоком конструкцию, постепенно представляющую более сложные способы создать - конструкция из Набора - конструкция от Iterator - конструкция от массива - конструкция от генераторов - конструкция от spliterator XXX - следующее является довольно низким уровнем, но важным аспектом потокового сжатия

Конвейер первоначально создается из spliterator (см. Spliterator) предоставленный потоковым источником. spliterator покрывает элементы источника и обеспечивает операции обхода элемента для возможно параллельного вычисления. См. методы на <коде> Потоки </код> для конструкции конвейеров, используя spliterators.

Источник может непосредственно предоставить spliterator. Если так, spliterator пересекается, разделяется, или запрашивается для предполагаемого размера после, и никогда прежде, терминальная работа начинается. Строго рекомендуется, чтобы spliterator сообщили о характеристике IMMUTABLE или CONCURRENT, или будьте позднее связывание и не свяжите с элементами это покрываете пока не пересечено, разделите или запрошенный для предполагаемого размера.

Если источник не может непосредственно предоставить рекомендуемый spliterator тогда, он может косвенно предоставить spliterator, использующий a Supplier. spliterator получается от поставщика после, и никогда прежде, терминальная работа потокового конвейера начинается.

Такие требования значительно уменьшают контекст потенциальной интерференции к интервалу, запускающемуся с открытия терминальной работы и окончания созданием результата или побочного эффекта. См. Невмешательство для большего количества деталей. XXX - перемещают следующий в раздел невмешательства

Источник может быть изменен прежде, чем терминальная работа начинается, и те модификации будут отражены в покрытых элементах. Впоследствии, и в зависимости от свойств источника, дальнейшие модификации не могли бы быть отражены и бросок a ConcurrentModificationException может произойти.

Например, рассмотрите следующий код:


     List<String> l = new ArrayList(Arrays.asList("one", "two"));
     Stream<String> sl = l.stream();
     l.add("three");
     String s = sl.collect(toStringJoiner(" ")).toString();
 
Сначала список создается состоящий из двух строк: "один"; и "два". Затем поток создается из того списка. Затем список изменяется, добавляя третью строку: "три". Наконец элементы потока собираются и объединялись. Так как список был изменен перед терминалом collect работа, начатая результат, будет строкой "один два три". Однако, если список изменяется после того, как терминальная работа начинается, как в:

     List<String> l = new ArrayList(Arrays.asList("one", "two"));
     Stream<String> sl = l.stream();
     String s = sl.peek(s -> l.add("BAD LAMBDA")).collect(toStringJoiner(" ")).toString();
 
тогда a ConcurrentModificationException будет брошен начиная с peek работа попытается добавить строку "ПЛОХАЯ ЛЯМБДА" к списку после того, как терминальная работа началась.
 Платформа Java™
Стандарт Эд. 8

Проект сборка-b92

Представьте ошибку или функцию
Для дальнейшей ссылки API и документации разработчика, см. Java Документация SE. Та документация содержит более подробные, предназначенные разработчиком описания, с концептуальными краткими обзорами, определениями сроков, обходных решений, и рабочих примеров кода.
Авторское право © 1993, 2013, Oracle и/или его филиалы. Все права защищены.

Проект сборка-b92




Spec-Zone.ru - all specs in one place