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 then
-th power of a number;n
cannot be a negative number;BigInteger gcd(BigInteger number)
returns aBigInteger
whose value is the greatest common divisor of absolute values of current object andnumber
.
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 byn
positions;BigInteger shiftRight(int n)
returns the operation of shifting the bits to the right byn
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)
returnstrue
if the specified bit is set to one;BigInteger setBit(int n)
returns aBigInteger
object in which the corresponding bit is set to one;BigInteger clearBit(int n)
returns aBigInteger
object in which the corresponding bit is set to zero;BigInteger flipBit(int n)
returns aBigInteger
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 ofbitLength
length of bitLength bits; the givencertainty
and thernd
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 ofbitLength
bits; thernd
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)
returnstrue
f the number is probably prime andfalse
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 thejava.lang
package. This interface declares an abstract methoditerator()
that returns an iterator object of typeIterator
. Objects of classes that implement this interface are collections that can be used in an alternative construction of thefor
loop (for each). In addition, theIterable
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 fromIterator
, which allows iterating in long directions as well as modifying the current elements. Used in lists.Collection
is an interface derived fromIterable
. It is basic for all collections except associative arrays (Map
). TheAbstractCollection
class directly implements theCollection
interface. This class is used to create a number of abstract classes that represent different types of collections.List
is interface derived fromCollection
; 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 areArrayList
(a list built on an array) andLinkedList
.Queue
is interface derived fromCollection
; a collection used to store elements in first-in-first-out (FIFO) access order. The most popular class that implements this interface isLinkedList
.Deque
is an interface derived fromQueue
. 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 areArrayDeque
andLinkedList
.Set
is an interface derived fromCollection
, 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 isHashSet
.SortedSet
is an interface derived fromSet
. Assumes that elements are arranged according to a specific sort attribute. The most common implementation isTreeSet
.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 isHashMap
.- 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()
returnstrue
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 ofnext()
.-
boolean hasPrevious()
returnstrue
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 ofprevious()
.-
void add(E e)
inserts the specified item into the list. -
void set(E e)
replaces the last element obtained vianext()
orprevious()
with the specified element.
Iterator methods will be discussed in the context of their use in collections.
2.4.2 Collection and List Interfaces
The Collection<E>
interface is the base for most Collection Framework interfaces. The most general
operations that are implemented in all container classes (except associative arrays) are declared in this interface.
The methods that do not change the collection are listed below:
int size()
returns the size of the collection;boolean isEmpty()
returnstrue
if the collection is empty;boolean contains(Object o)
returnstrue
if the collection contains an object;boolean containsAll(Collection<?> c)
returnstrue
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 toObject
, which contains copies of all elements of the collection;T[] toArray(T[] a)
returns an array of references toT
, 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. Returnstrue
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. Returnstrue
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 ofnext()
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 ofnext()
. The first call ofprevious()
returns the element with the specified index minus one.
The following example demonstrates the listed methods. An ArrayList
class (a list built on
the use of an array) is used for demonstration.
List<Integer> list = new ArrayList<>(); list.add(0, 10); System.out.println(list); // [10] list.addAll(0, Arrays.asList(1, 2, 3, 4, 5)); System.out.println(list); // [1, 2, 3, 4, 5, 10] System.out.println(list.get(4)); // 5 list.remove(2); System.out.println(list); // [1, 2, 4, 5, 10] list.set(4, 1); System.out.println(list); // [1, 2, 4, 5, 1] System.out.println(list.indexOf(1)); // 0 System.out.println(list.lastIndexOf(1)); // 4 System.out.println(list.subList(2, 4)); // [4, 5] // Outputs index / value pairs in reverse order: for (var iterator = list.listIterator(list.size()); iterator.hasPrevious(); ) { System.out.printf("%d %d%n", iterator.previousIndex(), iterator.previous()); } // Adds indexes to elements. Adds intermediate elements: for (var iterator = list.listIterator(); iterator.hasNext(); ) { iterator.set(iterator.nextIndex() + iterator.next()); iterator.add(100); } System.out.println(list); // [1, 100, 4, 100, 8, 100, 11, 100, 9, 100]
In cases where insertion and deletion are used more often than getting an element by index, it is more efficient
to use a LinkedList
class (a doubly linked list).
2.4.3 Working with Queues and Stacks
A queue in the broad sense is a data structure that is filled in by element, and it allows getting objects from it according to a certain rule. In the narrow sense, this rule is "First In - First Out" (FIFO). In a queue organized on the principle of FIFO, adding an element is possible only at the end of the queue, and getting is only possible from the beginning of the queue.
In the container library, the queue is represented by the Queue interface. Methods declared this interface are listed in the table below:
Type of operation | Throws an exception | Returns a special value |
---|---|---|
Adding | add(e) |
offer(e) |
Obtaining an item with removing | remove() |
poll() |
Obtaining an item without removing | element() |
peek() |
The offer()
method returns false
if the item could not be added, for example,
if the queue has a limited number of items. In this case, the add()
method throws an exception. Similarly, remove()
and element()
throw
an exception if the queue is empty, but poll()
and peek()
in this case return null
.
The most convenient way to implement the queue is the use of the LinkedList
class that implements
the Queue
interface. For example:
package ua.inf.iwanoff.java.advanced.first;import java.util.LinkedList;import java.util.Queue;public class SimpleQueueTest {public static void main(String[] args) { Queue<String> queue =new LinkedList<>(); queue.add("First"); queue.add("Second"); queue.add("Third"); queue.add("Fourth"); String s;while ((s = queue.poll()) !=null ) { System.out.print(s + " ");// First Second Third Fourth } } }
The PriorityQueue
class arranges the elements according to the comparator (the object that implements
the Comparator
interface) specified in the constructor as a parameter. If an object is created using
a constructor without parameters, the elements will be ordered in a natural way (ascending for numbers, in alphabetical
order for strings). For example:
package ua.inf.iwanoff.java.advanced.first;import java.util.PriorityQueue;import java.util.Queue;public class PriorityQueueTest {public static void main(String[] args) { Queue<String> queue =new PriorityQueue<>(); queue.add("First"); queue.add("Second"); queue.add("Third"); queue.add("Fourth"); String s;while ((s = queue.poll()) !=null ) { System.out.print(s + " ");// First Fourth Second Third } } }
The Deque
interface (double-ended-queue) provides the ability to add and remove items from both ends.
Methods declared in this interface are listed below:
Type of operation | Working with the first element | Working with the last element |
---|---|---|
Adding | addFirst(e) |
addLast(e) |
Obtaining an item with removing | removeFirst() |
removeLast() |
Obtaining an item without removing | getFirst() |
getLast() |
Each pair represents the function that throws an exception, and the function that returns some special value. There
are also methods for removing the first (or last) occurrence of a given element (removeFirstOccurrence()
and removeLastOccurrence()
,
respectively).
You can use whether the special ArrayDeque
class or LinkedList
to implement the interface.
A stack is a data structure organized on the principle "last in – first out" (LIFO). There are three stack operations: adding element (push), removing element (pop) and reading head element (peek).
In JRE 1.1, the stack is represented by the Stack
class. For example:
package ua.inf.iwanoff.java.advanced.first;import java.util.Stack;public class StackTest {public static void main(String[] args) { Stack<String> stack =new Stack<>(); stack.push("First"); stack.push("Second"); stack.push("Third"); stack.push("Fourth"); String s;while (!stack.isEmpty()) { s = stack.pop(); System.out.print(s + " ");// Fourth Third Second First } } }
This class is currently not recommended for use. Instead, you can use the Deque
interface, which declares
the similar methods. For example:
package ua.inf.iwanoff.java.advanced.first;import java.util.ArrayDeque;import java.util.Deque;public class AnotherStackTest {public static void main(String[] args) { Deque<String> stack =new ArrayDeque<>(); stack.push("First"); stack.push("Second"); stack.push("Third"); stack.push("Fourth"); String s;while (!stack.isEmpty()) { s = stack.pop(); System.out.print(s + " ");// Fourth Third Second First } } }
Stacks are often used in various algorithms. In particular, it is often possible to implement a complex algorithm without recursion with the help of a stack.
2.4.4 Additional Features for Collections in Java 8
Up to and including Java 7, the traditional imperative approach, based on the explicit use of loops, conditional
statements, switches, etc., was used to work with collections. The standard algorithms provided by classes Arrays
and Collections
, only partially met the need for data processing.
Starting with Java 8, the standard interfaces of the java.util
package are supplemented with methods
that focus on using lambda expressions and references to methods. To ensure compatibility with previous versions,
new interfaces provide the default implementation of the new methods. In particular, the Iterable
interface
defines the forEach()
method, which allows you to perform some actions in the loop that do not change
the elements of the collection. You can specify an action using a lambda expression or a reference to a method.
For example:
public class ForEachDemo {static int sum = 0;public static void main(String[] args) { Iterable<Integer> numbers =new ArrayList(Arrays.asList(2, 3, 4)); numbers.forEach(n -> sum += n); System.out.println(sum); } }
In the above example, the sum of collection elements is calculated. The variable that holds the sum is described as a static class field, since lambda expressions cannot change local variables.
The Collection
interface defines the removeIf()
method, which allows you to remove items
from the collection if items match a certain filter rule. In the following example, odd items are removed from the
collection of integers. The forEach()
method is used for columnwise output the collection items:
Collection<Integer> c =new ArrayList(Arrays.asList(2, 4, 11, 8, 12, 3)); c.removeIf(k -> k % 2 != 0);// The rest of the items are displayed columnwise: c.forEach(System.out::println);
The List
interface provides methods replaceAll()
and sort()
. The second
one can be used instead of the analogous static method of the Collections
class, but the definition
of the sorting feature is obligatory:
List<Integer> list =new ArrayList(Arrays.asList(2, 4, 11, 8, 12, 3)); list.replaceAll(k -> k * k);// replace the numbers with their second powers System.out.println(list);// [4, 16, 121, 64, 144, 9] list.sort(Integer::compare); System.out.println(list);// [4, 9, 16, 64, 121, 144] list.sort((i1, i2) -> Integer.compare(i2, i1)); System.out.println(list);// [144, 121, 64, 16, 9, 4]
Java 8 defines the Spliterator
interface that can be applied to collections. This interface
defines a special kind of iterator, a separator iterator. In particular, it allows you to split a sequence into
several subsequences that you can work with in parallel. You can get an instance of Spliterator
using the spliterator()
method
defined in the Collection
interface.
The reason in dividing collections into separate parts arises first of all in cases where parallel processing
of elements of a large data set is possible. But usage of Spliterator
can also be useful in a single-threaded environment.
The trySplit()
method divides the elements into two approximately equal parts. The method creates and returns a
new Spliterator
object using which you can work with the first half of the sequence. The object for
which the trySplit()
method was called will work with the second half of the sequence.
The forEachRemaining()
method provides an iteration for Spliterator
. The method is declared
as follows:
void forEachRemaining(Consumer<?super Double> action)
Example:
List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8); Spliterator<Integer> spliterator1 = list.spliterator(); Spliterator<Integer> spliterator2 = spliterator1.trySplit(); spliterator1.forEachRemaining(System.out::println); System.out.println("========"); spliterator2.forEachRemaining(System.out::println);
The result of this program fragment will be as follows:
5 6 7 8 ======== 1 2 3 4
Now you can work with the two parts of the list separately. As you can see from the example, after splitting, the first iterator works with the second half of the sequence, and the second with the first one.
There is also tryAdvance()
method that actually combines the hasNext()
and next()
those
declared in the Iterator
interface. The tryAdvance()
method is declared as follows::
boolean tryAdvance(Consumer<?super T> action);
If the remaining element exists, it performs the specified action on it, returning true
;
otherwise it returns false
. In other words, it performs an action on the next element
in the sequence and then shifts the iterator. You can also display elements using tryAdvance()
:
while (spliterator2.tryAdvance(System.out::println));
Starting with Java 8, new methods have been added to the Map
interface. The added methods listed in
the table:
Method | Description |
---|---|
V getOrDefault(Object key, V& defaultValue) |
Returns a value, or a default value, if the key is missing |
V putIfAbsent(K key, V value) |
Adds a pair if the key is missing and returns the value |
boolean remove(Object key, Object value) |
Removes a pair if it is present |
boolean replace(K key, V oldValue, V newValue) |
Replaces value with the new one if pair is present |
V replace(K key, V value) |
Replaces the value if the key is present, returns the old value |
V compute(K key, BiFunction<?& super K, super V, ? extends V> remappingFunction) |
Invokes the function to construct a new value. A new pair is added, a pair that existed before is deleted, and a new value is returned |
V computeIfPresent(K key, BiFunction<? super K, ? super V,
? extends V> remappingFunction) |
If a specified key is present, a new function is called to create a new value, and the new value replaces the previous one. |
V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) |
Returns the value by the key. If the key is missing, a new pair is added, the value is calculated by function |
V merge(K key, V value, BiFunction<? super V, ? super V,
? extends V> remappingFunction) |
If the key is absent, then a new pair is entered and the value 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; } @Overridepublic String toString() {return String.format("%-9s %d", name, population); } @Overridepublic boolean equals(Object o) {return toString().equals(o.toString()); } @Overridepublic int hashCode() {return name.hashCode(); } }
The traditional approach to solving the problem (before Java 7 inclusive) involves creating a list of cities, which will make it impossible to add the same objects. Next, you need to create a list from the set, sort by population and display the names of the cities using a loop:
Set<City> citiesSet =new HashSet<>(); citiesSet.add(new City("Kyiv", 2_967_360)); citiesSet.add(new City("Kharkiv", 1_443_207)); citiesSet.add(new City("Odesa", 1_017_699)); citiesSet.add(new City("Donetsk", 908_456)); citiesSet.add(new City("Odesa", 1_017_699)); List<City> cities =new ArrayList<>(citiesSet);for (int i = 0; i < cities.size(); i++) {if (cities.get(i).getPopulation() < 1_000_000) { cities.remove(i); } } Collections.sort(cities,new Comparator<City>() {public int compare(City a, City b) {return Integer.compare(a.getPopulation(), b.getPopulation()); } });for (City c : cities) { System.out.println(c.getName()); }
The disadvantages of this approach are obvious. A small task requires essential error-prone code. In addition, unnecessary objects are created in memory, in particular, a set.
Lambda expressions, method references, and new interface methods that make up the Java Collection Framework have made it easier to implement code.
Set<City> citiesSet =new HashSet<>(List.of(new City("Kyiv", 2_967_360),new City("Kharkiv", 1_443_207),new City("Odesa", 1_017_699),new City("Donetsk", 908_456),new City("Odesa", 1_017_699))); List<City> cities =new ArrayList<>(citiesSet); cities.removeIf(city -> city.getPopulation() < 1_000_000); cities.sort(Comparator.comparing(City::getPopulation)); cities.forEach(city -> System.out.println(city.getName()));
By calling the removeIf()
method with a lambda expression as a parameter, we delete unnecessary data.
Next, the list is sorted according to the specified criterion (call of sort()
method). The sort condition
is defined by a method reference. The output of city names in separate lines is carried out by the forEach()
function.
With streams, everything can be done in a single statement:
Stream.of(new City("Kyiv", 2_967_360),new City("Kharkiv", 1_443_207),new City("Odesa", 1_017_699),new City("Donetsk", 908_456),new City("Odesa", 1_017_699)) .filter(city -> city.getPopulation() > 1_000_000) .distinct() .sorted(Comparator.comparing(City::getPopulation)) .map(City::getName) .forEach(System.out::println);
In this example, a stream is created using the static function of(). The array of items that the stream
will work with is created on rom a list of actual parameters. Next, the stream is filtered by calling the filter()
function.
The filtering condition is defined by a lambda expression. and further, only different data are selected (the distinct()
method)
and sorting is performed according to the specified criterion (call of sorted()
method). The sort condition
is defined by a method reference. Using the call of map()
function, a stream is created to
work with city names, after which the names are output in separate lines using the forEach()
method.
2.5.2 Basic Methods for Working with Streams
The most significant methods of the generic java.util.stream.BaseStream
interface are given in the
table (S
- type of the stream, E
- type of the element, R
- container type):
Method | Description | Note |
---|---|---|
S parallel() |
returns a parallel stream received from the current one | intermediate operation |
S sequential() |
returns a sequential stream received from the current one | intermediate operation |
boolean isParallel() |
returns 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 |
|
applies the given function to the elements of the stream and returns a new stream | intermediate operation |
Optional<T> min(Comparator<? super T> comp) |
returns the minimum value using the specified comparison | terminal operation |
Optional<T> max(Comparator<? super T> comp) |
returns the maximum value using the specified comparison | terminal operation |
long count() |
returns the number of elements in the stream |
terminal operation |
Stream<T> distinct() |
returns a stream of differing elements | intermediate operation |
Optional<T> reduce(BinaryOperator<T> accumulator) |
returns the scalar result calculated by the values of the elements | terminal operation |
Object[] toArray() |
creates and returns an array of stream elements | terminal operation |
2.5.3 Creation of Streams
There are several ways to create a stream. You can use the factory methods added to the Collection
interface
(with default implementations), respectively stream()
(for synchronous work) and parallelStream()
(for
asynchronous work). This is how you can create a stream for synchronous work:
List<Integer> intList = List.of(3, 4, 1, 2); Stream<Integer> fromList = intList.stream();
You can create a stream from an array:
Integer[] a = { 1, 2, 3 }; Stream<Integer> fromArray = Arrays.stream(a);
You can create a data source with the specified items. To do this, use the "factory" method of()
:
Stream<Integer> newStream = Stream.of(4, 5, 6);
Streams of items can be created from input streams (BufferedReader.lines()
), filled with random values
(Random.ints()
), and also obtained from archives, bit sets, etc.
You can get an array from a stream using the toArray()
method. The following example creates a stream
and then outputs to the console by creating an array and obtaining a string representation using the static Arrays.toString()
method:
Stream<Integer> s = Stream.of(1, -2, 3); Object[] a = s.toArray(); System.out.println(Arrays.toString(a));// [1, -2, 3]
There is also a special class StreamSupport
that provides static methods for creating streams. An
example of use
StreamSupport
will be considered later.
2.5.4 Iteration by Elements
Streams provide iteration over data elements using the forEach()
method. The function parameter is
the standard Consumer
functional interface, which defines a method with a single parameter and a void
result
type. For example:
Stream<Integer> s = Stream.of(4, 5, 6, 1, 2, 3); s.forEach(System.out::println);
Streams provide iterators. The iterator()
method of the Stream
interface returns an object
that implements the java.util.Iterator
interface. The iterator can be used explicitly:
s = Stream.of(11, -2, 3); Iterator<Integer> it = s.iterator();while (it.hasNext()) { System.out.println(it.next()); }
You can also apply Spliterator
to streams. In particular, the static methods of the StreamSupport
class
allow you to create a stream from a Spliterator
object. Example:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); Spliterator<Integer> spliterator = numbers.spliterator();// true indicates that parallel processing can be used: Stream<Integer> stream = StreamSupport.stream(spliterator,true ); stream.forEach(System.out::println);
Use of Spliterator
in this context provides efficiency due to the possibility of parallel processing.
2.5.5 Operations with Streams
The simplest stream operation is filtering. The intermediate filter()
operation returns a filtered
stream, taking a parameter of Predicate
type. The Predicate
type is a functional interface
that describes a method with a single parameter and boolean
result type. For example,
you can filter out only even numbers from the stream s
:
s.filter(k -> k % 2 == 0).forEach(System.out::println);
The previous example illustrates the use of lambda expressions when working with streams, as well as a small conveyor that includes one intermediate operation.
The intermediate sorted()
operation returns the sorted representation of the stream. Elements are
ordered in the natural order (if it is defined). In other cases, the Comparator
interface should be
implemented, for example, using the lambda expression:
// Sort ascending: Stream<Integer> s = Stream.of(4, 5, 6, 1, 2, 3); s.sorted().forEach(System.out::println);// Sort descending: s = Stream.of(4, 5, 6, 1, 2, 3); s.sorted((k1, k2) -> Integer.compare(k2, k1)).forEach(System.out::println);
The last example shows that after each call to the terminal operation, the stream should be recreated.
Most operations are implemented in such a way that actions on individual elements do not depend on other elements.
Such operations are called stateless operations. Other operations that require working on all elements
at once (for example, sorted()
) are called stateful operations.
The intermediate operation map()
receives a functional interface that defines a certain function for
transforming and forming a new stream from the resulting transformed elements. For example, we calculate the squares
of numbers:
s = Stream.of(1, 2, 3); s.map(x -> x * x).forEach(System.out::println);
Using the distinct()
method, you can get a stream containing only different elements of the collection.
For example:
s = Stream.of(1, 1, -2, 3, 3); System.out.println(Arrays.toString(s.distinct().toArray()));// [1, -2, 3]
Intermediate operations are characterized by the so-called delayed behavior (lazy behavior): they are not performed immediately, but as needed, when the final operation works with a new data stream. Delayed behavior increases the efficiency of working with streams of elements.
The terminal operation count()
with the resulting type long
returns the number
of elements in the stream:
s = Stream.of(4, 5, 6, 1, 2, 3); System.out.println(s.count());// 6
The terminal operations min()
and max()
return Optional
objects with a minimum
and maximum value, respectively. A Comparator
type parameter is used for comparison. For example:
s = Stream.of(11, -2, 3); System.out.println(s.min(Integer::compare).get());// -2
Using a terminal reduce()
operation, we can calculate a scalar value. The reduce()
operation
in its simplest form performs the specified action with two operands, the first of which is the result of performing
the action on the previous elements, and the second is the current element. In the following example, we find the
sum of the elements of the data stream:
s = Stream.of(1, 1, -2, 3, 3); Optional<Integer> sum = s.reduce((s1, s2) -> s1 + s2); sum.ifPresent(System.out::println);// 6
The min()
, max()
, and reduce()
operations get a scalar value from the stream,
so they are called reduction operations.
Sometimes it is necessary to reproduce a stream to perform several terminal operations. A Supplier
functional
interface with an abstract function get()
will be used to create identical streams according to a defined
rule. The rule can be described as a lambda expression. Example:
Supplier<Stream<Integer>> supplier = () -> Stream.of(1, 2, 3); supplier.get().forEach(System.out::println); System.out.println(Arrays.toString(supplier.get().toArray()));
The concat()
method joins two streams:
Stream<Integer> stream1 = Stream.of(1, 2, 3); Stream<Integer> stream2 = Stream.of(8, 9); Stream<Integer> result = Stream.concat(stream1, stream2); result.forEach(System.out::print);// 12389
The skip()
method creates a new stream in which the first n
elements are omitted. Example:
Stream<Integer> stream1 = Stream.of(1, 2, 3, 4, 5); Stream<Integer> stream2 = stream1.skip(2);// 3 4 5
There are also findFirst()
and findAny()
operations for searching for a certain object.
These methods return Optional
type. To check the presence of objects that satisfy certain conditions,
anyMatch()
, allMatch()
and noneMatch()
methods
are used::
Supplier<Stream<Integer>> supplier = () -> Stream.of(1, 2, 3, -5); System.out.println(supplier.get().allMatch(i -> i > 0));// false System.out.println(supplier.get().anyMatch(i -> i > 0));// true System.out.println(supplier.get().noneMatch(i -> i > 10));// true
2.5.6 Using the Collectors Class
A special generic interface java.util.stream.Collector
is defined for receiving collections from streams.
The terminal operation collect()
of the Stream
class allows you to retrieve a traditional
collection from a stream. The type of collection depends on the parameter. The parameter is an instance of the Collector
type.
The Collectors
class provides static methods for obtaining objects of type Collector
,
such as toCollection()
, toList()
, toSet()
and toMap()
.
Example:
Supplier<Stream<Integer>> supplier = () -> Stream.of(1, 2, 3); List<Integer> list = supplier.get().collect(Collectors.toList()); System.out.println("list: " + list);// list: [1, 2, 3] Set<Integer> set = supplier.get().collect(Collectors.toSet()); System.out.println("set: " + set);// set: [1, 2, 3] Map<Integer, Double> map = supplier.get().collect( Collectors.toMap(Integer::intValue, Integer::doubleValue)); System.out.println("map :" + map);// map :{1=1.0, 2=2.0, 3=3.0}
2.5.7 Using Streams to Work with Primitive Types
There are also streams for working with primitive types: IntStream
, LongStream
and DoubleStream
.
Consider the work with IntStream
and DoubleStream
.
The easiest way to create streams is to use a static of()
function:
IntStream intStream = IntStream.of(1, 2, 4, 8); DoubleStream doubleStream = DoubleStream.of(1, 1.5, 2);
You can create streams from the corresponding arrays:
int [] intArr = { 10, 11, 12 };double [] doubleArr = { 10, 10.5, 11, 11.5, 12 }; intStream = Arrays.stream(intArr); doubleStream = Arrays.stream(doubleArr);
With the help of a range()
method of IntStream
class, you can create streams by filling
them with sequential values. You can also simultaneously define a filter:
intStream = IntStream.range(0, 10).filter(n -> n % 2 == 0);// 0 2 4 6 8
The iterate()
method can be used to create an infinite stream. The next element is calculated
from the previous one. You can limit the stream using the limit()
function. So, for example, you can
get consecutive powers of 3:
intStream = IntStream.iterate(1, i -> i * 3).limit(6);// 1 3 9 27 81 243
The generate()
method lso allows you to generate elements, but without taking into account the previous
ones. For example, you can fill an array with random numbers:
doubleStream = DoubleStream.generate(() -> (Math.random() * 10000)).limit(20);
Further work is similar to work with common streams. For example, you can sort and display only odd values:
intStream = IntStream.of(11, 2, 43, 81, 8, 0, 5, 3); intStream.sorted().filter(n -> n % 2 != 0).forEach(System.out::println);
The resulting streams can be used to create new arrays:
int [] newIntArr = intStream.toArray();double [] newDoubleArr = doubleStream.toArray();
Note:
it is assumed that the streams intStream
and doubleStream
were not used in the terminal operations.
2.6 Testing in Java. Using JUnit
2.6.1 Overview
Testing is one of the most important components of the software development process. Software testing is performed in order to obtain information about the quality of the software product. There are many approaches and techniques for testing and verifying software.
The paradigm of test-driven development (development through testing) defines the technique of software development, based on the use of tests to stimulate the writing of code, and to verify it. Code development is reduced to repeating the test-code-test cycle with subsequent refactoring.
The level of testing at which the least possible component to be tested, such as a single class or function, is called unit testing. Appropriate testing technology assumes that tests are developed in advance, before writing the real code, and the development of the code of the unit (class) is completed when its code passes all the tests.
2.6.2 Java Tools for Diagnosing Runtime Errors
Many modern programming languages, including Java, include syntactic assertions. The assert
keyword
has appeared in Java since version JDK 1.4 (Java 2). The assert
work can be turned
on or off. If the execution of diagnostic statements is enabled, the work of assert
is
as follows: an expression of type boolean
is executed and if the result is true
,
the program continues, otherwise an exception of java.lang.AssertionError
throws. Suppose, according
to the logic of the program, the variable c
must always be positive. Execution of such a fragment of
the program will not lead to any consequences (exceptions, emergency stop of the program, etc.):
int a = 10;int b = 1;int c = a - b;assert c > 0;
If, due to an incorrect software implementation of the algorithm, the variable c
still received a
negative value, the execution of a fragment of the program will lead to the throwing of an exception and an abnormal
termination of the program, if the processing of this exception was not provided:
int a = 10;int b = 11;int c = a - b;assert c > 0;// exception is thrown
After the assertion, you can put a colon, followed by a string of the message. Example:
int a = 10;int b = 11;int c = a - b;assert c > 0 : "c must be positive";
In this case, the corresponding string is the exception message string.
Assert execution is usually disabled in integrated development environments. To enable assert execution in the
IntelliJ IDEA environment, use the Run | Edit Configurations menu function. In the Run/Debug Configurations window,
enter -ea
in the VM Options input line.
In these examples, the values that are checked with assert
are not entered from the
keyboard, but are defined in the program to demonstrate the correct use of assert
-
the search for logical errors, rather than checking the correctness of user input. Exceptions, conditional statements,
etc. should be used to verify the correctness of the data entered. The use of assertion validation is not allowed,
because in the future the program will be started without the -ea
option and all assertions will be
ignored. The expression specified in the statement should not include actions that are important in terms of program
functionality. For example, if the assertion check is the only place in the program from which a very important
function is called,
public static void main(String[] args) {//... assert f() : "failed";//... }public static boolean f() {// Very important calculations return true ; }
then after disabling assertions the function will not be called at all.
2.6.3 Basics of Using JUnit
In contrast to the use of diagnostic statements, which performs testing of algorithms "from the inside", unit testing provides verification of a particular unit as a whole, testing "outside" the functionality of the unit.
The most common unit testing support for Java software is JUnit, an open unit testing library. JUnit allows:
- create tests for individual classes;
- create test suits;
- create a series of tests on repeating sets of objects.
Now the JUnit 5 version is now relevant. But also a very widespread is JUnit 4 version.
To create a test, you need to create a class that needs to be tested, as well as create a public class for testing
with a set of methods that implement specific tests. Each test method must be public
, void
,
and have no parameters. The method must be marked with an annotation @Test
:
public class MyTestCase {//... @Testpublic void testXXX() {//... }//... }
Note: to use the @Test
and other similar annotations should be added import statements import org.junit.jupiter.api.*;
for
JUnit 5) or import
org.junit.*;
(for JUnit 4)
.
Within such methods, you can use the following assertion methods:
assertTrue(expression);// Fails the test if false assertFalse(expression);// Fails the test if true assertEquals(expected, actual);// Fails the test if not equivalent assertNotNull(new MyObject(params));// Fails the test if null assertNull(new MyObject(params));// Fails the test if not null assertNotSame(expression1, expression2);// Fails the test if both links refer to the same object assertSame(expression1, expression2);// Fails the test if the objects are different fail(message)// Immediately terminates the test with a failure message
Here MyObject
is a class that is being tested. These Assertion
class methods (Assert
class
methods for JUnit 4) are accessed using static import: import static org.junit.jupiter.api.Assertion.*;
(for
JUnit 5) or import static org.junit.Assert.*;
. These methods also are implemented
with an additional message
parameter of type String
, which specifies the message that
will be displayed if the test failed.
The IntelliJ IDEA provides built-in JUnit support. Suppose a new project has been created. The project contains a class with two functions (static and non-static) that should be tested:
package ua.inf.iwanoff.java.advanced.first;public class MathFuncs {public static int sum(int a,int b) {return a + b; }public int mult(int a,int b) {return a * b; } }
Within the project, we can manually create a folder, for example, tests
. Next we should set Mark
Directory as | Test Sources Root with the context menu.
Returning to the MathFuncs
class, choosing it in the code editor, through the context menu we can
generate tests: Generate... | Test.... In the dialog that opened, we select the version of the JUnit library.
The desired option is JUnit5. We can also correct the class name that we offer: MathFuncsTest
.
In most cases, the correction of this name is not needed. Then we select the names of methods that are subject to
testing. In our case, there are sum()
and mult()
. Such a code will be received:
package ua.inf.iwanoff.java.advanced.first;import static org.junit.jupiter.api.Assertions.*;class MathFuncsTest { @org.junit.jupiter.api.Testvoid sum() { } @org.junit.jupiter.api.Testvoid mult() { } }
IntelliJ IDEA indicates errors in this code (Cannot resolve symbol 'junit'). By clicking Alt+Enter, we get a hint: Add 'JUnit 5.7.0' to classpath. Taking advantage of this prompt, we add the relevant library and get the code without errors.
We can optimize the code by adding imports. We add testing of MathFuncs
class methods into MathFuncsTest
methods.
To test the work of mult()
we need to create an object:
package ua.inf.iwanoff.java.advanced.first;import org.junit.jupiter.api.Test;import static org.junit.jupiter.api.Assertions.*;class MathFuncsTest { @Testvoid sum() { assertEquals(MathFuncs.sum(4, 5), 9); } @Testvoid mult() { assertEquals(new MathFuncs().mult(3, 4), 12); } }
You can run tests to run through the Run menu. The normal completion of the process indicates no errors
during verification. If you add a code that distorts computing in the MathFuncs
class, for example
public int mult(int a,int b) {return a * b + 1; }
running tests will result in AssertionFailedError
message. You can see how many tests have been successful,
and how much it is not passed.
If some actions need to be taken before performing the test function, for example, to format the values of variables,
then such initialization is made in a separate static method, which is preceded by an annotation @BeforeAll
(@BeforeClass
in
JUnit 4):
@BeforeAllpublic static void setup() {//... }
Similarly, the methods in which the actions needed after testing are preceded by@AfterAll
annotation
(@AfterClass
in JUnit 4). Methods must be public static void
.
In our example, we can create an object in advance, as well as add messages after the tests are completed:
package ua.inf.iwanoff.java.advanced.first;import org.junit.jupiter.api.*;import static org.junit.jupiter.api.Assertions.*;class MathFuncsTest {private static MathFuncs funcs; @BeforeAllpublic static void init() { funcs =new MathFuncs(); } @Testvoid sum() { assertEquals(MathFuncs.sum(4, 5), 9); } @Testvoid mult() { assertEquals(funcs.mult(3, 4), 12); } @AfterAllpublic static void done() { System.out.println("Tests finished"); } }
Annotation @BeforeEach
(@Before
in JUnit 4) indicates that the method is
called before each test method. Accordingly, @AfterEach
(@After
in JUnit 4) indicates
that the method is called after each successful test method. Methods marked by these annotations should not be static.
You can also test methods that return void
. Calling such a method involves performing
an action (for example, creating a file, changing the value of a field, etc.). It is necessary to check whether
such action took place. For example:
void setValue(into value) {this .value = value; }//... @Testpublic void testSetValue() { someObject.setValue(123); assertEquals(123, someObject.getValue()); }
However, as a rule, testing the simplest access methods (setters and getters) seems excessive and is not recommended.
3 Sample Programs
3.1 Finding Factorials
The traditional mathematical problem of calculating the factorial for large integers causes difficulties associated
with restrictions on the size of the result. For int
type, int the maximum certain
value is 12!
, for the long
type it is 20!
For values 171!
and
more, even an approximate value cannot be obtained using double. Using BigInteger
and BigDecimal
allow
you to get factorials of large numbers. The size of the result is actually limited by the display capabilities of
the console window.
The following program calculates the factorial of integers using different types: long
, double
, BigInteger
and
BigDecimal
.
package ua.inf.iwanoff.java.advanced.first;import java.math.BigDecimal;import java.math.BigInteger;import java.util.Scanner;public class Factorial {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.Testvoid factorialBigInteger() { } @org.junit.jupiter.api.Testvoid factorialBigDecimal() { } @org.junit.jupiter.api.Testvoid factorialDouble() { } }
Errors related to the use of JUnit tools are corrected according to the instructions that were described above.
To test the factorialDouble()
function, it is necessary to add an auxiliary compareDoubles()
function,
as well as to create constants for some factorial values. The code of the FactorialTest.java
file will
be as follows:
package ua.inf.iwanoff.java.advanced.first;package ua.inf.iwanoff.java.advanced.first;import java.math.BigDecimal;import static org.junit.jupiter.api.Assertions.*;class FactorialTest {public static final String FACTORIAL_5 = "120";public static final String FACTORIAL_50 = "30414093201713378043612608166064768844377641568960512000000000000";public static final String FACTORIAL_500 = "12201368259911100687012387854230469262535743428031928421924135883858453731538819" + "97605496447502203281863013616477148203584163378722078177200480785205159329285477" + "90757193933060377296085908627042917454788242491272634430567017327076946106280231" + "04526442188787894657547771498634943677810376442740338273653974713864778784954384" + "89595537537990423241061271326984327745715546309977202781014561081188373709531016" + "35632443298702956389662891165897476957208792692887128178007026517450776841071962" + "43903943225364226052349458501299185715012487069615681416253590566934238130088562" + "49246891564126775654481886506593847951775360894005745238940335798476363944905313" + "06232374906644504882466507594673586207463792518420045936969298102226397195259719" + "09452178233317569345815085523328207628200234026269078983424517120062077146409794" + "56116127629145951237229913340169552363850942885592018727433795173014586357570828" + "35578015873543276888868012039988238470215146760544540766353598417443048012893831" + "38968816394874696588175045069263653381750554781286400000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "000000000000000";public static final BigDecimal EPS = BigDecimal.ONE;private boolean compareDoubles(double d, String s) {return new BigDecimal(d).subtract(new BigDecimal(s)).abs().compareTo(EPS) <= 0; } @org.junit.jupiter.api.Testvoid factorialLong() { assertEquals(Factorial.factorialLong(5) + "", FACTORIAL_5); assertEquals(Factorial.factorialLong(50) + "", FACTORIAL_50); assertEquals(Factorial.factorialLong(500) + "", FACTORIAL_500); } @org.junit.jupiter.api.Testvoid factorialDouble() { assertTrue(compareDoubles(Factorial.factorialDouble(5), FACTORIAL_5)); assertTrue(compareDoubles(Factorial.factorialDouble(50), FACTORIAL_50)); assertTrue(compareDoubles(Factorial.factorialDouble(500), FACTORIAL_500)); } @org.junit.jupiter.api.Testvoid factorialBigInteger() { assertEquals(Factorial.factorialBigInteger(5) + "", FACTORIAL_5); assertEquals(Factorial.factorialBigInteger(50) + "", FACTORIAL_50); assertEquals(Factorial.factorialBigInteger(500) + "", FACTORIAL_500); } @org.junit.jupiter.api.Testvoid factorialBigDecimal() { assertEquals(Factorial.factorialBigDecimal(5) + "", FACTORIAL_5); assertEquals(Factorial.factorialBigDecimal(50) + "", FACTORIAL_50); assertEquals(Factorial.factorialBigDecimal(500) + "", FACTORIAL_500); } }
The values of 50!
and 500!
were obtained from the page https://zeptomath.com/calculators/factorial.php and
are used as a reference in our tests.
As expected, the correct values of 50!
and 500!
can be obtained only using BigInteger
and BigDecimal
.
3.2 The Product of the Entered Numbers
In the example below, integers are entered, displayed as a decrease, and their product is calculated. The entering ends with zero:
package ua.inf.iwanoff.java.advanced.first;import java.util.*;public class Product {public static void main(String[] args) { Queue<Integer> queue =new PriorityQueue<>(100,new Comparator<Integer>() { @Overridepublic int compare(Integer i1, Integer i2) {return -Double.compare(i1, i2); } }); Scanner scanner =new Scanner(System.in); Integer k;do { k = scanner.nextInt();if (k != 0) { queue.add(k); } }while (k != 0);int p = 1;while ((k = queue.poll()) !=null ) { p *= k; System.out.print(k + " "); } System.out.println(); System.out.println(p); } }
3Obtaining a Table of Prime Numbers using Data Streams
The following program allows you to get a table of prime numbers in a given range. To obtain simple numbers, it
is advisable to use IntStream
:
package ua.inf.iwanoff.java.advanced.first;import java.util.stream.IntStream;public class PrimeFinder {private static boolean isPrime(int n) {return n > 1 && IntStream.range(2, n - 1).noneMatch(k -> n % k == 0); }public static void printAllPrimes(int from,int to) { IntStream primes = IntStream.range(from, to + 1).filter(PrimeFinder::isPrime); primes.forEach(System.out::println); }public static void main(String[] args) { printAllPrimes(6, 199); } }
The isPrime()
method checks whether the number n is prime. For numbers greater than 1, a set of consecutive
integers is formed, for each of which it is checked whether n is divisible by this number. In the printAllPrimes()
method,
we form a stream of simple numbers using a filter and output the numbers using the forEach()
method.
3.4"Country" and "Census" Classes
In the examples from the "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 { @Testvoid containsWord() { } }
If errors are highlighted in the generated code, we correct them as described above.
Now we can add the necessary testing. The code of the file CensusCheckerTest.java
will be as follows:
package ua.inf.iwanoff.java.advanced.first;import ua.inf.iwanoff.java.third.Census;import static org.junit.jupiter.api.Assertions.*;import org.junit.jupiter.api.Test;class CensusCheckerTest { @Testvoid containsWord() { Census census =new Census(); census.setComments("The first census in independent Ukraine"); assertTrue(CensusChecker.containsWord(census, "Ukraine")); assertTrue(CensusChecker.containsWord(census, "FIRST")); assertFalse(CensusChecker.containsWord(census, "rain")); assertFalse(CensusChecker.containsWord(census, "censuses")); } }
After completing the tests, we will receive a successful exit code. If the expected results are changed in the code, the tests will throw an exception and the assertion that failed will be underlined in the code.
We create a separate class to work with the list of censuses via the Stream API:
package ua.inf.iwanoff.java.advanced.first;import ua.inf.iwanoff.java.fourth.CountryWithArrayList;import ua.inf.iwanoff.java.third.Census;import static ua.inf.iwanoff.java.advanced.first.CensusChecker.containsWord;import java.util.Comparator;/** * Class for processing data about the country in which the census is being conducted. * Stream API tools are used to process sequences */ public class CountryProcessorWithStreams {/** * Returns the population density for the specified year * * @param country reference to a country * @param year specified year (e.g. 1959, 1979, 1989, etc.) * @return population density for the specified year */ public static double density(CountryWithArrayList country,int year) { Census census = country.getList().stream() .filter(c -> c.getYear() == year) .findFirst().orElse(null );return census ==null ? 0 : census.getPopulation() / country.getArea(); }/** * Finds and returns a year with the maximum population * * @param country reference to a country * @return year with the maximum population */ public static int maxYear(CountryWithArrayList country) {return country.getList().stream() .max(Comparator.comparing(Census::getPopulation)) .get().getYear(); }/** * Creates and returns an array of censuses with the specified word in the comments * * @param country reference to a country * @param word a word that is found * @return array of censuses with the specified word in the comments */ public static Census[] findWord(CountryWithArrayList country, String word) {return country.getList().stream() .filter(c -> containsWord(c, word)) .toArray(Census[]::new ); }/** * Sorts the sequence of censuses by population * * @param country reference to a country */ public static void sortByPopulation(CountryWithArrayList country) { country.setList(country.getList().stream() .sorted() .toList()); }/** * Sorts the sequence of censuses in the alphabetic order of comments * * @param country reference to a country */ public static void sortByComments(CountryWithArrayList country) { country.setList(country.getList().stream().sorted(Comparator.comparing(Census::getComments)).toList()); } }
The given code uses a function call: toArray(Census[]::new)
. This ensures that an
array of the required type (references to Census
) is created, rather than an array of references to Object
,
which is returned by the corresponding function without parameters.
We add a CountryWithStreams
class for testing. This is done in the same way as for CensusChecker
.
It is advisable to choose all methods,
as well as setUp/@Defore. We
will get the following code:
package ua.inf.iwanoff.java.advanced.first;import org.junit.jupiter.api.BeforeEach;import org.junit.jupiter.api.Test;import static org.junit.jupiter.api.Assertions.*;class CountryProcessorWithStreamsTest { @BeforeEachvoid setUp() { } @Testvoid density() { } @Testvoid maxYear() { } @Testvoid findWord() { } @Testvoid sortByPopulation() { } @Testvoid sortByComments() { } }
Since it is necessary to perform several tests on the object and these tests must be independent, it is advisable
to create the object before executing each test method. For this, we need to add a method with @BeforeEach
annotation.
The corresponding method can also be generated automatically (Generate... | SetUp Method in the context
menu). We create a new country object in the method called setUp()
. We create the appropriate field
of type CountryWithStreams
manually. The object will be used in test methods.
For convenient testing of functions related to searching and sorting, we can create getYears()
function
that retrieves an array of years from an array of censuses. This static function will use a static variable index
to fill specific array items. The variable cannot be local because we are using it in a lambda expression. We get
the following code:
package ua.inf.iwanoff.java.advanced.first;import org.junit.jupiter.api.BeforeEach;import org.junit.jupiter.api.Test;import static org.junit.jupiter.api.Assertions.*;import ua.inf.iwanoff.java.fourth.CountryWithArrayList;import ua.inf.iwanoff.java.third.Census;import java.util.Arrays;class CountryProcessorWithStreamsTest {private CountryWithArrayList country;static int index;static int [] getYears(Census[] censuses) {int [] years =new int [censuses.length]; index = 0; Arrays.stream(censuses).forEach(c -> years[index++] = c.getYear());return years; } @BeforeEachvoid setUp() { country =new CountryWithArrayList(); country.setArea(603628); country.addCensus(1959, 41869000, "The first census after World War II"); country.addCensus(1970, 47126500, "Population increases"); country.addCensus(1979, 49754600, "No comments"); country.addCensus(1989, 51706700, "The last soviet census"); country.addCensus(2001, 48475100, "The first census in the independent Ukraine"); } @Testvoid density() { assertEquals(CountryProcessorWithStreams.density(country, 1979), 82.42593120266125); } @Testvoid maxYear() { assertEquals(CountryProcessorWithStreams.maxYear(country), 1989); } @Testvoid findWord() { assertArrayEquals(getYears(CountryProcessorWithStreams.findWord(country, "census")),new int [] { 1959, 1979, 1989, 2001 }); } @Testvoid sortByPopulation() { CountryProcessorWithStreams.sortByPopulation(country); assertArrayEquals(getYears(country.getCensuses()),new int [] { 1959, 1970, 2001, 1979, 1989 }); } @Testvoid sortByComments() { CountryProcessorWithStreams.sortByComments(country); assertArrayEquals(getYears(country.getCensuses()),new int [] { 1970, 1989, 2001, 1959, 1979 }); } }
Arrays of years corresponding to correct sorting and searching results were manually prepared.
We demonstrate the features new classes in the main()
function of the CountryWithStreamsDemo
class:
package ua.inf.iwanoff.java.advanced.first;import ua.inf.iwanoff.java.fourth.CountryWithArrayList;import ua.inf.iwanoff.java.third.StringRepresentations;import ua.inf.iwanoff.java.third.Census;import static ua.inf.iwanoff.java.third.CountryDemo.setCountryData;/** * The class demonstrates data processing using the StreamAPI */ public class CountryWithStreamsDemo {/** * Displays census data that contains a certain word in comments * @param country reference to a country * @param word a word that is found */ public static void printWord(CountryWithArrayList country, String word) { Census[] result = CountryProcessorWithStreams.findWord(country, word);if (result.length == 0) { System.out.println("The word \"" + word + "\" is not present in the comments."); }else { System.out.println("The word \"" + word + "\" is present in the comments:");for (Census census : result) { System.out.println(StringRepresentations.toString(census)); } } }/** * Performs testing search methods * @param country reference to a country */ public static void testSearch(CountryWithArrayList country) { System.out.println("Population density in 1979: " + CountryProcessorWithStreams.density(country, 1979)); System.out.println("The year with the maximum population: " + CountryProcessorWithStreams.maxYear(country) + "\n"); printWord(country, "census"); printWord(country, "second"); }/** * Performs testing search methods * @param country reference to a country */ public static void testSorting(CountryWithArrayList country) { CountryProcessorWithStreams.sortByPopulation(country); System.out.println("\nSorting by population:"); System.out.println(StringRepresentations.toString(country)); CountryProcessorWithStreams.sortByComments(country); System.out.println("\nSorting comments alphabetically:"); System.out.println(StringRepresentations.toString(country)); }/** * Demonstration of the program. * @param args command line arguments (not used) */ public static void main(String[] args) { CountryWithArrayList country = (CountryWithArrayList) setCountryData(new CountryWithArrayList()); testSearch(country); testSorting(country); } }
4 Exercises
- Implement the function of obtaining the integer part of the square root of a number of type
BigInteger
. - Initialize a list of real numbers of
BigDecimal
type with an array containing a list of initial values. Find the sum of positive elements. - In the array of integer values (
BigInteger
), replace negative values with modules, positive values with zeros. Apply Stream API facilities.
5 Quiz
- Name the main components of the Java SE platform.
- What are classes
BigInteger
andBigDecimal
? - How can you create a type number
BigInteger
? - What is the difference between internal representation of
double
andBigDecimal
? - Is it possible to apply mathematical operations to numbers of types
BigInteger
andBigDecimal
? - What is the
MathContext
class used for? - What is JCF? What standard interfaces does JCF provide?
- How to create a read-only collection?
- What are the
Queue
interface methods used to add items? - Why are the methods for working with the queue implemented in two versions: with exception throwing and without exception throwing?
- What is the use of the
PriorityQueue
class? - What are the stacks used for?
- What are the standard ways to implement the stack?
- What are the advantages and features of Stream API?
- How to get a stream from a collection?
- How to get a stream from an array?
- What is the difference between intermediate and terminal operations?
- What are the streams for working with primitive types?
- What are the standard means of checking assertions in Java?
- What is unit testing?
- What is JUnit?
- How are test methods annotated in JUnit?
- How to make a logical grouping of tests?
- How to use JUnit in a development environment?