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 course "Fundamentals of Java Programming". 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 course "Fundamentals of Java Programming";
  • 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; n cannot be a negative number;
  • BigInteger gcd(BigInteger number) returns a BigInteger whose 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 n positions;
  • BigInteger shiftRight(int n) returns the operation of shifting the bits to the right by n positions.

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 true if the specified bit is set to one;
  • BigInteger setBit(int n) returns a BigInteger object in which the corresponding bit is set to one;
  • BigInteger clearBit(int n)returns a BigInteger object in which the corresponding bit is set to zero;
  • BigInteger flipBit(int n) returns a BigInteger object 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 bitLength length of bitLength bits; the given certainty and the rnd random 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 bitLength bits; the rnd random 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 true f the number is probably prime and false if 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.

  • Iterable is the basic interface implemented in the java.lang package. 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 for loop (for each). In addition, the Iterable interface provides a number of methods with default implementations.
  • Iterator is 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.
  • ListIterator an interface derived from Iterator, which allows iterating in long directions as well as modifying the current elements. Used in lists.
  • Collection is an interface derived from Iterable. It is basic for all collections except associative arrays (Map). The AbstractCollection class directly implements the Collection interface. This class is used to create a number of abstract classes that represent different types of collections.
  • List is 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.
  • Queue is 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.
  • Deque is 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 ArrayDeque and LinkedList.
  • Set is 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.
  • SortedSet is an interface derived from Set. Assumes that elements are arranged according to a specific sort attribute. The most common implementation is TreeSet.
  • Map is 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 true if 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() returns true if 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 via next() or previous() 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 true if the collection is empty;
  • boolean contains(Object o) returns true if the collection contains an object;
  • boolean containsAll(Collection<?> c) returns true if 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 true if 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 true if 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)
offerFirst(e)
addLast(e)
offerLast(e)
Obtaining an item with removing removeFirst()
pollFirst()
removeLast()
pollLast()
Obtaining an item without removing getFirst()
peekFirst()
getLast()
peekLast()

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 v is 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;
    }

    @Override
    public String toString() {
        return String.format("%-9s %d", name, population);
    }

    @Override
    public boolean equals(Object o) {
        return toString().equals(o.toString());
    }

    @Override
    public 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 true if the stream is parallel or false if 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 action for 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

<R> Stream<R> map(Function<? super T,
? extends R> mapFunc)

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 { 
    //...
    @Test
    public 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.Test
    void sum() {
    }

    @org.junit.jupiter.api.Test
    void 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 {

    @Test
    void sum() {
        assertEquals(MathFuncs.sum(4, 5), 9);
    }

    @Test
    void 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):

@BeforeAll
public 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;

    @BeforeAll
    public static void init() {
        funcs = new MathFuncs();
    }

    @Test
    void sum() {
        assertEquals(MathFuncs.sum(4, 5), 9);
    }

    @Test
    void mult() {
        assertEquals(funcs.mult(3, 4), 12);
    }

    @AfterAll
    public 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;
}

//...

@Test
public 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 {
    private 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.Test
    void factorialBigInteger() {
    }

    @org.junit.jupiter.api.Test
    void factorialBigDecimal() {
    }

    @org.junit.jupiter.api.Test
    void 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.Test
    void factorialLong() {
        assertEquals(Factorial.factorialLong(5) + "", FACTORIAL_5);
        assertEquals(Factorial.factorialLong(50) + "", FACTORIAL_50);
        assertEquals(Factorial.factorialLong(500) + "", FACTORIAL_500);
    }

    @org.junit.jupiter.api.Test
    void 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.Test
    void factorialBigInteger() {
        assertEquals(Factorial.factorialBigInteger(5) + "", FACTORIAL_5);
        assertEquals(Factorial.factorialBigInteger(50) + "", FACTORIAL_50);
        assertEquals(Factorial.factorialBigInteger(500) + "", FACTORIAL_500);
    }

    @org.junit.jupiter.api.Test
    void 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>() {
            @Override
            public 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 "Fundamentals of Java 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 {

    @Test
    void 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 {

    @Test
    void 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 {

    @BeforeEach
    void setUp() {
    }

    @Test
    void density() {
    }

    @Test
    void maxYear() {
    }

    @Test
    void findWord() {
    }

    @Test
    void sortByPopulation() {
    }

    @Test
    void 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;
    }

    @BeforeEach
    void 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");
    }

    @Test
    void density() {
        assertEquals(CountryProcessorWithStreams.density(country, 1979), 82.42593120266125);
    }

    @Test
    void maxYear() {
        assertEquals(CountryProcessorWithStreams.maxYear(country), 1989);
    }

    @Test
    void findWord() {
        assertArrayEquals(getYears(CountryProcessorWithStreams.findWord(country, "census")),
                                   new int[] { 1959, 1979, 1989, 2001 });
    }

    @Test
    void sortByPopulation() {
        CountryProcessorWithStreams.sortByPopulation(country);
        assertArrayEquals(getYears(country.getCensuses()),
                                   new int[] { 1959, 1970, 2001, 1979, 1989 });
    }

    @Test
    void 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

  1. Implement the function of obtaining the integer part of the square root of a number of type BigInteger.
  2. Initialize a list of real numbers of BigDecimal type with an array containing a list of initial values. Find the sum of positive elements.
  3. In the array of integer values (BigInteger), replace negative values with modules, positive values with zeros. Apply Stream API facilities.

5 Quiz

  1. Name the main components of the Java SE platform.
  2. What are classes BigInteger and BigDecimal?
  3. How can you create a type number BigInteger?
  4. What is the difference between internal representation of double and BigDecimal?
  5. Is it possible to apply mathematical operations to numbers of types BigInteger and BigDecimal?
  6. What is the MathContext class used for?
  7. What is JCF? What standard interfaces does JCF provide?
  8. How to create a read-only collection?
  9. What are the Queue interface methods used to add items?
  10. Why are the methods for working with the queue implemented in two versions: with exception throwing and without exception throwing?
  11. What is the use of the PriorityQueue class?
  12. What are the stacks used for?
  13. What are the standard ways to implement the stack?
  14. What are the advantages and features of Stream API?
  15. How to get a stream from a collection?
  16. How to get a stream from an array?
  17. What is the difference between intermediate and terminal operations?
  18. What are the streams for working with primitive types?
  19. What are the standard means of checking assertions in Java?
  20. What is unit testing?
  21. What is JUnit?
  22. How are test methods annotated in JUnit?
  23. How to make a logical grouping of tests?
  24. How to use JUnit in a development environment?

 

up