Laboratory Training 5
Reflection. Creation of Database Applications
1 Training Tasks
1.1 Creating a Database Console Application
Create a database console application for working with the entities of individual tasks of previous laboratory trainings.
Domain entity data must be stored in a relational database. The MySQL DBMS should be used (or another DBMS, upon agreement with the teacher). In the console application, you should create two database tables, respectively for the first and second entity of the individual task (first and second table).
After creating the tables, you must perform the following actions:
- import into the database from a JSON file;
- display of both tables;
- search by features defined in the third laboratory training of the course "Fundamentals of Java Programming";
- performing sorting according to the features defined in the fourth laboratory training of the course "Fundamentals of Java Programming";
- adding a new record to the table;
- deleting certain records from the table;
- export from the database to a JSON file.
1.2 View All Class Fields
Create a console application in which the user enters the class name and receives information about all the fields (including private and protected).
1.3 Creating a Database GUI Application (Advanced Task)
Use JavaFX tools for creation of graphical user interface application in which the data of individual tasks of previous laboratory works is processed. Data about domain area entities is stored in a relational database. The main window should contain a menu in which you need to implement the functions listed in task 1.1. Adding a new record to the table should be implemented in a separate window with control of the entered data.
In the left part of the window, you should place the display area for the search results, as well as the buttons that ensure the execution of the main functions of the program. Tables for data display should be placed in the middle part of the window.
1.4 Obtaining Data about Method Annotations (Advanced Task)
Create a console program in which the user enters the name of a class method and receives information about all
the annotations with RUNTIME
retention policy with which this method is marked.
2 Instructions
2.1 Using RTTI
The Runtime Type Identification mechanism (RTTI) allows you to determine the type of object at runtime. Most object-oriented programming languages support the RTTI mechanism. This mechanism, as a rule, applies to so-called polymorphic types (classes or structures that provide virtual functions). RTTI allows to determine the exact type of object by reference (or by pointer for languages that support pointers). Due to the extension of type compatibility rules for types, a reference to a base type can actually point to objects of derived types, which is the basis of polymorphism. In some cases, you must explicitly get the actual object type, or check whether the type of object is the one that interests you.
In Java, all reference types are polymorphic. RTTI is implemented through information about the type that is stored
in each object. The instanceof
keyword allows you to check whether an object is an instance
of a particular type. The expression
object instanceof class
returns the value of the boolean
type that can be used before calling class-specific methods:
if(x instanceof SomeClass) ((SomeClass)x).someMethod();
Without this check, the type cast leads to throwing ClassCastException
.
In the following example, depending on the number entered by the user, objects of different types are created:
package ua.inf.iwanoff.java.advanced.fifth; import java.util.Scanner; class First { void first() { System.out.println("First"); } } class Second { void second() { System.out.println("Second"); } } public class InstanceOf { public static void main(String[] args) { try { Object o; int n = new Scanner(System.in).nextInt(); switch (n) { case 1: o = new First(); break; case 2: o = new Second(); break; default: return; } if (o instanceof First) { ((First) o).first(); } if (o instanceof Second) { ((Second) o).second(); } } catch (Exception e) { e.printStackTrace(); } } }
2.2 Using Class Loaders
One of the main features of the Java platform is a model of dynamic class loading. Class loaders (classloaders) are special objects that allow you to load class code while the program is running.
Dynamic class loading in Java has several features:
- delayed (lazy) loading: classes are loaded only if necessary, which saves resources and improve load distribution;
- code validation (type usage checking) is performed during class load, not during code execution;
- the ability to create custom loaders that fully control the process of obtaining the requested class;
- the ability to control the reliability and security of the code through the so-called security attributes.
Each loader has its own namespace associated with classes created by it. Classes created by two different loaders based on a common bytecode are treated as different.
When loading a class, an object of type java.lang.Class
is created. The Class
type is
a class whose instances are classes, interfaces, and other types when executing a Java application. Primitive Java
types (boolean
, byte
, char
, short
, int
, long
, float
, double
)
and void
are also represented as objects of Class
type. A Class
object
can be obtained for existing types (including primitive ones) using the class
keyword:
Class<?> c1 = String.class; Class<?> c2 = double.class;
As can be seen from the previous example, the Class
class is generic. Most often it is used with parameter <?>
(particular
type is determined automatically).
You can also get a Class
object for a previously created reference type object:
Integer i = new Integer(100); Class<?> c3 = i.getClass();
The bject of Class
type provides a variety of information about the type that can be used at runtime
and is the basis of reflection. These features will be considered below.
Further, the term "load classes" will mean loading different types.
There are three types of class loaders in Java:
- Bootstrap class loader loads basic classes that are usually found in jar files in the
jre/lib
directory; bootstrap class loader is not available programmatically; - Extension class loader loads classes of different extensions packages that are usually contained in
jre/lib/ext
; - System class loader implements a standard class loading algorithm from the directories and JARs listed
in the
CLASSPATH
variable.
An abstract class java.lang.ClassLoader
is used to create the types of objects responsible for loading
classes. Each Class
object has a reference to ClassLoader
. For each class, the current
loader is defined. In order to get the loader that was used for loading the SomeClass
class, you need
to use the SomeClass.class.getClassLoader()
method. Java allows you to create custom class loaders.
When loading classes, a special hierarchy of loaders (bootstrap class loader / extension class loader / system class loader) is used. The loading algorithm is usually as follows:
- search for previously loaded classes (so that the class was not loaded twice);
- if the class was not loaded, its loading is delegated to the parent loader;
- If the parent loader could not load the requested class, the current loader tries to load the required class itself.
It should be noted that all "system" classes (java.lang.Object
, java.lang.String
,
etc.) are loaded only by the base bootstrap loader. Therefore, getClassLoader()
returns null
for
these classes.
2.3 Reflection
The previously discussed RTTI mechanism is based on information that can be obtained during compilation. Along with RTTI, Java provides a more complex and powerful mechanism, so-called reflection. This mechanism uses information that may not be available during compilation.
Reflection is a mechanism that allows the program to track and modify its own structure and behaviour at runtime.
Direct information about the type stored in an object of type java.lang.Class
. The java.lang.reflect
package
contains classes that provide information about classes and objects and allow access to the members of classes and
interfaces.
With capabilities of Class
type, you can create a Class
object with a specific name.
To obtain a Class
object, a static method forName()
is used.
String name = "SomeClass"; Class c = Class.forName(name);
In the example below, the name of the required class is entered from the keyboard, and then the object is created
using the newInstance()
function:
package ua.inf.iwanoff.java.advanced.fifth; import java.util.Scanner; class One { void one() { System.out.println("one"); } } public class NewInstance { public static void main(String[] args) { try { String name = new Scanner(System.in).next(); Object o = Class.forName(name).newInstance(); if (o instanceof One) { ((One) o).one(); } } catch (Exception e) { e.printStackTrace(); } } }
The Class
object contains, in particular, the type name information that could be obtained in the
example above:
System.out.println(o.getClass().getName());
Information about types can be presented in a convenient form using the functions getSimpleName()
or getCanonicalName()
.
A Class
object can also be obtained for interfaces and even for anonymous classes. The isInterface()
method
returns true
for interfaces, and the isAnonymousClass()
method returns true
for
anonymous classes. You can also get an array of nested types, and so on.
The getSuperclass()
method returns the base class. For the Object
class, this method
returns null
. The getInterfaces()
method returns an array of implemented interfaces
(also objects of Class
type).
You can get a list of class methods - an array of java.lang.reflect.Method
class objects. This array
will contain references to public methods of this class and all its ancestors:
for (int i = 0; i < o.getClass().getMethods().length; i++) { System.out.println(o.getClass().getMethods()[i].getName()); }
The getMethod()
function allows you to get some method by its name.
The getDeclaredMethods()
function allows you to get a list of all methods (public, protected, package,
and private), but only defined in this class (not in the base classes). Similarly, the getDeclaredMethod()
function
allows you to get any method by name.
You can get information about the class in which method was declared ((getDeclaringClass()
), the method
name (getName()
), as well as the result type (getReturnType()
). You can also get an array
of parameter types (objects of type Class<?>
) using the getParameterTypes()
method.
The array received using the getExceptionTypes()
function contains types of possible exceptions. The
overridden toString()
function returns the full signature of the method with the list of exceptions.
Information about method modifiers can be obtained using getModifiers()
function. This function returns
an integer that can be used, for example, for the charAt()
function of the String
class:
Class<?> c = String.class; String methodName = "charAt"; Method m = c.getDeclaredMethod(methodName, int.class); // a method with an integer parameter int mod = m.getDeclaringClass().getModifiers(); if (Modifier.isAbstract(mod)) { System.out.print("abstract "); } if (Modifier.isPublic(mod)) { System.out.print("public "); } if (Modifier.isPrivate(mod)) { System.out.print("private "); } if (Modifier.isProtected(mod)) { System.out.print("protected "); } if (Modifier.isFinal(mod)) { System.out.print("final "); }
Method can be invoked using the function invoke()
. The first parameter of this function is the reference
to the class object for which the method is called. For static methods, this is null
. Then actual
parameters are specified. Since several methods may differ only in the list of parameters, you must specify the
types of parameters (an array of objects of type Class
). If the list of parameters is empty, this array
can be omitted.
For example, you can call a static function without the parameters for the class specified by the name:
method = Class.forName(className).getMethod(methodName); method.invoke(null);
In the example below, a non-static method is called:
package ua.inf.iwanoff.java.advanced.fifth; import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; public class ListSize { public static void main(String[] args) throws Exception { List<String> list = Arrays.asList("one", "two"); Method method = Class.forName("java.util.List").getMethod("size"); System.out.println(method.invoke(list)); } }
Getting information about fields is carried out similarly. The java.lang.reflect.Field
class represents
data about the field: name (getName()
), type (getType()
), the class in which the field
is defined (getDeclaringClass()
), access modifiers (getModifiers()
), etc. To get the value
stored in the field, you can apply the get()
method, which returns the result of the Object
type.
Using the set()
method, you can set a new value.
If you want to access private or protected fields, but defined exactly in this class, getDeclaredField()
(by
name) and getDeclaredFields()
methods (returns an array of fields) are used.
The reflection mechanism allows you to bypass the restrictions on access to the elements of the class. A private
method obtained with getDeclaredMethod()
or as an array item returned by getDeclaredMethods()
cannot
be directly called. However, to call it, you can allow access using the function setAccessible()
sending true
as
actual argument. You can also get private fields. The following example shows the ability to work with private class
members:
package ua.inf.iwanoff.java.advanced.fifth; import java.lang.reflect.Field; import java.lang.reflect.Method; class TestClass { private int k = 0; private void testPrivate() { System.out.println("Private method. k = " + k); } } public class AccessTest { public static void main(String[] args) throws Exception { // Get a class: Class<?> c = Class.forName("ua.inf.iwanoff.oop.fifth.TestClass"); // Create an object: Object obj = c.newInstance(); // Access to the private field: Field f = c.getDeclaredField("k"); f.setAccessible(true); f.set(obj, 1); // Access to the private method: Method m = c.getDeclaredMethod("testPrivate"); m.setAccessible(true); m.invoke(obj); } }
The reflection mechanism should be used with caution, since it provides the ability to bypass encapsulation.
2.4 Creating and Using Annotations
2.4.1 The concept of Annotation. Standard Annotations
Starting with JDK version 1.5, Java supports the annotation mechanism. Annotation is a special tag added to any description. In the source code, annotations begin with the @ symbol. Annotations are often referred to as so-called metadata (data about data), but in the strict sense of the word, they are data about source code. Adding annotations to the code itself does not affect the execution of the program. Annotations are used by external tools – compiler, class generators, visual editors, etc. In other words, a special source code handler is required that uses the information provided by the annotations.
Annotations can be without parameters (properties), for example @Override
, such that require the specification
of any property, @SuppressWarnings("resource")
or require the specification of several properties
in the form of key/value pairs, for example, @GenericGenerator (name="increment", strategy = "increment")
(the
last example refers to the Hibernate library).
Annotations can differ in their lifecycle, which is determined by the so-called "annotation retention policies".
According to their life cycle, annotations can be divided into three groups, defined by the java.lang.annotation.RetentionPolicy
enumeration:
SOURCE
: annotations are only stored in the source file and are discarded at compile timeCLASS
: annotations are stored in the.class
file at compile time, but not available at runtimeRUNTIME
: annotations are stored in the.class
file and remain available to the JVM at runtime.
The use of annotations @Override
, @SuppressWarnings
and @Deprecated
was
mentioned earlier. These annotations are used by the compiler. To
them, you can also add the @SafeVarargs
annotation that appeared in Java 7. This annotation is used
before generic functions with a variable number of parameters and can be used instead of @SuppressWarnings("unchecked")
.
For example, when compiling such a function
static <T>void f(T... a) { for (T t : a) { System.out.println(t); } }
the compiler will show a warning related to potential dynamic memory pollution: "Type safety: Potential heap pollution
via varargs parameter a". The easiest way to suppress this message is to use @SafeVarargs
before the
method.
2.4.2 Custom Annotations
To create your own annotation, use the @interface
keyword, after which the name of the
future annotation is defined. In the body of the annotation, you can place declarations of functions that return
values of primitive types or String
type. Example:
public @interface NewAnnotation { int firstValue(); String secondValue() default "second"; }
Before definition of the annotation, it is advisable to specify the target object and the "retention policy", for example:
package ua.inf.iwanoff.java.advanced.fifth; import java.lang.annotation.*; @Target(value=ElementType.TYPE) @Retention(value=RetentionPolicy.RUNTIME) public @interface NewAnnotation { int firstValue(); String secondValue() default "second"; }
As can be seen from the example, the corresponding annotations require the java.lang.annotation
package.
The target object is the element of source code to which the instruction will refer. The java.lang.annotation.ElementType
enumeration,
in addition to TYPE
constant (the annotation is placed before the description of the types), provides FIELD
(before
the definition of the field), METHOD
(before the definition of the method), PARAMETER
(before
the description of the function parameter), ANNOTATION_TYPE
(before the description of another annotation)
and also CONSTRUCTOR
, LOCAL_VARIABLE
and PACKAGE
constants.
Methods in the body of the annotation description define properties: keys, the values of which are specified
when using the annotation. Methods inside an annotation cannot have parameters. Their names always start with a
lowercase letter The function result types are the standard primitive types and String
type.
The default
modifier indicates a fixed value, which makes explicit definition of
the corresponding value optional. The absence of a default
modifier makes
the property mandatory: its value must be specified in parentheses when using the annotation.
We can give an example of using our annotation:
package ua.inf.iwanoff.java.advanced.fifth; @NewAnnotation(firstValue=10, secondValue="A") public class SomeAnnotatedClass { }
Now we can use this information at runtime. For this, reflection mechanisms are used. It is necessary to create an external annotation handler. First, we load the class, for example by name. Next, we get its annotations and check the presence of our annotation. If there is an annotation, we use values of its properties:
package ua.inf.iwanoff.java.advanced.fifth; public class NewAnnotationProcessor { public static void main(String[] args) { try { Class<?> cls = Class.forName("ua.inf.iwanoff.java.advanced.fifth.SomeAnnotatedClass"); if (cls.isAnnotationPresent(NewAnnotation.class)) { NewAnnotation ann = cls.getAnnotation(NewAnnotation.class); System.out.println(ann.firstValue()); System.out.println(ann.secondValue()); } else { System.out.println("No such annotation!"); } } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
In addition to the specified methods (isAnnotationPresent()
and getAnnotation()
), the Class<>
also
supports methods getAnnotations()
(returns an array of references to java.lang.annotation.Annotation
)
and isAnnotation()
(checks whether the type itself is an annotation).
The java.lang.annotation.Annotation
class provides an annotationType()
function (returns
a reference to the annotation type as Class<>
object).
To create a new annotation in the IntelliJ IDEA environment, you can use the File | New | Java Class menu
function and select Annotation from the list. In the created code, we add the necessary annotations before
our new annotation using the IntelliSence technology (type @
, then Ctrl-space and select the necessary
annotation).
2.5 Using Annotations in Java Libraries and Frameworks
2.5.1 Overview
Annotations provide a powerful mechanism for extending the capabilities of Java's syntax. Practically, annotations are used everywhere, where any manipulation of the source code is expected.
Numerous frameworks of programs are built on the use of annotations. In particular, annotations are heavily used in the Spring Framework. The Enterprise JavaBeans 3.0 specification uses annotations to provide freedom in naming methods. The Java Persistence API specification uses annotations to provide object-relational mapping. Annotations are also used by JUnit testing tools.
To work with some of these libraries in the IntelliJ IDEA environment, it is necessary to enable annotation processing (File | Settings, then Build, Execution, Deployment | Compiler | Annotation Processors, turn on Enable annotation processing)
2.5.2 Using the Lombok Library
As an example, we can give the Project Lombok library, which can be used to reduce boilerplate code. Lombok can automatically generate getters and setters, constructors, hash code generation, equivalence checking, string representation, and more using annotations. The library was named after the Indonesian island Lombok. Translated from Indonesian, Lombok means "chili pepper".
For the application to work with the Project Lombok library, the following dependency should be added to
the pom.xml
file:
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.26</version> </dependency>
The library version may change.
Then you should reload the Maven project (being in the window with the file pom.xml, you need to select Maven | Reload project).
The source code used by Lombok is not valid Java code. Therefore, to work with Project Lombok in the IDE, you need to install the necessary plugin. Starting with IntelliJ IDEA 2020.3, this plugin is included automatically.
Suppose a class City
is created, preceded by several annotations:
package ua.inf.iwanoff.java.advanced.fifth; import lombok.*; @Getter @Setter @AllArgsConstructor @NoArgsConstructor @ToString public class City { private String name; private int population; }
In fact, a class is created that has a constructor with no parameters, a constructor with parameters, getters
and setters for the fields, and an overridden toString()
function. All this will appear after processing
the code and performing the actions provided by the annotations. Therefore, you can use all these methods in
the main()
function:
package ua.inf.iwanoff.java.advanced.fifth; public class Main { public static void main(String[] args) { City Kharkiv = new City("Kharkiv", 1419000); System.out.println(Kharkiv); City Kyiv = new City(); Kyiv.setName("Kyiv"); Kyiv.setPopulation(2884000); System.out.println(Kyiv.getName() + " " + Kyiv.getPopulation()); } }
Note. Before running the program, the message "Lombok requires enabled annotation processing" will appear in the IntelliJ IDEA window. In order for the message not to appear every time the program is started, it is necessary to press the button with the text "Enable annotation processing".
The code of the City
class can be simplified by applying the annotation @Data
, which
actually includes several annotations (@ToString
, @EqualsAndHashCode
, @Getter
@Setter
and @RequiredArgsConstructor
).
Annotations @AllArgsConstructor
@NoArgsConstructor
should be added separately. Now our code
will look simpler:
package ua.inf.iwanoff.java.advanced.fifth; import lombok.*; @Data @AllArgsConstructor @NoArgsConstructor public class City { private String name; private int population; }
There is also annotation @Value
, this is an option of @Data
that generates a class
without the possibility of changing data. In fact, we get what the record
type offers
in the latest versions of Java. After using the annotation before the City
class
package ua.inf.iwanoff.java.advanced.fifth; import lombok.*; @Value @AllArgsConstructor public class City { private String name; private int population; }
some actions on the object will not be able to be performed:
City Kyiv = new City(); // Error Kyiv.setName("Kyiv"); // Error Kyiv.setPopulation(2884000); // Error
There are also annotations for more complex cases, such as @Cleanup
which generates a try-with-resources construct,
@Builder
, which creates a "builder" of objects, @Log
, which simplifies the logging
process, and so on.
2.6 Java tools for working with databases
2.6.1 Overview
Working with databases is one of the most widespread tasks that occur at almost all levels of the use of modern information technologies. The most common type of databases are relational databases. Next, work with relational databases will be considered.
There are two approaches to implementing database applications, depending on the task at hand:
- creation of relational tables that reflect the relationship between classes and objects that were created earlier;
- designing model classes that reflect entities stored in previously created relational tables.
The Java platform offers its solutions for working with databases at different levels:
- JDBC, a universal low-level solution;
- SQL frameworks such as jOOQ;
- full-scale Object Relational Mapping (ORM), such as Hibernate or any other JPA (Java Persistence API) implementation.
jOOQ Object-Oriented Querying is a Java database mapping software library that provides objects for constructing SQL records in code from classes generated from the database schema.
Object-relational mapping allows you to work with objects of classes that represent entities that are stored in the database.
2.6.2 JDBC Basics
JDBC (Java Database Connectivity) is a standard database application programming interface (API). JDBC
provides means of organizing access of application programs to databases, which do not depend on a specific DBMS
implementation. The JDBC API is part of the Java SE platform. JDBC API classes and interfaces are contained injava.sql
and javax.sql
packages.
JDBC provides access to a wide range of DBMS, as well as any tabular data, for example, to spreadsheets.
A set of JDBC classes and interfaces, included in the java.sql
package, provides the ability to perform the following
actions:
- establishing a connection to the database;
- creation and transmission of SQL queries and commands to the database;
- processing results.
The JDBC architecture involves the use of the following parts:
- a Java application that uses JDBC facilities;
- driver manager #8211; a JDBC component that distributes client calls to relevant databases (JDBC Driver Manager);
- a JDBC driver that supports a connection session with a specific database (JDBC Driver),
- Database.
The JDBC Driver Manager ensures that the appropriate driver is used to access each data source. Below is a diagram that shows the location of the driver manager in relation to the JDBC driver and the Java application:
The JDBC API supports both two-tier and three-tier database access processing models.
If a JDBC driver is provided by the database manufacturer, a Java application can access it through the JDBC mechanism without intermediaries. This variant of JDBC allows you to work in a two-tier architecture:
The JDBC API provides a number of interfaces and classes.
TheDriver
interface is responsible for communicating with the database server. The JDBC driver can be
loaded as follows:
Class.forName("com.mysql.jdbc.Driver");
The driver is automatically registered during download. You cannot change the DBMS without recompiling the program.
The DriverManager
class provides identification, selection and loading of the database driver depending
on the specified JDBC URL (Uniform Resource Locator), as well as the creation of new connections
to the database using the getConnection()
method. The URL that JDBC uses to identify
the database has the following syntax:
jdbc:<subprotocol>:<subname>
Here <subprotocol>
is the type of database system used, <subname>
depends on the subprotocol, for example:
jdbc:sqlite:MyDataBaseName
The following syntax is recommended for a network URL:
//hostname:port/subsubname
Examples:
jdbc:mysql://localhost/MyDataBaseNamejdbc:mysql://localhost:3306/MyDataBaseName
The Connection
interface provides a wide range of methods of communication with the database, from
the execution of SQL queries to transaction processing.
Interfaces Statement, PreparedStatement
and CallableStatement
define the methods of transferring
SQL commands to the database. The Statement
object created by the createStatement()
method
of Connection
class, is used when the SQL command is created to be executed once; the PreparedStatement
object
created by the prepareStatement(String sql)
method of Connection
object allows you to
save the SQL command in a precompiled form for further repeated use, the CallableStatement
object allows
to represent the SQL command as a call to stored procedures.
The Statement
interface provides three different methods of executing SQL commands:
executeQuery(String sql)
for queries, the result of which is a set of values, for example forSELECT
queries;executeUpdate(String sql)
for execution of commands likeINSERT
,UPDATE
orDELETE
, as well as for commandsCREATE TABLE
andDROP TABLE
;execute(String sql)
is used if SQL statements return more than one data set.
The PreparedStatement
interface derived from Statement
and the CallableStatement
interface
derived from PreparedStatement
, have their versions of the methods executeQuery()
, executeUpdate()
and execute()
.
The ResultSet
interface provides a resulting dataset generated according to the executed
SQL command. The resulting dataset is a table with column headers and values corresponding to the SQL query. Data
in the rows of such a table is accessed using getters, which organize access to the columns of the current row.
To go to the next line, the next()
method is used. Example:
java.sql.Statement stmt = conn.createStatement(); ResultSet r = stmt.executeQuery("SELECT a, b, c FROM Table"); while (r.next()) { int i = r.getInt("a"); String s = r.getString("b"); double d = r.getDouble("c"); System.out.printf(i + "\t" + s + "\t" + d); }
The java.sql
package also includes classes Date
(SQL-type DATE
), Numeric
(SQL-types
DECIMAL
and NUMERIC
) and Time
(SQL-type TIME
).
2.6.3 Organization of DBMS Interaction with the Application
According to the method of access, databases can be file-server, client-server and embedded.
In file-server DBMS, data is stored in the form of separate files on the server, the DBMS is located on the client's computer, so data processing takes place on the client's side. DBMS access to data is carried out through the network, which has quite heavy loads. Such an architecture is considered insufficiently reliable and secure. File server databases are Microsoft Access, Paradox, dBase, FoxPro.
The client-server DBMS together with the database is located on the server. In this case, data processing takes place behind the server, the local network is potentially less loaded. Collective use of such a database is possible, and centralized management is ensured. As a rule, special installation is required. This architecture has high reliability and security. Examples of such databases: Oracle, MS SQL Server, MySQL, IBM DB2, PostgreSQL, Firebird, H2.
An embedded DBMS can be distributed as a component of some software product. It works on the same computer as the application program, does not require a self-installation procedure, and is implemented as a pluggable library. The built-in DBMS is intended for local storage of program data. They are used when access from many computers is not required. It is characterized by high speed and low memory consumption, but the maximum size of the base is relatively small. Reliability and security are quite high, but such solutions are inferior to client-server DBMS in terms of reliability. Examples of embedded DBMS: SQLite, H2, BerkeleyDB, Firebird Embedded, Microsoft SQL Server Compact.
Some DBMSs exist in both client-server and embedded versions. These are, for example, databases Firebird (Firebird Embedded), MySQL (MySQL Embedded Server Library), H2, etc.
3 Sample Programs
3.1 Obtaining Data about Class Methods
Suppose we need to create a program that displays data about methods of a class: name, resulting type and types of parameters. The program will look like this:
package ua.inf.iwanoff.oop.fifth; import java.lang.reflect.Method; import java.util.Scanner; public class ShowAllMethods { public static void main(String[] args) { System.out.print("Enter the full name of the class: "); String className = new Scanner(System.in).next(); try { Class<?> c = Class.forName(className); for (Method m : c.getMethods()) { System.out.printf("Name: %s Resulting type: %s%n", m.getName(), m.getReturnType().getCanonicalName()); for (Class<?> type : m.getParameterTypes()) { System.out.printf(" Parameter type: %s%n", type.getCanonicalName()); } } } catch (ClassNotFoundException e) { System.err.println("Error entering class name!"); } } }
3.2 Calculating the Results of Math Class Functions
In the following example, functions getMethod()
and invoke()
are used to call static
methods of java.lang.Math
class (only methods with one parameter can be called):
package ua.inf.iwanoff.oop.fifth; import java.lang.reflect.Method; import java.util.InputMismatchException; import java.util.Scanner; public class SomeFunction { private Method method = null; private String methodName = null; private String className = null; public SomeFunction(String className, String methodName) { this.className = className; this.methodName = methodName; } public double getY(double x) throws Exception { if (method == null) { // Create an object of type Method, if it was not created before. // The method must accept one double argument: method = Class.forName(className).getMethod(methodName, double.class); } // Call the method: return (Double) method.invoke(null, x); } public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("Enter the name of the static class Math method:"); SomeFunction someFunction = new SomeFunction("java.lang.Math", scanner.next()); System.out.println("Enter the beginning, end of the interval, and step:"); try { double from = scanner.nextDouble(); double to = scanner.nextDouble(); double step = scanner.nextDouble(); for (double x = from; x <= to; x += step) { System.out.println(x + "\t" + someFunction.getY(x)); } } catch (InputMismatchException e) { System.out.println("Error entering data"); } catch (Exception e) { System.out.println("Error calling function"); } } }
3.3 Output of information about methods marked with a certain annotation
In the following example, several classes are loaded by name and methods without parameters marked with the annotation @ToInvoke
are
called. We define the annotation:
package ua.inf.iwanoff.java.advanced.fifth; import java.lang.annotation.*; @Target(value = ElementType.METHOD) @Retention(value = RetentionPolicy.RUNTIME) public @interface ToInvoke { }
In the program, we create two classes with methods marked with an annotation @ToInvoke
and call these
methods:
package ua.inf.iwanoff.java.advanced.fifth; import java.lang.reflect.*; class ATest { public void aFirst() { System.out.println("aFirst launched"); } @ToInvoke public void aSecond() { System.out.println("aSecond launched"); } public void aThird() { System.out.println("aThird launched"); } } class BTest { @ToInvoke public void bFirst() { System.out.println("bFirst launched"); } public void bSecond() { System.out.println("bSecond launched"); } @ToInvoke public void bThird() { System.out.println("bThird launched"); } } public class LaunchIfAnnotated { static void invokeFromClass(String className) { System.out.println("---------- class " + className + " ----------"); try { Class<?> cls = Class.forName(className); Method[] methods = cls.getMethods(); for (Method m : methods) { if (m.isAnnotationPresent(ToInvoke.class)) { m.invoke(cls.newInstance()); } else { System.out.println("No annotation before " + m.getName()); } } } catch (ClassNotFoundException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { e.printStackTrace(); } } public static void main(String[] args) { invokeFromClass("ua.inf.iwanoff.java.advanced.fifth.ATest"); invokeFromClass("ua.inf.iwanoff.java.advanced.fifth.BTest"); } }
3.4 Database Application for Working with Census Data
In laboratory training No. 2, a Maven project was created, in which work is carried out with data about the country and population censuses. Now this project can be expanded.
In the console application, we should create two relational database tables, for countries and censuses, respectively.
After creating the tables, we should perform the following actions:
- import into the database from a JSON file;
- display data of both tables;
- sorting of censuses of a certain country by population growth;
- adding a new census to a specific country;
- deletion of the census;
- adding a new country;
- search for censuses with a certain word in the comment;
- export from the database to a JSON file.
We add a new package ua.inf.iwanoff.java.advanced.fifth
to the project. In it, we create derived
classes to represent the census and the country, adding the necessary constructors. The new field id
added
in these classes can be useful when interacting with the database. The CensusForDB
class will be as
follows:
package ua.inf.iwanoff.java.advanced.fifth; import ua.inf.iwanoff.java.advanced.first.CensusWithStreams; /** * Class to represent the census. * Added identifier for potential database storage */ public class CensusForDB extends CensusWithStreams { private long id = -1; /** * Returns the census ID * * @return Census ID */ public long getId() { return id; } /** * Sets the census ID * * @param id Census ID */ public void setId(long id) { this.id = id; } /** * The constructor initializes the object with default values */ public CensusForDB() { } /** * The constructor initializes the object with the specified values * * @param year census year * @param population the population * @param comments comment text */ public CensusForDB(int year, int population, String comments) { super(year, population, comments); } }
The code of class CountryForDB
will be as follows
package ua.inf.iwanoff.java.advanced.fifth; import ua.inf.iwanoff.java.advanced.first.CountryWithStreams; /** * A class to represent the country in which the census is conducted. * The identifier added for potential database storage */ public class CountryForDB extends CountryWithStreams { private long id = -1; /** * Returns the country ID * * @return Country ID */ public long getId() { return id; } /** * Sets the country ID * * @param id Country ID */ public void setId(long id) { this.id = id; } /** * The constructor initializes the object with default values */ public CountryForDB() { } /** * The constructor initializes the object with the specified values * * @param name the name of the country * @param area the area of the country */ public CountryForDB(String name, double area) { setName(name); setArea(area); } }
Since several countries can be present, it is advisable to create a new class Countries
that
contains a list of countries:
package ua.inf.iwanoff.java.advanced.fifth; import java.util.List; import java.util.ArrayList; /** * A class to represent a list of countries */ public class Countries { private List<CountryForDB> list = new ArrayList<>(); /** * Returns the list of countries (java.util.List) * @return list of countries */ public List<CountryForDB> getList() { return list; } /** * Returns a string representation of the object * * @return a string representation of the list */ @Override public String toString() { return list.toString(); } }
Now you should go to the settings related to the database. First, install the MySQL database server (if not already
installed). You can install the client-server version of the MySQL DBMS for Windows using the MySQL Installer program
(http://dev.mysql.com/downloads/installer/).
This program can be downloaded without registration. Choose an option mysql-installer-community-8.0.36.0.msi
(the
version may change). On the first page of the MySQL installer, select a configuration option. It is possible to
choose either Developer Default or Server only, which is enough for our work. There are also options
for advanced users. If Server only option was selected, click the Execute button
on the next page. The server installation process begins. The Next button should be clicked several times.
The Type
and Networking page can be retained unchanged. Next, press Next twice again, enter the
password. In real practice, complex passwords should be used, but for our work we will set a password
"root"
. On the Apply Configuration page, click
the Execute button, then Finish. On the following pages, we confirm the installation
of MySQL.
The following dependency must be added to the pom.xml
file:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>
The main work will be done in the class DbUtils
:
package ua.inf.iwanoff.java.advanced.fifth; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.io.json.JettisonMappedXmlDriver; import com.thoughtworks.xstream.security.AnyTypePermission; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.sql.*; import java.util.ArrayList; /** * The class implements work with a database in the task of processing data on population censuses. * MySQL database server is used */ public class DbUtils { /** * Enumeration for defining order of census data output */ public enum Show { SORTED, UNSORTED }; // Constants that contain the necessary SQL queries: public static final String DROP_TABLES = "DROP TABLES IF EXISTS censuses, countries"; public static final String DROP_DATABASE = "DROP DATABASE IF EXISTS countriesDB"; public static final String CREATE_DATABASE = "CREATE DATABASE countriesDB"; public static final String CREATE_TABLE_COUNTRIES = """ CREATE TABLE countriesDB.countries ( CountryID INT NOT NULL AUTO_INCREMENT, Name VARCHAR(128) NULL, Area DECIMAL(10,2) NULL, PRIMARY KEY (CountryID)); """; public static final String CREATE_TABLE_CENSUSES = """ CREATE TABLE countriesDB.censuses ( CensusID INT NOT NULL AUTO_INCREMENT, Year INT NULL, Population INT NULL, CountryID INT NULL, Comment VARCHAR(256) NULL, PRIMARY KEY (CensusID), INDEX CountryID_idx (CountryID ASC) VISIBLE, CONSTRAINT CountryID FOREIGN KEY (CountryID) REFERENCES countriesDB.countries (CountryID) ON DELETE NO ACTION ON UPDATE NO ACTION); """; private static final String INSERT_INTO_COUNTRIES = """ INSERT INTO countriesDB.countries (Name, Area) VALUES (?, ?); """; private static final String INSERT_INTO_CENSUSES = """ INSERT INTO countriesDB.censuses (Year, Population, CountryID, Comment) VALUES (?, ?, ?, ?); """; private static final String SELECT_BY_NAME = "SELECT * FROM countriesDB.countries WHERE Name = ?"; private static final String SELECT_ALL_COUNTRIES = "SELECT * FROM countriesDB.countries"; private static final String SELECT_FROM_CENSUSES = "SELECT * FROM countriesDB.censuses WHERE CountryID = ?"; private static final String SELECT_FROM_CENSUSES_ORDER_BY_POPULATION = "SELECT * FROM countriesDB.censuses WHERE CountryID = ? ORDER BY Population"; private static final String SELECT_FROM_CENSUSES_WHERE_WORD = """ SELECT c.CensusID, c.Year, c.Population, c.Comment, l.Name FROM countriesDB.censuses c INNER JOIN countriesDB.countries l ON c.CountryID = l.CountryID WHERE c.Comment LIKE '%key_word%'; """; private static final String DELETE_BY_YEAR = "DELETE FROM countriesDB.censuses WHERE CountryID=? AND Year=?"; private static Connection connection; /** * Deserializes data from the specified JSON file * * @param fileName the name of the file * @return the object that was created */ public static Countries importFromJSON(String fileName) { try { XStream xStream = new XStream(new JettisonMappedXmlDriver()); xStream.addPermission(AnyTypePermission.ANY); xStream.alias("countries", Countries.class); xStream.alias("country", CountryForDB.class); xStream.alias("census", CensusForDB.class); return (Countries) xStream.fromXML(new File(fileName)); } catch (Exception e) { throw new RuntimeException(e); } } /** * Serializes data into the specified JSON file * * @param country the country * @param fileName the name of the file */ public static void exportToJSON(Countries countries, String fileName) { XStream xStream = new XStream(new JettisonMappedXmlDriver()); xStream.alias("countries", Countries.class); xStream.alias("country", CountryForDB.class); xStream.alias("census", CensusForDB.class); String xml = xStream.toXML(countries); try (FileWriter fw = new FileWriter(fileName); PrintWriter out = new PrintWriter(fw)) { out.println(xml); } catch (IOException e) { throw new RuntimeException(e); } } /** * Serializes data from database into the specified JSON file * * @param fileName the name of the file */ public static void exportToJSON(String fileName) { Countries countries = getCountriesFromDB(); exportToJSON(countries, fileName); } /** * Creating a connection to the database */ public static void createConnection() { try { connection = DriverManager.getConnection("jdbc:mysql://localhost/mysql?user=root&password=root"); } catch (SQLException e) { throw new RuntimeException(e); } } /** * Creation of database and tables with deletion of previous ones */ public static boolean createDatabase() { try { Statement statement = connection.createStatement(); statement.executeUpdate(DROP_TABLES); statement.executeUpdate(DROP_DATABASE); statement.executeUpdate(CREATE_DATABASE); statement.executeUpdate(CREATE_TABLE_COUNTRIES); statement.executeUpdate(CREATE_TABLE_CENSUSES); return true; } catch (SQLException e) { return false; } } /** * Entry of all data from the object to the database * * @param countries an object with data about countries */ public static void addAll(Countries countries) { for (CountryForDB c : countries.getList()) { addCountry(c); } } /** * Putting all data from the object to the database * * @param country the country about which data is entered */ public static void addCountry(CountryForDB country) { try { PreparedStatement preparedStatement = connection.prepareStatement(INSERT_INTO_COUNTRIES); preparedStatement.setString(1, country.getName()); preparedStatement.setDouble(2, country.getArea()); preparedStatement.execute(); for (int i = 0; i < country.censusesCount(); i++) { addCensus(country.getName(), (CensusForDB) country.getCensus(i)); } } catch (SQLException e) { throw new RuntimeException(e); } } /** * Creates a Countries object into which all data from the database is entered * * @return object to which data about countries is put */ public static Countries getCountriesFromDB() { try { Countries countries = new Countries(); Statement statement = connection.createStatement(); ResultSet setOfCountries = statement.executeQuery(SELECT_ALL_COUNTRIES); while (setOfCountries.next()) { countries.getList().add(getCountryFromDB(setOfCountries)); } return countries; } catch (SQLException e) { throw new RuntimeException(e); } } /** * Creates a country object, filling it with data from the database * * @param name the name of the country * @return an object filled with data from the database */ public static CountryForDB getCountryByName(String name) { try { PreparedStatement preparedStatement = connection.prepareStatement(SELECT_BY_NAME); preparedStatement.setString(1, name); ResultSet setOfCountries = preparedStatement.executeQuery(); setOfCountries.next(); return getCountryFromDB(setOfCountries); } catch (SQLException e) { throw new RuntimeException(e); } } /** * Creates and returns a country object from the result dataset * * @param setOfCountries the resulting data set from which the country data is obtained * @return created country with census data * @throws SQLException exception related to SQL query error */ public static CountryForDB getCountryFromDB(ResultSet setOfCountries) throws SQLException { CountryForDB country = new CountryForDB(setOfCountries.getString("Name"), setOfCountries.getDouble("Area")); int id = setOfCountries.getInt("CountryID"); country.setId(id); PreparedStatement preparedStatement = connection.prepareStatement(SELECT_FROM_CENSUSES); preparedStatement.setInt(1, id); ResultSet setOfCensuses = preparedStatement.executeQuery(); while (setOfCensuses.next()) { CensusForDB census = new CensusForDB(setOfCensuses.getInt("Year"), setOfCensuses.getInt("Population"), setOfCensuses.getString("Comment")); census.setId(setOfCensuses.getInt("CensusID")); country.addCensus(census); } return country; } /** * Gets the country ID by name * * @param countryName the name of the country * @return Country ID in database */ public static int getIdByName(String countryName) { try { PreparedStatement preparedStatement = connection.prepareStatement(SELECT_BY_NAME); preparedStatement.setString(1, countryName); ResultSet resultSet = preparedStatement.executeQuery(); resultSet.next(); return resultSet.getInt("CountryID"); } catch (SQLException e) { throw new RuntimeException(e); } } /** * Outputs all data from the database to the console, sequentially for each country */ public static void showAll() { try { PreparedStatement preparedStatement = connection.prepareStatement(SELECT_ALL_COUNTRIES); ResultSet resultSet = preparedStatement.executeQuery(); ArrayList<String> names = new ArrayList<>(); while (resultSet.next()) { String name = resultSet.getString("Name"); names.add(name); } resultSet.close(); for (String name : names) { showCountry(name, Show.UNSORTED); } } catch (Exception e) { throw new RuntimeException(e); } } /** * Displays data about countries by name on the console * * @param countryName the name of the country * @param byPopulation the output order specified by the Show enumeration */ public static void showCountry(String countryName, Show byPopulation) { try { PreparedStatement preparedStatement = connection.prepareStatement(SELECT_BY_NAME); preparedStatement.setString(1, countryName); ResultSet resultSet = preparedStatement.executeQuery(); System.out.printf("%s\t %s\t %s%n", "ID", "Country", "Area"); resultSet.next(); System.out.printf("%s\t %s\t %s%n", resultSet.getString("CountryID"), resultSet.getString("Name"), resultSet.getString("Area")); resultSet.close(); PreparedStatement anotherStatement; if (byPopulation == Show.SORTED) { anotherStatement = connection.prepareStatement(SELECT_FROM_CENSUSES_ORDER_BY_POPULATION); } else { anotherStatement = connection.prepareStatement(SELECT_FROM_CENSUSES); } anotherStatement.setInt(1, getIdByName(countryName)); ResultSet anotherSet = anotherStatement.executeQuery(); System.out.printf("%s\t %s\t %s \t%s%n", "ID", "Year", "Population", "Comments"); while (anotherSet.next()) { System.out.printf("%s\t %s\t %s\t\t%s%n", anotherSet.getString("CensusID"), anotherSet.getString("Year"), anotherSet.getString("Population"), anotherSet.getString("Comment")); } } catch (SQLException e) { throw new RuntimeException(e); } } /** * Displays census data sorted by population growth * * @param countryName the country for which censuses are output */ public static void showSortedByPopulation(String countryName) { showCountry(countryName, Show.SORTED); } /** * Outputs to the console data about all censuses, the comments of which contain a certain word * * @param word the word to search for */ public static void findWord(String word) { try { String query = SELECT_FROM_CENSUSES_WHERE_WORD.replace("key_word", word); PreparedStatement preparedStatement = connection.prepareStatement(query); ResultSet resultSet = preparedStatement.executeQuery(); while (resultSet.next()) { System.out.printf("%s\t %s\t %s\t %s\t\t%s%n", resultSet.getString("CensusID"), resultSet.getString("Name"), resultSet.getString("Year"), resultSet.getString("Population"), resultSet.getString("Comment")); } } catch (SQLException e) { throw new RuntimeException(e); } } /** * Adds information about the census of a specific country to the database * * @param countryName the name of the country whose census is being added * @param census the census to add */ public static void addCensus(String countryName, CensusForDB census) { CountryForDB country = getCountryByName(countryName); try { PreparedStatement preparedStatement = connection.prepareStatement(INSERT_INTO_CENSUSES); preparedStatement.setInt(1, census.getYear()); preparedStatement.setInt(2, census.getPopulation()); preparedStatement.setInt(3, getIdByName(country.getName())); preparedStatement.setString(4, census.getComments()); preparedStatement.execute(); } catch (SQLException e) { throw new RuntimeException(e); } } /** * Deletes the specified census from the database * * @param countryName the name of the country * @param year the year of the census to remove */ public static void removeCensus(String countryName, int year) { try { PreparedStatement preparedStatement = connection.prepareStatement(DELETE_BY_YEAR); preparedStatement.setInt(1, getIdByName(countryName)); preparedStatement.setInt(2, year); preparedStatement.execute(); } catch (SQLException e) { throw new RuntimeException(e); } } /** * Closes the connection to the database */ public static void closeConnection() { try { connection.close(); } catch (SQLException e) { throw new RuntimeException(e); } } }
In the class with the main()
function, we also add a function for creating the necessary objects with
saving data into a JSON file. From this file, data is then read and imported into the database, then all planned
actions on the data are performed:
package ua.inf.iwanoff.java.advanced.fifth; import static ua.inf.iwanoff.java.advanced.fifth.DbUtils.*; /** * The class demonstrates working with the MySQL database */ public class DbProgram { /** * Demonstration of the program. * Data is imported from a JSON file, sorted, added and deleted. * The result is exported to another JSON file * * @param args command line arguments (not used) */ public static void main(String[] args) { Countries countries = createCountries(); exportToJSON(countries, "Countries.json"); countries = importFromJSON("Countries.json"); createConnection(); if (createDatabase()) { addAll(countries); showAll(); System.out.println("\nCensuses in Ukraine by population growth:"); showSortedByPopulation("Ukraine"); System.out.println("\nAdding census:"); addCensus("Ukraine", new CensusForDB(2023, 49000000, "Maybe there will be?")); showAll(); System.out.println("\nRemoving census:"); removeCensus("Ukraine", 2023); showAll(); System.out.println("\nAdding the country:"); addCountry(new CountryForDB("Germany", 357588)); addCensus("Germany", new CensusForDB(2011, 80200000, "The first census in united Germany")); showAll(); System.out.println("\nSearching for the word \"census\":"); findWord("census"); exportToJSON("CountriesFromDB.json"); closeConnection(); } } /** * Creating the Countries object and filling it with data to demonstrate the program * * @return an object that contains the necessary data to demonstrate the program */ static Countries createCountries() { CountryForDB country = new CountryForDB(); country.setName("Ukraine"); country.setArea(603628); country.addCensus(new CensusForDB(1959, 41869000, "The first postwar census")); country.addCensus(new CensusForDB(1970, 47126500, "Population increases")); country.addCensus(new CensusForDB(1979, 49754600, "No comments")); country.addCensus(new CensusForDB(1989, 51706700, "The last soviet census")); country.addCensus(new CensusForDB(2001, 48475100, "The first census in the independent Ukraine")); Countries countries = new Countries(); countries.getList().add(country); return countries; } }
The result of execution is output to the console of working with data, as well as a new file CountriesFromDB.json
.
4 Exercises
- Create a console application in which the user enters the name of a class and receives information about all public fields of this class.
- Create the Student class by generating the necessary methods using Lombok tools.
- Create a Student class with read-only data by generating the necessary methods using Lombok tools.
5 Quiz
- What is the essence of RTTI?
- Why use the keyword
instanceof
? - Give a definition of reflection.
- What information is stored in the
class
field? - How to load classes in Java?
- What are the different types of class loaders?
- How to load a class by name?
- What are custom loaders for?
- How to get information about class methods?
- How to call a method by name?
- How to call a static method?
- How to get information about class fields?
- Why are annotations used?
- What are the standard annotations?
- How does the presence of an instruction affect the execution of an annotated method?
- How are the annotation characteristics determined?
- How to set the lifetime of the annotation?
- How to create your own annotation?
- How to check the presence of an annotation?
- How to get a list of all annotations?
- What features does the Lombok library provide?
- What are the approaches to the implementation of database applications, depending on the task?
- Define JDBC.
- What is a JDBC driver?
- What are the different methods of executing SQL commands?