Sie sind auf Seite 1von 57

Java is a programming language created by James Gosling from Sun Microsystems (Sun)

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.

Target Audience - Developers

Concepts you must know before doing this course

Fundamentals of Java programming

Syntax and semantics of Java language

Object oriented programming using Java

Important Java classes like Object, String, LocalDate, LocalTime and Calendar

Recommended resources to learn the prerequisite concepts

Fundamentals of Java Programming (learn here)

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

Eclipse IDE (download here)

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

After completing this course, you will be able to:

Develop code that makes use of exceptions and exception handling clauses and
understand the effect of an exception raised in a code fragment.

Develop code using

Regular Expressions for String manipulation

Built in Annotations in Java


Collection APIs

Java Input/Output Stream for file handling

JDBC to connect to a database and persist and retrive the data

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

Try this code and observe the output

public class ExceptionDemo {

public static void divide(int x, int y) {
int z = x/y;

public static void main(String args[]) {

divide(10, 0);

What do you see?

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.

In the case above, an exception is generated inside divide(). Since divide()

doesn't take care of it, the exception propagates to its calling environment, which
is the main() method. The main() method also doesn't take care of it, and the
exception propagates to its calling environment, which is the runtime.

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
Checked exceptions occur at compile time, and should be handled or declared for

Unchecked exceptions occur at runtime, and need not be handled or declared for
An exception object can make use of the below methods of Throwable class:

Have a look at how to use the methods:

System.out.println("Exception message: " + throwable.getMessage());

System.out.println("Exception stack trace: " + throwable.printStackTrace());

System.out.println("Description of the exception: " + throwable.toString());

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

Whenever there is a chance of an exception to occur in a method, we have two


Handle the exception there itself


allow it to propagate to be handled somewhere else

We'll start with handling exceptions and later see how to allow propagation.

Handling exceptions involves using the try-catch block for constructing an

exception handler.

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

Now let's create an exception handler block for our ArithmeticException example

public static void divide(int x, int y) {

try {
int z = x/y;
catch(ArithmeticException e) {
System.out.println("The divisor should not be zero");

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.

public static void divide(int x, int y) {

try {
int z = x/y; // If an exception occurs here, the control jumps to
the first matching catch block
System.out.println(z); // Execution of this line will be skipped
catch(ArrayIndexOutOfBoundsException e) {
System.out.println("Index not found");
catch(ArithmeticException e) { // This is the matching exception
System.out.println("The divisor should not be zero");
System.out.println("Method execution ends"); // Program execution will
continue from this line
If no matching catch block is found, the exception will remain unhandled and will
be propagated.

If no exception is thrown inside a try block, the catch blocks following it are

A catch block which can handle objects of Exception class can catch all the
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.

If we need a similar exception handling logic for multiple exceptions, a multi-

catch block can be used. The catch blocks in Java 7 and higher can handle more than
one type of exception:

catch(Exception1 | Exception2 | Exception3 e) {

// Code for handling exceptions

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.

In such situations, the finally block plays an important role.

It always executes, irrespective of whether an exception occurs or not.

try {
// Code that can throw exceptions
catch(Exception1 e1) {
finally {
// Code to be executed no matter what

The finally block will be disrupted if an exception occurs inside it, or if

System.exit() is invoked before it.

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:

Exception e = new Exception();

throw e;
Any object of type Throwable can be thrown.

Exceptions also accept a message for themselves:

throw new Exception("The divisor should not be zero");

Having exceptions with custom messages increases the readability of our

Now let's see how we can use this in our example�

public static void divide(int x, int y) {

try {
if(y == 0)
throw new Exception("The divisor should not be zero");
int z = x/y;
catch(Exception e) {
The above code will generate an exception with the given message if the condition
is satisfied.

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!

If there is a checked exception which the method doesn't handle, it has to be

declared using the throws clause:

public static void divide(int x, int y) throws Exception {

if(y == 0)
throw new Exception("The divisor should not be zero");
int z = x/y;
If the exception happens to be thrown from this method, it will be propagated to
the calling method (main() method in our example). Because of the declaration,
main() will be aware and be prepared to handle the exception.

Or, main() could also just declare it to be thrown.

Have a look at the implementation of main() method, to handle the exception thrown
from the divide() method:

public static void main(String args[]) {

try {
divide(10, 0);
catch(Exception e) {
Note: Exceptions are either handled or declared to be thrown.

Checked Exceptions should be either declared in the throws clause of method or

handled inside the catch block. Exception and it subclasses are Checked Exceptions.

Few checked exceptions are:

Unchecked exceptions are not mandatory to either declare or handle and are ignored
at compile time. Error, RuntimeException and their subclasses are Unchecked

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

Having user-defined exceptions not only increases the flexibility of our

applications, but also makes the code more manageable.
We can create a user-defined exception class by extending the Exception class:

public class DivisionException extends Exception { }

We can also set messages by using the parameterized constructor:

public DivisionException(String message) {


Now we can use our user-defined exception in our example:

public static void divide(int x, int y) throws DivisionException {

if(y == 0)
throw new DivisionException("The divisor should not be zero");
int z = x/y;
Note: Extending RuntimeException instead of Exception will make the custom
exception unchecked.

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
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

If the record is supposed to hold only Books, we cannot restrict it to do so

Record bookRecord = new Record();

bookRecord.add(new Book("Java"));
bookRecord.add(new Student("Mark Smith")); // This will be allowed
The Object that we get from the record has to be type casted back to the required
type before use

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.

Book book = (Book) bookRecord.get(1); // Type casting Object to Book

Since we have added Student record at index 1 the above type cast will fail at run
time giving ClassCastException. Hence making type cast unsafe.

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.

In Java, this can be done using Generics.

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:

Type checking is done at compile time

Eliminates the need for casting

Generic algorithms can be implemented

The syntax to create a generic class is as follows:

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)

Now our Record class can be redefined using generics:

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

E is the type parameter

Elements of type E can be added into and retrieved from the Record

E can be used anywhere inside the Record class

Now our Record class is a generic class.

This allows us to declare Record<Student> studentRecord = new Record<Student>();

Then studentRecord.add(new Student("Mark Smith")) will be allowed but

studentRecord.add(new Integer(0)) will give a compilation error.

Also, it will be known to the compiler that studentRecord.get(32) will give a

Student. So, no type casting is required.

Generic class can be instantiated in the following ways:

Record<Integer> integerRecord = new Record<Integer>();

Record<String> stringRecord = new Record<String>();
Record<Professor> professorRecord = new Record<>(); // Java 7 or higher versions

Generic types can also be used without type parameters. Such a type is called raw
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.

Record<Object> objectRecord = new Record<Object>();

objectRecord.add(new Student("Mark Smith")); // Allowed
However, a Record<Student> object cannot be assigned to a Record<Object> reference

Record<Object> objectRecord = new Record<Student>(); // Compilation error

Among all the flexibility and security, Generics however, possess some

A type parameter cannot be instantiated

E e1 = new E(); // Compilation error

A generic class cannot extend the java.lang.Throwable class.

A generic type cannot be used with a primitive type. It must always be a reference

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.


<type-parameter-list> return-type method-name(parameter-list) { }

For example,

public class UserInterface {

public static <E> void display(E[] list) { // A generic method to display
the elements in an array
for (int i = 0; i < list.length; i++)
System.out.println(list[i] + ", ");

Generic methods can be invoked by prefixing the method name with the actual type in
angle brackets.

A generic method can return a generic type value.

A generic method can be declared inside a non-generic class.

We have a method to update records of students as follows:

class EdfordUtility {
public static void update(Record<Student> studentRecord) {
// Code to update the student records

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:

Record<DayScholar> dayScholarRecord = new Record<DayScholar>();

EdfordUtility.update(dayScholarRecord); // Compilation error:The method
update(Record<Student>) in the type EdfordUtility is not applicable for the
arguments (Record<DayScholar>)

How can we solve this?

Should we create separate methods for each type of record?

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-

? super T - Lower-bounded wildcard which supports types that are T or its super-

? - Unbounded wildcard which supports all types

Upper-bounded Wildcard:

Requirement: To have a method that should take an entity of type Student as well as
its sub-types.

Our update() method needs to be modified as follows:

public static void update(Record<? extends Student> studentRecord) {

// Code to update the records
The method above will accept parameters of type Record<Student>, Record<DayScholar>
and Record<Hosteller>

Lower-bounded Wildcard:

Requirement: To have a method that should take an entity of type DayScholar as well
as its super-types.

Our update() method needs to be modified as follows:

public static void update(Record<? super DayScholar> studentRecord) {

// Code to update the records
The method above will accept parameters of type Record<Student>,
Record<DayScholar>, Record<Object>
Unbounded Wildcard:

Requirement: To have a method that should take an entity of any type.

Our update() method needs to be modified as follows:

public static void update(Record<?> studentRecord) {

// Code to update the records
The method above will accept parameters of type Record<Student>, Record<Professor>,
Record<Object>, etc.

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?

We know arrays have advantages like

Compile time type checking

Holding primitive type data as well as objects

But they cannot grow and shrink dynamically. Moreover, they do not have any built-
in algorithm for searching or sorting.

To overcome these disadvantages, Java gives us the Collections Framework

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

The framework has the following advantages:

Ready to use classes and algorithms

Better program speed and quality

Reduced programming effort

These are some of the interfaces and classes in the Collections Framework:

The root of the collection hierarchy is the java.util.Collection<E> interface.

It provides the basic operations for manipulating elements in a collection.

Have a look at some of the important methods:

List Interface
Let's start with lists and see how they can be of help to us.

The java.util.List<E> represents an ordered collection of elements.

It is easy to use, and like arrays, allows index based access to its elements.
Also, duplicate elements are allowed.

Apart from the methods from the Collection interface, some methods specific to List

These are the classes implementing the List interface

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.

Lists can be created and used as below:

//Creating a list
ArrayList<Integer> numList = new ArrayList<>();
numList.add(new Integer(22));

// Using list

Let us define a class named Course as below:

public class Course {

String courseName;

public Course(String courseName) {

this.courseName = courseName;

public String toString() {
return courseName ;

Now we can have list of courses for students in the following way:

ArrayList<Course> courseList = new ArrayList<>();

courseList.add(new Course("Java"));
courseList.add(new Course("Hibernate"));
courseList.add(new Course("AngularJS"));

Best practice: Use generic type and diamond operator while declaring collections.

Accessing the elements of a collection is a very frequent and common operation.

Let's see the different ways of accessing the elements of a collection.

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
Now let's see how lists can be traversed using loops:

Using for loop:

for(int index=0; index < courseList.size(); index++) {


Using enhanced for loop (for-each):

for(Course c : courseList) { // Can be read as: for each Course c in courseList


Iterator follows the Iterator Design Pattern, and provides the following methods
for accessing and removing the elements of a collection:

Using Iterator:

Iterator<Course> courseIterator = courseList.iterator();

#while(courseIterator.hasNext()) {
Course c =;#
The code above sequentially traverses the list in the forward direction.

The ListIterator, which is exclusive to lists, is similar to an Iterator. It allows

bidirectional traversal and modification of elements.
Some useful additional methods of ListIterator are:

Let's have a look at a complete demo:

List<Course> courseList = new LinkedList<>();

courseList.add(new Course("Java"));
courseList.add(new Course("Hibernate"));
courseList.add(new Course("AngularJS"));

Iterator<Course> courseIterator = courseList.iterator();

while(courseIterator.hasNext()) {
Course c =;
System.out.println(c); // toString() method has been overridden
in the Course class

The output will be:


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.

The java.util.Set<E> represents a collection of unique items, i.e. it does not

allow duplicate elements.

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.

Let's have a look at some classes implementing the Set interface.

Note: The add() method will return false if our program attempts to add a duplicate

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.


Hash table is a data structure used to implement an associative array, a structure

that can map keys to values.

A Tree is a non-linear data structure made up of nodes that form a hierarchy

consisting of a root node and potentially many levels of additional nodes.

// Creating a new Set object of type Integer

Set<Integer> numberSet = new LinkedHashSet<>();

// Adding elements to the set


// Displaying the Set


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.

// Creating an iterator over the set

Iterator iter = numberSet.iterator();
while(iter.hasNext()) {

Output: 12
Note: For sets to detect duplicates among user-defined objects, the equals() and
hashCode() methods must be overridden.

Apart from preventing duplicates, if a set is required to have sorted elements, we

can use a TreeSet:

// Creating a new TreeSet object

Set<String> courseSet = new TreeSet<>();

// Adding elements to the Set

courseSet.add("Angular JS");

// Iterating over the set using enhanced for loop

for(String s: courseSet) {

Output: Angular JS

There is a requirement of linking student ID with their corresponding list of


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.

java.util.Map<K,V> represents a collection which allows mapping of keys and values

to form key-value pairs.

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.

To overcome this, Map provides us with methods to retrieve a Collection which we

can traverse.
There are three different approaches for this:

Working with the set of keys:

Set setOfKeys = map.keySet();

Working with the Collection of values:

Collection valueCollection = map.values();

Working with the set of entries:

Set<Entry> setOfEntries = map.entrySet();

The java.util.Map.Entry interface provides two useful methods:

getKey() returns the key from an entry

getValue() returns the value from an entry

Working with maps

A map to associate student IDs to their courses can be created in the following

Set<Course> courseSet1 = new HashSet<>();

courseSet1.add(new Course("Java"));
courseSet1.add(new Course("DBMS"));

Set<Course> courseSet2 = new HashSet<>();

courseSet2.add(new Course("PHP"));
courseSet2.add(new Course("HTML"));
courseSet2.add(new Course("CSS"));

Map<Integer, Set<Course>> studentCourses = new HashMap<>();

studentCourses.put(1001, courseSet1);
studentCourses.put(1002, courseSet2);

Now map elements can be accessed in one of the following ways:

//Retrieving the set of Courses by studentID using get() method

Set<Course> courseSet = studentCourses.get(1001);

// Iterating over the set of keys using for-each loop

Set<Integer> setOfKeys = studentCourses.keySet();
for(Integer i : setOfKeys) {

// Iterating over the collection using values() method

for(Set<Course> courseSet : studentCourses.values()) {

Choose the right collection based on the requirement to improve performance , as

If duplicate elements are not allowed choose Set otherwise choose List.

ArrayList is faster than LinkedList to randomly access elements.

For quick removal and addition LinkedList is better than ArrayList.

Use collections such as TreeMap, TreeSet when elements in the collection to be

sorted and ordered.

Use concurrent collections to support concurrent access

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:

ArrayList<String> companies = new ArrayList<>();



// Output: [Google, IBM, Infosys]

Like sort(), the Collections class contains several other utility methods like
reverse(), shuffle(), swap(), etc.

Collections.sort() will work on any kind of element which implements the Comparable
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.

The Comparable Interface is a part of java.lang package. It has a single method

compareTo() which should return a negative, zero or a positive number based on the

So after implementing the Comparable interface, our Course class would look like

public class Course implements Comparable<Course> {

public int courseId;
public String courseName;

public Course(int courseId, String courseName) {

this.courseId = courseId;
this.courseName = courseName;

public int compareTo(Course otherCourse) {
return this.courseName.compareTo(otherCourse.courseName);

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:

ArrayList<Course> courseList = new ArrayList<>();

courseList.add(new Course(124, "AngularJS"));
courseList.add(new Course(120, "Java"));
courseList.add(new Course(121, "Hibernate"));


// output: [124:AngularJS, 121:Hibernate, 120:Java]

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.

Set<Course> courseSet = new TreeSet<>();

courseSet.add(new Course(124, "AngularJS"));
courseSet.add(new Course(120, "Java"));
courseSet.add(new Course(121, "Hibernate"));

// output: [124:AngularJS, 121:Hibernate, 120:Java]

Best practice: Implement the Comparable interface to custom types. When their
elements are added to collections that sort elements by natural ordering, such as
TreeSet and TreeMap. It also helps to sort elements in a list collection based on
the natural ordering of the elements.

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.

This can be done with the help of comparator objects.

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

public interface Comparator<T> {

public int compare(T o1, T o2);

So we can have a class implementing the Comparator interface like this:

public class CourseIdComparator implements Comparator<Course> {

public int compare(Course c1, Course c2) {
return c1.courseId - c2.courseId;
Now let's use our Comparator to provide the sorting logic:

ArrayList<Course> courseList = new ArrayList<>();

courseList.add(new Course(124, "AngularJS"));
courseList.add(new Course(120, "Java"));
courseList.add(new Course(121, "Hibernate"));

Collections.sort(courseList, new CourseIdComparator());


// output: [120:Java, 121:Hibernate, 124:AngularJS]

For use with TreeSet and TreeMap classes, the Comparator instances should be passed
to their constructors:

TreeSet<Course> courseSet = new TreeSet<>(new CourseIdComparator());

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 validateMobileNumber( String mobileNumber) {

boolean valid=true;
int length=mobileNumber.length();
if (length == 10) {
for (int i=0; i<length; i++) {
if (Character.isDigit(mobileNumber.charAt(i)) != true) {
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.

An easier way of performing such validations is by using Regular Expressions.

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.

The Regex API gives us the following:

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...

Now let's try to validate mobile numbers using a regular expression.

The conditions are:

It should be a 10 digit number

Hyphens should come after the 3rd and the 6th digits

Examples: 881-123-3333, 881-123-3334, 881-123-3335

First, let's create the regex pattern.

We need 3 digits in the beginning. A pattern for that could be \\d{3}
This is followed by a hyphen and another 3 digits, i.e. \\d{3}-\\d{3}
Following the same approach, the final regex pattern will be \\d{3}-\\d{3}-\\d{4}

Now let's use the regex API to validate a mobile number:

public boolean validateMobileNumber(String mobileNumber) {

Pattern regex = Pattern.compile("\\d{3}-\\d{3}-\\d{4}");
Matcher mobileMatcher = regex.matcher(mobileNumber);
return mobileMatcher.matches();
The method above will return true for any string that matches the given pattern.

Now let's try validating names.

Names should contain only alphabets and optional white spaces.

For example: Tom, Tom Jerry, Tom and Jerry

The regex pattern for this would be ([A-Za-z]+\\s?)+

Which means a combination of lowercase alphabets, uppercase alphabets followed by
an optional white space. And the same pattern can repeat any number of times to
form a group of words.

The method to validate a name would be:

public boolean validateName(String name) {

Pattern regex = Pattern.compile("([A-Za-z]+\\s?)+");
Matcher nameMatcher = regex.matcher(name.trim());
return nameMatcher.matches();
This method will return true for any string that matches the given pattern.

Note: java.lang.String.trim() method eliminates leading and trailing spaces.

Best practice: Its preferred to use Pattern and Matcher classes than
String.matches, as it compiles the regular expression each time they are called.


Edford wanted to hire part-time professors to fulfill their increased load.

A new class needed to be created to represent these visiting professors. We already

had a Professor class with similar functionalities, especially to calculate salary.

class Professor {
// Other class members

public double calculateSalary() {

// Calculate and return the salary of a professor
So this class could be reused by extending it. But the salary calculation turned
out to be slightly different, and our developers decided to override it:

class VisitingProfessor extends Professor {

public double calcualteSalary() {
// Calculate and return the salary of a visiting professor
This would allow us to:

Professor professor = new VisitingProfessor();


This did not work!

Every time a visiting professor's salary was required, we got the salary of a
professor instead.

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:

class VisitingProfessor extends Professor {

public double calcualteSalary() { // Compilation error: The method
calcualteSalary() of type VisitingProfessor must override or implement a supertype
// Calculate and return the salary of a visiting professor

This way the compiler checks the method signature and makes sure the parent method
is overridden.

Just like @Override, Java provides us with other built-in annotations.

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

Compile-time and deployment-time processing - Software tools can process annotation

information to generate code, XML files, etc.

Run-time processing - Some annotations are available to be examined at run-time

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:

Marker (without any element)

Single-valued (with a single element)

Multivalued (with multiple elements)

Sometimes we come up with a better version of an existing functionality which is

more efficient. Being better, this new functionality would need to be used

How can we make sure the new functionality is being used? Shall we delete the old

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.

The annotation for this would be @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:

EdfordUtility.update(recordObject); // Calling a deprecated method works, but

shows a strikethrough

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.

public static void main(String args[]) {
Professor professor1 = new Professor(); // Line where an unused local
variable exists
Professor professor2 = new VisitingProfessor();

Unlike @Deprecated and @Override annotations, @SuppressWarnings takes values.

A specific warning can be suppressed by mentioning that particular warning as
value. Some of the values that could be given are:





Also, a combination of multiple values can be given as a String array.

It can be used with classes, interfaces, constructors, methods, fields, parameters,
and local variables.

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.

First of all, this is how a custom annotation can be declared:

public @interface ChangeDetail {

String authorName();
String methodName();

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.

Based on the requirement, define @Documented, @Retention, @Target, @Inherited for

the annotation.

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.*;

public @interface ChangeDetail {
String authorName();
String methodName();

Now we can use this annotation in code wherever needed:

abstract class NewStudent {

private String studentName;

public String getStudentName() {

return studentName;

public void setStudentName(String studentName) {

this.studentName = studentName;
abstract double calculateFee();

@ChangeDetail(authorName="Emily", methodName="calculateFee")
class DayScholar extends NewStudent {
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.

To make the annotation available till run-time, the meta annotation

@Retention(RetentionPolicy.RUNTIME) must be used.

Let us look at the code sample for getting details of @ChangeDetail

public class CustomAnnotationDemo {

public static void main(String[] args) throws Exception {
NewStudent student = new DayScholar();
Class c = student.getClass();
//Fetches the list of annotations of the class, DayScholar
Annotation annotations[] = c.getDeclaredAnnotations();
for (Annotation annotation : annotations) {
//Print the name of the annotation
//Fetches the list of attributes of the annotation
Method[] fields = annotation.annotationType().getDeclaredMethods();
for (Method field : fields) {
//Print the attribute name
//Fetch the value for the attribute
Object value = field.invoke(annotation, (Object[]) null);
System.out.println("\t" + value);

// Output:
authorName Emily
methodName calculateFee

With the current implementation of @ChangeDetails, we can use it only once in a

class. What we need is to maintain the list of people who have modified the class.
For this purpose, Java provides @Repeatable meta annotation since Java 8.

@Repeatable enables the annotation to be repeated at a place for any number of

So if multiple authors involved, @ChangeDetail can be repeated for each one of

Let's have a look at how such an annotation can be defined:

public @interface ChangeDetail {
String authorName();
String methodName();

public @interface ChangeDetails { // The definition for repeatable annotation
ChangeDetail[] value();

Now, @ChangeDetail can be repeated in the Student class:

@ChangeDetail(authorName="Emily", methodName="calculateFee")
@ChangeDetail(authorName="Mark", methodName="calculateBusFee")
class DayScholar extends NewStudent {
public double calculateFee() {
// Code for calculating total fee which includes bus fee

public double calculateBusFee() {

// Code for calculating bus fee

I/O Stream

Let's see how we can use FileOutputStream to write to a file:

// Create a stream instance to write to a file

FileOutputStream outFile = new FileOutputStream("CandidateNames.txt");

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

// Close the stream to release resources held by it


The output of the program is a file (CandidateNames.txt) created in the project

location with the following content:

Ahalya Bhairav Chitra Dushyant Eshwari Falgun Gargi Hiren

Now let's see how we can use FileInputStream to read from our file:

// Create an stream instance with the file to read as input

FileInputStream inFile = new FileInputStream("CandidateNames.txt");

// Read the first byte

int i =;

// Keep reading till end of file

while (i != -1) {
// Read the next byte
i =;

// Close the stream to release resources held by it


The output of the program will be displayed on the console as:

Ahalya Bhairav Chitra Dushyant Eshwari Falgun Gargi Hiren

Let's see how we can use FileReader to read a file's content and FileWriter to
write the same into another file:


public class FileStreamTester {

public static void main(String[] args) throws IOException {

Reader inFile = null;

Writer outFile = null;
try {
inFile = new FileReader("ReadFrom.txt");
outFile = new FileWriter("WriteTo.txt");

int i =;
while(i != -1) {
i =;
catch(IOException io) {
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:

BufferedInputStream and BufferedOutputStream create buffered byte streams

BufferedReader and BufferedWriter create buffered character stream

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 process of passing a FileReader instance to a BufferedReader object is called


Reader inFile = new FileReader("ReadFrom.txt");

BufferedReader readBuffer = new BufferedReader(inFile); // BufferedReader wraps
over FileReader to add buffering capability
Writer outFile = new FileWriter("WriteTo.txt");
BufferedWriter writeBuffer = new BufferedWriter(outFile); // Similarly wrapping

The code example to write the content of one file into another can now been
modified to perform buffered read and write operations:

public static void main(String[] args) throws IOException {

Reader inFile = null;

Writer outFile = null;
try {
inFile = new BufferedReader(new FileReader("ReadFrom.txt"));
outFile = new BufferedWriter(new FileWriter("WriteTo.txt"));

int i =;
while(i != -1) {
outFile.write(i); // Writing into the buffer
i =;
catch(IOException io) {
finally {
// Closing will first flush the buffers
if(inFile != null) inFile.close();
if(outFile != null) outFile.close();

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.

Have a look at this code:

BufferedWriter bw = null;
try {
bw = new BufferedWriter(new OutputStreamWriter(new
// Writing unicode string
System.out.println("Data written successfully");
catch(IOException ioe) {
System.out.println("ERROR: " + ioe.getMessage);
finally {
if(bw != null) bw.close();

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.

Have a look at the code:

BufferedReader br = null;
try {
br = new BufferedReader(new InputStreamReader(new
System.out.println("Data in the file is:");
int data =;
while(data != -1) { // Checking for the end of
System.out.print((char) data);
data =; // 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.

try( InputStream is = new FileInputStream();

OutputStream os = new FileOutputStream(); )
// Reading/Writing
// Rest of the code
catch(Exception e) {
// Code
finally {
// It is not necessary to invoke close() method now, as it is invoked
automatically when this try-catch-finally section ends
So far we've been creating, writing and reading files using file streams. What if
we want to do more with files in our Java application?

The File class of 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.

A File object can be created in this way

File file = new File("Readme.doc")

File demo
Have a look at the following code sample which shows how to create and use a File


public class FileDemo {

public static void main(String args[]) throws Exception {
File file = new File("D:\\Test\\Demo.txt");
boolean isCreated = file.createNewFile();
System.out.println("File created: " + isCreated);
System.out.println("Name of the file: " + file.getName());
System.out.println("Path of the file: " + file.getPath());
System.out.println("isDirectory: " + file.isDirectory());
System.out.println("list: " + file.list());

The output of the code will be:

File created: true

Name of the file: Demo.txt
Path of the file: D:\Test\Demo.txt
isDirectory: false
list: null

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

The class represents a random access file. This file

behaves like a large array of bytes stored in the file system.
It offers a seek feature that can take us directly to a particular position in the

Unlike the input and output stream classes in, 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.

It provides two constructors:

Two important values of mode are:

r - Read only mode. Invoking any of the write methods of the resulting object will
cause IOException to be thrown.
rw - Open for reading and writing. If the file does not already exist then an
attempt will be made to create it.

For example:

RandomAccessFile randomFile = new RandomAccessFile("Output.txt", "r");

Here is a program performing some operations on a RandomAccessFile object:


public class RandomAccessDemo {

public static void main(String args[]) throws Exception {
RandomAccessFile randomFile = new
RandomAccessFile("D:\\Test\\Output.txt", "rw");
System.out.println("Current position:" +
System.out.println("Current position:" + randomFile.getFilePointer());

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.

serialization and deserialization

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

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

parse the text file

create objects

populate appropriate values to each object

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

We will begin with serialization.

In the application, student data is present as an object, and not in byte or text
The process of converting an object into a stream of bytes is called Serialization.

For any object to be serialized, the concerned class must implement the interface.
Serializable is a marker interface (has no body). It is just used to "mark" Java
classes to support serialization.

Have a look at the Student class:

public class Student implements Serializable {

private int studentId;
private String firstName;
private String lastName;
private String dateOfBirth;
private Set<Course> courses;
private int age;
//Code for getter and setter methods

public Student(int studentId, String firstName, String lastName, String

dateOfBirth, Set<Course> courses, int age) {

this.studentId = studentId;
this.firstName = firstName;
this.lastName = lastName;
this.dateOfBirth = dateOfBirth; = courses;
this.age = age;

Serialization can be achieved by using the ObjectOutputStream class.

Have a look at the following code that serializes a student object:

// Create a stream instance to write to a file

FileOutputStream outFile = new FileOutputStream("StudentData.bin");

Set<Course>courses=new HashSet<>();
courses.add(new Course("Java"));
courses.add(new Course("Python"));

// Get a student object

Student student=new Student(20156,"Peter","Johnson","29/05/1995",courses,23);

// Create ObjectStream
ObjectOutputStream objStream = new ObjectOutputStream(outFile);

// Write the student object



Deserialization is the process of reading an object from a stream of bytes.

ObjectInputStream can be used for for this purpose.

Have a look at how it is done:

FileInputStream inFile = new FileInputStream("StudentData.bin");

ObjectInputStream objStream = new ObjectInputStream(inFile);

// Read an object from the file stream

Student student = (Student) objStream.readObject();

// Rest of the code


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?

You will get The reason is, Java automatically

assigns a unique version identifier called SerialVersionUID to every serializable
class, based on the structure of the class. Since the updated class's version id
does not match with the version id of the serialized object, an exception will be
thrown during deserialization.

However, if we think about it, why should an exception be thrown? Wouldn�t it be

better if the new field is set with default value during deserialization?

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.

The attribute that is defined as transient will not be serialized.

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 transient int age;

A few things to keep in mind about Serializable:

If a class implements Serializable, all its sub classes will also become

If a class implementing Serializable has a reference of another class, all such

classes must implement Serializable.
Otherwise, NotSerializableException will be thrown at runtime.

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.


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 - This class interacts with database and fetches all the information
related to a student. How can StudentDAO connect to database and get the required

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

JDBC Driver

As a Java environment doesn't know how to interact with a database environment,

we'll need an interface between the two.
A driver is such an interface which will act like a translator between the two

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.

There are 4 Types of Drivers:

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

Type 3 (Network Protocol Driver): A three-tier approach is used to access databases

and is extremely flexible. If multiple databases are accessed by a Java application
then this type of driver is preferred.

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.

The process of interacting with a database consists of the following steps:

1. Load the driver

2. Make a connection to the database

3. Send SQL queries to the database

4. Process the result

Step 1 is to load and register a JDBC Driver.

This can be done by using either Class.forName() or DriverManager.registerDriver().

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.


Or DriverManager class can be used to register a driver :

Driver orclDriver = new oracle.jdbc.driver.OracleDriver();


Step 2 is to connect to the database.

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:

getConnection(String url) - url is a database address that points to the database

getConnection(String url, Properties info) - info is a list of String tag/value

pairs as connection arguments eg: username/password

getConnection(String url, String user, String password) - user is the database

user's username on whose behalf the connection is being established, and password
is the user's password

Have a look at the URL formats of some Databases:

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:

String url = "jdbc:oracle:thin:Mark/passwd@kecmachine:1521:edford";

Connection conn = DriverManager.getConnection(url);

String url = "jdbc:oracle:thin:@kecmachine:1521:edford";

String user = "Mark";
String password = "passwd";
Connection conn = DriverManager.getConnection(url, user, password);

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.

Step 3 is to send an SQL statement to the database.

This is done in two parts:

creating a statement

sending and executing it

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:

// Create and get the connection
String url = "jdbc:oracle:thin:Mark/passwd@kecmachine:1521:edford";
Connection conn = DriverManager.getConnection(url);

// Create the statement

Statement stmt = conn.createStatement();
// Execute the query
ResultSet rs = stmt.executeQuery("select * from course where type='online'");
// Rest of the code


PreparedStatement interface helps us to work with precompiled SQL statements.

Precompiled SQL statements are faster than normal SQL statements. So if an SQL
statement is to be reused, it is better to use PreparedStatement.

Have a look at how to work with a prepared statement:

// Create the prepared statement

PreparedStatement preStmt = conn.prepareStatement("select * from faculty where
ResultSet rsFaculty = preStmt.executeQuery();
The query is compiled once and kept ready with an execution plan, which can be
reused every time the record needs to be accessed. This improves the performance of
the application.

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:

String facultyId = "1001";

PreparedStatement preStmt = connection.prepareStatement("SELECT * FROM faculty
WHERE faculty_id = "+facultyId);
Now, the SQL statement fired to the database will be

SELECT * FROM faculty WHERE faculty_id = 1001

This query can be reused several times with different values of the facultyId.

But this way of passing values dynamically as strings is proved to be unsafe.

If a malicious code enters the value of facultyId as "1001 or 1=1", then the query

SELECT * FROM faculty WHERE faculty_id = 1001 or 1 = 1

What will be the result of above query?

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

String sql = "SELECT * FROM faculty WHERE faculty_id = ? ";

//indicates position of parameter in the query
PreparedStatement preStmt = connection.prepareStatement(sql);
preStmt.setInt(1001, facultyId); //setting the
parameter value
Here �?� is used in the query to indicate the position of parameter which starts
from 1(one). The parameter can be bound with a value using setXXX(parameterIndex,
value) of PreparedStatement, where XXX represents the Java data type of the
parameter that we wish to bind.

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.

Best practice: Its preferred to use preparedstatement as it provides features like

prevention from SQL injection, precompiled SQL queries and use of bind variables

CallableStatement interface helps us to call stored procedures and functions.

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.

// Create a callable statement

CallableStatement callStmt = conn.prepareCall("{call registerStudent(?,?}");
// Set the student_id parameter
callStmt.setInt(1, 1001);
// Register the out parameter
callStmt.registerOutParameter(2, java.sql.Types.INTEGER);

// Get the out parameter value
int outRet = callStmt.getInt(2);

System.out.println("Registration No: " + outRet);

Step 4 is to process the result.

The java.sql.ResultSet interface represents the result of a database query.

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.

Viewing and Manipulating Results

Commonly used navigational methods of ResultSet are:

Commonly used methods to view the data of ResultSet are:

Similarly, there are get methods in the ResultSet interface for each of the Java
primitive types.

Commonly used methods to update the data of ResultSet are:

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
// Create and get the connection
conn =
// 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 ( {
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");
} catch (SQLException se) {
System.out.print("SQL Exception occurred");
} finally {
if (preStmt != null)
if (conn != null)
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.

Type of a ResultSet implies how it allows us to manipulate its cursor.

The concurrency of a ResultSet object implies how it gets updated with concurrent
changes on the underlying data source.

When we create a statement as below, it returns a ResultSet object with default

type FORWARD_ONLY and concurrency level CONCUR_READ_ONLY.

Statement stmt = conn.createStatement();

The overloaded method below takes the type and concurrency level of ResultSet
object that the statement will return upon execution.

Statement stmt = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,

// This will return a ResultSet object that is scroll-sensitive and doesn't allow
concurrent modifications

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:

PreparedStatement preStmt = conn.prepareStatement("Insert into Student

values(?,?,?,?)")) { // conn is the connection object

// Setting parameters
preStmt.setInt(1, 1001);
preStmt.setString(2, "John");
preStmt.setString(3, "Mysore");
preStmt.setLong(4, 9213456780l);

int rowsAffected = preStmt.executeUpdate(); // Executing the query

if(rowsAffected > 0){ // processing the result

System.out.println(rowsAffected + " rows inserted successfully");

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


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.

For such situations, JDBC provides transactions.

A transaction treats one or more statements as a single logical entity.
If and only if all the statements finish successfully, the transaction is
considered successful and is committed.
In any other case (even if one statement fails), the entire transaction is assumed
to have failed and is rolled back.

A transaction can be created using the following methods of the Connection


setAutoCommit(boolean autoCommit) - Sets the auto-commit mode of the connection. It

is true by default.

commit() - Commits the transaction and makes all its changes permanent in the

rollback() - Discards the changes made inside the transaction and reverts to the
previous consistent state of the database.

Have a look at the following code that works with a transaction:

// Turn off auto-commit

try {
// Code for inserting student registration details
// Code for inserting courses for which the student has registered
// Only if both the above insert operations are successful, changes must be
made permanent in the database
catch(SQLException e) {
// If an exception occurs in any of the insert statements, rollback the changes

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

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.

To solve this, we need to enable our application to perform multiple activities


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.

Java allows multiple parts of a program to run simultaneously.

A sequence of statements in a program (process) that are executed independent of

the rest of the program is called a thread.

The programming technique where multiple threads are executed concurrently is

called 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.

In enterprise application environments, multithreading is used by Application

Servers for maintaining thread pools to serve multiple users simultaneously. A
thread pool is a group of ready-made threads with no overhead of creating new ones.

In Java, threads can be created in two ways.

The first way is to extend the java.lang.Thread class.

Follow these steps to create a thread by extending the Thread class:

Define a class that extends the java.lang.Thread class

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 UploadResult extends Thread {

public void run() {
// Thread implementation

class ThreadTester {
UploadResult uploadThread = new UploadResult();
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.

The other way of creating threads is to implement the java.lang.Runnable interface.

It contains only one abstract method, run(), which needs to be overridden to define
the thread's task.

Follow these steps to create a thread by implementing the Runnable interface:

Define a class that implements the java.lang.Runnable interface

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

Implementing our functionality using Runnable:

class UploadResult implements Runnable {

public void run() {
// Thread implementation

class Test {
public static void main(String[] args) {
UploadResult uploadRunnable = new UploadResult();
Thread threadObj = new Thread(uploadRunnable);
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:

Note: The Thread class implements the Runnable Interface

Thread Methods
Here are some useful methods of the Thread class:

Have a look at a demo:

public static void main(String[] args) throws InterruptedException

System.out.println("Main thread starts");
MyThread t = new MyThread(); // MyThread extends Thread
System.out.println(t.isAlive()); // true
t.join(); // main method waits for thread t to
System.out.println(t.isAlive()); // false
Thread.sleep(3000); // main method sleeps for 3 seconds
System.out.println("Main thread ends");

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:



Non-Runnable (Sleeping, Waiting, Blocked)


A thread is said to be alive if it is in the Runnable, Running and Non-Runnable


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.

Java runtime system�s thread scheduling algorithm is preemptive but it depends on

the implementation. Solaris is preemptive, Macintosh and Windows are time sliced.
In theory, a thread with high priority should get more CPU time, but practically it
may depend on the type of scheduling algorithm of the Operating System as well.

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

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.

Here is the Course class:

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:

class RegisterThread extends Thread {

Course c;

RegisterThread(Course c) {
this.c = c;
public void run() {
And the main method:

public static void main(String args[]) {

Course cse = new Course("CSE", 1);
RegisterThread regObj1 = new RegisterThread(cse);
RegisterThread regObj2 = new RegisterThread(cse);
What do you think will happen?

The output turned out to be this:

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.

Java provides the synchronized keyword for this.

In a multithreaded environment, two or more threads may access a shared resource.

Synchronization is used to ensure that only one thread can access the shared
resource at a time.

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.

Here is a synchronized method:

public synchronized void myMethod() {

// Code
And a synchronized block:

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:

public synchronized 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());

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

Inter Thread Communication

The technical team at Edford University wants the registration thread to wait for
some time to check if someone cancels his/her seat. This way there will be more
chances for someone booking a seat to get one.

For this, the registration thread will need to communicate with the cancellation
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.

Communicating Among threads

The methods discussed can be used in inter-thread communication as follows:

If a thread needs to wait for an object's state to change while executing a

synchronized method, it may call wait()

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

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

The code for synchronization should be written carefully to prevent a dead-lock


Applying the same to our scenario by modifying the registerForCourse() method of

the Course class:

public synchronized void registerForCourse(int rollNo) {

try {
if(this.numOfSeats - 1 < 0) {
this.wait(5000); // This releases the lock on the
object and waits for 5 seconds
if(this.numOfSeats - 1 < 0) // Check if any seat got released
by the Cancellation
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());

The corresponding cancelSeats() method is added in the Course class that notifies
our registerForSeats() method:

public synchronized void cancelSeats() {

try {
this.numOfSeats += 1;
System.out.println("Cancellation successful!");
System.out.println("Available seats: " + this.numOfSeats);
this.notify(); // Notifies one of the waiting threads to resume
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
And the cancellation thread class:

class CancelRegistration extends Thread {

Course c;

CancelRegistration(Course c) {
this.c = c;
public void run() {

Here is the main method:

public class CourseRegistrationDemo {

public static void main(String args[]) throws InterruptedException {
Course cse = new Course("CSE", 1);
RegisterThread regObj1 = new RegisterThread(cse);
RegisterThread regObj2 = new RegisterThread(cse);

// Running a cancellation thread after 2 seconds
CancelRegistration cancelObj1 = new CancelRegistration(cse);

The output will be:

Booking successful!
Available seats: 0
Cancellation successful!
Available seats: 1
Booking successful!
Available seats: 0

Thread Groups

Applications usually have a lot of threads to cater to various kinds of activities.

A number of these threads perform similar tasks, and are easier to manage when
grouped together. For example, five threads performing downloading of files can be
grouped together as download threads.

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.

Thread groups can be created using the following constructors:

Creating a group for download threads:

ThreadGroup downloadTG = new ThreadGroup("DownloadThreads");

Thread downloadThread1 = new Thread(downloadTG, "Download Thread 1");
Thread downloadThread2 = new Thread(downloadTG, "Download Thread 2");
Thread downloadThread3 = new Thread(downloadTG, "Download Thread 3");

Sometimes, threads need to run in the background to perform background activities.

Such threads are called daemon threads.

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()

Garbage Collector is one of the examples of a Daemon thread in Java.


While working with multi-threaded applications, a developer needs to take care of

several aspects of threads like

creating and supervising a number of threads

synchronizing them

handling their communication

organizing their individual results

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

reduces programmer's effort and improves maintainability

increases performance and reliability

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:

The executor framework provides abstraction over multithreading by allowing easier

creation and management of threads.

It is part of the java.util.concurrent package, and facilitates standardized

invocation, scheduling and execution of threads.
An Executor initiates and controls the execution of threads, thereby improving the
maintainability of programs.
The java.util.concurrent.Executor interface provides the execute() method:

The java.util.concurrent.ExecutorService interface extends Executor. Following are

some of the methods provided by ExecutorService:

And following are the factory methods from the java.util.concurrent.Executors class
for creating executor services with thread pools:

Remember how we created our UploadResult thread earlier!

UploadResult uploadRunnable = new UploadResult(); // UploadResult implements


Thread threadObj = new Thread(uploadRunnable);


The same thing can be done using the executor framework:

UploadResult uploadRunnable = new UploadResult(); // UploadResult implements


ExecutorService exService = Executors.newSingleThreadExecutor();

Hence, Executor takes care of thread management activities, without having us deal
with the Thread class directly.

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.

Here's how we can create a a thread pool using Executors:

ExecutorService exService = Executors.newFixedThreadPool(4);

This creates a fixed size thread pool. Cached thread pools can be used if an
expandable pool is required.

Let's create a result uploading thread pool to see how it works. Here is the
implementation of UploadResult:

public class UploadResult implements Runnable {

private String taskName;
public UploadResult(String name) {
taskName = name;

public void run() {
System.out.println(Thread.currentThread().getName() + " (Start) " +
// Code for uploading result: Dummy implementation
try {
} catch(InterruptedException e) { System.out.println(e.getMessage()); }
System.out.println(Thread.currentThread().getName() + " (End) " +

And here is our main method that creates the thread pool:

public static void main(String[] args) {

ExecutorService exServicePool = Executors.newFixedThreadPool(2); // Creating
a thread Pool of size 2
int noOfUploadTasks = 3; // Assuming we have 3 UploadResult task which needs
to be processed
for (int i = 1; i <= noOfUploadTasks; i++) {
UploadResult uploadRunnable = new UploadResult("UploadResult:" + i);
while (!exServicePool.isTerminated()) { } // Checking if shutdown is
System.out.println("Finished all threads");

Here is the output:

pool-1-thread-2 (Start) UploadResult:2

pool-1-thread-1 (Start) UploadResult:1
pool-1-thread-2 (End) UploadResult:2
pool-1-thread-1 (End) UploadResult:1
pool-1-thread-2 (Start) UploadResult:3
pool-1-thread-2 (End) UploadResult:3
Finished all threads
The threads in the thread pool are assigned each of the UploadResult task. Notice
how thread 2 is reused to execute the 3rd UploadResult task.


There is a requirement to know whether the uploading of marks has been successful
or not.

We will look into Callable interface followed by Future interface.

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:

public class UploadResult implements Callable<Boolean> {

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

If the result is ready, it will be returned

If not, the calling thread will be blocked

It also has methods to check if the computation is complete, to wait for its
completion, etc.

Executing callable threads

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:

UploadResult uploadCallable = new UploadResult();

ExecutorService exService=Executors.newSingleThreadExecutor();
Future<Boolean> future = exService.submit(uploadCallable);
try {
System.out.println("Upload successful: " + future.get());//Getting value from
Future object
} catch (InterruptedException e) {
} catch (ExecutionException e) {

Let's create a result uploading thread which can return a confirmation. Here is an

public class UploadResult implements Callable<Boolean> {

private String taskName;
public UploadResult(String name) {
taskName = name;

public Boolean call() throws Exception {
System.out.println(Thread.currentThread().getName() + " (Start) " +
// Code for uploading result: Dummy implementation
Boolean retValue = null;
try {
// code to Upload result
// Set retValue to true
retValue = Boolean.TRUE;
} catch(Exception e) {
// Set retValue to false
retValue = Boolean.FALSE;
System.out.println(Thread.currentThread().getName() + " (End) " +
return retValue;

Here is the main method that creates the callable threads:

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) {

Here is the output:

pool-1-thread-1 (Start) Batch 1

pool-1-thread-1 (End) Batch 1
Upload completed: true

Let's revisit synchronization. It allowed us to control access to shared resources.

However, a limitation here is that synchronized threads might end up waiting

indefinitely for the lock.
The java.util.concurrent.locks package provides an alternative to the synchronized
keyword - the Lock interface.

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.

Here are some methods in the Lock interface:

One of the implementations of the Lock interface is ReentrantLock.

It has an additional constructor where fairness policy (longest waiting thread to

acquire lock first) can be set to true.

ReentrantLock(boolean fair)
ReentrantLock can be repeatedly entered by the thread that currently holds the

The following code is used for using a lock:

Lock lock = new ReentrantLock();

// Critical code section

We have a requirement to count the number of uploads. Here is what our UploadResult
class would look like:

class UploadResultWithLock implements Runnable {

private ReentrantLock lock;
private int resultCount;
public UploadResultWithLock(ReentrantLock lock) {
this.lock = lock;
public void run() {
System.out.println(Thread.currentThread().getName() + " (Start) - Results
uploaded: " + resultCount);
// Code for uploading result: Dummy implementation
try {
catch(InterruptedException e) {
finally {
System.out.println(Thread.currentThread().getName() + " (End) - Results
Uploaded: " + resultCount);
Here, lock is being used to synchronize the section uploading the result and
incrementing the counter.

Now we'll use executor service to manage the thread with a lock:

public static void main(String[] args) {

ExecutorService exServicePool = Executors.newFixedThreadPool(2); //
Creating a thread Pool of size 2
int noOfUploadsTask = 3; // Assuming we have 3 UploadResult tasks which
need to be processed
ReentrantLock lock = new ReentrantLock(); // The lock to be used
UploadResultWithLock uploadRunnable = new UploadResultWithLock(lock);
for (int i = 1; i <= noOfUploadsTask; i++) {
while (!exServicePool.isTerminated()) { }
System.out.println("Finished all threads");

Here's the output:

pool-1-thread-2 (Start) - Results uploaded: 0

pool-1-thread-1 (Start) - Results uploaded: 0
pool-1-thread-2 (End) - Results uploaded: 1
pool-1-thread-2 (Start) - Results uploaded: 1
pool-1-thread-1 (End) - Results uploaded: 2
pool-1-thread-2 (End) - Results uploaded: 3
Finished all threads