Laboratory Training 1
Working with Big Numbers and Data Sets
1 Training Tasks
1.1 Individual Task
Design and implement classes to represent the entities of the third laboratory training of the Java Basic Programming Course. The solution should be based on the previously created classes.
The program must demonstrate:
- reproduction of the functionality of laboratory trainings No. 3 and No. 4 of the Java Basic Programming Course;
- using Stream API tools for all sequence processing and output functions;
- testing methods of individual classes using JUnit.
1.2 Finding an Integer Power
Write a program that fills a number of type BigInteger with random digits and calculates the integer
        power of this number. For the result, use BigInteger type. Implement two ways:  using a pow() method
        and a function that provides multiplication of long integers. Compare the results. 
Provide testing of class methods using JUnit.
1.3 Filtering and Sorting
Create a list of objects of type BigDecimal. Fill the list with random values. Sort by decreasing
        absolute value. Find the product of positive numbers. Implement three approaches: 
- using loops and conditional statements (without facilities added in Java 8);
- without explicit loops and branches, using functions that have been defined in the Java Collection Framework interfaces since Java 8;
- using Stream API tools.
Provide testing of classes using JUnit.
1.4 Finding all Divisors (Advanced Task)
Use the Stream API to organize a search for all divisors of a positive integer. Create a separate static function
        that accepts an integer and returns an array of integers. Inside the function create an IntStream object.
        Apply
      range() function and filter. Do not use explicit loops.
2 Instructions
2.1 General Features of the Java SE Platform
Java Platform, Standard Edition, (Java SE) is the standard version of the Java platform, developed for creating and executing applications designed for individual use or for use in small enterprise. Java SE is defined by the specification of packages and classes that provide solutions to solving problems in the following directions:
- work with mathematical functions;
- working with container classes;
- work with time and calendar;
- work with text;
- internationalization and localization;
- work with regular expressions;
- work with input-output streams and the file system;
- working with XML;
- serialization and deserialization;
- creation of graphical user interface programs;
- use of graphic tools;
- support for printing;
- support for working with sound;
- using RTTI, reflection, and class loaders;
- use of multithreading;
- working with databases;
- Java Native Interface;
- means of executing scripts;
- network interaction support;
- interaction with the software environment;
- ensuring application security;
- support for logs;
- deployment of Java applications.
Next, some of the capabilities of Java SE will be considered.
2.2 Using BigInteger and BigDecimal Types
2.2.1 Overview
For mathematical calculations, in addition to built-in value types, you can use objects of classes derived from
        java.lang.Number. Previously, the classes Byte, Double, Float, Integer, Long and Short,
        derived from Number, were considered. There are also java.math.BigInteger and java.math.BigDecimal classes
        that allow you to work with numbers of arbitrary precision.
Like all classes derived from the java.lang.Number abstract
          class, the BigInteger and BigDecimal classes implement conversion methods to existing
          primitive types:
- byte byteValue()
- double doubleValue()
- float floatValue()
- int intValue()
- long longValue()
- short shortValue()
It should be remembered that such a transformation very often leads to a partial loss of accuracy in the representation of numbers.
Both classes implement the Comparable interface and you can use the compareTo() method to compare such
        numbers. This method returns –1 if the current object is less than the parameter, 1 if the current object
      is greater, and 0 if the values are equal.
2.2.2 Using BigInteger
 Constructors of the BigInteger class allow you to create numbers from strings, byte arrays, or generate
      it randomly, specifying the desired length.
BigInteger number1 =new BigInteger("12345678901234567890"); BigInteger number2 =new BigInteger(new byte [] { 1, 2, 3 }); BigInteger number3 =new BigInteger(100,new Random());// 100 - number of bits 
 If a BigInteger object was created from an array of bytes, they define the internal binary representation
        of the number. In our case, number2 will represent the integer 10000001000000011 in binary notation, or 66051.
To initialize a BigInteger object with an integer,
        use the valueOf() static factory method:
int n = 100000; BigInteger number4 = BigInteger.valueOf(n);
For the convenience of working, the constants BigInteger.ZERO, BigInteger.ONE, BigInteger.TWO and BigInteger.TEN are
      defined. 
 In addition to overloading the standard toString() method, the toString() function is also implemented with a
        parameter that is the 
 radix:
BigInteger number = BigInteger.valueOf(122); System.out.println(number.toString(2));// 1111010 System.out.println(number.toString(3));// 11112 System.out.println(number.toString(12));// a2 System.out.println(number.toString(16));// 7a 
 Instead of operators, it is necessary to use methods of the BigInteger class:
- BigInteger add(BigInteger secondOperand)returns the sum of two numbers;
- BigInteger subtract(BigInteger secondOperand)returns the result of subtraction of two numbers;
- BigInteger multiply(BigInteger secondOperand)returns the product of two numbers;
- BigInteger divide(BigInteger secondOperand)returns the result of division of two numbers;
- BigInteger negate()returns the result of multiplication by –1;
- BigInteger remainder(BigInteger val)returns the remainder from division (positive or negative)
- BigInteger mod(BigInteger secondOperand)returns the absolute value of the remainder from the division of two numbers.
Below is an example of using these operations.
BigInteger ten = BigInteger.TEN; BigInteger two = BigInteger.TWO; BigInteger eleven = ten.add(BigInteger.ONE); System.out.println(eleven);// 11 BigInteger minusEleven = eleven.negate(); System.out.println(minusEleven);// -11 System.out.println(ten.add(two));// 12 System.out.println(ten.subtract(two));// 8 System.out.println(ten.multiply(two));// 20 System.out.println(eleven.divide(two));// 5 System.out.println(minusEleven.mod(two));// 1 System.out.println(minusEleven.remainder(two));// -1 
 The max() and min() methods allow you to compare the current object with a parameter.
        These methods also return a BigInteger:
System.out.println(ten.max(two));// 10 System.out.println(ten.min(two));// 2 
You can also call mathematical methods:
- BigInteger abs()returns the absolute value of a number;
- BigInteger pow(int n)returns the- n-th power of a number;- ncannot be a negative number;
- BigInteger gcd(BigInteger number)returns a- BigIntegerwhose value is the greatest common divisor of absolute values of current object and- number.
Example:
System.out.println(minusEleven.abs());// 11 System.out.println(two.pow(10));// 1024 System.out.println(two.gcd(ten));// 2 
There are a number of functions for implementing bitwise operations.
- BigInteger and(BigInteger number)returns the result of the AND operation;
- BigInteger or(BigInteger number)returns the result of the OR operation;
- BigInteger not()returns the result of the NOT operation (bit inversion);
- BigInteger xor(BigInteger number)returns the result of the XOR operation (exclusive or);
- BigInteger shiftLeft(int n)returns the operation of shifting bits to the left by- npositions;
- BigInteger shiftRight(int n)returns the operation of shifting the bits to the right by- npositions.
There are methods that are a superposition of two operations, for example, andNot(). The following
        example demonstrates how bitwise operations work:
BigInteger first = BigInteger.valueOf(23); System.out.println(first + " " + first.toString(2));// 23 10111 BigInteger second = BigInteger.valueOf(12); System.out.println(second + " " + second.toString(2));// 12 1100 BigInteger n; n = first.and(second); System.out.println(n + " " + n.toString(2));// 4 100 n = first.or(second); System.out.println(n + " " + n.toString(2));// 31 11111 n = first.not(); System.out.println(n + " " + n.toString(2));// -24 -11000 n = first.xor(second); System.out.println(n + " " + n.toString(2));// 27 11011 n = first.shiftLeft(2); System.out.println(n + " " + n.toString(2));// 92 1011100 n = first.shiftRight(1); System.out.println(n + " " + n.toString(2));// 11 1011 
There are a number of methods for working with individual bits:
- boolean testBit(int n)returns- trueif the specified bit is set to one;
- BigInteger setBit(int n)returns a- BigIntegerobject in which the corresponding bit is set to one;
- BigInteger clearBit(int n)returns a- BigIntegerobject in which the corresponding bit is set to zero;
- BigInteger flipBit(int n)returns a- BigIntegerobject in which the corresponding bit is set to the opposite value.
Bits are numbered starting from zero from the rightmost bit. The following example demonstrates working with individual bits:
BigInteger n = BigInteger.valueOf(48); System.out.println(n + " " + n.toString(2));// 48 110000 n = n.setBit(0); System.out.println(n + " " + n.toString(2));// 49 110001 n = n.setBit(2); System.out.println(n + " " + n.toString(2));// 53 110101 n = n.clearBit(5); System.out.println(n + " " + n.toString(2));// 21 10101 n = n.flipBit(1); System.out.println(n + " " + n.toString(2));// 23 10111 
There are also interesting methods for working with prime numbers. These are numbers that are probably prime.
        In most practical applications of prime numbers, for example in cryptography, it is sufficient to assume that some
        large number is probably prime and can be used for encryption. Miller-Rabin
        algorithm is implemented to obtain probably
        prime numbers. In some methods of the BigInteger class, you can adjust the probability that the resulting random
        number is prime by specifying the certainty parameter. The probability that the number will be prime is estimated
        as 1 - 1/2certainty and higher. It should be remembered that increasing the probability is associated with a significant
      increase in time and spending other resources. If the parameter is not defined, it defaults to 100.
 A special constructor allows you to create a BigInteger object with a presumably prime number inside.
        A similar job is performed by the factory static method:
- BigInteger(int bitLength, int certainty, Random rnd)creates an object that contains a probably prime number with a representation length of- bitLengthlength of bitLength bits; the given- certaintyand the- rndrandom number generator object will be used for generation;
- static BigInteger probablePrime(int bitLength, Random rnd)creates and returns an object that contains a probably prime number with a representation length of- bitLengthbits; the- rndrandom number generator object will be used for generation;
In addition, there are two methods for working with probably prime numbers:
- boolean isProbablePrime(int certainty)returns- truef the number is probably prime and- falseif it is definitely not prime;
- BigInteger nextProbablePrime()returns the first probably prime number greater than current object.
The following example we search for consecutive probably prime numbers from 50 to 100:
BigInteger n = BigInteger.valueOf(50);while (n.intValue() <= 100) { n = n.nextProbablePrime();if (n.intValue() > 100) {break ; } System.out.printf("%d ", n); }
 Additional methods intValueExact(), byteValueExact(), shortValueExact() and longValueExact() are
        implemented for conversion into primitive integer types. The specificity of these methods is that the value of the
        range of the corresponding primitive type is checked. If there is no way to accurately convert the type, the methods
        throw an ArithmeticException.
2.2.2 Using BigDecimal
 The main difference between the BigDecimal type and double is that
        it uses the decimal notation instead of binary to represent floating-point numbers. The traditional representation
        of double as mantissa × 2 exponent does not accurately represent simpler
        decimal fractions such as 0.3, 0.6, 0.7, etc.
Data in a BigDecimal type object is represented in the
          form of mantissa × 10 exponent, and the precision of the number representation
          is practically unlimited. This approach naturally reduces the efficiency of working with floating-point numbers,
          but there are many problems in which efficiency can be sacrificed for the sake of accuracy. First of all, these
          are financial transactions.
In general, the following can be attributed to the advantages:
- bringing the internal representation closer to the one adopted in everyday activities;
- high accuracy of number representation;
- powerful tools for managing the accuracy of mathematical operations and mechanisms of getting results;
- no overflow errors.
Disadvantages include the following:
- inefficient use of memory;
- slowness of calculations;
- problems with storage in databases.
The presence of these shortcomings limits the use in ordinary calculations. This type should only be used if efficiency is less important than accuracy.
The internal representation of BigDecimal consists of an "unscaled" arbitrary-precision integer (unscaledValue)
        and a 32-bit integer scale. If the scale is zero or positive, the scale is the number of digits
        to the right of the decimal point. If the scale is negative, the unscaled value of the number is multiplied by ten
        to the power of -scale . Therefore, the value of the number represented is unscaledValue × 10 -scale.
The scale() and setScale() methods of BigDecimal class allow you
        to get and set the value of the scale, respectively. The unscaledValue() method allows you to get an
        unscaled value.
There are several ways to create an object of type BigDecimal. You can create an object from an integer and a double      number:
BigDecimal fromInt =new BigDecimal(1295); BigDecimal fromDouble =new BigDecimal(1.27);
 To ensure better accuracy, the object should be created from a string, not from a number of type double:
BigDecimal fromDouble =new BigDecimal(1.27); System.out.println(fromDouble);// 1.270000000000000017763568394002504646778106689453125 BigDecimal fromString =new BigDecimal("1.27"); System.out.println(fromString);// 1.27 
 The inaccuracy occurs when a number from a user-defined decimal representation is converted to a binary (double)
      and then vice versa.
 The java.math.MathContext class  is associated with the BigDecimal class. It encapsulates
        the rules for performing arithmetic operations, in particular, precision and rounding rules are defined in the
        constructors. The value 0 assumes an unlimited length of the number, positive integers are the number of digits
        of the representation:
- MathContext(int setPrecision)
-  MathContext(int setPrecision, RoundingMode setRoundingMode)
The java.math.RoundingMode enumeration lists the constants for defining the rounding rule:  
- UP:away from zero;
- CEILING:towards positive infinity;
- DOWN:towards zero;
- FLOOR:towards negative infinity;
- HALF_DOWN:if "neighbors" are at the same distance, away from zero;
- HALF_EVEN:if "neighbors" are at the same distance, towards an even value;
- HALF_UP:if "neighbors" are at the same distance, towards zero;
- UNNECESSARY: rounding cannot be performed; if rounding is required, an exception is thrown.
The default rounding option is HALF_UP.
An  object of MathContext class can be applied, in particular, as a parameter of round() method.
        The example below demonstrates different rounding rules for positive and negative numbers:
import static java.math.RoundingMode.*; ... BigDecimal positive1 =new BigDecimal("2.4"); System.out.println(positive1.round(new MathContext(1, UP)));// 3 System.out.println(positive1.round(new MathContext(1, CEILING)));// 3 System.out.println(positive1.round(new MathContext(1, DOWN)));// 2 System.out.println(positive1.round(new MathContext(1, FLOOR)));// 2 System.out.println(positive1.round(new MathContext(1, HALF_DOWN)));// 2 System.out.println(positive1.round(new MathContext(1, HALF_UP)));// 2 BigDecimal positive2 =new BigDecimal("2.5"); System.out.println(positive2.round(new MathContext(1, UP)));// 3 System.out.println(positive2.round(new MathContext(1, CEILING)));// 3 System.out.println(positive2.round(new MathContext(1, DOWN)));// 2 System.out.println(positive2.round(new MathContext(1, FLOOR)));// 2 System.out.println(positive2.round(new MathContext(1, HALF_DOWN)));// 2 System.out.println(positive2.round(new MathContext(1, HALF_UP)));// 3 BigDecimal negative1 =new BigDecimal("-2.4"); System.out.println(negative1.round(new MathContext(1, UP)));// -3 System.out.println(negative1.round(new MathContext(1, CEILING)));// -2 System.out.println(negative1.round(new MathContext(1, DOWN)));// -2 System.out.println(negative1.round(new MathContext(1, FLOOR)));// -3 System.out.println(negative1.round(new MathContext(1, HALF_DOWN)));// -2 System.out.println(negative1.round(new MathContext(1, HALF_UP)));// -2 BigDecimal negative2 =new BigDecimal("-2.5"); System.out.println(negative2.round(new MathContext(1, UP)));// -3 System.out.println(negative2.round(new MathContext(1, CEILING)));// -2 System.out.println(negative2.round(new MathContext(1, DOWN)));// -2 System.out.println(negative2.round(new MathContext(1, FLOOR)));// -3 System.out.println(negative2.round(new MathContext(1, HALF_DOWN)));// -2 System.out.println(negative2.round(new MathContext(1, HALF_UP)));// -3 
You can use the DECIMAL32, DECIMAL64, DECIMAL128, and UNLIMITED constants to define a MathContext object. with
      appropriate precision and HALF_UP rounding rule.
There are several constructors of the BigDecimal class that use MathContext, for example,
- BigDecimal(double val, MathContext mc)
-  BigDecimal(int val, MathContext mc)
-  BigDecimal(String val, MathContext mc)
To create an object, you can use BigInteger:
- BigDecimal(BigInteger val)
- BigDecimal(BigInteger unscaledVal, int scale)
- BigDecimal(BigInteger unscaledVal, int scale, MathContext mc)
- BigDecimal(BigInteger val, MathContext mc)
Like BigInteger, the BigDecimal class provides BigDecimal.ZERO, BigDecimal.ONE, BigDecimal.TWO and BigDecimal.TEN      constants.
The use of arithmetic operations on similar BigInteger. Additional methods are implemented, the
        last parameter of which is MathContext. There is a problem with division. In cases where the result
        is an infinite fraction, division without a limit on the length of the result results in the generation of an exception java.lang.ArithmeticException.
        For example, we will receive such an exception when trying to calculate 1/3:
BigDecimal three =new BigDecimal("3"); System.out.println(BigDecimal.ONE.divide(three));// exception 
In order to prevent this, it is necessary to specify MathContext object during division, for example:
BigDecimal three =new BigDecimal("3"); System.out.println(BigDecimal.ONE.divide(three, MathContext.DECIMAL128));// 0.3333333333333333333333333333333333 
You can also apply getting the integer part and the remainder separately :
- BigDecimal divideToIntegralValue(BigDecimal divisor)
- BigDecimal divideToIntegralValue(BigDecimal divisor, MathContext mc)
- BigDecimal remainder(BigDecimal divisor)
- BigDecimal remainder(BigDecimal divisor, MathContext mc)
The divideAndRemainder(BigDecimal divisor) method returns an array of two objects of BigDecimal type.
      The two elements of this array are the integer part and the remainder, respectively.
Mathematical functions are also implemented in the BigDecimal class, for example:
- BigDecimal sqrt(MathContext mc)obtaining the square root;
-  BigDecimal pow(int n)obtaining an integer power;
-  BigDecimal pow(int n, MathContext mc) obtaining an integer power;
-  BigDecimal abs()obtaining an absolute value;
-  BigDecimal abs(MathContext mc)obtaining an absolute value.
 There are also functions for finding the maximum and minimum of two BigDecimal numbers, as well as
      a number of auxiliary functions.
2.3 Problems of Storage and Processing of Data Sets
During the creation of modern information systems, working with data sets (data collections) is one of the most widespread. Typical tasks of data processing can be given:
- data collection and registration;
- data accumulation;
- search and filtering;
- transformations and calculations;
- unification and separation;
- sorting;
- data output in the required form.
Very often, data processing algorithms do not differ from information storage methods. For example, you can sort arrays, lists, data loaded from a database, strings of text, etc.
One of the principles of generalized programming is to separate data structures from processing algorithms. Such separation is appropriate in all cases where it is possible. The expediency of such a separation lies in
- Ability to create universal containers that do not depend on algorithms and algorithms that do not depend on data structures
- Ability to independently develop and modify code related to data storage and processing.
- The possibility of separate testing of data storage and processing tasks, which increases the reliability of the software.
- Improved code readability
In addition, this separation of code corresponds to the principle of th Single responsibility principle, the most important principle of object-oriented design.
However, there are some cases where the separation of data structures from algorithms does not make sense. For example, it is impractical in simple tasks that do not involve scaling, as well as when the efficiency of the program, the amount of memory, etc. are critical.
Separation of the tasks of information storage and processing would be implemented in libraries of various programming
        languages and platforms. For example, the C++ standard includes the Standard Template Library (STL), which provides
        separate data structures for storing collections of objects, such as vector, list, set, etc., and separate template
        functions for working with arbitrary sequences, including arrays. These are generalized functions for_each(), find_if(), sort(), etc.
Its approach to the implementation of algorithms was provided in the Java 2 Collection Framework.
2.4 Using the Java Collection Framework for Data Processing
2.4.1 Overview
The Java Collection Framework (JCF) is a set of interfaces, classes, and algorithms designed to store and manipulate collections of objects.
In addition, Java 8 supports Java 1.1 containers. These are Vector, Enumeration, Stack, BitSet and
        some others. For example, the Vector class provides functions similar to ArrayList. These
        containers did not provide a standardized interface in the first version, they do not allow the user to omit excessive
        synchronization, which is relevant only in a multithreading environment, and therefore not sufficiently effective.
        As a result, they are considered obsolete and not recommended for use. Instead, you should use the corresponding
      generic Java 5 containers.
JFC interfaces and classes were defined in Java 2. Starting with Java 5, all types are implemented as generic. Significant enhancements appeared in JDK 1.8. Different types were added and expanded in later versions.
We'll look at the facilities that were implemented before Java 8 first. Below are the most
        important JCF standard generic interfaces with the standard classes that implement them. In addition to these classes,
        for almost every interface there is an abstract class that implements it. For example, AbstractCollection,
        as well as classes derived from it: AbstractList, AbstractQueue, AbstractSet,
        etc. These classes are the base for the corresponding standard implementations and can also be applied to create
        custom collections. 
- Iterableis the basic interface implemented in the- java.langpackage. This interface declares an abstract method- iterator()that returns an iterator object of type- Iterator. Objects of classes that implement this interface are collections that can be used in an alternative construction of the- forloop (for each). In addition, the- Iterableinterface provides a number of methods with default implementations.
- Iteratoris an interface, the implementation of whose methods ensures the sequential traversal over collection items. Starting with Java 5, iterators allow iterating through elements using a "for each" loop. All JFC collections provide their own iterators for traversing elements.
- ListIteratoran interface derived from- Iterator, which allows iterating in long directions as well as modifying the current elements. Used in lists.
- Collectionis an interface derived from- Iterable. It is basic for all collections except associative arrays (- Map). The- AbstractCollectionclass directly implements the- Collectioninterface. This class is used to create a number of abstract classes that represent different types of collections.
- Listis interface derived from- Collection; it defines a data structure in which elements can be accessed using indexes; lists support duplication and can store elements in the order they were added; the most popular standard classes that implement this interface are- ArrayList(a list built on an array) and- LinkedList.
- Queueis interface derived from- Collection; a collection used to store elements in first-in-first-out (FIFO) access order. The most popular class that implements this interface is- LinkedList.
- Dequeis an interface derived from- Queue. It extends the functionality of the queue by allowing elements to be inserted and removed both from the beginning and from the end of the collection (double-ended queue). The classes that implement this interface are- ArrayDequeand- LinkedList.
- Setis an interface derived from- Collection, representing a collection that does not allow duplicate elements. It ensures that each element is stored in the collection only once. The most common implementation is- HashSet.
- SortedSetis an interface derived from- Set. Assumes that elements are arranged according to a specific sort attribute. The most common implementation is- TreeSet.
- Mapis an interface that represents a separate branch. This interface describes a collection of key-value pairs. Each key is unique and is used to access the corresponding value. The most common implementation is- HashMap.
- SortedMap is an interface derived from Map. Assumes that the keys are arranged according to a certain sort feature. The most common implementation is TreeMap.
The listed interfaces (except Iterable) and the standard classes that implement them are located in
        the
        java.util package and require the use of appropriate import directives. 
All JCF collections can be either read-only collections or read-write collections. There are no separate
        interfaces for such collections. It's just that read-only collections throw an exception UnsupportedOperationException        in
        the body of all methods that could potentially modify the collection. This approach reduces the number of different
        interfaces, but it has a drawback: calls to unimplemented methods cannot be checked at compile time, only at runtime.
The Iterator<E> interface declares the following methods:
- boolean hasNext()returns- trueif there are still elements in the collection.
- E next()returns the next element in the iteration. After the first call, the initial element of the collection is referenced.
- void remove()removes the element referenced by the iterator from the collection.
The ListIterator<E> interface adds the following methods:
- int nextIndex()returns the index of the element that will be returned by the next call of- next().
-  boolean hasPrevious()returnstrueif this list iterator has more elements when traversing the list backwards.
- E previous()returns the previous item in the list and moves the cursor position in the reverse direction.
- int previousIndex()returns the index of the element that will be returned by the next call of- previous().
-  void add(E e)inserts the specified item into the list.
-  void set(E e)replaces the last element obtained vianext()orprevious()with the specified element.
Iterator methods will be discussed in the context of their use in collections.
2.4.2 Collection and List Interfaces
The Collection<E> interface is the base for most Collection Framework interfaces. The most general
        operations that are implemented in all container classes (except associative arrays) are declared in this interface.
        The methods that do not change the collection are listed below: 
- int size()returns the size of the collection;
- boolean isEmpty()returns- trueif the collection is empty;
- boolean contains(Object o)returns- trueif the collection contains an object;
- boolean containsAll(Collection<?> c)returns- trueif the collection contains another collection;
- Iterator<E> iterator()returns an iterator - an object that sequentially points to elements;
- Object[] toArray()returns an array of references to- Object, which contains copies of all elements of the collection;
- T[] toArray(T[] a)returns an array of references to- T, which contains copies of all elements of the collection.
The following example demonstrates how the methods work on the elements of a read-only list that is created from
        an array using the Arrays.asList() method:
Collection<Integer> unmodifiable = Arrays.asList(1, 2, 4, 8); System.out.println(unmodifiable.size());// 4 System.out.println(unmodifiable.isEmpty());// false System.out.println(unmodifiable.contains(4));// true System.out.println(unmodifiable.contains(6));// false System.out.println(unmodifiable.containsAll(Arrays.asList(1, 4)));// true // Display elements in separate lines: for (var iterator = unmodifiable.iterator(); iterator.hasNext(); ) { System.out.println(iterator.next()); }// Output elements using an implicit iterator: for (Integer k : c) { System.out.println(k); }// The parameter is necessary to create an array of a certain type: Integer[] arr = unmodifiable.toArray(new Integer[0]); System.out.println(Arrays.toString(arr));// [1, 2, 4, 8] 
Interface methods Collection<E> that modify a collection: 
- boolean add(E e)adds an object to the collection. Returns- trueif the object is added.
- boolean remove(Object o)removes an object from the collection.
- boolean addAll(Collection<? extends E> c)adds objects to the collection. Returns- trueif objects are added.
- boolean removeAll(Collection<?> c)removes objects from a collection if they are present in another collection.
- boolean retainAll(Collection<?> c)leaves objects present in another collection.
- void clear()removes all elements from the collection.
To test the specified methods, you can use the ArrayList class: 
Collection<Integer> modifiable =new ArrayList<>(Arrays.asList(16, 32, 64)); System.out.println(modifiable);// [16, 32, 64] modifiable.add(128); System.out.println(modifiable);// [16, 32, 64, 128] modifiable.remove(16); System.out.println(modifiable);// [32, 64, 128] modifiable.addAll(unmodifiable); System.out.println(modifiable);// [32, 64, 128, 1, 2, 4, 8] modifiable.removeAll(unmodifiable); System.out.println(modifiable);// [32, 64, 128] modifiable.addAll(unmodifiable); System.out.println(modifiable);// [32, 64, 128, 1, 2, 4, 8] modifiable.retainAll(unmodifiable); System.out.println(modifiable);// [1, 2, 4, 8] modifiable.clear(); System.out.println(modifiable);// [] 
Methods added in the List<E> interface:
- E get(int index)returns the element with the specified index.
- E set(int index, E element)replaces the element at the specified index with the specified object. Returns the previous value that was stored at the specified index.
- void add(int index, E element)inserts the specified element into the specified position in the list.
- boolean addAll(int index, Collection<? extends E> c)inserts elements from the specified collection into the specified position in the list.
- E remove(int index)removes the element at the specified position.
- int indexOf(Object o)returns the index of the first occurrence of the object, or -1 if the object does not exist.
- int lastIndexOf(Object o)returns the index of the last occurrence of the object, or -1 if the object is not present.
- List<E> subList(int fromIndex, int toIndex)returns the part of the list from fromIndex (including) to toIndex (not including). Memory for the new list is not allocated.
- ListIterator<E> listIterator()returns a list iterator. After the first call of- next()method the iterator will point to the initial element of the list.
- ListIterator<E> listIterator(int index)returns a list iterator. The index points to the element that will be returned by the first call of- next(). The first call of- previous()returns the element with the specified index minus one.
The following example demonstrates the listed methods. An ArrayList class (a list built on
        the use of an array) is used for demonstration.
        List<Integer> list = new ArrayList<>();
list.add(0, 10);
System.out.println(list);                // [10]
list.addAll(0, Arrays.asList(1, 2, 3, 4, 5));
System.out.println(list);                // [1, 2, 3, 4, 5, 10]
System.out.println(list.get(4));         // 5
list.remove(2);
System.out.println(list);                // [1, 2, 4, 5, 10]
list.set(4, 1);
System.out.println(list);                // [1, 2, 4, 5, 1]
System.out.println(list.indexOf(1));     // 0
System.out.println(list.lastIndexOf(1)); // 4
System.out.println(list.subList(2, 4));  // [4, 5]
// Outputs index / value pairs in reverse order:
for (var iterator = list.listIterator(list.size()); iterator.hasPrevious(); ) {
    System.out.printf("%d %d%n", iterator.previousIndex(), iterator.previous());
}
// Adds indexes to elements. Adds intermediate elements:
for (var iterator = list.listIterator(); iterator.hasNext(); ) {
    iterator.set(iterator.nextIndex() + iterator.next());
    iterator.add(100);
}
System.out.println(list); // [1, 100, 4, 100, 8, 100, 11, 100, 9, 100]
      
      In cases where insertion and deletion are used more often than getting an element by index, it is more efficient
      to use a LinkedList class (a doubly linked list).
2.4.3 Working with Queues and Stacks
A queue in the broad sense is a data structure that is filled in by element, and it allows getting objects from it according to a certain rule. In the narrow sense, this rule is "First In - First Out" (FIFO). In a queue organized on the principle of FIFO, adding an element is possible only at the end of the queue, and getting is only possible from the beginning of the queue.
In the container library, the queue is represented by the Queue interface. Methods declared this interface are listed in the table below:
| Type of operation | Throws an exception | Returns a special value | 
|---|---|---|
| Adding | add(e) | offer(e) | 
| Obtaining an item with removing | remove() | poll() | 
| Obtaining an item without removing | element() | peek() | 
The offer() method returns false if the item could not be added, for example,
        if the queue has a limited number of items. In this case, the add() method throws an exception. Similarly, remove() and element() throw
        an exception if the queue is empty, but poll() and peek() in this case return null. 
The most convenient way to implement the queue is the use of the LinkedList class that implements
        the Queue interface. For example:
package ua.inf.iwanoff.java.advanced.first;import java.util.LinkedList;import java.util.Queue;public class SimpleQueueTest {public static void main(String[] args) { Queue<String> queue =new LinkedList<>(); queue.add("First"); queue.add("Second"); queue.add("Third"); queue.add("Fourth"); String s;while ((s = queue.poll()) !=null ) { System.out.print(s + " ");// First Second Third Fourth } } }
The PriorityQueue class arranges the elements according to the comparator (the object that implements
        the Comparator interface) specified in the constructor as a parameter. If an object is created using
        a constructor without parameters, the elements will be ordered in a natural way (ascending for numbers, in alphabetical
        order for strings). For example:
package ua.inf.iwanoff.java.advanced.first;import java.util.PriorityQueue;import java.util.Queue;public class PriorityQueueTest {public static void main(String[] args) { Queue<String> queue =new PriorityQueue<>(); queue.add("First"); queue.add("Second"); queue.add("Third"); queue.add("Fourth"); String s;while ((s = queue.poll()) !=null ) { System.out.print(s + " ");// First Fourth Second Third } } }
The Deque interface (double-ended-queue) provides the ability to add and remove items from both ends.
        Methods declared in this interface are listed below:
| Type of operation | Working with the first element | Working with the last element | 
|---|---|---|
| Adding | addFirst(e) | addLast(e) | 
| Obtaining an item with removing | removeFirst() | removeLast() | 
| Obtaining an item without removing | getFirst() | getLast() | 
Each pair represents the function that throws an exception, and the function that returns some special value. There
        are also methods for removing the first (or last) occurrence of a given element (removeFirstOccurrence() and removeLastOccurrence(),
        respectively).
You can use whether the special ArrayDeque class or LinkedList to implement the interface.
A stack is a data structure organized on the principle "last in – first out" (LIFO). There are three stack operations: adding element (push), removing element (pop) and reading head element (peek).
In JRE 1.1, the stack is represented by the Stack class. For example:
package ua.inf.iwanoff.java.advanced.first;import java.util.Stack;public class StackTest {public static void main(String[] args) { Stack<String> stack =new Stack<>(); stack.push("First"); stack.push("Second"); stack.push("Third"); stack.push("Fourth"); String s;while (!stack.isEmpty()) { s = stack.pop(); System.out.print(s + " ");// Fourth Third Second First } } }
This class is currently not recommended for use. Instead, you can use the Deque interface, which declares
        the similar methods. For example:
package ua.inf.iwanoff.java.advanced.first;import java.util.ArrayDeque;import java.util.Deque;public class AnotherStackTest {public static void main(String[] args) { Deque<String> stack =new ArrayDeque<>(); stack.push("First"); stack.push("Second"); stack.push("Third"); stack.push("Fourth"); String s;while (!stack.isEmpty()) { s = stack.pop(); System.out.print(s + " ");// Fourth Third Second First } } }
Stacks are often used in various algorithms. In particular, it is often possible to implement a complex algorithm without recursion with the help of a stack.
2.4.4 Additional Features for Collections in Java 8
Up to and including Java 7, the traditional imperative approach, based on the explicit use of loops, conditional
      statements, switches, etc., was used to work with collections. The standard algorithms provided by classes Arrays      and Collections, only partially met the need for data processing. 
Starting with Java 8, the standard interfaces of the java.util package are supplemented with methods
        that focus on using lambda expressions and references to methods. To ensure compatibility with previous versions,
        new interfaces provide the default implementation of the new methods. In particular, the Iterable interface
        defines the forEach() method, which allows you to perform some actions in the loop that do not change
        the elements of the collection. You can specify an action using a lambda expression or a reference to a method.
        For example:
public class ForEachDemo {static int sum = 0;public static void main(String[] args) { Iterable<Integer> numbers =new ArrayList(Arrays.asList(2, 3, 4)); numbers.forEach(n -> sum += n); System.out.println(sum); } }
In the above example, the sum of collection elements is calculated. The variable that holds the sum is described as a static class field, since lambda expressions cannot change local variables.
The Collection interface defines the removeIf() method, which allows you to remove items
        from the collection if items match a certain filter rule. In the following example, odd items are removed from the
        collection of integers. The forEach() method is used for columnwise output the collection items:
Collection<Integer> c =new ArrayList(Arrays.asList(2, 4, 11, 8, 12, 3)); c.removeIf(k -> k % 2 != 0);// The rest of the items are displayed columnwise: c.forEach(System.out::println);
The List interface provides methods replaceAll() and sort(). The second
        one can be used instead of the analogous static method of the Collections class, but the definition
        of the sorting feature is obligatory:
List<Integer> list =new ArrayList(Arrays.asList(2, 4, 11, 8, 12, 3)); list.replaceAll(k -> k * k);// replace the numbers with their second powers System.out.println(list);// [4, 16, 121, 64, 144, 9] list.sort(Integer::compare); System.out.println(list);// [4, 9, 16, 64, 121, 144] list.sort((i1, i2) -> Integer.compare(i2, i1)); System.out.println(list);// [144, 121, 64, 16, 9, 4] 
Java 8 defines the Spliterator interface that can be applied to collections. This interface
        defines a special kind of iterator,  a separator iterator. In particular, it allows you to split a sequence into
        several subsequences that you can work with in parallel. You can get an instance of Spliterator using the spliterator() method
        defined in the Collection interface.
The reason in dividing collections into separate parts arises first of all in cases where parallel processing
        of elements of a large data set is possible. But usage of Spliterator can also be useful in a single-threaded environment.
The trySplit() method divides the elements into two approximately equal parts. The method creates and returns a
        new Spliterator object using which you can work with the first half of the sequence. The object for
        which the trySplit() method was called will work with the second half of the sequence.
The forEachRemaining() method provides an iteration for Spliterator. The method is declared
      as follows:
void forEachRemaining(Consumer<?super Double> action)
Example:
        List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8);
Spliterator<Integer> spliterator1 = list.spliterator();
Spliterator<Integer> spliterator2 = spliterator1.trySplit();
spliterator1.forEachRemaining(System.out::println);
System.out.println("========");
spliterator2.forEachRemaining(System.out::println);
      
      The result of this program fragment will be as follows:
5 6 7 8 ======== 1 2 3 4
Now you can work with the two parts of the list separately. As you can see from the example, after splitting, the first iterator works with the second half of the sequence, and the second with the first one.
There is also  tryAdvance() method that actually combines the  hasNext() and next() those
        declared in the Iterator interface. The tryAdvance() method is declared as follows::
boolean tryAdvance(Consumer<?super T> action);
If the remaining element exists, it performs the specified action on it, returning true;
        otherwise it returns false. In other words, it performs an action on the next element
        in the sequence and then shifts the iterator. You can also display elements using tryAdvance():
while (spliterator2.tryAdvance(System.out::println));
Starting with Java 8, new methods have been added to the Map interface. The added methods listed in
        the table:
| Method | Description | 
|---|---|
| V getOrDefault(Object key, V& defaultValue) | Returns a value, or a default value, if the key is missing | 
| V putIfAbsent(K key, V value) | Adds a pair if the key is missing and returns the value | 
| boolean remove(Object key, Object value)  | Removes a pair if it is present | 
| boolean replace(K key, V oldValue, V newValue) | Replaces value with the new one if pair is present | 
| V replace(K key, V value) | Replaces the value if the key is present, returns the old value | 
| V compute(K key, BiFunction<?& super K, super V, ? extends V> remappingFunction)  | Invokes the function to construct a new value. A new pair is added, a pair that existed before is deleted, and a new value is returned | 
|  V computeIfPresent(K key, BiFunction<? super K, ? super V,
              ? extends V> remappingFunction) | If a specified key is present, a new function is called to create a new value, and the new value replaces the previous one. | 
| V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) | Returns the value by the key. If the key is missing, a new pair is added, the value is calculated by function | 
|  V merge(K key, V value, BiFunction<? super V, ? super V,
              ? extends V> remappingFunction)  | If the key is absent, then a new pair is entered and the value vis returned. Otherwise, the
            given function returns a new value based on the previous value and the key is updated to access this value.
            and then it returns | 
|  void forEach(BiConsumer<? super K, ? super V> action) | Performs a given action on each element | 
The following example demonstrates the use of some of these methods:
package ua.inf.iwanoff.java.advanced.first;import java.util.HashMap;import java.util.Map;public class MapDemo {static void print(Integer i, String s) { System.out.printf("%3d %10s %n", i, s); }public static void main(String[] args) { Map<Integer, String> map =new HashMap<>(); map.put(1, "one"); map.put(2, "two"); map.put(7, "seven"); map.forEach(MapDemo::print);// columnwise output System.out.println(map.putIfAbsent(7, "eight"));// seven System.out.println(map.putIfAbsent(8, "eight"));// null System.out.println(map.getOrDefault(2, "zero"));// two System.out.println(map.getOrDefault(3, "zero"));// zero map.replaceAll((i, s) -> i > 1 ? s.toUpperCase() : s); System.out.println(map);// {1=one, 2=TWO, 7=SEVEN, 8=EIGHT} map.compute(7, (i, s) -> s.toLowerCase()); System.out.println(map);// {1=one, 2=TWO, 7=seven, 8=EIGHT} map.computeIfAbsent(2, (i) -> i + ""); System.out.println(map);// nothing changed map.computeIfAbsent(4, (i) -> i + ""); System.out.println(map);// {1=one, 2=TWO, 4=4, 7=seven, 8=EIGHT} map.computeIfPresent(5, (i, s) -> s.toLowerCase()); System.out.println(map);// nothing changed map.computeIfPresent(2, (i, s) -> s.toLowerCase()); System.out.println(map);// {1=one, 2=two, 4=4, 7=seven, 8=EIGHT} // Adding a new pair: map.merge(9, "nine", (value, newValue) -> value.concat(newValue)); System.out.println(map.get(9));// nine // The text is concatenated with the previous one: map.merge(9, " as well", (value, newValue) -> value.concat(newValue)); System.out.println(map.get(9));// nine as well } }
2.4.3 Additional JCF Features in Later JDK Versions
Let's consider some innovations that have appeared after Java 8.
In version 9, the of() methods were added to the interfaces that represent collections, which provide
        convenient creation of collections, for example:
        List<String> list = List.of("one", "two", "three");
Set<String> set = Set.of("one", "two", "three");
Map<String, String> map = Map.of("first", "one", "second", "two");
      
      In Java 10, methods for creating immutable collections have been added.
In version 21, the useful functions  addFirst(), addLast(), getFirst(), getLast(), removeFirst() and removeLast(),
        as well as reversed() were  added to collections. The putFirst(), putLast() and reversed() methods
        were added to SortedMap interface.
2.5 Using the Java 8 Stream API
2.5.1 Overview
Streams for work with collections, or streams of elements, data streams (Stream API) are designed for high-level processing of data stored in containers. They should not be confused with input / output streams.
Stream API's tools have been added to the standard starting with Java 8.
The Stream API is used to search, filter, transform, find the minimum and maximum values, as well as other data manipulation. An important advantage of the Stream API is the ability to work reliably and efficiently in a multithreading environment.
Streams should be understood not as a new kind of collections, but as a channel for transmission and processing of data. The stream of elements works with some data source, such as an array or collection. The stream does not store data directly, but performs transferring, filtering, sorting, etc. The actions performed by the stream do not change the source data. For example, sorting data in a stream does not change their order in the source, but creates a separate resulting collection.
You can create sequential and parallel streams of elements. Parallel streams are secure in terms of multithreading. From the available parallel stream you can get sequential one and vice versa.
To work with streams Java 8 java.util.stream package provides a set of interfaces and classes that
        provide operations on a stream of elements in the style of functional programming. The stream is represented by
        an object that implements the java.util.stream.Stream interface. In turn, this interface inherits the
        methods of the general interface java.util.stream.BaseStream.
Stream operations (methods) defined in the BaseStream, Stream, and other derived interfaces
        are divided into intermediate and terminal. Intermediate operations receive and generate data streams and
        serve to create so-called pipelines, in which a sequence of actions is performed over a sequence. Terminal operations give
        the final result and thus "consume" the output stream. This means that the output stream cannot be reused
        and, if necessary, must be re-created.
Intermediate operations are characterized by so-called lazy behavior: they are performed not instantaneously, but as the need arises - when the final operation is working with a new data stream. Lazy behavior increases the efficiency of work with the stream of elements.
The advantages of the approach built on data streams can be demonstrated on the problem of processing data about cities. Suppose you need to create a list of cities, leaving in it only different cities whose population exceeds one million inhabitants, sort the list by population growth and display the names of cities sorted by population.
First you need to create a City class:
public class City {private String name;private int population;public City(String name,int population) {this .name = name;this .population = population; }public String getName() {return name; }public int getPopulation() {return population; } @Overridepublic String toString() {return String.format("%-9s %d", name, population); } @Overridepublic boolean equals(Object o) {return toString().equals(o.toString()); } @Overridepublic int hashCode() {return name.hashCode(); } }
The traditional approach to solving the problem (before Java 7 inclusive) involves creating a list of cities, which will make it impossible to add the same objects. Next, you need to create a list from the set, sort by population and display the names of the cities using a loop:
Set<City> citiesSet =new HashSet<>(); citiesSet.add(new City("Kyiv", 2_967_360)); citiesSet.add(new City("Kharkiv", 1_443_207)); citiesSet.add(new City("Odesa", 1_017_699)); citiesSet.add(new City("Donetsk", 908_456)); citiesSet.add(new City("Odesa", 1_017_699)); List<City> cities =new ArrayList<>(citiesSet);for (int i = 0; i < cities.size(); i++) {if (cities.get(i).getPopulation() < 1_000_000) { cities.remove(i); } } Collections.sort(cities,new Comparator<City>() {public int compare(City a, City b) {return Integer.compare(a.getPopulation(), b.getPopulation()); } });for (City c : cities) { System.out.println(c.getName()); }
The disadvantages of this approach are obvious. A small task requires essential error-prone code. In addition, unnecessary objects are created in memory, in particular, a set.
Lambda expressions, method references, and new interface methods that make up the Java Collection Framework have made it easier to implement code.
Set<City> citiesSet =new HashSet<>(List.of(new City("Kyiv", 2_967_360),new City("Kharkiv", 1_443_207),new City("Odesa", 1_017_699),new City("Donetsk", 908_456),new City("Odesa", 1_017_699))); List<City> cities =new ArrayList<>(citiesSet); cities.removeIf(city -> city.getPopulation() < 1_000_000); cities.sort(Comparator.comparing(City::getPopulation)); cities.forEach(city -> System.out.println(city.getName()));
By calling the removeIf() method with a lambda expression as a parameter, we delete unnecessary data.
        Next, the list is sorted according to the specified criterion (call of sort() method). The sort condition
        is defined by a method reference. The output of city names in separate lines is carried out by the forEach() function.
With streams, everything can be done in a single statement:
        Stream.of(
        new  City("Kyiv", 2_967_360),
        new  City("Kharkiv", 1_443_207),
        new  City("Odesa", 1_017_699),
        new  City("Donetsk", 908_456),
        new  City("Odesa", 1_017_699))
        .filter(city -> city.getPopulation() > 1_000_000)
        .distinct()
        .sorted(Comparator.comparing(City::getPopulation))
        .map(City::getName)
        .forEach(System.out::println);
      
      In this example, a stream is created using the static function  of(). The array of items that the stream
        will work with is created on rom a list of actual parameters. Next, the stream is filtered by calling the filter()        function.
        The filtering condition is defined by a lambda expression. and further, only different data are selected (the distinct() method)
        and sorting is performed according to the specified criterion (call of sorted() method). The sort condition
        is defined by a method reference. Using the  call of  map() function,  a stream is created to
        work with city names, after which the names are output in separate lines using the forEach() method.
2.5.2 Basic Methods for Working with Streams
The most significant methods of the generic java.util.stream.BaseStream interface are given in the
        table (S - type of the stream, E - type of the element, R - container type):
| Method | Description | Note | 
|---|---|---|
| S parallel() | returns a parallel stream received from the current one | intermediate operation | 
| S sequential() | returns a sequential stream received from the current one | intermediate operation | 
| boolean isParallel() | returns trueif the stream is parallel orfalseif
            it is sequential | |
| S unordered() | returns an unordered data stream obtained from the current | intermediate operation | 
| Iterator<T> iterator() | returns an iterator for the elements of this stream | terminal operation | 
| Spliterator<T> spliterator() | returns a spliterator (split iterator) for the elements of this stream. | terminal operation | 
The use of stream iterators will be discussed later.
The Stream interface extends the set of methods for working with streaming elements. It is also a
        generic interface and is suitable for working with any reference types. The following are the most commonly used Stream interface
        methods:
| Method | Description | Note | 
|---|---|---|
| void forEach(Consumer<? super T> action) | executes the code specified by the actionfor each element of the stream | terminal operation | 
| Stream<T> filter(Predicate<? super T> pred) | returns a stream of elements satisfying the predicate | intermediate operation | 
| Stream<T> sorted() | returns a stream of elements sorted in natural order | intermediate operation | 
| Stream<T> sorted(Comparator<? super T> comparator) | returns a stream of elements sorted in the specified order | intermediate operation | 
| 
 | applies the given function to the elements of the stream and returns a new stream | intermediate operation | 
| Optional<T> min(Comparator<? super T> comp) | returns the minimum value using the specified comparison | terminal operation | 
| Optional<T> max(Comparator<? super T> comp) | returns the maximum value using the specified comparison | terminal operation | 
| long count() | returns the number of elements in the stream | terminal operation | 
| Stream<T> distinct() | returns a stream of differing elements | intermediate operation | 
| Optional<T> reduce(BinaryOperator<T> accumulator) | returns the scalar result calculated by the values of the elements | terminal operation | 
| Object[] toArray() | creates and returns an array of stream elements | terminal operation | 
2.5.3 Creation of Streams
There are several ways to create a stream. You can use the factory methods added to the Collection interface
        (with default implementations), respectively stream() (for synchronous work) and parallelStream() (for
        asynchronous work).  This is how you can create a stream for synchronous work:
List<Integer> intList = List.of(3, 4, 1, 2); Stream<Integer> fromList = intList.stream();
You can create a stream from an array:
        Integer[] a = { 1, 2, 3 };
Stream<Integer> fromArray = Arrays.stream(a);
      
      You can create a data source with the specified items. To do this, use the "factory" method of():
Stream<Integer> newStream = Stream.of(4, 5, 6);
Streams of items can be created from input streams (BufferedReader.lines()), filled with random values
        (Random.ints()), and also obtained from archives, bit sets, etc.
You can get an array from a stream using the toArray() method. The following example creates a stream
        and then outputs to the console by creating an array and obtaining a string representation using the static Arrays.toString() method:
Stream<Integer> s = Stream.of(1, -2, 3); Object[] a = s.toArray(); System.out.println(Arrays.toString(a));// [1, -2, 3] 
There is also a special class StreamSupport that provides static methods for creating streams. An
        example of use
      StreamSupport will be considered later.
2.5.4 Iteration by Elements
Streams provide iteration over data elements using the forEach() method. The function parameter is
        the standard Consumer functional interface, which defines a method with a single parameter and a void result
      type. For example: 
Stream<Integer> s = Stream.of(4, 5, 6, 1, 2, 3); s.forEach(System.out::println);
Streams provide iterators. The iterator() method of the Stream interface returns an object
        that implements the java.util.Iterator interface. The iterator can be used explicitly:
s = Stream.of(11, -2, 3); Iterator<Integer> it = s.iterator();while (it.hasNext()) { System.out.println(it.next()); }
You can also apply Spliterator to streams. In particular, the static methods of the StreamSupport class
      allow you to create a stream from a Spliterator object. Example:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); Spliterator<Integer> spliterator = numbers.spliterator();// true indicates that parallel processing can be used: Stream<Integer> stream = StreamSupport.stream(spliterator,true ); stream.forEach(System.out::println);
Use of Spliterator in this context provides efficiency due to the possibility of parallel processing.
2.5.5 Operations with Streams
The simplest stream operation is filtering. The intermediate filter() operation returns a filtered
        stream, taking a parameter of Predicate type. The Predicate type is a functional interface
        that describes a method with a single parameter and boolean result type. For example,
        you can filter out only even numbers from the stream s:
s.filter(k -> k % 2 == 0).forEach(System.out::println);
The previous example illustrates the use of lambda expressions when working with streams, as well as a small conveyor that includes one intermediate operation.
The intermediate sorted() operation returns the sorted representation of the stream. Elements are
        ordered in the natural order (if it is defined). In other cases, the Comparator interface should be
        implemented, for example, using the lambda expression:
// Sort ascending: Stream<Integer> s = Stream.of(4, 5, 6, 1, 2, 3); s.sorted().forEach(System.out::println);// Sort descending: s = Stream.of(4, 5, 6, 1, 2, 3); s.sorted((k1, k2) -> Integer.compare(k2, k1)).forEach(System.out::println);
The last example shows that after each call to the terminal operation, the stream should be recreated.
Most operations are implemented in such a way that actions on individual elements do not depend on other elements.
        Such operations are called stateless operations. Other operations that require working on all elements
      at once (for example, sorted()) are called stateful operations.
The intermediate operation map() receives a functional interface that defines a certain function for
        transforming and forming a new stream from the resulting transformed elements. For example, we calculate the squares
        of numbers:
s = Stream.of(1, 2, 3); s.map(x -> x * x).forEach(System.out::println);
Using the distinct() method, you can get a stream containing only different elements of the collection.
        For example:
s = Stream.of(1, 1, -2, 3, 3); System.out.println(Arrays.toString(s.distinct().toArray()));// [1, -2, 3] 
Intermediate operations are characterized by the so-called delayed behavior (lazy behavior): they are not performed immediately, but as needed, when the final operation works with a new data stream. Delayed behavior increases the efficiency of working with streams of elements.
The terminal operation count() with the resulting type long returns the number
        of elements in the stream:
s = Stream.of(4, 5, 6, 1, 2, 3); System.out.println(s.count());// 6 
The terminal operations min() and max() return Optional objects with a minimum
        and maximum value, respectively. A Comparator type parameter is used for comparison. For example:
s = Stream.of(11, -2, 3); System.out.println(s.min(Integer::compare).get());// -2 
Using a terminal reduce() operation, we can calculate a scalar value. The reduce() operation
        in its simplest form performs the specified action with two operands, the first of which is the result of performing
        the action on the previous elements, and the second is the current element. In the following example, we find the
        sum of the elements of the data stream:
s = Stream.of(1, 1, -2, 3, 3); Optional<Integer> sum = s.reduce((s1, s2) -> s1 + s2); sum.ifPresent(System.out::println);// 6 
The min(), max(), and reduce() operations get a scalar value from the stream,
        so they are called reduction operations.
 Sometimes it is necessary to reproduce a stream to perform several terminal operations. A  Supplier functional
        interface with an abstract function get() will be used to create identical streams according to a defined
        rule. The rule can be described as a lambda expression. Example:
Supplier<Stream<Integer>> supplier = () -> Stream.of(1, 2, 3); supplier.get().forEach(System.out::println); System.out.println(Arrays.toString(supplier.get().toArray()));
The concat() method joins two streams:
Stream<Integer> stream1 = Stream.of(1, 2, 3); Stream<Integer> stream2 = Stream.of(8, 9); Stream<Integer> result = Stream.concat(stream1, stream2); result.forEach(System.out::print);// 12389 
The skip() method creates a new stream in which the first n elements are omitted. Example:
Stream<Integer> stream1 = Stream.of(1, 2, 3, 4, 5); Stream<Integer> stream2 = stream1.skip(2);// 3 4 5 
There are also findFirst() and findAny() operations for searching for a certain object.
        These methods return Optional type. To check the presence of objects that satisfy certain conditions,
        anyMatch(), allMatch() and noneMatch()  methods
        are used::
Supplier<Stream<Integer>> supplier = () -> Stream.of(1, 2, 3, -5); System.out.println(supplier.get().allMatch(i -> i > 0));// false System.out.println(supplier.get().anyMatch(i -> i > 0));// true System.out.println(supplier.get().noneMatch(i -> i > 10));// true 
2.5.6 Using the Collectors Class
A special generic interface java.util.stream.Collector is defined for receiving collections from streams.
The terminal operation collect() of the Stream class allows you to retrieve a traditional
        collection from a stream. The type of collection depends on the parameter. The parameter is an instance of the Collector type.
        The Collectors class provides static methods for obtaining objects of type Collector,
      such as toCollection(), toList(), toSet() and toMap().
Example:
        Supplier<Stream<Integer>> supplier = () -> Stream.of(1, 2, 3);
List<Integer> list = supplier.get().collect(Collectors.toList());
System.out.println("list: " + list); // list: [1, 2, 3] 
Set<Integer> set = supplier.get().collect(Collectors.toSet());
System.out.println("set: " + set); // set: [1, 2, 3] 
Map<Integer, Double> map = supplier.get().collect(
Collectors.toMap(Integer::intValue, Integer::doubleValue));
System.out.println("map :" + map); // map :{1=1.0, 2=2.0, 3=3.0} 
      
      2.5.7 Using Streams to Work with Primitive Types
There are also streams for working with primitive types: IntStream, LongStream and DoubleStream.
        Consider the work with IntStream and DoubleStream.
The easiest way to create streams is to use a static  of() function:
IntStream intStream = IntStream.of(1, 2, 4, 8); DoubleStream doubleStream = DoubleStream.of(1, 1.5, 2);
You can create streams from the corresponding arrays:
int [] intArr = { 10, 11, 12 };double [] doubleArr = { 10, 10.5, 11, 11.5, 12 }; intStream = Arrays.stream(intArr); doubleStream = Arrays.stream(doubleArr);
With the help of a range() method of IntStream class, you can create streams by filling
        them with sequential values. You can also simultaneously define a filter:
intStream = IntStream.range(0, 10).filter(n -> n % 2 == 0);// 0 2 4 6 8 
The iterate() method can be used to create an infinite stream. The next element is calculated
        from the previous one. You can limit the stream using the limit() function. So, for example, you can
        get consecutive powers of 3:
intStream = IntStream.iterate(1, i -> i * 3).limit(6);// 1 3 9 27 81 243 
The generate() method lso allows you to generate elements, but without taking into account the previous
      ones. For example, you can fill an array with random numbers:
doubleStream = DoubleStream.generate(() -> (Math.random() * 10000)).limit(20);
Further work is similar to work with common streams. For example, you can sort and display only odd values:
intStream = IntStream.of(11, 2, 43, 81, 8, 0, 5, 3); intStream.sorted().filter(n -> n % 2 != 0).forEach(System.out::println);
The resulting streams can be used to create new arrays:
int [] newIntArr = intStream.toArray();double [] newDoubleArr = doubleStream.toArray();
Note: 
 it is assumed that the streams intStream and doubleStream were  not used in the terminal operations.
2.6 Testing in Java. Using JUnit
2.6.1 Overview
Testing is one of the most important components of the software development process. Software testing is performed in order to obtain information about the quality of the software product. There are many approaches and techniques for testing and verifying software.
The paradigm of test-driven development (development through testing) defines the technique of software development, based on the use of tests to stimulate the writing of code, and to verify it. Code development is reduced to repeating the test-code-test cycle with subsequent refactoring.
The level of testing at which the least possible component to be tested, such as a single class or function, is called unit testing. Appropriate testing technology assumes that tests are developed in advance, before writing the real code, and the development of the code of the unit (class) is completed when its code passes all the tests.
2.6.2 Java Tools for Diagnosing Runtime Errors
Many modern programming languages, including Java, include syntactic assertions. The assert keyword
        has appeared in Java since version JDK 1.4 (Java 2). The assert work can be turned
        on or off. If the execution of diagnostic statements is enabled, the work of assert is
        as follows: an expression of type boolean is executed and if the result is true,
        the program continues, otherwise an exception of java.lang.AssertionError throws. Suppose, according
        to the logic of the program, the variable c must always be positive. Execution of such a fragment of
        the program will not lead to any consequences (exceptions, emergency stop of the program, etc.):
int a = 10;int b = 1;int c = a - b;assert c > 0;
If, due to an incorrect software implementation of the algorithm, the variable c still received a
        negative value, the execution of a fragment of the program will lead to the throwing of an exception and an abnormal
        termination of the program, if the processing of this exception was not provided:
int a = 10;int b = 11;int c = a - b;assert c > 0;// exception is thrown 
After the assertion, you can put a colon, followed by a string of the message. Example:
int a = 10;int b = 11;int c = a - b;assert c > 0 : "c must be positive";
In this case, the corresponding string is the exception message string.
Assert execution is usually disabled in integrated development environments. To enable assert execution in the
        IntelliJ IDEA environment, use the Run | Edit Configurations menu function. In the Run/Debug Configurations window,
        enter -ea in the VM Options input line.
In these examples, the values that are checked with assert are not entered from the
        keyboard, but are defined in the program to demonstrate the correct use of assert -
        the search for logical errors, rather than checking the correctness of user input. Exceptions, conditional statements,
        etc. should be used to verify the correctness of the data entered. The use of assertion validation is not allowed,
        because in the future the program will be started without the -ea option and all assertions will be
        ignored. The expression specified in the statement should not include actions that are important in terms of program
        functionality. For example, if the assertion check is the only place in the program from which a very important
        function is called,
public static void main(String[] args) {//... assert f() : "failed";//... }public static boolean f() {// Very important calculations return true ; }
then after disabling assertions the function will not be called at all.
2.6.3 Basics of Using JUnit
In contrast to the use of diagnostic statements, which performs testing of algorithms "from the inside", unit testing provides verification of a particular unit as a whole, testing "outside" the functionality of the unit.
The most common unit testing support for Java software is JUnit, an open unit testing library. JUnit allows:
- create tests for individual classes;
- create test suits;
- create a series of tests on repeating sets of objects.
Now the JUnit 5 version is now relevant. But also a very widespread is JUnit 4 version.
To create a test, you need to create a class that needs to be tested, as well as create a public class for testing
        with a set of methods that implement specific tests. Each test method must be public, void,
        and have no parameters. The method must be marked with an annotation @Test:
public class MyTestCase {//... @Testpublic void testXXX() {//... }//... }
Note: to use the @Test and other similar annotations should be added import statements import org.junit.jupiter.api.*; for
        JUnit 5) or import org.junit.*; (for JUnit 4) .
Within such methods, you can use the following assertion methods:
assertTrue(expression);// Fails the test if false assertFalse(expression);// Fails the test if true assertEquals(expected, actual);// Fails the test if not equivalent assertNotNull(new MyObject(params));// Fails the test if null assertNull(new MyObject(params));// Fails the test if not null assertNotSame(expression1, expression2);// Fails the test if both links refer to the same object assertSame(expression1, expression2);// Fails the test if the objects are different fail(message)// Immediately terminates the test with a failure message 
Here MyObject is a class that is being tested. These Assertion class methods (Assert class
        methods for JUnit 4) are accessed using static import: import static org.junit.jupiter.api.Assertion.*; (for
        JUnit 5) or import static org.junit.Assert.*;. These methods also are implemented
        with an additional message parameter of type String, which specifies the message that
        will be displayed if the test failed.
The IntelliJ IDEA provides built-in JUnit support. Suppose a new project has been created. The project contains a class with two functions (static and non-static) that should be tested:
package ua.inf.iwanoff.java.advanced.first;public class MathFuncs {public static int sum(int a,int b) {return a + b; }public int mult(int a,int b) {return a * b; } }
 Within the project, we can manually create a folder, for example, tests. Next we should set Mark
          Directory as | Test Sources Root with the context menu.
 Returning to the MathFuncs class, choosing it in the code editor, through the context menu we can
        generate tests: Generate... | Test.... In the dialog that opened, we select the version of the JUnit library.
        The desired option is JUnit5. We can also correct the class name that we offer: MathFuncsTest.
        In most cases, the correction of this name is not needed. Then we select the names of methods that are subject to
        testing. In our case, there are sum() and mult(). Such a code will be received:
package ua.inf.iwanoff.java.advanced.first;import static org.junit.jupiter.api.Assertions.*;class MathFuncsTest { @org.junit.jupiter.api.Testvoid sum() { } @org.junit.jupiter.api.Testvoid mult() { } }
IntelliJ IDEA indicates errors in this code (Cannot resolve symbol 'junit'). By clicking Alt+Enter, we get a hint: Add 'JUnit 5.7.0' to classpath. Taking advantage of this prompt, we add the relevant library and get the code without errors.
 We can optimize the code by adding imports. We add testing of MathFuncs class methods into MathFuncsTest methods.
        To test the work of mult() we need to create an object: 
package ua.inf.iwanoff.java.advanced.first;import org.junit.jupiter.api.Test;import static org.junit.jupiter.api.Assertions.*;class MathFuncsTest { @Testvoid sum() { assertEquals(MathFuncs.sum(4, 5), 9); } @Testvoid mult() { assertEquals(new MathFuncs().mult(3, 4), 12); } }
 You can run tests to run through the Run menu. The normal completion of the process indicates no errors
        during verification. If you add a code that distorts computing in the MathFuncs class, for example
public int mult(int a,int b) {return a * b + 1; }
running tests will result in AssertionFailedError message. You can see how many tests have been successful,
        and how much it is not passed.
If some actions need to be taken before performing the test function, for example, to format the values of variables,
        then such initialization is made in a separate static method, which is preceded by an annotation @BeforeAll(@BeforeClass in
        JUnit 4):
@BeforeAllpublic static void setup() {//... }
Similarly, the methods in which the actions needed after testing are preceded by@AfterAll annotation
        (@AfterClass in JUnit 4). Methods must be public static void.
In our example, we can create an object in advance, as well as add messages after the tests are completed:
package ua.inf.iwanoff.java.advanced.first;import org.junit.jupiter.api.*;import static org.junit.jupiter.api.Assertions.*;class MathFuncsTest {private static MathFuncs funcs; @BeforeAllpublic static void init() { funcs =new MathFuncs(); } @Testvoid sum() { assertEquals(MathFuncs.sum(4, 5), 9); } @Testvoid mult() { assertEquals(funcs.mult(3, 4), 12); } @AfterAllpublic static void done() { System.out.println("Tests finished"); } }
Annotation @BeforeEach (@Before in JUnit 4) indicates that the method is
        called before each test method. Accordingly, @AfterEach (@After in JUnit 4) indicates
        that the method is called after each successful test method. Methods marked by these annotations should not be static.
You can also test methods that return void. Calling such a method involves performing
        an action (for example, creating a file, changing the value of a field, etc.). It is necessary to check whether
        such action took place. For example:
void setValue(into value) {this .value = value; }//... @Testpublic void testSetValue() { someObject.setValue(123); assertEquals(123, someObject.getValue()); }
However, as a rule, testing the simplest access methods (setters and getters) seems excessive and is not recommended.
3 Sample Programs
3.1 Finding Factorials
The traditional mathematical problem of calculating the factorial for large integers causes difficulties associated
        with restrictions on the size of the result. For int type, int the maximum certain
        value is 12!, for the long type it is 20! For values 171! and
        more, even an approximate value cannot be obtained using double. Using BigInteger and BigDecimal allow
        you to get factorials of large numbers. The size of the result is actually limited by the display capabilities of
        the console window.
The following program calculates the factorial of integers using different types: long, double, BigInteger and
      BigDecimal.
package ua.inf.iwanoff.java.advanced.first;import java.math.BigDecimal;import java.math.BigInteger;import java.util.Scanner;public class Factorial {public static long factorialLong(int n) {long result = 1;for (int i = 1; i <= n; i++) { result *= i; }return result; }public static double factorialDouble(int n) {double result = 1;for (int i = 1; i <= n; i++) { result *= i; }return result; }public static BigInteger factorialBigInteger(int n) { BigInteger result = BigInteger.ONE; BigInteger current = BigInteger.ZERO;for (int i = 1; i <= n; i++) { result = result.multiply(current = current.add(BigInteger.ONE)); }return result; }public static BigDecimal factorialBigDecimal(int n) { BigDecimal result = BigDecimal.ONE; BigDecimal current = BigDecimal.ZERO;for (int i = 1; i <= n; i++) { result = result.multiply(current = current.add(BigDecimal.ONE)); }return result; }public static void main(String[] args) {int n =new Scanner(System.in).nextInt(); System.out.println(factorialLong(n)); System.out.println(factorialDouble(n)); System.out.println(factorialBigInteger(n)); System.out.println(factorialBigDecimal(n)); } }
The execution of the program for large values of n takes a long time.
Manual testing is not enough to verify the certainty of the results. To test these functions, we can use the capabilities
        of the JUnit library. In advance, we need to create a separate test folder and mark it as the root of the tests
        (Mark Directory as | Test Sources Root function of the context menu). Next, you can generate tests: the
        Code | Generate... | Test... function of main menu. In the dialog box, select the functions for which
        tests should be created. In our case, these are all functions except main(). The following code will
        be generated:
package ua.inf.iwanoff.java.advanced.first;import static org.junit.jupiter.api.Assertions.*;class FactorialTest { @org.junit.jupiter.api.Testvoid factorialLong() { } @org.junit.jupiter.api.Testvoid factorialBigInteger() { } @org.junit.jupiter.api.Testvoid factorialBigDecimal() { } @org.junit.jupiter.api.Testvoid factorialDouble() { } }
Errors related to the use of JUnit tools are corrected according to the instructions that were described above.
To test the factorialDouble() function, it is necessary to add an auxiliary compareDoubles() function,
        as well as to create constants for some factorial values. The code of the FactorialTest.java file will
        be as follows:
package ua.inf.iwanoff.java.advanced.first;package ua.inf.iwanoff.java.advanced.first;import java.math.BigDecimal;import static org.junit.jupiter.api.Assertions.*;class FactorialTest {public static final String FACTORIAL_5 = "120";public static final String FACTORIAL_50 = "30414093201713378043612608166064768844377641568960512000000000000";public static final String FACTORIAL_500 = "12201368259911100687012387854230469262535743428031928421924135883858453731538819" + "97605496447502203281863013616477148203584163378722078177200480785205159329285477" + "90757193933060377296085908627042917454788242491272634430567017327076946106280231" + "04526442188787894657547771498634943677810376442740338273653974713864778784954384" + "89595537537990423241061271326984327745715546309977202781014561081188373709531016" + "35632443298702956389662891165897476957208792692887128178007026517450776841071962" + "43903943225364226052349458501299185715012487069615681416253590566934238130088562" + "49246891564126775654481886506593847951775360894005745238940335798476363944905313" + "06232374906644504882466507594673586207463792518420045936969298102226397195259719" + "09452178233317569345815085523328207628200234026269078983424517120062077146409794" + "56116127629145951237229913340169552363850942885592018727433795173014586357570828" + "35578015873543276888868012039988238470215146760544540766353598417443048012893831" + "38968816394874696588175045069263653381750554781286400000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "000000000000000";public static final BigDecimal EPS = BigDecimal.ONE;private boolean compareDoubles(double d, String s) {return new BigDecimal(d).subtract(new BigDecimal(s)).abs().compareTo(EPS) <= 0; } @org.junit.jupiter.api.Testvoid factorialLong() { assertEquals(Factorial.factorialLong(5) + "", FACTORIAL_5); assertEquals(Factorial.factorialLong(50) + "", FACTORIAL_50); assertEquals(Factorial.factorialLong(500) + "", FACTORIAL_500); } @org.junit.jupiter.api.Testvoid factorialDouble() { assertTrue(compareDoubles(Factorial.factorialDouble(5), FACTORIAL_5)); assertTrue(compareDoubles(Factorial.factorialDouble(50), FACTORIAL_50)); assertTrue(compareDoubles(Factorial.factorialDouble(500), FACTORIAL_500)); } @org.junit.jupiter.api.Testvoid factorialBigInteger() { assertEquals(Factorial.factorialBigInteger(5) + "", FACTORIAL_5); assertEquals(Factorial.factorialBigInteger(50) + "", FACTORIAL_50); assertEquals(Factorial.factorialBigInteger(500) + "", FACTORIAL_500); } @org.junit.jupiter.api.Testvoid factorialBigDecimal() { assertEquals(Factorial.factorialBigDecimal(5) + "", FACTORIAL_5); assertEquals(Factorial.factorialBigDecimal(50) + "", FACTORIAL_50); assertEquals(Factorial.factorialBigDecimal(500) + "", FACTORIAL_500); } }
The values of 50! and 500! were obtained from the page https://zeptomath.com/calculators/factorial.php and
      are used as a reference in our tests. 
As expected, the correct values of 50! and 500! can be obtained only using BigInteger        and BigDecimal. 
3.2 The Product of the Entered Numbers
In the example below, integers are entered, displayed as a decrease, and their product is calculated. The entering ends with zero:
package ua.inf.iwanoff.java.advanced.first;import java.util.*;public class Product {public static void main(String[] args) { Queue<Integer> queue =new PriorityQueue<>(100,new Comparator<Integer>() { @Overridepublic int compare(Integer i1, Integer i2) {return -Double.compare(i1, i2); } }); Scanner scanner =new Scanner(System.in); Integer k;do { k = scanner.nextInt();if (k != 0) { queue.add(k); } }while (k != 0);int p = 1;while ((k = queue.poll()) !=null ) { p *= k; System.out.print(k + " "); } System.out.println(); System.out.println(p); } }
3Obtaining a Table of Prime Numbers using Data Streams
The following program allows you to get a table of prime numbers in a given range. To obtain simple numbers, it
        is advisable to use IntStream:
package ua.inf.iwanoff.java.advanced.first;import java.util.stream.IntStream;public class PrimeFinder {private static boolean isPrime(int n) {return n > 1 && IntStream.range(2, n - 1).noneMatch(k -> n % k == 0); }public static void printAllPrimes(int from,int to) { IntStream primes = IntStream.range(from, to + 1).filter(PrimeFinder::isPrime); primes.forEach(System.out::println); }public static void main(String[] args) { printAllPrimes(6, 199); } }
The isPrime() method checks whether the number n is prime. For numbers greater than 1, a set of consecutive
        integers is formed, for each of which it is checked whether n is divisible by this number. In the printAllPrimes() method,
        we form a stream of simple numbers using a filter and output the numbers using the forEach() method.
3.4"Country" and "Census" Classes
In the examples from the Java Basic Programming Course, class hierarchies to represent country
        and population censuses were considered. In laboratory training
        # 3  of this course, an abstract class AbstractCountry was created, as well as concrete classes Census and CountryWithArray.
        Next, in laboratory training # 4 , classes CountryWithList and CountryWithSet were
      created. 
Now, using the CountryWithList and Census classes, we will create an application that
        reproduces the search and sorting implemented in the examples of the specified labs through the use of the Stream
      API.
We can create a new package called  ua.inf.iwanoff.java.advanced.first within previously
        created project. We add CensusChecker class
        to the package. It is advisable to create the containsWord() method, implementing
        it with the help of streams. For example, the class code could be as follows:
package ua.inf.iwanoff.java.advanced.first;import ua.inf.iwanoff.java.third.Census;import java.util.Arrays;/** * Provides a static method to search for a word in a comment * The method uses StreamAPI facilities */ public class CensusChecker {/** * Checks whether the word can be found in the comment text * @param census reference to a census * @param word a word that should be found in a comment * @return {@code true}, if the word is contained in the comment text * {@code false} otherwise */ public static boolean containsWord(Census census, String word) {return Arrays.stream(census.getComments().split("\s")).anyMatch(s -> s.equalsIgnoreCase(word)); } }
It is also possible to define main() function to test class, but a better approach is to use the capabilities
        of unit testing (JUnit). 
 In advance, create a folder test in the root of the project and mark it as the root of the tests (Mark Directory
          as | Test Sources Root function of the context menu). In the code window, we select the name of the class
          and using the context menu Generate... | Test... select the functions for which test methods should be
          generated. In our case, this is the containsWord() method. IntelliJ IDEA automatically generates
          all necessary parallel packages of the test branch and creates the class called CensusWithStreamsTest.
          It looks like this:
package ua.inf.iwanoff.java.advanced.first;import org.junit.jupiter.api.Test;import static org.junit.jupiter.api.Assertions.*;class CensusCheckerTest { @Testvoid containsWord() { } }
If errors are highlighted in the generated code, we correct them as described above.
Now we can add the necessary testing. The code of the file CensusCheckerTest.java will be as follows:
package ua.inf.iwanoff.java.advanced.first;import ua.inf.iwanoff.java.third.Census;import static org.junit.jupiter.api.Assertions.*;import org.junit.jupiter.api.Test;class CensusCheckerTest { @Testvoid containsWord() { Census census =new Census(); census.setComments("The first census in independent Ukraine"); assertTrue(CensusChecker.containsWord(census, "Ukraine")); assertTrue(CensusChecker.containsWord(census, "FIRST")); assertFalse(CensusChecker.containsWord(census, "rain")); assertFalse(CensusChecker.containsWord(census, "censuses")); } }
After completing the tests, we will receive a successful exit code. If the expected results are changed in the code, the tests will throw an exception and the assertion that failed will be underlined in the code.
We create a separate class to work with the list of censuses via the Stream API:
package ua.inf.iwanoff.java.advanced.first;import ua.inf.iwanoff.java.fourth.CountryWithArrayList;import ua.inf.iwanoff.java.third.Census;import static ua.inf.iwanoff.java.advanced.first.CensusChecker.containsWord;import java.util.Comparator;/** * Class for processing data about the country in which the census is being conducted. * Stream API tools are used to process sequences */ public class CountryProcessorWithStreams {/** * Returns the population density for the specified year * * @param country reference to a country * @param year specified year (e.g. 1959, 1979, 1989, etc.) * @return population density for the specified year */ public static double density(CountryWithArrayList country,int year) { Census census = country.getList().stream() .filter(c -> c.getYear() == year) .findFirst().orElse(null );return census ==null ? 0 : census.getPopulation() / country.getArea(); }/** * Finds and returns a year with the maximum population * * @param country reference to a country * @return year with the maximum population */ public static int maxYear(CountryWithArrayList country) {return country.getList().stream() .max(Comparator.comparing(Census::getPopulation)) .get().getYear(); }/** * Creates and returns an array of censuses with the specified word in the comments * * @param country reference to a country * @param word a word that is found * @return array of censuses with the specified word in the comments */ public static Census[] findWord(CountryWithArrayList country, String word) {return country.getList().stream() .filter(c -> containsWord(c, word)) .toArray(Census[]::new ); }/** * Sorts the sequence of censuses by population * * @param country reference to a country */ public static void sortByPopulation(CountryWithArrayList country) { country.setList(country.getList().stream() .sorted() .toList()); }/** * Sorts the sequence of censuses in the alphabetic order of comments * * @param country reference to a country */ public static void sortByComments(CountryWithArrayList country) { country.setList(country.getList().stream().sorted(Comparator.comparing(Census::getComments)).toList()); } }
The given code uses a function call: toArray(Census[]::new). This ensures that an
        array of the required type (references to Census) is created, rather than an array of references to Object,
      which is returned by the corresponding function without parameters.
We add a CountryWithStreams class for testing. This is done in the same way as for CensusChecker.
        It is advisable to choose all methods,  as well as setUp/@Defore. We
        will get the following code:
package ua.inf.iwanoff.java.advanced.first;import org.junit.jupiter.api.BeforeEach;import org.junit.jupiter.api.Test;import static org.junit.jupiter.api.Assertions.*;class CountryProcessorWithStreamsTest { @BeforeEachvoid setUp() { } @Testvoid density() { } @Testvoid maxYear() { } @Testvoid findWord() { } @Testvoid sortByPopulation() { } @Testvoid sortByComments() { } }
Since it is necessary to perform several tests on the object and these tests must be independent, it is advisable
        to create the object before executing each test method. For this, we need to add a method with @BeforeEach annotation.
        The corresponding method can also be generated automatically (Generate... | SetUp Method in the context
        menu). We create a new country object in the method called setUp(). We create the appropriate field
        of type CountryWithStreams manually. The object will be used in test methods.
For convenient testing of functions related to searching and sorting, we can create getYears() function
        that retrieves an array of years from an array of censuses. This static function will use a static variable index
        to fill specific array items. The variable cannot be local because we are using it in a lambda expression. We get
        the following code:
package ua.inf.iwanoff.java.advanced.first;import org.junit.jupiter.api.BeforeEach;import org.junit.jupiter.api.Test;import static org.junit.jupiter.api.Assertions.*;import ua.inf.iwanoff.java.fourth.CountryWithArrayList;import ua.inf.iwanoff.java.third.Census;import java.util.Arrays;class CountryProcessorWithStreamsTest {private CountryWithArrayList country;static int index;static int [] getYears(Census[] censuses) {int [] years =new int [censuses.length]; index = 0; Arrays.stream(censuses).forEach(c -> years[index++] = c.getYear());return years; } @BeforeEachvoid setUp() { country =new CountryWithArrayList(); country.setArea(603628); country.addCensus(1959, 41869000, "The first census after World War II"); country.addCensus(1970, 47126500, "Population increases"); country.addCensus(1979, 49754600, "No comments"); country.addCensus(1989, 51706700, "The last soviet census"); country.addCensus(2001, 48475100, "The first census in the independent Ukraine"); } @Testvoid density() { assertEquals(CountryProcessorWithStreams.density(country, 1979), 82.42593120266125); } @Testvoid maxYear() { assertEquals(CountryProcessorWithStreams.maxYear(country), 1989); } @Testvoid findWord() { assertArrayEquals(getYears(CountryProcessorWithStreams.findWord(country, "census")),new int [] { 1959, 1979, 1989, 2001 }); } @Testvoid sortByPopulation() { CountryProcessorWithStreams.sortByPopulation(country); assertArrayEquals(getYears(country.getCensuses()),new int [] { 1959, 1970, 2001, 1979, 1989 }); } @Testvoid sortByComments() { CountryProcessorWithStreams.sortByComments(country); assertArrayEquals(getYears(country.getCensuses()),new int [] { 1970, 1989, 2001, 1959, 1979 }); } }
Arrays of years corresponding to correct sorting and searching results were manually prepared.
We demonstrate the features new classes in the main() function of the CountryWithStreamsDemo class:
package ua.inf.iwanoff.java.advanced.first;import ua.inf.iwanoff.java.fourth.CountryWithArrayList;import ua.inf.iwanoff.java.third.StringRepresentations;import ua.inf.iwanoff.java.third.Census;import static ua.inf.iwanoff.java.third.CountryDemo.setCountryData;/** * The class demonstrates data processing using the StreamAPI */ public class CountryWithStreamsDemo {/** * Displays census data that contains a certain word in comments * @param country reference to a country * @param word a word that is found */ public static void printWord(CountryWithArrayList country, String word) { Census[] result = CountryProcessorWithStreams.findWord(country, word);if (result.length == 0) { System.out.println("The word \"" + word + "\" is not present in the comments."); }else { System.out.println("The word \"" + word + "\" is present in the comments:");for (Census census : result) { System.out.println(StringRepresentations.toString(census)); } } }/** * Performs testing search methods * @param country reference to a country */ public static void testSearch(CountryWithArrayList country) { System.out.println("Population density in 1979: " + CountryProcessorWithStreams.density(country, 1979)); System.out.println("The year with the maximum population: " + CountryProcessorWithStreams.maxYear(country) + "\n"); printWord(country, "census"); printWord(country, "second"); }/** * Performs testing search methods * @param country reference to a country */ public static void testSorting(CountryWithArrayList country) { CountryProcessorWithStreams.sortByPopulation(country); System.out.println("\nSorting by population:"); System.out.println(StringRepresentations.toString(country)); CountryProcessorWithStreams.sortByComments(country); System.out.println("\nSorting comments alphabetically:"); System.out.println(StringRepresentations.toString(country)); }/** * Demonstration of the program. * @param args command line arguments (not used) */ public static void main(String[] args) { CountryWithArrayList country = (CountryWithArrayList) setCountryData(new CountryWithArrayList()); testSearch(country); testSorting(country); } }
4 Exercises
- Implement the function of obtaining the integer part of the square root of a number of type BigInteger.
- Initialize a list of real numbers of BigDecimaltype with an array containing a list of initial values. Find the sum of positive elements.
- In the array of integer values (BigInteger), replace negative values with modules, positive values with zeros. Apply Stream API facilities.
5 Quiz
- Name the main components of the Java SE platform.
- What are classes BigIntegerandBigDecimal?
- How can you create a type number BigInteger?
-  What is the difference between internal representation of doubleandBigDecimal?
- Is it possible to apply mathematical operations to numbers of types BigIntegerandBigDecimal?
-  What is the MathContextclass used for?
- What is JCF? What standard interfaces does JCF provide?
- How to create a read-only collection?
- What are the Queueinterface methods used to add items?
- Why are the methods for working with the queue implemented in two versions: with exception throwing and without exception throwing?
- What is the use of the PriorityQueueclass?
- What are the stacks used for?
- What are the standard ways to implement the stack?
- What are the advantages and features of Stream API?
- How to get a stream from a collection?
- How to get a stream from an array?
- What is the difference between intermediate and terminal operations?
- What are the streams for working with primitive types?
- What are the standard means of checking assertions in Java?
- What is unit testing?
- What is JUnit?
- How are test methods annotated in JUnit?
- How to make a logical grouping of tests?
- How to use JUnit in a development environment?
