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 class hierarchy.
You should create a derived class that represents the base entity. The class created in the fourth laboratory of the course "Fundamentals of Java Programming" should be used as base class. You should use a class that represents a sequence as a list. Create derived classes in which to override the implementation of all methods related to sequence processing through the use of Stream API tools. If the class representing the second entity has no sequence processing, the class can be left unmodified.
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.
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.
The most general operations that are implemented in all container classes (except associative arrays) are declared
in the Collection<T>
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).
Work with lists was considered in more detail in Laboratory training No. 4 of the previous semester.
The following methods are declared in the Queue<E>
interface:
-
boolean add(E e)
inserts an element into the queue, throwing theIllegalStateException
if the queue is full. -
boolean offer(E e)
inserts an element into the queue and returnstrue
. If the queue is full, does not generate an exception, but returnsfalse
. -
E remove()
gets and removes a queue element. Throws an exception if the queue is empty. -
E poll()
gets and removes the queue element or returnsnull
if the queue is empty. Does not generate exceptions. E element()
gets, but does not remove, the next element. Throws an exception if the queue is empty.-
E peek()
gets, but does not remove, the queue element or returnsnull
if the queue is empty. Does not generate exceptions.
The methods add(E e)
, remove()
and element()
are used in those cases when an attempt to get an element from an empty
queue or to overflow the queue is unlikely and is not part of a normal process.
The LinkedList
class is most often used for implementation of Queue
.
The following methods are additionally declared in the Deque<E>
interface derived from Queue<E>
:
void
addFirst(E e)
inserts an element at the beginning of the queue, throwing theIllegalStateException
if the queue is full.void
addLast(E e)
inserts an element at the end of the queue, throwing theIllegalStateException
if the queue is full.boolean
offerFirst(E e)
inserts an element at the beginning of the queue and returnstrue
. If the queue is full, does not generate an exception, but returnsfalse
.boolean offerLast(E e)
inserts an element at the end of the queue and returnstrue
. If the queue is full, does not generate an exception, but returnsfalse
.E removeFirst()
gets and removes the first element of the queue. Throws an exception if the queue is empty.E removeLast()
gets and removes the last element of the queue. Throws an exception if the queue is empty.E pollFirst()
gets and removes the first element of the queue or returnsnull
if the queue is empty. Does not generate exceptions.E pollLast()
gets and removes the first element of the queue or returnsnull
if the queue is empty. Does not generate exceptions.E getFirst()
gets, but does not remove, the first element of the queue. Throws an exception if the queue is empty.E getLast()
gets, but does not remove, the last element of the queue. Throws an exception if the queue is empty.E peekFirst()
gets, but does not remove, the first element of the queue or returnsnull
if the queue is empty. Does not generate exceptions.E peekLast()
gets, but does not remove, the last element of the queue or returnsn
null
if the queue is empty. Does not generate exceptions.
Also, work with queues was considered in Laboratory training No. 4 of the previous semester.
Working with sets in JCF (the Set
interface) is based on the use of methods declared in the Collection
interface.
The Map<K,V>
interface is not derived from Collection
Methods
defined in the Map
interface
(up to and including Java version 7):
V put(K key, V value)
adds a pair or modifies the value if the key exists. Returns the previous value ornull
if the key was missing.void putAll(Map<? extends K,? extends V> m)
copies key-value pairs from another associative array.int size()
returns the number of pairs in the associative array.V get(Object key)
returns the value by key ornull
if the key is missing.V remove(Object key)
deletes pair by the specified keySet<K> keySet()
returns the set of keys of an associative array.Collection<V> values()
returns the collection of values contained in an associative array.boolean containsKey(Object key)
returnstrue
if the associative array contains the specified key.boolean containsValue(Object value)
returnstrue
if the associative array contains the specified value.Set<Map.Entry<K,V>> entrySet()
returns an array of objects that represent pairs.boolean isEmpty()
returnstrue
if the associative array is empty.void clear()
removes all pairs from the associative array.
Work with sets and associative arrays was considered in more detail in Laboratory training No. 4 of the previous semester.
The Collections
class provides a number of static methods. These are functions for creating various
special collections and so-called algorithms that are generic static methods for working with collections. The static
methods of the class were considered in more detail in Laboratory training
No. 4 of the previous semester.
2.4.2 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:
import java.util.HashMap; import java.util.Map; public class MapDemo { static void print(Integer i, String s) { System.out.printf("%3d %10s %n", i, s); } public static void main(String[] args) { Map<Integer, String> map = new HashMap<>(); map.put(1, "one"); map.put(2, "two"); map.put(7, "seven"); map.forEach(MapDemo::print); // columnwise output System.out.println(map.putIfAbsent(7, "eight")); // seven System.out.println(map.putIfAbsent(8, "eight")); // null System.out.println(map.getOrDefault(2, "zero")); // two System.out.println(map.getOrDefault(3, "zero")); // zero map.replaceAll((i, s) -> i > 1 ? s.toUpperCase() : s); System.out.println(map); // {1=one, 2=TWO, 7=SEVEN, 8=EIGHT} map.compute(7, (i, s) -> s.toLowerCase()); System.out.println(map); // {1=one, 2=TWO, 7=seven, 8=EIGHT} map.computeIfAbsent(2, (i) -> i + ""); System.out.println(map); // nothing changed map.computeIfAbsent(4, (i) -> i + ""); System.out.println(map); // {1=one, 2=TWO, 4=4, 7=seven, 8=EIGHT} map.computeIfPresent(5, (i, s) -> s.toLowerCase()); System.out.println(map); // nothing changed map.computeIfPresent(2, (i, s) -> s.toLowerCase()); System.out.println(map); // {1=one, 2=two, 4=4, 7=seven, 8=EIGHT} // Adding a new pair: map.merge(9, "nine", (value, newValue) -> value.concat(newValue)); System.out.println(map.get(9)); // nine // The text is concatenated with the previous one: map.merge(9, " as well", (value, newValue) -> value.concat(newValue)); System.out.println(map.get(9)); // nine as well } }
2.4.3 Additional JCF Features in Later JDK Versions
Let's consider some innovations that have appeared after Java 8.
In version 9, the of()
methods were added to the interfaces that represent collections, which provide
convenient creation of collections, for example:
List<String> list = List.of("one", "two", "three"); Set<String> set = Set.of("one", "two", "three"); Map<String, String> map = Map.of("first", "one", "second", "two");
In Java 10, methods for creating immutable collections have been added.
In version 21, the useful functions addFirst()
, addLast()
, getFirst()
, getLast()
, removeFirst()
and removeLast()
,
as well as reversed()
were added to collections. The putFirst()
, putLast()
and reversed()
methods
were added to SortedMap interface.
2.5 Using the Java 8 Stream API
2.5.1 Overview
Streams for work with collections, or streams of elements, data streams (Stream API) are designed for high-level processing of data stored in containers. They should not be confused with input / output streams.
Stream API's tools have been added to the standard starting with Java 8.
The Stream API is used to search, filter, transform, find the minimum and maximum values, as well as other data manipulation. An important advantage of the Stream API is the ability to work reliably and efficiently in a multithreading environment.
Streams should be understood not as a new kind of collections, but as a channel for transmission and processing of data. The stream of elements works with some data source, such as an array or collection. The stream does not store data directly, but performs transferring, filtering, sorting, etc. The actions performed by the stream do not change the source data. For example, sorting data in a stream does not change their order in the source, but creates a separate resulting collection.
You can create sequential and parallel streams of elements. Parallel streams are secure in terms of multithreading. From the available parallel stream you can get sequential one and vice versa.
To work with streams Java 8 java.util.stream
package provides a set of interfaces and classes that
provide operations on a stream of elements in the style of functional programming. The stream is represented by
an object that implements the java.util.stream.Stream
interface. In turn, this interface inherits the
methods of the general interface java.util.stream.BaseStream
.
Stream operations (methods) defined in the BaseStream
, Stream
, and other derived interfaces
are divided into intermediate and terminal. Intermediate operations receive and generate data streams and
serve to create so-called pipelines, in which a sequence of actions is performed over a sequence. Terminal operations give
the final result and thus "consume" the output stream. This means that the output stream cannot be reused
and, if necessary, must be re-created.
Intermediate operations are characterized by so-called lazy behavior: they are performed not instantaneously, but as the need arises - when the final operation is working with a new data stream. Lazy behavior increases the efficiency of work with the stream of elements.
The advantages of the approach built on data streams can be demonstrated on the problem of processing data about cities. Suppose you need to create a list of cities, leaving in it only different cities whose population exceeds one million inhabitants, sort the list by population growth and display the names of cities sorted by population.
First you need to create a City
class:
public class City { private String name; private int population; public City(String name, int population) { this.name = name; this.population = population; } public String getName() { return name; } public int getPopulation() { return population; } @Override public String toString() { return String.format("%-9s %d", name, population); } @Override public boolean equals(Object o) { return toString().equals(o.toString()); } @Override public int hashCode() { return name.hashCode(); } }
The traditional approach to solving the problem (before Java 7 inclusive) involves creating a list of cities, which will make it impossible to add the same objects. Next, you need to create a list from the set, sort by population and display the names of the cities using a loop:
Set<City> citiesSet = new HashSet<>(); citiesSet.add(new City("Kyiv", 2_967_360)); citiesSet.add(new City("Kharkiv", 1_443_207)); citiesSet.add(new City("Odesa", 1_017_699)); citiesSet.add(new City("Donetsk", 908_456)); citiesSet.add(new City("Odesa", 1_017_699)); List<City> cities = new ArrayList<>(citiesSet); for (int i = 0; i < cities.size(); i++) { if (cities.get(i).getPopulation() < 1_000_000) { cities.remove(i); } } Collections.sort(cities, new Comparator<City>() { public int compare(City a, City b) { return Integer.compare(a.getPopulation(), b.getPopulation()); } }); for (City c : cities) { System.out.println(c.getName()); }
The disadvantages of this approach are obvious. A small task requires essential error-prone code. In addition, unnecessary objects are created in memory, in particular, a set.
Lambda expressions, method references, and new interface methods that make up the Java Collection Framework have made it easier to implement code.
Set<City> citiesSet = new HashSet<>(List.of( new City("Kyiv", 2_967_360), new City("Kharkiv", 1_443_207), new City("Odesa", 1_017_699), new City("Donetsk", 908_456), new City("Odesa", 1_017_699))); List<City> cities = new ArrayList<>(citiesSet); cities.removeIf(city -> city.getPopulation() < 1_000_000); cities.sort(Comparator.comparing(City::getPopulation)); cities.forEach(city -> System.out.println(city.getName()));
By calling the removeIf()
method with a lambda expression as a parameter, we delete unnecessary data.
Next, the list is sorted according to the specified criterion (call of sort()
method). The sort condition
is defined by a method reference. The output of city names in separate lines is carried out by the forEach()
function.
With streams, everything can be done in a single statement:
Stream.of( new City("Kyiv", 2_967_360), new City("Kharkiv", 1_443_207), new City("Odesa", 1_017_699), new City("Donetsk", 908_456), new City("Odesa", 1_017_699)) .filter(city -> city.getPopulation() > 1_000_000) .distinct() .sorted(Comparator.comparing(City::getPopulation)) .map(City::getName) .forEach(System.out::println);
In this example, a stream is created using the static function of(). The array of items that the stream
will work with is created on rom a list of actual parameters. Next, the stream is filtered by calling the filter()
function.
The filtering condition is defined by a lambda expression. and further, only different data are selected (the distinct()
method)
and sorting is performed according to the specified criterion (call of sorted()
method). The sort condition
is defined by a method reference. Using the call of map()
function, a stream is created to
work with city names, after which the names are output in separate lines using the forEach()
method.
2.5.2 Basic Methods for Working with Streams
The most significant methods of the generic java.util.stream.BaseStream
interface are given in the
table (S
- type of the stream, E
- type of the element, R
- container type):
Method | Description | Note |
---|---|---|
S parallel() |
returns a parallel stream received from the current one | intermediate operation |
S sequential() |
returns a sequential stream received from the current one | intermediate operation |
boolean isParallel() |
returns true if the stream is parallel or false if
it is sequential |
|
S unordered() |
returns an unordered data stream obtained from the current | intermediate operation |
Iterator<T> iterator() |
returns an iterator for the elements of this stream | terminal operation |
Spliterator<T> spliterator() |
returns a spliterator (split iterator) for the elements of this stream. | terminal operation |
The use of stream iterators will be discussed later.
The Stream
interface extends the set of methods for working with streaming elements. It is also a
generic interface and is suitable for working with any reference types. The following are the most commonly used Stream
interface
methods:
Method | Description | Note |
---|---|---|
void forEach(Consumer<? super T> action) |
executes the code specified by the action for each element of the stream |
terminal operation |
Stream<T> filter(Predicate<? super T> pred) |
returns a stream of elements satisfying the predicate | intermediate operation |
Stream<T> sorted() |
returns a stream of elements sorted in natural order | intermediate operation |
Stream<T> sorted(Comparator<? super T> comparator) |
returns a stream of elements sorted in the specified order | intermediate operation |
|
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):
List<Integer> intList = Arrays.asList(3, 4, 1, 2); Stream<Integer> sequential = intList.stream(); Stream<Integer> parallel = intList.parallelStream();
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 cannot be negative";
In this case, the corresponding string is the exception message string.
Assert execution is usually disabled in integrated development environments. To enable assert execution in the
IntelliJ IDEA environment, use the Run | Edit Configurations menu function. In the Run/Debug Configurations window,
enter -ea
in the VM Options input line.
In these examples, the values that are checked with assert
are not entered from the
keyboard, but are defined in the program to demonstrate the correct use of assert
-
the search for logical errors, rather than checking the correctness of user input. Exceptions, conditional statements,
etc. should be used to verify the correctness of the data entered. The use of assertion validation is not allowed,
because in the future the program will be started without the -ea
option and all assertions will be
ignored. The expression specified in the statement should not include actions that are important in terms of program
functionality. For example, if the assertion check is the only place in the program from which a very important
function is called,
public static void main(String[] args) { //... assert f() : "failed"; //... } public static boolean f() { // Very important calculations return true; }
then after disabling assertions the function will not be called at all.
2.6.3 Basics of Using JUnit
In contrast to the use of diagnostic statements, which performs testing of algorithms "from the inside", unit testing provides verification of a particular unit as a whole, testing "outside" the functionality of the unit.
The most common unit testing support for Java software is JUnit, an open unit testing library. JUnit allows:
- create tests for individual classes;
- create test suits;
- create a series of tests on repeating sets of objects.
Now the JUnit 5 version is now relevant. But also a very widespread is JUnit 4 version.
To create a test, you need to create a class that needs to be tested, as well as create a public class for testing
with a set of methods that implement specific tests. Each test method must be public
, void
,
and have no parameters. The method must be marked with an annotation @Test
:
public class MyTestCase { ... @Test public void testXXX() { ... } ... }
Note: to use the @Test
and other similar annotations should be added import statements import org.junit.jupiter.api.*;
for
JUnit 5) or import
org.junit.*;
(for JUnit 4)
.
Within such methods, you can use the following assertion methods:
assertTrue(expression); // Fails the test if false assertFalse(expression); // Fails the test if true assertEquals(expected, actual); // Fails the test if not equivalent assertNotNull(new MyObject(params)); // Fails the test if null assertNull(new MyObject(params)); // Fails the test if not null assertNotSame(expression1, expression2);// Fails the test if both links refer to the same object assertSame(expression1, expression2); // Fails the test if the objects are different fail(message) // Immediately terminates the test with a failure message
Here MyObject
is a class that is being tested. These Assertion
class methods (Assert
class
methods for JUnit 4) are accessed using static import: import static org.junit.jupiter.api.Assertion.*;
(for
JUnit 5) or import static org.junit.Assert.*;
. These methods also are implemented
with an additional message
parameter of type String
, which specifies the message that
will be displayed if the test failed.
The IntelliJ IDEA provides built-in JUnit support. Suppose a new project has been created. The project contains a class with two functions (static and non-static) that should be tested:
package ua.inf.iwanoff.java.advanced.second; public class MathFuncs { public static int sum(int a, int b) { return a + b; } public int mult(int a, int b) { return a * b; } }
Within the project, we can manually create a folder, for example, tests
. Next we should set Mark
Directory as | Test Sources Root with the context menu.
Returning to the MathFuncs
class, choosing it in the code editor, through the context menu we can
generate tests: Generate... | Test.... In the dialog that opened, we select the version of the JUnit library.
The desired option is JUnit5. We can also correct the class name that we offer: MathFuncsTest
.
In most cases, the correction of this name is not needed. Then we select the names of methods that are subject to
testing. In our case, there are sum()
and mult()
. Such a code will be received:
package ua.inf.iwanoff.java.advanced.first; import static org.junit.jupiter.api.Assertions.*; class MathFuncsTest { @org.junit.jupiter.api.Test void sum() { } @org.junit.jupiter.api.Test void mult() { } }
IntelliJ IDEA indicates errors in this code (Cannot resolve symbol 'junit'). By clicking Alt+Enter, we get a hint: Add 'JUnit 5.7.0' to classpath. Taking advantage of this prompt, we add the relevant library and get the code without errors.
We can optimize the code by adding imports. We add testing of MathFuncs
class methods into MathFuncsTest
methods.
To test the work of mult()
we need to create an object:
package ua.inf.iwanoff.java.advanced.first; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class MathFuncsTest { @Test void sum() { assertEquals(MathFuncs.sum(4, 5), 9); } @Test void mult() { assertEquals(new MathFuncs().mult(3, 4), 12); } }
You can run tests to run through the Run menu. The normal completion of the process indicates no errors
during verification. If you add a code that distorts computing in the MathFuncs
class, for example
public int mult(int a, int b) { return a * b + 1; }
running tests will result in AssertionFailedError
message. You can see how many tests have been successful,
and how much it is not passed.
If some actions need to be taken before performing the test function, for example, to format the values of variables,
then such initialization is made in a separate static method, which is preceded by an annotation @BeforeAll
(@BeforeClass
in
JUnit 4):
@BeforeAll public static void setup(){ ... }
Similarly, the methods in which the actions needed after testing are preceded by@AfterAll
annotation
(@AfterClass
in JUnit 4). Methods must be public static void
.
In our example, we can create an object in advance, as well as add messages after the tests are completed:
package ua.inf.iwanoff.java.advanced.second; import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; class MathFuncsTest { private static MathFuncs funcs; @BeforeAll public static void init() { funcs = new MathFuncs(); } @Test void sum() { assertEquals(MathFuncs.sum(4, 5), 9); } @Test void mult() { assertEquals(funcs.mult(3, 4), 12); } @AfterAll public static void done() { System.out.println("Tests finished"); } }
Annotation @BeforeEach
(@Before
in JUnit 4) indicates that the method is
called before each test method. Accordingly, @AfterEach
(@After
in JUnit 4) indicates
that the method is called after each successful test method. Methods marked by these annotations should not be static.
You can also test methods that return void
. Calling such a method involves performing
an action (for example, creating a file, changing the value of a field, etc.). It is necessary to check whether
such action took place. For example:
void setValue(into value) { this.value = value; } ... public void testSetValue() { someObject.setValue(123); assertEquals(123, someObject.getValue()); }
However, as a rule, testing the simplest access methods (setters and getters) seems excessive and is not recommended.
2.7 Version Control Systems. Using the GitHub Service Repository
During the development of large projects, there is a need for additional means of control over different versions of artifacts, in particular, the source code. You must be able to access previous versions of documents to trace changes. To solve these problems, as well as to provide collective access to the project, use version control systems – special software for managing changing documents and providing access to these documents. The daily cycle of interaction with the version control system includes updating the working copy, modifying the project and fixing changes. While working on the project, you can create several branches (forks) for different solutions, and then merge the versions.
Version control systems can be centralized and distributed. In centralized systems, version storage is performed on a special server. An example of a centralized system is Subversion (SVN). Distributed systems have a local copy of the repository and ensure that the data is reconciled with the repository on the remote computer. Git is an open version control system. For open source projects, using Git is free.
GitHub is a social repository for open source projects that use Git to control source versions. To create repositories, register at https://github.com.
Both implementations of IntelliJ IDEA support integrated work with version control systems (VCS submenu of the main menu). To work with GitHub in IntelliJ IDEA, you must first install the Git system. You can download the required software at https://git-scm.com/downloads for your operating system. You can retain unchanged selected options on the installation wizard pages.
After installing the necessary software, you need to make some settings. To do this, run the program Git
Bash
, in the command line set the name and address of the user, specified earlier during registration on
GitHub:
git config --global user.name "user_name" git config --global user.email user_address@mail
A .gitconfig
file is created containing the appropriate settings.
In the IntelliJ IDEA environment you should configure Git (File | Settings..., then Version Control
| Git). You should specify the path to the git.exe
file, for example, C:\Program Files\Git\bin\git.exe
.
In the GitHub settings (Version Control | GitHub) add an account with the + button and carry
out an authorization.
In order to add a previously created project in IntelliJ IDEA, you should first allow use of VCS: VCS | Enable Version Control Integration and select Git in the list of proposed VCS. Now in the main menu, instead of the VCS submenu will appear Git. A similar result can be obtained if you use the menu function VCS | Create Git Repository... and choose a project that interests you.
Note. GIT system can be installed from IntelliJ IDEA, if in the main menu select VCS | Enable Version Control Integration, then select git in the list of different VCS. In the lower right corner of the window there is a popup menu, which will be prompted to download Git.
You can now copy the project to GitHub using the Git | GitHub | Share Project on GitHub menu function.
If you have a project that was previously added to Git and want to add new files, such as classes, IntelliJ IDEA offers to add these files to a repository through the Add File to Git dialog box.
If you have previously added to GIT, add new files, such as classes, IntelliJ IDEA offers to add these files to a repository through the Add File to Git dialog box.
After making changes to the code, you should update the project in the local repository using the menu function Git | Commit.... You can individually update files through the context menu (Git | Commit File...) . The project update should be carried out after making changes related to the execution of a certain code of code modification. The meaning of this problem should be described in the Commit Message of Commit Changes dialog box.
After updating the project in the local repository using Commit function, the changes made can also be transferred to GitHub repository using the Git | Push... menu function (or similar to the function of the context menu).
At any other time, after closing all projects, you can use the Get from VCS function on the IntelliJ IDEA start window. Next, select GitHub, specify the folder for placing the project, and then confirm its opening.
3 Sample Programs
3.1 Finding Factorials
The traditional mathematical problem of calculating the factorial for large integers causes difficulties associated
with restrictions on the size of the result. For int
type, int the maximum certain
value is 12!
, for the long
type it is 20!
For values 171!
and
more, even an approximate value cannot be obtained using double. Using BigInteger
and BigDecimal
allow
you to get factorials of large numbers. The size of the result is actually limited by the display capabilities of
the console window.
The following program calculates the factorial of integers using different types: long
, double
, BigInteger
and
BigDecimal
.
package ua.inf.iwanoff.java.advanced.first; import java.math.BigDecimal; import java.math.BigInteger; import java.util.Scanner; public class Factorial { private static long factorialLong(int n) { long result = 1; for (int i = 1; i <= n; i++) { result *= i; } return result; } public static double factorialDouble(int n) { double result = 1; for (int i = 1; i <= n; i++) { result *= i; } return result; } public static BigInteger factorialBigInteger(int n) { BigInteger result = BigInteger.ONE; BigInteger current = BigInteger.ZERO; for (int i = 1; i <= n; i++) { result = result.multiply(current = current.add(BigInteger.ONE)); } return result; } public static BigDecimal factorialBigDecimal(int n) { BigDecimal result = BigDecimal.ONE; BigDecimal current = BigDecimal.ZERO; for (int i = 1; i <= n; i++) { result = result.multiply(current = current.add(BigDecimal.ONE)); } return result; } public static void main(String[] args) { int n = new Scanner(System.in).nextInt(); System.out.println(factorialLong(n)); System.out.println(factorialDouble(n)); System.out.println(factorialBigInteger(n)); System.out.println(factorialBigDecimal(n)); } }
The execution of the program for large values of n
takes a long time.
Manual testing is not enough to verify the certainty of the results. To test these functions, we can use the capabilities
of the JUnit library. In advance, we need to create a separate test folder and mark it as the root of the tests
(Mark Directory as | Test Sources Root function of the context menu). Next, you can generate tests: the
Code | Generate... | Test...
function of main menu. In the dialog box, select the functions for which
tests should be created. In our case, these are all functions except main()
. The following code will
be generated:
package ua.inf.iwanoff.java.advanced.first; import static org.junit.jupiter.api.Assertions.*; class FactorialTest { @org.junit.jupiter.api.Test void factorialBigInteger() { } @org.junit.jupiter.api.Test void factorialBigDecimal() { } @org.junit.jupiter.api.Test void factorialDouble() { } }
Errors related to the use of JUnit tools are corrected according to the instructions that were described above.
To test the factorialDouble()
function, it is necessary to add an auxiliary compareDoubles()
function,
as well as to create constants for some factorial values. The code of the FactorialTest.java
file will
be as follows:
package ua.inf.iwanoff.java.advanced.first; import java.math.BigDecimal; import static org.junit.jupiter.api.Assertions.*; class FactorialTest { public static final String FACTORIAL_5 = "120"; public static final String FACTORIAL_50 = "30414093201713378043612608166064768844377641568960512000000000000"; public static final String FACTORIAL_500 = "12201368259911100687012387854230469262535743428031928421924135883858453731538819" + "97605496447502203281863013616477148203584163378722078177200480785205159329285477" + "90757193933060377296085908627042917454788242491272634430567017327076946106280231" + "04526442188787894657547771498634943677810376442740338273653974713864778784954384" + "89595537537990423241061271326984327745715546309977202781014561081188373709531016" + "35632443298702956389662891165897476957208792692887128178007026517450776841071962" + "43903943225364226052349458501299185715012487069615681416253590566934238130088562" + "49246891564126775654481886506593847951775360894005745238940335798476363944905313" + "06232374906644504882466507594673586207463792518420045936969298102226397195259719" + "09452178233317569345815085523328207628200234026269078983424517120062077146409794" + "56116127629145951237229913340169552363850942885592018727433795173014586357570828" + "35578015873543276888868012039988238470215146760544540766353598417443048012893831" + "38968816394874696588175045069263653381750554781286400000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000000000000" + "000000000000000"; public static final BigDecimal EPS = BigDecimal.ONE; private boolean compareDoubles(double d, String s) { return new BigDecimal(d).subtract(new BigDecimal(s)).abs().compareTo(EPS) <= 0; } @org.junit.jupiter.api.Test void factorialLong() { assertEquals(Factorial.factorialLong(5) + "", FACTORIAL_5); assertEquals(Factorial.factorialLong(50) + "", FACTORIAL_50); assertEquals(Factorial.factorialLong(500) + "", FACTORIAL_500); } @org.junit.jupiter.api.Test void factorialDouble() { assertTrue(compareDoubles(Factorial.factorialDouble(5), FACTORIAL_5)); assertTrue(compareDoubles(Factorial.factorialDouble(50), FACTORIAL_50)); assertTrue(compareDoubles(Factorial.factorialDouble(500), FACTORIAL_500)); } @org.junit.jupiter.api.Test void factorialBigInteger() { assertEquals(Factorial.factorialBigInteger(5) + "", FACTORIAL_5); assertEquals(Factorial.factorialBigInteger(50) + "", FACTORIAL_50); assertEquals(Factorial.factorialBigInteger(500) + "", FACTORIAL_500); } @org.junit.jupiter.api.Test void factorialBigDecimal() { assertEquals(Factorial.factorialBigDecimal(5) + "", FACTORIAL_5); assertEquals(Factorial.factorialBigDecimal(50) + "", FACTORIAL_50); assertEquals(Factorial.factorialBigDecimal(500) + "", FACTORIAL_500); } }
The values of 50!
and 500!
were obtained from the page https://zeptomath.com/calculators/factorial.php and
are used as a reference in our tests.
As expected, the correct values of 50!
and 500!
can be obtained only using BigInteger
and BigDecimal
.
3.2 Obtaining 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.3 "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 # 3, classes CountryWithList
and CountryWithSet
were created. And finally,
in laboratory training # 5, an abstract class CountryWithFile
was
created, as well as concrete classes CountryWithTextFile
and CountryWithDataFile
.
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: ua.inf.iwanoff.java.advanced.
third
. We add CensusWithStreams
class
to the package. It is advisable to create constructors and override 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; /** * The class is responsible for presenting the census.
* Stream API tools are used to process the sequence of words */ public class CensusWithStreams extends Census { /** * The constructor initializes the object with default values */ public CensusWithStreams() { } /** * The constructor initializes the object with the specified values * * @param year census year * @param population the population * @param comments comment text */ public CensusWithStreams(int year, int population, String comments) { setYear(year); setPopulation(population); setComments(comments); } /** * Checks either the word is contained in the text of the comment * * @param word the word we're looking for in the comment * @return {@code true} if the word is in the comment text * {@code false} otherwise */ @Override public boolean containsWord(String word) { return Arrays.stream(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 CensusWithStreamsTest { @Test void containsWord() { } }
If errors are highlighted in the generated code, we correct them as described above.
Now we can add the necessary testing, which partially reproduces the behavior of the testWord()
method
of the Census
class. The code of the file CensusWithStreamsTest.java
will be as follows:
package ua.inf.iwanoff.java.advanced.first; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class CensusWithStreamsTest { @Test void containsWord() { CensusWithStreams census = new CensusWithStreams(); census.setComments("The first census in independent Ukraine"); assertTrue(census.containsWord("Ukraine")); assertTrue(census.containsWord("FIRST")); assertFalse(census.containsWord("rain")); assertFalse(census.containsWord("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.
In the class that is responsible for the country, we can also override all methods through the use of threads. There are two ways for creating a new class:
- derived class from
CountryWithArray
; - derived class from
CountryWithList
.
The advantages of the second way are in working with a ArrayList
, which is more convenient compared
to a regular array, and even more effective in the case of adding new elements. The disadvantage of the second way
compared to the first one is the need to provide direct access to the list, which in our case involves adding methods
to the CountryWithList
class that was created earlier. Making changes to the base classes is generally
not desirable, but if you must do so, you should not change the set of public methods of the class.
Choosing the second way, we should keep the set of public methods unchanged. To ensure this, we can add protected
methods to the CountryWithList
class:
protected List<Census> getList() { return list; } protected void setList(List<Census> list) { this.list = list; }
In addition to the sortByPopulation()
, sortByComments()
and maxYear()
methods
that work with the sequence, the methods for accessing the list should be overridden, since it should be monitored
so that only references of type CensusWithStreams
can be put into list. These methods are setCensus()
, addCensus()
in
two variants and setCensuses()
. The source code of the CountryWithStreams
class will be as follows:
package ua.inf.iwanoff.java.advanced.first; import ua.inf.iwanoff.java.third.Census; import ua.inf.iwanoff.java.fourth.CountryWithList; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; /** * A class to represent the country in which the census is conducted.
* Stream API tools are used to process the sequences */ public class CountryWithStreams extends CountryWithList { /** * Sets a reference to the new census inside the sequence position * by the indicated index. * * @param i number (index) of the position in the sequence * @param census reference to the new census */ @Override public void setCensus(int i, Census census) { if (census instanceof CensusWithStreams) { super.setCensus(i, census); } else { new RuntimeException(); } } /** * Adds a reference to the new census at the end of the sequence * * @param census reference to the new census * @return {@code true} if the reference was successfully added * {@code false} otherwise */ @Override public boolean addCensus(Census census) { if (census instanceof CensusWithStreams) { return super.addCensus(census); } return false; } /** * Adds a reference to the new census at the end of the sequence. * * @param year census year * @param population the number of the population * @param comments comment text * @return {@code true} if the reference was successfully added * {@code false} otherwise */ @Override public boolean addCensus(int year, int population, String comments) { return super.addCensus(new CensusWithStreams(year, population, comments)); } /** * Rewrites data from an array of censuses to a sequence * * @param censuses an array of censuses */ @Override public void setCensuses(Census[] censuses) { if (Arrays.stream(censuses).allMatch(c -> c instanceof CensusWithStreams)) { super.setCensuses(censuses); } else { new RuntimeException(); } } /** * Sorts the sequence of censuses by population */ @Override public void sortByPopulation() { setList(getList().stream().sorted().toList()); } /** * Sorts the sequence of censuses alphabetically by comment */ @Override public void sortByComments() { setList(getList().stream().sorted(Comparator.comparing(Census::getComments)).toList()); } /** * Finds and returns the year with the maximum population * * @return year with maximum population */ @Override public int maxYear() { return getList().stream().max(Comparator.comparing(Census::getPopulation)).get().getYear(); } /** * Creates and returns an array of censuses with the specified word in comments * * @param word the word to search for * @return an array of records with the specified word in comments */ @Override public Census[] findWord(String word) { return getList().stream().filter(c -> c.containsWord(word)).toArray(Census[]::new); } /** * Creates and returns a list of stings with data * about the country and about all population censuses * * @return a list of strings with country data */ public List<String> toListOfStrings() { ArrayList<String> list = new ArrayList<>(); list.add(getName() + " " + getArea()); Arrays.stream(getCensuses()).forEach(c -> list.add( c.getYear() + " " + c.getPopulation() + " " + c.getComments())); return list; } /** * Reads data about the country from the list of strings and puts data into the appropriate fields * * @param list a list of strings with country data */ public void fromListOfStrings(List<String> list) { String[] words = list.get(0).split("\s"); setName(words[0]); setArea(Double.parseDouble(words[1])); list.remove(0); list.stream().forEach(s -> { String[] line = s.split("\s"); addCensus(Integer.parseInt(line[0]), Integer.parseInt(line[1]), s.substring(s.indexOf(line[2]))); }); } /** * Creates and returns an object of type CountryWithStreams for testing * @return an object of type CountryWithStreams */ public static CountryWithStreams createCountryWithStreams() { CountryWithStreams country = new CountryWithStreams(); country.setName("Ukraine"); country.setArea(603628); country.addCensus(1959, 41869000, "The first postwar census"); 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"); return country; } }
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 CensusWithStreams
.
It is advisable to choose methods sortByPopulation()
, sortByComments()
, maxYear()
and findWord()
.
We will get the following code:
package ua.inf.iwanoff.java.advanced.first; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class CountryWithStreamsTest { @Test void sortByPopulation() { } @Test void sortByComments() { } @Test void maxYear() { } @Test void findWord() { } }
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 ua.inf.iwanoff.java.second.Census; import java.util.Arrays; import static org.junit.jupiter.api.Assertions.*; class CountryWithStreamsTest { private CountryWithStreams country; static int index; static int[] getYears(Census[] censuses) { int[] years = new int[censuses.length]; index = 0; Arrays.stream(censuses).forEach(c -> years[index++] = c.getYear()); return years; } @BeforeEach void setUp() { country = new CountryWithStreams(); country.addCensus(1959, 41869000, "The first postwar census"); country.addCensus(1970, 47126500, "Population increases"); country.addCensus(1979, 49754600, "No comments"); country.addCensus(1989, 51706700, "The last soviet census"); country.addCensus(2001, 48475100, "The first census in the independent Ukraine"); } @Test void sortByPopulation() { country.sortByPopulation(); assertArrayEquals(getYears(country.getCensuses()), new int[] { 1959, 1970, 2001, 1979, 1989 }); } @Test void sortByComments() { country.sortByComments(); assertArrayEquals(getYears(country.getCensuses()), new int[] { 1970, 1989, 2001, 1959, 1979 }); } @Test void maxYear() { assertEquals(country.maxYear(), 1989); } @Test void findWord() { assertArrayEquals(getYears(country.findWord("census")), new int[] { 1959, 1979, 1989, 2001 }); } }
Arrays of years corresponding to correct sorting and searching results were manually prepared.
We test the program in the main()
function of the Program
class:
package ua.inf.iwanoff.java.advanced.first; /** * The class demonstrates data processing using the StreamAPI */ public class Program { /** * Demonstration of the program. * @param args command line arguments (not used) */ public static void main(String[] args) { createCountryWithStreams().createCountry().testCountry(); } }
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 algorithms does the class provide
Collections
class? - 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?