JAva Lang
JAva Lang
in 1991. The target of Java is to write a program once and then run this program on
multiple operating systems. The first publicly available version of Java (Java 1.0)
was released in 1995.Over time new enhanced versions of Java have been released.
In this course we will learn more about the advanced features of Java like
Exception handling,Generics, Collection Frameworks,JBDC, Multi Threading etc.
Important Java classes like Object, String, LocalDate, LocalTime and Calendar
Software requirements
Java 7 or above (download here for Java 8)
JDK (Java Development Kit) provides tools for developing, debugging and monitoring,
as well as the Java runtime environment (JRE) for Java applications
The most widely used open-source Integrated Development Environment (IDE) for Java,
delivering the most extensive collection of add-on tools for software developers
Java Language Features course will help you to understand various APIs provided by
Java.
Generics
Collection APIs
Threads
Concurrent API for a multithreaded environment
To get familiar with exceptions, let's take a simple scenario of division. This
will help us understand how exceptions occur in a program and how we can deal with
them.
An exception occurs!
The output is the stack trace of the exception. It tells us the type, message,
method call stack, and the location of the exception. This helps us debug the code.
In Java, all exceptions are objects of the java.lang.Exception class. These objects
carry the information related to the exception, including the stack trace.
Whenever an exceptional event occurs, the runtime environment (JRE) generates the
exception object and throws it. The moment an exception object is thrown, further
execution stops. And if it is not taken care of, the exception is propagated to the
calling environment. The calling environment can be either a calling method, or the
runtime system.
When the runtime receives the exception, it shows the stack trace and terminates
the program.
All the exceptions belong to the Exception class, which is a child of the Throwable
class.
Checked exceptions occur at compile time, and should be handled or declared for
propagation.
Unchecked exceptions occur at runtime, and need not be handled or declared for
propagation.
An exception object can make use of the below methods of Throwable class:
Having seen what exception are and how they affect a program, let's see how we can
handle them.
To make things easier and convenient, Java provides excellent exception handling
mechanisms.
or
We'll start with handling exceptions and later see how to allow propagation.
try {
// Code that can throw exceptions
}
catch(Exception1 e1) {
// Code for handling Exception1
}
catch(Exception2 e2) {
// Code for handling Exception2
}
finally {
// Will be discussed soon
}
The code that can throw an exception is enclosed inside the try block. A try block
is immediately followed by one or more catch blocks or a finally block.
A catch block is an exception handler which can handle the exception specified as
its argument. A catch block can accept objects of type Throwable or its subclasses
only.
Now let's create an exception handler block for our ArithmeticException example
Best practice: In a catch block, prefer specific exceptions rather than general
exceptions or unspecific exceptions.
Whenever an exception is thrown inside a try block, it is immediately caught by the
first matching catch block which can handle it. The code inside the try block
following the line causing the exception is ignored.
When an exception is caught and handled by a catch block, the execution continues
from immediately after the try-catch block.
If no exception is thrown inside a try block, the catch blocks following it are
ignored.
A catch block which can handle objects of Exception class can catch all the
exceptions.
This should always be the last catch block in the catch sequence.
catch(Exception e) {
// Code for handling exception
}
Note: try-catch blocks can be nested.
An exception inside a try block causes the rest of the code to be skipped. This
might lead to the important parts of the code not being executed.
Code which closes connections, releases resources, etc. need to be executed in all
the conditions. Keeping them inside the try block cannot guarantee their execution.
try {
// Code that can throw exceptions
}
catch(Exception1 e1) {
}
finally {
// Code to be executed no matter what
}
Note: A try block should be always followed by either at least one catch or a
finally block.
Best practice: The finally clause should normally have code that does not throw
exceptions. If the finally clause does throw an exception, log it or handle it,
never allow it to bubble out.
Exceptions are generated when some predefined exceptional events like division by
zero occurs in a program. Sometimes it is reasonable and convenient to define our
own exceptional events.
Java allows us to explicitly generate or throw exceptions using the throw keyword:
Until now, we have been handling exceptions in the method in which they are thrown.
What if we need to propagate and handle the exceptions elsewhere!
Have a look at the implementation of main() method, to handle the exception thrown
from the divide() method:
Unchecked exceptions are not mandatory to either declare or handle and are ignored
at compile time. Error, RuntimeException and their subclasses are Unchecked
exceptions.
Error and its subclasses are used for serious errors from which programs are not
expected to recover. For example, out of memory error.
Runtime exceptions are used to indicate programming errors such as logic errors or
improper use of an API. For example,
Besides working with exceptions provided by Java, we can have our own custom
exceptions.
Generics
Edford University wants to manage records of students, staff, books in the library.
They already have a simple data structure for books:
class Book {
private String bookName;
public Book(String bookName) {
this.bookName = bookName;
}
public String getBookName() {
return bookName;
}
public void setBookName(String bookName) {
this.bookName = bookName;
}
}
class Student {
private String studentName;
public String getStudentName() {
return studentName;
}
public void setStudentName(String studentName) {
this.studentName = studentName;
}
public Student(String studentName) {
this.studentName = studentName;
}
class BookRecord {
private Book[] books;
public Book add(Book book) {
// Code to add book
}
public Book get(int index) {
// Code to get book at specified index
}
}
They want a similar functionality for students and staff as well. Should we create
new record classes for them?
So far, we have been creating and using programming components for a specified data
type.
In cases like this, we need classes, interfaces or methods that could be used for
multiple kinds of objects!
To make our components more general, we can use the Object class type. Have a look
at the following Record class:
class Record {
private Object[] record;
public Object add(Object item) {
// Code to add record item
}
public Object get(int index) {
// Code to get record at specified index
}
}
Now this class can be used for keeping a record of any kind of object.
The above code serves the purpose, but it is not the best approach. Why?
This approach has some drawbacks:
Any type of object (Student, String, Integer etc.) can be pushed into the record
This type cast is unsafe. The compiler won�t check whether the typecast is of the
same type as the data stored in the record. And so the cast may fail at runtime.
What we need is to have classes, interfaces and methods that could be used for
multiple kinds of objects, but still be tied to a particular type.
If they could accept the type at runtime, it would enable the reusability of the
same functionalities for different types.
Generics are used to create classes, interfaces and methods in which the type of
the object on which they operate is specified as a parameter.
They were introduced in Java 5, and provide the following advantages:
class class-name<type-parameter-list> { }
Here, the type-parameter-list specifies the type parameters. By convention, the
type parameter is denoted by a single capital letter and is commonly one among E
(element), T (type), K (key), N(number) and V (value)
class Record<E> {
private E[] record;
public E add(E item) {
// Code to add record item
}
public E get(int index) {
// Code to get record at specified index
}
}
Elements of type E can be added into and retrieved from the Record
Generic types can also be used without type parameters. Such a type is called raw
type.
Record record = new Record(); // Raw type
Raw types provide compatibility with older code, and should not be used anymore.
There can be more than one type parameter for a class or interface.
class MyClass<K,V> { }
We already know that child objects are compatible with their parent types. When
such kind of assignment is done while working with generic components, it is termed
as Inheritance with Generics.
Among all the flexibility and security, Generics however, possess some
restrictions:
A generic type cannot be used with a primitive type. It must always be a reference
type.
Just like generic classes and interfaces, a generic method can declare type
parameters of its own.
Such generic methods can perform operations on any kind of data as specified.
Syntax:
Generic methods can be invoked by prefixing the method name with the actual type in
angle brackets.
UserInterface.<String>display(cities);
A generic method can return a generic type value.
Edford University has two kinds of student, DayScholar and Hosteller which extend
the Student class.
The above update() method cannot be used for the child classes of Student:
Generics in Java allow wildcard constructs to denote a family of types. They can be
categorized as follows:
? extends T - Upper-bounded wildcard which supports types that are T or its sub-
types
? super T - Lower-bounded wildcard which supports types that are T or its super-
types
Upper-bounded Wildcard:
Requirement: To have a method that should take an entity of type Student as well as
its sub-types.
Lower-bounded Wildcard:
Requirement: To have a method that should take an entity of type DayScholar as well
as its super-types.
Collections Framework
Students at Edford University can opt for multiple courses. They require the system
to work with a list of courses for each student. Courses can be introduced or
removed at any time.
Can this be implemented using an array?
But they cannot grow and shrink dynamically. Moreover, they do not have any built-
in algorithm for searching or sorting.
The collections framework provides a set of interfaces and classes for representing
and manipulating collections. Introduced as part of J2SE 1.2, it standardizes the
way we store and access data from collections. It is a part of the java.util
package.
These are some of the interfaces and classes in the Collections Framework:
List Interface
Let's start with lists and see how they can be of help to us.
Apart from the methods from the Collection interface, some methods specific to List
are:
The equals() method works across all the implementations of the List interface and
returns true if and only if they contain the same elements in the same order.
//Creating a list
ArrayList<Integer> numList = new ArrayList<>();
numList.add(10);
numList.add(new Integer(22));
// Using list
System.out.print(numList.get(0));
@Override
public String toString() {
return courseName ;
}
}
Now we can have list of courses for students in the following way:
Best practice: Use generic type and diamond operator while declaring collections.
The for loop can be used for ordered collections like lists
The enhanced for loop (for-each) can be used for ordered and unordered collections
The Iterator interface can be used for collections implementing the Collection
interface
Now let's see how lists can be traversed using loops:
Iterator follows the Iterator Design Pattern, and provides the following methods
for accessing and removing the elements of a collection:
Using Iterator:
Java
Hibernate
AngularJS
The Edford data management team says that the courses are unique. Using lists to
hold them can allow duplicate values, which is not at all desired.
To solve this, we can use sets.
It uses the methods from the Collection interface and does not declare any new
method of its own.
Being unordered, sets cannot be accessed using indexes. The enhanced for-loop and
the iterator are two ways of traversing and accessing the elements of a set.
Note: The add() method will return false if our program attempts to add a duplicate
element.
Set Types
The equals() method works across all the implementations of the Set interface and
returns true if and only if they contain the same elements.
Note:
Output: [12,24]
It is clear from the above code that set eliminates duplicates. Also, LinkedHashSet
maintains the order of insertion.
Set doesn't provide any method for directly accessing its elements. We can use an
iterator or the for-each loop to do this.
Output: 12
24
Note: For sets to detect duplicates among user-defined objects, the equals() and
hashCode() methods must be overridden.
Output: Angular JS
Hibernate
Java
These are times when we need to maintain data corresponding to some other data like
associating words and their meanings in a dictionary, employees with their employee
numbers, and so on.
To store such data, which need to exist in pairs, Java provides us with Maps.
Values in a map can be duplicates, but keys have to be unique. Having unique keys
allows easier and faster access to the values.
You must have noticed that Map does not extend the Collection interface. So there
is no iterator for maps. Moreover, map values cannot be retrieved without knowing
the keys. Hence, there is no direct way of traversing a map.
A map to associate student IDs to their courses can be created in the following
way:
If duplicate elements are not allowed choose Set otherwise choose List.
Edford University's data management team wants the list of courses to be sorted in
the order of the course names.
This can be done using the static Collections.sort() method, which can sort the
element of a list in their natural order.
For example:
Collections.sort(companies);
System.out.println(companies);
Collections.sort() will work on any kind of element which implements the Comparable
interface.
Usually in-built Java objects like String, Date, etc. implement the Comparable
interface, and hence, sort() works on them.
This means that sort() cannot work on user-defined objects on its own. As in our
case, the Course class will need to implement the Comparable interface.
So after implementing the Comparable interface, our Course class would look like
this:
@Override
public int compareTo(Course otherCourse) {
return this.courseName.compareTo(otherCourse.courseName);
}
@Override
public String toString() {
return this.courseId + ":" + this.courseName;
}
}
Now that our Course class implements the Comparable interface, we can use
Collections.sort() to sort the courses:
Collections.sort(courseList);
System.out.println(courseList);
TreeSet and TreeMap classes automatically use the compareTo() method to sort
elements when they are added.
So objects of classes overriding compareTo() will automatically be sorted if they
are added to TreeSet or TreeMap.
Edford has come up with another similar requirement. They would sometimes need to
sort the courses according to the courseId as well.
Since our Course class already implements compareTo() to sort according to the
courseName, we'll need an additional component to define another comparison logic.
A class can implement the Comparator interface to define a comparison logic in its
compare() method. An object of this class can then be passed along with a list to
the sort() method.
The Comparator interface is part of the java.util package. It has a single method
compare() that needs to returns a negative, zero or a positive number based on the
comparison.
For use with TreeSet and TreeMap classes, the Comparator instances should be passed
to their constructors:
Regular Expressions
Edford's data management team wants to validate student or employee details like
name, mobile number, email Id, etc. before storing them in the database.
Consider a logic to validate mobile number which checks its length to be 10 and all
the characters to be digits:
boolean valid=true;
int length=mobileNumber.length();
if (length == 10) {
for (int i=0; i<length; i++) {
if (Character.isDigit(mobileNumber.charAt(i)) != true) {
valid=false;
break;
}
}
}
else
valid=false;
return valid;
}
For all such validations, we usually define logic that runs loops to repeatedly
check for specific patterns, or a logic that compares each character of the input.
Regular expressions are most widely used for validating details entered in a form.
The regex API is distributed under the java.util.regex package, and provides
classes and interfaces to work with regular expressions.
The String class uses this API to support regex in four methods:
matches(), split(), replaceFirst(), replaceAll()
We'll start with learning how to create regex patterns, and then move to using the
regex API provided by Java.
So let's have a look at the components used to create regex patterns along with
some examples...
Regex API
Now that we have seen how to create regex patterns, let's see how we can use them
for string searching and manipulation.
MatchResult interface
Matcher class
Pattern class
PatternSyntaxException class
The Pattern and Matcher classes are the most widely used.
Let's see what they can do...
Hyphens should come after the 3rd and the 6th digits
Best practice: Its preferred to use Pattern and Matcher classes than
String.matches, as it compiles the regular expression each time they are called.
Annotations
class Professor {
// Other class members
On careful observation, it was found out that the method in the child class had a
typographical mistake in its signature, and was not overriding at all. This was
causing the parent method to be invoked every time.
This often happens while programming, and can cause issues difficult to identify in
large programs.
What we need is some kind of automatic check. Something that can tell us whether a
method is actually being overridden or not.
In Java, this can be achieved using the @Override annotation:
This way the compiler checks the method signature and makes sure the parent method
is overridden.
An annotation is a meta-data that provides information about the program and is not
part of the program itself.
Annotations have a number of uses:
Information for the compiler - Annotations can be used by the compiler to detect
errors or suppress warnings
Annotations can be used with classes, methods, variables, parameters and packages.
Annotations are written starting with an '@' symbol, followed by the annotation
name and its elements, if any.
They can be of the following types:
How can we make sure the new functionality is being used? Shall we delete the old
one?
No, we cannot delete the old one, as it would break existing codes. We need to
ensure backward compatibility.
This can be done by declaring the old method as obsolete.
@Deprecated
public static Object update(Object object) { // The method gets a strikethrough
when preceded with @Deprecated
// Code to update a record
return object;
}
While invoking:
The @Deprecated annotation is a marker annotation, and can be used to mark a class,
method or field as deprecated. This would mean they should no longer be used.
A lot of times, we come across code which has warnings related to unused local
variable, unused private methods, unchecked type operations, using deprecated
methods, etc.
At times, we need to ignore such warnings and go ahead with using the code.
If we don't want the compiler to issue such warnings, we can suppress them using
the @SuppressWarnings annotation.
@SuppressWarnings("unused")
public static void main(String args[]) {
Professor professor1 = new Professor(); // Line where an unused local
variable exists
Professor professor2 = new VisitingProfessor();
System.out.println(professor2.calculateSalary());
}
unused
unchecked
deprecated
all
The team of developers at Edford wants to keep track of the developers modifying a
functionality. This record needs to stay at the source level.
We can do this by creating a custom annotation which would be used to maintain the
list of people who have modified a particular class or one of its methods.
For example, if Emily and Mark modify Student class at any point of time, the
annotation needs to be supplied with their entries, making the code self-readable.
Apart from this, there are other constructs that can be used to add more meaning to
our annotations.
Now the steps below will help us create the annotation we need:
As already seen, create an interface with a meaningful name (this will be the name
of the annotation we will use in code).
The keyword interface needs to be preceded with @ symbol.
If the annotation needs to take values, declare a public method for each such value
inside the interface.
If no method is present, the annotation will be treated as a marker annotation.
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ChangeDetail {
String authorName();
String methodName();
}
@ChangeDetail(authorName="Emily", methodName="calculateFee")
class DayScholar extends NewStudent {
@Override
public double calculateFee() {
// Code for calculating total fee which includes bus fee
}
}
To be able to see the details of the annotations at run-time, we can take help of
the Reflection API. It provides various methods to retrieve annotation name, type,
methods, etc.
// Output:
ChangeDetail
authorName Emily
methodName calculateFee
@Repeatable(value=ChangeDetails.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ChangeDetail {
String authorName();
String methodName();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ChangeDetails { // The definition for repeatable annotation
ChangeDetail[] value();
}
I/O Stream
String data = "Ahalya Bhairav Chitra Dushyant Eshwari Falgun Gargi Hiren";
// Convert string to byte array
byte bArray[] = data.getBytes();
// Write bytes into the file with overloaded method which takes a byte array
outFile.write(bArray);
Now let's see how we can use FileInputStream to read from our file:
import java.io.*;
int i = inFile.read();
while(i != -1) {
outFile.write(i);
i = inFile.read();
}
}
catch(IOException io) {
io.printStackTrace();
}
finally {
if(inFile != null) inFile.close();
if(outFile != null) outFile.close();
}
}
}
This program will read the content of 'ReadFrom.txt' and write it to 'WriteTo.txt'.
Buffered streams
Reading and writing data, a byte or character at a time, can be quite expensive due
to frequent disk access.
This can be optimized by buffering a group of bytes or characters together, and
then making use of them.
Buffering helps to store an entire block of values into a buffer, and then make the
data available for use.
There are four buffered stream classes:
Best practice: To improve performance make sure you're properly buffering streams
when reading or writing streams, especially when working with files. Just decorate
your FileInputStream with a BufferedInputStream.
The code example to write the content of one file into another can now been
modified to perform buffered read and write operations:
int i = inFile.read();
while(i != -1) {
outFile.write(i); // Writing into the buffer
i = inFile.read();
}
}
catch(IOException io) {
io.printStackTrace();
}
finally {
// Closing will first flush the buffers
if(inFile != null) inFile.close();
if(outFile != null) outFile.close();
}
}
OutputStreamWriter
Now there is a requirement to write character data into a file byte by byte i.e.
Unicode data has to be written into a file through byte stream. This is usually
needed when a specific UTF encoding scheme is being used, say UTF-8 or UTF-16.
The OutputStreamWriter class can be used for this. It converts character stream
data to byte stream data by wrapping the OutputStream.
BufferedWriter bw = null;
try {
bw = new BufferedWriter(new OutputStreamWriter(new
FileOutputStream("Kannada.txt")));
// Writing unicode string
bw.write("\u0c87\u0ca8\u0ccd\u0cab\u0cc6\u0cc2\u0cd5\u0cb8\u0cbf\u0cb8\u0ccd");
System.out.println("Data written successfully");
}
catch(IOException ioe) {
System.out.println("ERROR: " + ioe.getMessage);
}
finally {
if(bw != null) bw.close();
InputStreamreader
Now to read such a Unicode file byte by byte and display it on the console as
characters, the InputStreamReader class can be used in between the byte stream and
character stream. It converts byte stream data to character stream data.
BufferedReader br = null;
try {
br = new BufferedReader(new InputStreamReader(new
FileInputStream("Kannada.txt")));
System.out.println("Data in the file is:");
int data = br.read();
while(data != -1) { // Checking for the end of
file
System.out.print((char) data);
data = br.read(); // Reading the content
}
}
catch(IOException ioe) {
System.err.println("ERROR: "+ioe.getMessage());
}
finally {
if(br != null) br.close();
}
Across the examples seen until now, you must have noticed that every stream object
used had to be closed before the program ends. This optionally performs some
concluding tasks, and then releases the system resources being used.
All resources like streams, database connections, etc. need to be closed by the
developer, and the usual place to do this is inside the finally block.
To make things easier, Java 7 introduced the automatic resource management feature
that helps to close these resources automatically. This allows specifying such
resources as part of a try block.
Note: Only resources that implement the AutoCloseable interface can be used with
try. This interface has a single method close() which needs to be implemented. All
I/O classes implements AutoCloseable interface.
The File class of java.io package represents a file in the file system. This allows
modification of file properties, access to file path and size, operations such as
rename and delete, listing directory content, and more.
The File class represents files and directory pathnames in an abstract manner. A
File object represents the actual file/directory on the disk.
File objects can be passed to the constructors of FileInputStream,
FileOutputStream, FileReader and FileWriter to read and write files, instead of
specifying just the file names.
File demo
Have a look at the following code sample which shows how to create and use a File
object:
import java.io.File;
There is a requirement to write the Edford university's name at the end of all the
files that are specific to the university. This calls for inserting content into
existing files.
For such requirements, where you need random access inside the file, Java provides
RandomAccessFile.
Unlike the input and output stream classes in java.io, RandomAccessFile is used for
both reading and writing files. It does not inherit from InputStream or
OutputStream. In fact, it implements the DataInput and DataOutput interfaces.
For example:
import java.io.*;
The seek() method can be used to read or write to a specific location in the file.
The getFilePointer() method returns the current position of the file pointer.
The read() and write() methods can be used to read and write to the file. The
cursor position moves after each read()/write() of data.
The readXXX() and writeXXX() methods are used to read and write boolean, double,
int, String etc.
Edford University now wants to store all student details like ID, name, date of
birth, courses, etc in files. They would need to retrieve the data as and when
required.
Assume that you have used one or few of the stream classes that you have learnt so
far, and you have written the state of some student objects into StudentDetails.txt
file as shown below.
Now when you retrieve data from the file and want to receive them as objects, we
need to
create objects
All of it takes a great deal of effort. Moreover, even if a single value is missed
there are chances that we receive corrupted data.
What we need is an easier way to deserialize objects with a single method call.
Let us discuss serialization and deserialization and see how it can help us in this
scenario.
For any object to be serialized, the concerned class must implement the
java.io.Serializable interface.
Serializable is a marker interface (has no body). It is just used to "mark" Java
classes to support serialization.
this.studentId = studentId;
this.firstName = firstName;
this.lastName = lastName;
this.dateOfBirth = dateOfBirth;
this.courses = courses;
this.age = age;
}
Set<Course>courses=new HashSet<>();
courses.add(new Course("Java"));
courses.add(new Course("Python"));
// Create ObjectStream
ObjectOutputStream objStream = new ObjectOutputStream(outFile);
objStream.close();
outFile.close();
objStream.close();
inFile.close();
Assume that, after serializing a Student object into a file, you have modified the
Student class structure by adding a new field. Now, what will happen if you try to
deserialize the Candidate object from the file?
To solve the version mismatch problem, we can control the versioning by assigning a
version id manually.
public class Student implements Serializable {
private static final long serialVersionUID = 1L;
private int studentId;
private String firstName;
private String lastName;
private String dateOfBirth;
private Set courses;
private int age;
}
Now, deserialization will successfully happen assigning default value to the newly
added field as the version id is manually maintained as a constant value.
The Student class has a property 'age' which is used for certain functionalities,
and is calculated from the date of birth.
Since age will be derived from the date of birth, it need not be stored in the
file. In cases like this, we can declare these properties as transient.
If a class implements Serializable, all its sub classes will also become
Serializable.
If there is any static data member in a class, it will not be serialized because
static is part of the class and not its objects.
In case of an array or collection, all the objects of the array or collection must
be Serializable. If any object is not Serializable, serialization will fail.
JDBC
The amount of data at Edford is increasing with time. Following are the various
problems we would need to address while using File system to manage the
university's data:
This demands for a better way of data organization. As most of the data is
structured, the university has decided to move them to a database.
Since all the data is going to be moved to a database, we'll need our application
to interact with the database.
Consider a Java class which takes care of persisting student related data -
StudentDAO.java. This class interacts with database and fetches all the information
related to a student. How can StudentDAO connect to database and get the required
information?
Java Database Connectivity (JDBC) API is the answer. JDBC makes it very easy to
connect to databases and perform database related operations.
Using JDBC, a Java application can access a variety of relational databases such as
Oracle, MS Access, MySQL, SQL Server, etc.
The JDBC API belongs to the java.sql package and consists of various interfaces and
classes.
JDBC Driver
To be able to use JDBC API for any particular database, we would need drivers for
that database. The driver can establish a connection with the database, and
exchange queries and their results with it.
JDBC is a specification that tells the database vendors how to write a driver
program to interface Java programs with their database. A driver written according
to this standard is called a JDBC Driver. All JDBC Drivers implement the Driver
interface of the java.sql package.
Type 1 (JDBC - ODBC Bridge Driver): A JDBC bridge is used to access ODBC drivers
installed on each client machine. This type of driver is recommended only for
experimental use or when no other alternative is available.
Type 2 (Native - API Driver): JDBC API calls are converted into native C/C++ API
calls, which are unique to the database. Used only when Type 3 and 4 are not
available.
Type 4 (Pure Java Based Driver): A pure Java-based driver communicates directly
with the vendor's database through socket connection. This is the highest
performance driver available for the database and is usually provided by the vendor
itself. This is the most preferred driver for accessing a single database from an
application .
Let's see how we can work with JDBC to perform the required database operations.
Edford University is using Oracle database and hence needs to use Oracle specific
driver oracle.jdbc.driver.OracleDriver.
Class.forName() method is used to dynamically load the driver's class file into
memory. This also automatically registers it.
Class.forName(oracle.jdbc.driver.OracleDriver);
The DriverManager class not only helps in managing and registering drivers, but
also in connecting to databases.
To connect to a database, the DriverManager class has the static methods which
return a Connection object:
Connection string
Edford's database details are:
Hostname: kecmachine
Database: edford
Port: 1521
Username: Mark
Password: passwd
To connect to the database with the above details:
Once the connection is established, the connection object can be used to interact
with the database.
Note: Within an application, we can have more than one connection with a single
database, or multiple connections with multiple databases.
creating a statement
SQL statements can be created using the connection object. Some methods provided by
the Connection interface are:
Once we have a statement object, we can use it to send and execute SQL queries over
the connection object.
Some methods provided by the Statement interface are:
Combining the above components for creating statements and executing them, we can
write the following code:
Class.forName("oracle.jdbc.driver.OracleDriver");
// Create and get the connection
String url = "jdbc:oracle:thin:Mark/passwd@kecmachine:1521:edford";
Connection conn = DriverManager.getConnection(url);
stmt.close();
conn.close();
Precompiled SQL statements are faster than normal SQL statements. So if an SQL
statement is to be reused, it is better to use PreparedStatement.
Since we write the SQL statements as a String in our JDBC application, we can pass
some dynamic values at run time and concatenate it with the query as shown below:
If a malicious code enters the value of facultyId as "1001 or 1=1", then the query
becomes
This will fetch all the rows present in the table because 1=1 is always true. This
could allow unauthorised users to get the details of all the students.
Insertion of such malicious SQL statements into the application is known as SQL
injection attack.
Observe the code given below where dynamic values are passed as parameters to the
query:
These type of queries where parameters are set at run time using parameter index
are called as Parameterized queries. This is the solution for SQL injection attack.
As we set the parameters using the type of parameters itself (i.e. setXXX()), it
will consider the whole value as of that type. In this case, if some erroneous code
supplies value for facultyId as "1001 or 1=1", it will be a compilation error as
Integer will not accept the String value.
Thus preparedStatement helps us prevent SQLInjection vulnerability.
If the stored procedure has an out parameter, it will need to be registered using
the registerOutParameter() method. This out parameter is returned after execution
of the stored procedure.
Let us look at the code for calling a procedure 'registerStudent' which takes
studentId as a parameter and returns an int 0 for success and 1 for failure.
callStmt.execute();
// Get the out parameter value
int outRet = callStmt.getInt(2);
A ResultSet object maintains a cursor that points to the current row in the result
set. It has methods for navigating, viewing and updating the data.
Similarly, there are get methods in the ResultSet interface for each of the Java
primitive types.
Similarly, there are update methods in the ResultSet interface for each of the Java
primitive types.
ResultSet Demo
Have a look at how various methods of ResultSet are used to process results:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
class StudentDAO {
public void getFaculty(int facultyId) throws SQLException {
Connection conn = null;
PreparedStatement preStmt = null;
ResultSet rsFaculty = null;
try {
// Load the driver
Class.forName("oracle.jdbc.driver.OracleDriver");
// Create and get the connection
conn =
DriverManager.getConnection("jdbc:oracle:thin:Mark/passwd@kecmachine:1521:edford");
// Create the statement
preStmt = conn.prepareStatement("select faculty_id, faculty_name
from faculty where faculty_id=?");
// Set the facultyId parameter
preStmt.setInt(1, facultyId);
rsFaculty = preStmt.executeQuery();
// Processing the result
while (rsFaculty.next()) {
String facultyName = rsFaculty.getString("faculty_name");
System.out.println("Faculty Id is: " + facultyId);
System.out.println("Faculty Name is: " + facultyName);
}
} catch (ClassNotFoundException ce) {
System.out.print("Database driver class not found");
ce.printStackTrace();
} catch (SQLException se) {
System.out.print("SQL Exception occurred");
se.printStackTrace();
} finally {
if (preStmt != null)
preStmt.close();
if (conn != null)
conn.close();
}
}
}
Best practice: Close any resource such as Statement, PreparedStatement, Connection
etc in finally block.
The ResultSet interface allows us to access and manipulate the results of executed
queries. The ResultSet object also has characteristics like type and concurrency.
The overloaded method below takes the type and concurrency level of ResultSet
object that the statement will return upon execution.
Similarly, we have overloaded methods for both Prepared statements and Callable
statements that allow us to override the default values of ResultSet type and
concurrency levels.
We have seen how to retrieve data i.e. perform a SELECT operation. Now let us
INSERT some data into the database using JDBC:
// Setting parameters
preStmt.setInt(1, 1001);
preStmt.setString(2, "John");
preStmt.setString(3, "Mysore");
preStmt.setLong(4, 9213456780l);
The steps used to insert records are same as the steps involved in retrieving
records, except the method used to execute the statement.
The executeUpdate() method is used for executing queries which manipulate values in
databases, i.e. insert, update, delete. It returns the number of rows affected upon
execution.
Transactions
Edford University's application adds the student and course details together.
If student data fails to get added, their list of courses should not get persisted
and vice versa. i.e. there are two statements that must be executed in tandem, else
their actions must be taken back.
commit() - Commits the transaction and makes all its changes permanent in the
database.
rollback() - Discards the changes made inside the transaction and reverts to the
previous consistent state of the database.
Best practice: Set auto commit mode disabled when executing multiple statements so
that you can group SQL Statement in one transaction. In case of auto commit mode
every SQL statement runs in its own transaction and committed as soon as it
finishes.
Multithreading
The team at Edford University has noticed that while uploading marks for batches,
they are not able to view batch wise reports until the marks for all the batches
have been uploaded. This is because the system is performing all the tasks
sequentially, causing other functionalities to wait while marks are being uploaded.
This is quite common in many existing applications. For instance, as we type code
into the Eclipse editor, it performs compile time error check in the background,
and provides suggestions as well. Similarly when we play a game, a lot of objects
are animated independently and simultaneously. Such a behavior is quite common in
applications like IDEs, word processors, computer games, web browsers, media
players, etc. and they are called multithreaded applications.
Multithreading:
With multithreading, programs and their GUIs can be made faster and more responsive
as more than one task can be performed at the same time.
Override the run() method of the Thread class to define the operations to be
performed by the thread
Create an object of the Thread subclass and invoke the start() method
Let's make our marks uploading functionality using the Thread class:
class ThreadTester {
UploadResult uploadThread = new UploadResult();
uploadThread.start();
}
The start() method begins the thread's execution, after which the JVM invokes its
run() method.
Note: The operating system is responsible for scheduling threads for execution.
Implement the run() method to define the operations to be performed by the thread
Create an instance of Thread class by passing an instance of the class implementing
the Runnable interface, and then invoke the start() method
class Test {
public static void main(String[] args) {
UploadResult uploadRunnable = new UploadResult();
Thread threadObj = new Thread(uploadRunnable);
threadObj.start();
}
}
As in the first case, the start() method is called upon a thread object. But here,
the thread object accepts a Runnable object which provides the implementation of
the run() method.
Now that you have seen how threads can be created, have a look at the following
overloaded constructors of the Thread class:
Thread Methods
Here are some useful methods of the Thread class:
Whenever a thread is created to perform its task, it goes through different states
between the time it is created and when it completes.
During its life cycle, a thread can move to the following different states:
New
Runnable
Running
Dead
We know that when the scheduler schedules a thread, it moves from RUNNABLE to
RUNNING state. The scheduler makes use of two techniques of thread scheduling here.
Preemptive: the thread with a higher priority preempts threads with lower priority
and grabs the CPU.
Time Sliced: Also referred to as round robin scheduling, each thread will get some
time of the CPU.
Thread priorities can be used by the scheduler to decide which threads to run.
The thread with highest priority is supposed to be executed first, but it is not
guaranteed that the thread will start running immediately. Rather it goes to the
scheduler and starts running once it gets CPU time.
The priority of a thread can be set using the setPriority() method before starting
the thread. The getPriority() method will return the priority of a thread.
The priority can vary from 1 (Thread.MIN_PRIORITY) to 10 (Thread.MAX_PRIORITY). The
default priority is 5 (Thread.NORM_PRIORITY).
Assigning Thread.MAX_PRIORITY does not guarantee that the thread starts running
immediately. Rather it goes to the scheduler and once it gets CPU time it starts
running.
Edford University has an introductory course with limited seats for admission. At
this point, only 1 seat is left.
Seeing this, two students try to register for the course simultaneously.
class Course {
String courseName;
int numOfSeats;
public Course(String courseName, int numOfSeats) {
this.courseName = courseName;
this.numOfSeats = numOfSeats;
}
public void registerForCourse(int rollNo) {
try {
if(this.numOfSeats - 1 < 0) {
throw new Exception("No more seats available for this course");
}
System.out.println("Booking successful!");
this.numOfSeats -= 1;
System.out.println("Available seats: " + this.numOfSeats);
}
catch (Exception e) {
System.out.println("Error: " + e.getMessage());
}
}
}
Here is our thread class:
RegisterThread(Course c) {
this.c = c;
}
public void run() {
c.registerForCourse(1);
}
}
And the main method:
Booking successful!
Booking successful!
Available seats: 0
Available seats: -1
The number of seats is now -1.
This happened because both the registering threads read the available seats at the
same time, even before the other could change its value.
This made both of them continue registering, resulting in the negative value of
available seats. This is not desirable.
To solve this, we need to make one thread wait for the other to finish completely.
Every object has a built in mutually exclusive lock called monitor. Only one thread
can acquire the monitor on an object at a time.
On synchronization, a thread obtains the lock to an object, and other threads wait
until the lock is released.
The synchronized keyword can be used only with a method or a block of code.
synchronized(this) {
// Code
}
For block code, the object to be synchronized needs to be passed as parameter.
Note : To reduce locking of an object by a Thread for longer period of time the
best practice is to use synchronized(object) so that lock is released as soon as
the synchronized code block is executed.
Now let's see how we can fix our code using synchronization. Here is our modified
registerForCourse() method:
This time, only one thread will be able to access this method at a time. While one
thread is inside the method, others will wait.
Here is the output:
Booking successful!
Available seats: 0
Error: No more seats available for this course
For this, the registration thread will need to communicate with the cancellation
thread.
We need a mechanism which allows synchronized threads to communicate with each
other. This is termed as inter-thread communication.
It helps a thread to release lock or monitor on an object for the other threads.
This can be done with the help of the following methods of the Object class:
These methods can be called from within a synchronized context only.
The thread calling wait() will release the lock on the particular object and will
wait for a notification
It will wait till it is notified by another thread holding the lock on the same
object
If there are multiple threads waiting on the same object, the notify() method will
notify any one among them. All the waiting threads can be notified using
notifyAll()
The corresponding cancelSeats() method is added in the Course class that notifies
our registerForSeats() method:
CancelRegistration(Course c) {
this.c = c;
}
public void run() {
c.cancelSeats();
}
}
Thread.sleep(2000);
// Running a cancellation thread after 2 seconds
CancelRegistration cancelObj1 = new CancelRegistration(cse);
cancelObj1.start();
}
}
Booking successful!
Available seats: 0
Cancellation successful!
Available seats: 1
Booking successful!
Available seats: 0
Thread Groups
Thread groups can be formed using the ThreadGroup class. It represents a set of
threads and provides a single-point control on those threads.
Thread groups can also contain other thread groups, creating a hierarchy.
A Daemon Thread is a thread that runs in the background, serving other threads.
A program will not wait for a daemon thread to finish execution.
A thread can be made a daemon by calling the setDaemon(true) method before calling
its start() method. And we can check if a thread is daemon using the isDaemon()
method.
Concurrency
synchronizing them
All of it takes a great deal of effort. Moreover, they do not contribute to the
business functionality.
What we need is a convenient way for managing threads and the separation of its
code from the business logic.
The high level Concurrency API provided by Java can help us conveniently develop
concurrent applications.
This API
Executor framework
To get familiar with the concurrency API, let's start with the Executor Framework.
Have a look at the problems addressed by the set of interfaces and class in the
Executor framework:
And following are the factory methods from the java.util.concurrent.Executors class
for creating executor services with thread pools:
Thread Pools
Have a look at the various problems associated with creation of threads for new
tasks frequently:
Threads in the thread pool are ready to perform any task given to them.
Instead of starting a new thread for every task to execute concurrently, the task
can be passed to idle threads in a thread pool.
As soon as the pool has any idle thread, the task is assigned to one of them and
executed. After completion of the job, the thread is returned to the thread pool to
be reused again.
Thread Pools are used in Servlet and JSP where container creates a thread pool to
process requests.
Let's create a result uploading thread pool to see how it works. Here is the
implementation of UploadResult:
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " (Start) " +
taskName);
// Code for uploading result: Dummy implementation
try {
Thread.sleep(2000);
} catch(InterruptedException e) { System.out.println(e.getMessage()); }
System.out.println(Thread.currentThread().getName() + " (End) " +
taskName);
}
}
And here is our main method that creates the thread pool:
Callable
There is a requirement to know whether the uploading of marks has been successful
or not.
The Callable interface has only one method, call(), which represents the task to be
completed by the thread.
The call() method uses generics to define its return type.
A callable thread class has to implement the Callable interface and provide an
implementation of the call() method in the same way as the run() method is
implemented while using the Runnable interface.
If we want to return true or false once upload is done, our UploadResult class
would look like this using Callable:
@Override
public Boolean call() throws Exception {
// Code to upload result
return Boolean.TRUE; // For successful upload
}
}
Future Object
A Future object represents the value that will be returned by a callable thread in
the future. This value can be retrieved using the get() method of the Future
object:
It also has methods to check if the computation is complete, to wait for its
completion, etc.
The Future object returned from a callable thread can be retrieved by using these
methods of ExecutorService:
Here's how we can check whether an upload has been successful or not:
Let's create a result uploading thread which can return a confirmation. Here is an
implementation:
@Override
public Boolean call() throws Exception {
System.out.println(Thread.currentThread().getName() + " (Start) " +
taskName);
// Code for uploading result: Dummy implementation
Boolean retValue = null;
try {
Thread.sleep(2000);
// code to Upload result
// Set retValue to true
retValue = Boolean.TRUE;
} catch(Exception e) {
System.out.println(e.getMessage());
// Set retValue to false
retValue = Boolean.FALSE;
}
System.out.println(Thread.currentThread().getName() + " (End) " +
taskName);
return retValue;
}
}
import java.util.concurrent.*;
public class RunTaskService {
public static void main(String[] args) {
ExecutorService exService = Executors.newSingleThreadExecutor();
UploadResult uploadCallable = new UploadResult("Batch 1");
Future<Boolean> future = exService.submit(uploadCallable);
try {
System.out.println("Upload completed: " + future.get());
}
catch(InterruptedException | ExecutionException e) {
System.out.println(e.getMessage());
}
}
}
Locks
Let's revisit synchronization. It allowed us to control access to shared resources.
Using Lock, we can use its tryLock() method to make sure threads wait for a
specific time only.
Moreover, synchronized can cover only one method or block, whereas, Lock can be
acquired in one method and released in another method.
Also, synchronized keyword doesn�t provide fairness, whereas, in Lock we can set
fairness to true so that the longest waiting thread gets the lock first.
ReentrantLock(boolean fair)
ReentrantLock can be repeatedly entered by the thread that currently holds the
lock.
We have a requirement to count the number of uploads. Here is what our UploadResult
class would look like:
Now we'll use executor service to manage the thread with a lock: