Вы находитесь на странице: 1из 49

BSC :310 ADVANCED JAVA

NOMINAL HOURS -160

COURSE AIM: To equip students with knowledge in the advanced concepts of


programming in Java

LEARNING OUTCOMES: By the end of this course students should be able to:

1. Apply the advanced concepts of Java Collections


2. Apply the advanced concepts of Multithreading
3. Explain Networking in relation to programming
4. Understand the Enterprise Java Bean
5. Understand the Java Database Connectivity
6. Apply the concepts of Servlets
7. Understand the basics of JavaServer Pages
8. Understand the concept of Remote Method Invocation
9.Understand the principles of Common Object Request Broker Architecture
10.Apply the basics of Smart Phone Application Development

COURSE CONTENT
UNIT 1: APPLYING THE ADVANCED CONCEPTS OF JAVA COLLECTIONS

1.1 Collection Interfaces

Java Collections Framework (JCF)

A lot has changed since the inception of the Java programming language, but

the Java Collections Framework (JCF) has been a consistent backbone of Java

since its early days. Although many improvements have been made to the JCF,

not the least of which has been the addition of generics and concurrency, the JCF
has remained largely the same. Throughout this time, collections have become an

indispensable part of nearly every Java application and learning to use these

interfaces and classes has become a requirement for most aspiring Java

developers.

The primary collection data structures: Lists, sets, and queues. The fundamentals

of each of these concepts as well as the advanced topics, including complete

coverage of the primary methods of each interface and class, the intention

behind each collection implementation, the benefits and trade-offs each

implementation offers, and when to select a specific implementation.

A Trip Down Memory Lane

During the early days of Java, no common collection framework existed; the best

that was available were loosely collected data structures in the form of

arrays, Vectors, or Hashtables. Unlike many popular programming languages at the

time, Java did not combine these disparate types into a framework, with common

ancestors and universal characteristics. With the introduction of the Standard

Template Library (STL) in C++ in 1994, Java lagged behind in creating a common

framework with hierarchical data structures.

Up to the release of Java Development Kit (JDK) 2 in late 1998, the most popular,

full-featured collections frameworks that existed were the Generic Collection

Library (JGL) by Object Space and Collections Package by Doug Lea. Using the

JGL as a basis, Joshua Bloch (author of the Effective Java series) designed much
of the JCF that we know today. As a matter of fact, the author tags for many of

the collection classes still have his name to this day. With the advent of JDK 2, a

basic collections framework was introduced to Java, but this collections

framework got a major upgrade in Fall of 2004 with the release of JDK 5. This

Java release introduced type-erased generics, which transformed the JCF from a

type-unsafe framework (requiring explicit casts when retrieving elements) to a

full-fledged generic framework. JDK 5 also introduced concurrency in the JCF

(through Java Specification Request (JSR) 166), spearheaded by the forefather of

the JCF: Doug Lea.

Since that time, various upgrades have been made to the JCF, including the

introduction of functional programming concepts in the form of the Streams

Application Programming Interface (API), but the JCF has largely remained the

same. Being that it is one of the most widely used of the Java frameworks,

updates and improvements to the JCF have been one of the foremost concerns

of the Java community, even from the infant days of Java.

The Concept of a Collection

In the Java environment, a collection:

A collection represents a group of objects, known as its elements

Objects have states and behaviors

It makes no statement about how the elements in a collection are grouped, if

these elements are randomly accessible, if duplicate elements are allowed, or if


elements are ordered. Instead, Java only requires that elements can be added to

a collection, removed from a collection, and iterated over (without declaring an

iteration order). Additionally, collections must provide queries for its current state,

such as the current number of elements it contains, if the collection is empty, and

if an arbitrary element is within the collection; they must also provide conversion

methods to arrays (for compatibility with existing array-based applications). With

the addition of the Streams (API) in JDK 8, collections must also be convertible to

a stream of its elements.

Based on this description of a collection, we can define the following set of

responsibilities:

 Query number of elements

 Query if collection is empty

 Query if an arbitrary element is contained in the collection

 Iterate over the elements

 Produce an array of its elements

 Add a new element

 Remove an existing element

 Remove all existing elements

 Produce a stream of its elements

The Collection Interface


As expected, Java captures these responsibilities in the Collection interface, which is
parameterized with a formal generic parameter, E, that represents the type of its
elements. JDK 9 defines this interface as follows:

public interface Collection<E> extends Iterable<E> {


int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean removeAll(Collection<?> c);
default boolean removeIf(Predicate<? super E> filter) { /* ... */ }
boolean retainAll(Collection<?> c);
void clear();
boolean equals(Object o);
int hashCode();
default Spliterator<E> spliterator() { /* ... */ }
default Stream<E> stream() { /* ... */ }
default Stream<E> parallelStream() { /* ... */ }
}

size and isEmpty

In the vernacular of collections, the number of elements currently contained in a


collection is called its size. Thus, in order to query the number of elements in a
collection, we must call the size() method. A collection is
considered empty (isEmpty() returns true) if its size is 0.

contains

The contains(Object o) method checks if the supplied object is contained in the collection
according to the following rule:

An object a is contained in a collection if there exists at least one element b in the

collection such that a.equals(b) returns true. If a is null, a is contained in the

collection if there exists at least one null element in the collection.


Stated more formally:

An object a is contained in a collection if there exists some element b in the

collection that satisfies the ternary expression (a == null ? b == null : a.equals(b))

It is important to note that this does not mean that the equals method of a will actually be
invoked. It is left to the implementation to decide how to test for equality. Optimizations
may be made, depending on the nature of the collection. For
example, the hashCode() method specification states that two objects with
different hashCode() values are by definition not equal, and therefore, an implementation
may favor this means of testing equality, rather than explicitly executing a.equals(b).

iterator

The Collection interface is actually not the top of the collections hierarchy: It extends
the Iterable interface, which defines a type that can be iterated over. The JDK 9
definition for the Iterable interface is as follows:

public interface Iterable<T> {


Iterator<T> iterator();
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
default Spliterator<T> spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
}

The first method, iterator(), simply returns an Iterator object (which we address
shortly). The forEach method is a straightforward default implementation that allows a
non-null action to be performed on each of the elements in a collection. This method
utilizes the for-each loop (formally called the enhanced for loop), which is a syntactic
optimization that allows a for loop to be condensed for any iterable object:
class FooIterable implements Iterable<Foo> { /* ... */ }
FooIterable fooIterable;
for (Foo foo: fooIterable) {
// Do something with "foo"
}

There are some limitations to this style, such as removing an element during iteration,
as we will see shortly. The last method creates a Spliterator, which can partition a
collection and facilitates iteration over these partitions. Spliterators are a complex topic
and are closely related with parallelism in the collections framework, and are therefore
not covered in this article. The curious reader should consult
the Spliteratordocumentation and Java 8: A Quick Introduction to Parallelism and the
Spliterator for more information.

The main purpose of the Iterable interface is to create an Iterator object. An Iterator is
the primary interface in the Iterator pattern and allows for iteration over a collection.
The Iterator interface is defined as follows for JDK 9:

public interface Iterator<E> {


boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}

Following the Iterator pattern, the primary methods in this interface are
the hasNext() method, which returns true if there are more elements to iterate, and
the next() method, which returns the next element to be iterated and advances the
current element in the iterator (i.e. ensure that the next call to next() will produce the
next element of the collection, not the same element ad infinitum). Beyond these
fundamental methods, the Iterator interface also includes two important
methods: remove() and forEachRemaining(Consumer<? super E> action).
The remove()) method is essential to removing elements from a collection during
iteration. In general, it is not permissible to iterate over a collection using an enhanced
for loop and remove an element from the collection in the body of the for loop. This
results in a ConcurrentModificationException being thrown. For example, the following
results in a ConcurrentModificationException:

for (Foo foo: fooIterable) {


fooIterable.remove(foo);
}

Instead, remove() must be called on an Iterator object to remove an element from a


collection while iterating over that collection. For example:

for(Iterator<Foo> i = fooIterable.iterator(); i.hasNext();) {


Foo foo = i.next();
i.remove();
}

Note that next() must be called before calling remove(), as the next() method advances the
current element in the iterator. While the combination of the next(), hasNext(),
and remove() method cover a vast majority of the functionality that a developer will
commonly use when dealing with iterators, there are countless great resources that go
much deeper on this important topic. For more information, consult the Iterator
documentation and the Oracle Collection Interface documentation.

toArray

Due to the limitation of Java generics, conversions of collections to arrays is quirky and
two methods are provided. The first is the simple toArray() method that returns an array
of Objects that holds the elements of the collection ordered by the same rules that
govern the order of the elements obtained through an iterator associated with the
collection (i.e. if there are ordering rules established for an iterator returned
by iterator(), these rules also govern the order of the elements in this array). This
method is a non-generic method and the type of the elements in the array do not reflect
the type of the elements, as specified by the formal generic parameter for the collection.
The second method is the toArray(T[] a) method, which returns an array of the elements
in the collection, but retains the type of the elements. Thus, the array returned by this
method is an array of objects with the same type as the formal generic parameter of the
collection. Due type erasure of generics in Java, the type of the formal generic parameter
is inaccessible at run-time, and therefore, creating an array at run-time with the same
type as the elements is not feasible. Thus, the caller is responsible for providing the type
of array at run-time (in the form of an array of the element type). If the provided array
has a size equal to greater than the size of the collection, the supplied array is filled with
the elements of the collection and the element directly following the last element of the
collection is set to null (if the length of the array minus the size of the collection is
greater than or equal to 1). If the length of the supplied array is less than the size of the
collection, a new array is returned with a length that matches the size of the collection
and the type of the supplied array.

For example, a call to this parameterized method would resemble the following:

Collection<Foo> foos = someMethod();


Foo[] fooArray = foos.toArray(new Foo[0]);

It can be tempting to optimize this call by pre-allocating an array of the same length as
the size of the collection. For example:

Collection<Foo> foos = someMethod();


Foo[] fooArray = foos.toArray(new Foo[foos.size()]);

As noted by Joshua Bloch in Effective Java, 3rd Edition (pg. 248), Item 55, this pre-
allocation optimization should be avoided. For a quantitative analysis of the
performance differences between these two array pre-allocation techniques, see Arrays
of Wisdom of the Ancients.

add

The add(E e) method adds an element to a collection and returns true if the collection
was changed. Implementations are left to decide if e is acceptable. For example, some
implementations may not accept duplicate values (i.e. if contains(e) is true) while others
may not accept null values.

remove

The remove(Object o) method removes one element from a collection if at least one
element equal to the supplied object is contained in the collection. If an element is
removed, this method returns true. The equality rules for removal are identical to those
of the contains(Object o) method.

containsAll, addAll, and removeAll

The containsAll(Collection<?> c) method returns true if and only if all of the elements
of c are contained in the collection. Likewise, the addAll(Collection<?> c) method adds all
of the elements in c to the collection, returning true if the collection is altered (i.e. at
least one addition was performed). Note that the behavior of the addAll method is
undefined if the collection is modified between the initiation of the addAll method and
its completion. Lastly, removeAll(Collection<?> c) removes all elements in common with c,
returning true if the collection was modified (i.e. if at least one element was removed).
Upon completion, the collection is guaranteed to contain common elements with c.

removeIf

The removeIf(Predicate<? super E> filter) default implementation removes all elements
that satisfy the supplied predicate. In practice, this method filters out any element that
satisfies the supplied predicate and returns true if the collection was modified (i.e. at
least one element was removed). The implementation of this method is as follows:

default boolean removeIf(Predicate<? super E> filter) {


Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
Internally, this method uses the iterator provided by the collection to test each of the
elements in the collection, removing those that satisfy the supplied predicate (as
demonstrated in the iterator section above).

retainAll

The retainAll(Collection<?> c) method removes all elements of the collection not in


common with c. This amounts to the intersection of the collection with c. For example, if
a collection contains the elements [1, 2, 2, 3, 4, 5, 5], and a collection containing [1, 2,

4, 6] is supplied to the retainAll method, the original collection will be reduced to [1, 2,

2, 4]. A value of true is returned if this method modifies the collection (i.e. if at least one
element is removed).

clear

Removes all elements from the collection. Upon completion of this method, the
collection is considered empty.

equals and hashCode

The equals(Object o) and hashCode() methods mirror those of all subclasses of


the Object class in Java. Implementations of collections who customize
the equals and hashCode methods are obligated to follow the general restrictions placed on
all implementations of the Object class, found in the Object class documentation.

spliterator

The Collection interface overrides the default spliterator implementation provided by


the Iterable interface with its own, replacing a Spliterator associated with
the Iterator returned by the Iterable interface with one associated with the collection
itself. The default implementation of this method is as follows:

default Spliterator<E> spliterator() {


return Spliterators.spliterator(this, 0);
}

For more information on Spliterators, see the Spliterator documentation.


stream and parallelStream

One of the major additions to JDK 8 was the inclusion of the Streams API. This API
introduces functional programming semantics to Java collections, treating collections as
a stream of data that can be mapped, filtered, reduced, etc. The default implementations
of the stream-based methods for the Collection interface are as follows:

default Stream<E> stream() {


return StreamSupport.stream(spliterator(), false);
}
default Stream<E> parallelStream() {
return StreamSupport.stream(spliterator(), true);
}

As with Spliterators, streams are a very involved topic and beyond the scope of this
series. The curious reader can find more information at the Stream class
documentation.

Implicit Rules

While most of the rules regarding collection implementations are checked at compile-
time with language constructs, some rules are extra-linguistic. Although not checked at
compile-time, all implementations of collections should abide by the following rules:

 Have a no-args constructor that creates an empty collection (of the


implementation type) and a constructor that accepts a Collection and creates a
copy of the supplied Collection (conversion constructor or copy constructor)
 Unsupported destructive methods (i.e. methods that modify the collection)
should throw an UnsupportedOperationException if not supported; for example,
calling add or remove on an immutable collection should result in
an UnsupportedOperationException being thrown
 Synchronization is determined by each implementation
1.2 Concrete Collection

Concrete Collection
With a solid understanding of the general characteristics of a list and details of each method
of the List interface, we can examine the concrete implementations of the List interface.
The JDK 9 List hierarchy (disregarding concurrent List implementations for brevity) is
illustrated in the following figure, where green boxes represent interfaces, blue boxes
represent abstract classes, and purple boxes represent concrete implementations.

As covered in The Developer's Guide to Collections, the implementation root of


the List hierarchy is the AbstractList, which is a subclass of
the AbstractCollection class. From this root abstract class,
the ArrayList and Vector classes are directly derived. In turn, the Stack class directly
extends the Vector class. Lastly, the AbstractSequentialList extends
the AbstractList class, which in turn is extended by the LinkedList class.

ArrayList
An ArrayList is a general-purpose List implementation that is backed by an internal array
that expands to store the elements of the list. At any given time, the length of the internal
array is called its capacity and may be greater than or equal to the size of the list (the number
of elements in the list, not the size of the array used to store the elements). For example, it is
possible that an ArrayList contains 10 elements, but its capacity is 12 (enough to store the
10 elements).
Additionally, the ArrayList class implements all optional operations in the List interface
and allows for any element, including duplicates and null elements, to be added.
An ArrayList is essentially a Vector without the overhead of synchronization. Unless there
is a specific need to use a Vector, an ArrayList should be used in place of a Vector. For
more information, see the official ArrayList documentation.

LinkedList
A ListedList is a doubly linked list that allows for bidirectional traversal of the elements in
the list. This internal doubly link representation is based on the private static Node class,
which includes a data field (called an item) of the type of the elements in the LinkedList
(matching the type of the formal generic parameter of the LinkedList), a previous link, and
a next link, as illustrated in the listing below.

private static class Node<E> {

E item;

Node<E> next;

Node<E> prev;

Node(Node<E> prev, E element, Node<E> next) {

this.item = element;

this.next = next;

this.prev = prev;

}
Using this Node class, the LinkedList implementation stores a reference to the head and tail
of the list, allowing for forward traversal from the head of the list or backward traversal from
the tail of the list (depending on which is closer to the needed index). This implementation
should be used if elements are frequently added to the head of the list or if an iterator is used
to delete elements far from the head or the tail of the list, as these operations are performed in
constant time as compared to the linear time of an ArrayList. Note, though, that positional
access requires linear time in a LinkedList but is a constant time operation for an
ArrayList. For more information on the performance trade-offs
between LinkedList and ArrayList, see the official List Implementation tutorial.
The LinkedList class also implements the Queue interface (which will be discussed in a later
article in this series) and includes the following methods not included in List interface:
• addFirst
• getFirst
• removeFirst
• addLast
• getLast
• removeLast
For more information on the LinkedList implementation, see the official LinkedList
documentation.

Vector
A legacy implementation predates JDK 2. This implementation is essentially an ArrayList
with a simplistic internal synchronization mechanism (each mutation method is marked
as synchronized). Due to the unneeded overhead of synchronization, an ArrayList should
generally be used instead of a Vector. For more information, see the official Vector
documentation.

Stack
A legacy Last-in-First-out (LIFO) stack implementation. This implementation includes stack-
based methods, such as push, pop, and peek to add an element to the stack, retrieve and
remove the top element from the stack, and to retrieve the top element without removing it,
respectively. This implementation, like Vector, uses internal synchronization and a Deque
implementation should generally be used instead. For more information, see the official
Stack documentation.

Comparison
The following table captures some of the trade-offs and benefits of each of the
standard List implementations covered in this section:

LIST
DESCRIPTION
TYPE

A good, general purpose list implementation that is efficient in most cases. This implementation
uses an internal array to store the elements and must expand when the number of elements
exceeds the capacity of the array.If a list frequently accesses its elements by index, this
ArrayLi
implementation provides constant time access. If instead, elements are frequently added to the
st
head of the list or are removed from the center of the list (generally halfway between the head
and tail of the list), these operations are linear and a LinkedList should be used instead.
When in doubt, use an ArrayList by default.
An efficient implementation for adding elements to the head of the list or sequentially removing
elements (or traversing the list and removing selective elements along the way). This list is
LinkedL
internally implemented as a doubly linked list with references to both the head and tail of the
ist
list, allowing for efficient forward traversal from the head of the list or backward from the tail of
the list.
Uses internal synchronization. In most cases, ArrayList should be used in place of
a Vector since the overhead of the internal synchronization of the Vector class is inefficient.
The synchronization performed by the Vector class is a method-by-method synchronization,
Vector
where each method that modifies the list is marked as synchronized. Synchronization
mechanics for a list can be more efficiency introduced by using a concurrent list implementation,
such as the CopyOnWriteArrayList implementation.
Uses internal synchronization. A legacy implementation that supports stack-based
Stack operations (such as push and pop). The Deque list type should be used instead of this
implementation.

Conclusion

Lists are one of the most frequently used collection types, and for good reason. By definition,
lists are ordered and its elements can be randomly accessed by index, as well as traversed
through a special-purpose ListIterator. In addition, due to the ordered nature of a list, the
elements in a List are sortable by either an explicit Comparator or through natural ordering.
While some of the mutation methods (such as add) are optional operations, the two most
common List implementations, ArrayList and LinkedList, provide implementations for
all of these optional operations. Although we have not covered every aspect of
the List interface and its various implementations, we have covered a wide breadth of
knowledge that will allow us to start in the correct direction, digging into the official
documentation where needed. In the next entry in this series, we will cover one of the more
mathematical collection representations: The set.

1.3 The Collection Framework

The Collection Framework

Although we can use an array to store a group of elements of the same type

(either primitives or objects). The array, however, does not support so-

called dynamic allocation - it has a fixed length which cannot be changed once

allocated. Furthermore, array is a simple linear structure. Many applications may

require more complex data structure such as linked list, stack, hash table, sets, or

trees.

In Java, dynamically allocated data structures (such

as ArrayList, LinkedList, Vector, Stack, HashSet, HashMap, Hashtable) are

supported in a unified architecturecalled the Collection Framework, which

mandates the common behaviors of all the classes.

A collection, as its name implied, is simply an object that holds a collection (or a

group, a container) of objects. Each item in a collection is called an element.

A framework, by definition, is a set of interfaces that force you to adopt some


design practices. A well-designed framework can improve your productivity and

provide ease of maintenance.

The collection framework provides a unified interface to store, retrieve and

manipulate the elements of a collection, regardless of the underlying and actual

implementation. This allows the programmers to program at the interfaces,

instead of the actual implementation.

The Java Collection Framework package (java.util) contains:

1. A set of interfaces,

2. Implementation classes, and

3. Algorithms (such as sorting and searching).

Similar Collection Framework is the C++ Standard Template Library (STL).

Prior to JDK 1.2, Java's data structures consist of array, Vector, and Hashtable that

were designed in a non-unified way with inconsistent public interfaces. JDK 1.2

introduced the unified collection framework, and retrofits the legacy classes

(Vector and Hashtable) to conform to this unified collection framework.

JDK 1.5 introduced Generics (which supports passing of types), and many related

features (such as auto-boxing and unboxing, enhance for-loop). The collection

framework is retrofitted to support generics and takes full advantages of these

new features.

To understand this chapter, you have to be familiar with:

 Polymorphism, especially the upcasting and downcasting operations.


 Interfaces, abstract methods and their implementations.

 Generics, Auto-boxing & unboxing, and enhanced for-loop (introduced in JDK

1.5).

UNIT 2: APPLYING THE ADVANCED CONCEPTS OF MULTITHREADING

Multithreading as a widespread programming and execution model allows multiple threads to exist
within the context of a single process. These threads share the process' resources but are able to
execute independently. The threaded programming model provides developers with a useful
abstraction of concurrent execution. However, perhaps the most interesting application of the
technology is when it is applied to a single process to enable parallel execution on a multiprocessor
system.

That means that a single process can have many different "functions" executing concurrently,
allowing the application to better use the available hardware (multiple cores/processors). Threads
can communicate between them (they have shared memory), but its a hard problem to have every
thread behave well with others when accesing shared objects/memory.
Threading allows an application to remain responsive, without the use of a catch all application
loop, when doing lengthy operations.

2.1 Creating Thread and Running

Java is a multi-threaded programming language which means we can develop multi-threaded program using
Java. A multi-threaded program contains two or more parts that can run concurrently and each part can
handle a different task at the same time making optimal use of the available resources specially when your
computer has multiple CPUs.
By definition, multitasking is when multiple processes share common processing resources such as a CPU.
Multi-threading extends the idea of multitasking into applications where you can subdivide specific
operations within a single application into individual threads. Each of the threads can run in parallel. The OS
divides processing time not only among different applications, but also among each thread within an
application.
Multi-threading enables you to write in a way where multiple activities can proceed concurrently in the same
program.

Create a Thread by Implementing a Runnable Interface


If your class is intended to be executed as a thread then you can achieve this by implementing a Runnable
interface. You will need to follow three basic steps −
Step 1
As a first step, you need to implement a run() method provided by a Runnable interface. This method
provides an entry point for the thread and you will put your complete business logic inside this method.
Following is a simple syntax of the run() method −
public void run( )
Step 2
As a second step, you will instantiate a Thread object using the following constructor −
Thread(Runnable threadObj, String threadName);
Where, threadObj is an instance of a class that implements the Runnable interface and threadName is the
name given to the new thread.
Step 3
Once a Thread object is created, you can start it by calling start() method, which executes a call to run( )
method. Following is a simple syntax of start() method −
void start();
Example
Here is an example that creates a new thread and starts running it −

Live Demo
class RunnableDemo implements Runnable {
private Thread t;
private String threadName;

RunnableDemo( String name) {


threadName = name;
System.out.println("Creating " + threadName );
}

public void run() {


System.out.println("Running " + threadName );
try {
for(int i = 4; i > 0; i--) {
System.out.println("Thread: " + threadName + ", " + i);
// Let the thread sleep for a while.
Thread.sleep(50);
}
} catch (InterruptedException e) {
System.out.println("Thread " + threadName + " interrupted.");
}
System.out.println("Thread " + threadName + " exiting.");
}

public void start () {


System.out.println("Starting " + threadName );
if (t == null) {
t = new Thread (this, threadName);
t.start ();
}
}
}

public class TestThread {

public static void main(String args[]) {


RunnableDemo R1 = new RunnableDemo( "Thread-1");
R1.start();

RunnableDemo R2 = new RunnableDemo( "Thread-2");


R2.start();
}
}
This will produce the following result −
Output
Creating Thread-1
Starting Thread-1
Creating Thread-2
Starting Thread-2
Running Thread-1
Thread: Thread-1, 4
Running Thread-2
Thread: Thread-2, 4
Thread: Thread-1, 3
Thread: Thread-2, 3
Thread: Thread-1, 2
Thread: Thread-2, 2
Thread: Thread-1, 1
Thread: Thread-2, 1
Thread Thread-1 exiting.
Thread Thread-2 exiting.

Create a Thread by Extending a Thread Class


The second way to create a thread is to create a new class that extends Thread class using the following two
simple steps. This approach provides more flexibility in handling multiple threads created using available
methods in Thread class.
Step 1
You will need to override run( ) method available in Thread class. This method provides an entry point for the
thread and you will put your complete business logic inside this method. Following is a simple syntax of run()
method −
public void run( )
Step 2
Once Thread object is created, you can start it by calling start() method, which executes a call to run( )
method. Following is a simple syntax of start() method −
void start( );
Example
Here is the preceding program rewritten to extend the Thread −

Live Demo
class ThreadDemo extends Thread {
private Thread t;
private String threadName;

ThreadDemo( String name) {


threadName = name;
System.out.println("Creating " + threadName );
}

public void run() {


System.out.println("Running " + threadName );
try {
for(int i = 4; i > 0; i--) {
System.out.println("Thread: " + threadName + ", " + i);
// Let the thread sleep for a while.
Thread.sleep(50);
}
} catch (InterruptedException e) {
System.out.println("Thread " + threadName + " interrupted.");
}
System.out.println("Thread " + threadName + " exiting.");
}

public void start () {


System.out.println("Starting " + threadName );
if (t == null) {
t = new Thread (this, threadName);
t.start ();
}
}
}

public class TestThread {

public static void main(String args[]) {


ThreadDemo T1 = new ThreadDemo( "Thread-1");
T1.start();

ThreadDemo T2 = new ThreadDemo( "Thread-2");


T2.start();
}
}
This will produce the following result −
Output
Creating Thread-1
Starting Thread-1
Creating Thread-2
Starting Thread-2
Running Thread-1
Thread: Thread-1, 4
Running Thread-2
Thread: Thread-2, 4
Thread: Thread-1, 3
Thread: Thread-2, 3
Thread: Thread-1, 2
Thread: Thread-2, 2
Thread: Thread-1, 1
Thread: Thread-2, 1
Thread Thread-1 exiting.
Thread Thread-2 exiting.

Thread Methods
Following is the list of important methods available in the Thread class.
Sr.No. Method & Description
public void start()
1 Starts the thread in a separate path of execution, then invokes the run() method
on this Thread object.
public void run()
2 If this Thread object was instantiated using a separate Runnable target, the run()
method is invoked on that Runnable object.
public final void setName(String name)
3 Changes the name of the Thread object. There is also a getName() method for
retrieving the name.
public final void setPriority(int priority)
4
Sets the priority of this Thread object. The possible values are between 1 and 10.
public final void setDaemon(boolean on)
5
A parameter of true denotes this Thread as a daemon thread.
public final void join(long millisec)
6
The current thread invokes this method on a second thread, causing the current
thread to block until the second thread terminates or the specified number of
milliseconds passes.
public void interrupt()
7 Interrupts this thread, causing it to continue execution if it was blocked for any
reason.
public final boolean isAlive()
8 Returns true if the thread is alive, which is any time after the thread has been
started but before it runs to completion.
The previous methods are invoked on a particular Thread object. The following methods in the Thread class
are static. Invoking one of the static methods performs the operation on the currently running thread.
Sr.No. Method & Description
public static void yield()
1 Causes the currently running thread to yield to any other threads of the same
priority that are waiting to be scheduled.
public static void sleep(long millisec)
2 Causes the currently running thread to block for at least the specified number of
milliseconds.
public static boolean holdsLock(Object x)
3
Returns true if the current thread holds the lock on the given Object.
public static Thread currentThread()
4 Returns a reference to the currently running thread, which is the thread that
invokes this method.
public static void dumpStack()
5 Prints the stack trace for the currently running thread, which is useful when
debugging a multithreaded application.
Example
The following ThreadClassDemo program demonstrates some of these methods of the Thread class. Consider
a class DisplayMessage which implements Runnable −
// File Name : DisplayMessage.java
// Create a thread to implement Runnable

public class DisplayMessage implements Runnable {


private String message;

public DisplayMessage(String message) {


this.message = message;
}

public void run() {


while(true) {
System.out.println(message);
}
}
}
Following is another class which extends the Thread class −
// File Name : GuessANumber.java
// Create a thread to extentd Thread

public class GuessANumber extends Thread {


private int number;
public GuessANumber(int number) {
this.number = number;
}

public void run() {


int counter = 0;
int guess = 0;
do {
guess = (int) (Math.random() * 100 + 1);
System.out.println(this.getName() + " guesses " + guess);
counter++;
} while(guess != number);
System.out.println("** Correct!" + this.getName() + "in" + counter + "guesses.**");
}
}
Following is the main program, which makes use of the above-defined classes −
// File Name : ThreadClassDemo.java
public class ThreadClassDemo {

public static void main(String [] args) {


Runnable hello = new DisplayMessage("Hello");
Thread thread1 = new Thread(hello);
thread1.setDaemon(true);
thread1.setName("hello");
System.out.println("Starting hello thread...");
thread1.start();

Runnable bye = new DisplayMessage("Goodbye");


Thread thread2 = new Thread(bye);
thread2.setPriority(Thread.MIN_PRIORITY);
thread2.setDaemon(true);
System.out.println("Starting goodbye thread...");
thread2.start();

System.out.println("Starting thread3...");
Thread thread3 = new GuessANumber(27);
thread3.start();
try {
thread3.join();
} catch (InterruptedException e) {
System.out.println("Thread interrupted.");
}
System.out.println("Starting thread4...");
Thread thread4 = new GuessANumber(75);

thread4.start();
System.out.println("main() is ending...");
}
}
This will produce the following result. You can try this example again and again and you will get a different
result every time.
Output
Starting hello thread...
Starting goodbye thread...
Hello
Hello
Hello
Hello
Hello
Hello
Goodbye
Goodbye
Goodbye
Goodbye
Goodbye

2.2 Multiple Thread


2.3 Synchronization

When we start two or more threads within a program, there may be a


situation when multiple threads try to access the same resource and
finally they can produce unforeseen result due to concurrency issues. For
example, if multiple threads try to write within a same file then they may
corrupt the data because one of the threads can override data or while
one thread is opening the same file at the same time another thread
might be closing the same file.

So there is a need to synchronize the action of multiple threads and


make sure that only one thread can access the resource at a given point
in time. This is implemented using a concept called monitors. Each
object in Java is associated with a monitor, which a thread can lock or
unlock. Only one thread at a time may hold a lock on a monitor.

Java programming language provides a very handy way of creating


threads and synchronizing their task by using synchronized blocks. You
keep shared resources within this block. Following is the general form of
the synchronized statement −

Syntax

synchronized(objectidentifier) {
// Access shared variables and other shared resources
}

Here, the objectidentifier is a reference to an object whose lock


associates with the monitor that the synchronized statement represents.
Now we are going to see two examples, where we will print a counter
using two different threads. When threads are not synchronized, they
print counter value which is not in sequence, but when we print counter
by putting inside synchronized() block, then it prints counter very much
in sequence for both the threads.

Multithreading Example without


Synchronization
Here is a simple example which may or may not print counter value in
sequence and every time we run it, it produces a different result based
on CPU availability to a thread.

Example

class PrintDemo {

public void printCount() {

try {

for(int i = 5; i > 0; i--) {

System.out.println("Counter --- " + i );

} catch (Exception e) {

System.out.println("Thread interrupted.");

class ThreadDemo extends Thread {

private Thread t;

private String threadName;


PrintDemo PD;

ThreadDemo( String name, PrintDemo pd) {

threadName = name;

PD = pd;

public void run() {

PD.printCount();

System.out.println("Thread " + threadName + " exiting.");

public void start () {

System.out.println("Starting " + threadName );

if (t == null) {

t = new Thread (this, threadName);

t.start ();

public class TestThread {

public static void main(String args[]) {


PrintDemo PD = new PrintDemo();

ThreadDemo T1 = new ThreadDemo( "Thread - 1 ", PD );

ThreadDemo T2 = new ThreadDemo( "Thread - 2 ", PD );

T1.start();

T2.start();

// wait for threads to end

try {

T1.join();

T2.join();

} catch ( Exception e) {

System.out.println("Interrupted");

This produces a different result every time you run this program −

Output
Starting Thread - 1
Starting Thread - 2
Counter --- 5
Counter --- 4
Counter --- 3
Counter --- 5
Counter --- 2
Counter --- 1
Counter --- 4
Thread Thread - 1 exiting.
Counter --- 3
Counter --- 2
Counter --- 1
Thread Thread - 2 exiting.

Multithreading Example with


Synchronization
Here is the same example which prints counter value in sequence and
every time we run it, it produces the same result.

Example

class PrintDemo {

public void printCount() {

try {

for(int i = 5; i > 0; i--) {

System.out.println("Counter --- " + i );

} catch (Exception e) {

System.out.println("Thread interrupted.");

class ThreadDemo extends Thread {

private Thread t;

private String threadName;


PrintDemo PD;

ThreadDemo( String name, PrintDemo pd) {

threadName = name;

PD = pd;

public void run() {

synchronized(PD) {

PD.printCount();

System.out.println("Thread " + threadName + " exiting.");

public void start () {

System.out.println("Starting " + threadName );

if (t == null) {

t = new Thread (this, threadName);

t.start ();

public class TestThread {


public static void main(String args[]) {

PrintDemo PD = new PrintDemo();

ThreadDemo T1 = new ThreadDemo( "Thread - 1 ", PD );

ThreadDemo T2 = new ThreadDemo( "Thread - 2 ", PD );

T1.start();

T2.start();

// wait for threads to end

try {

T1.join();

T2.join();

} catch ( Exception e) {

System.out.println("Interrupted");

This produces the same result every time you run this program −

Output
Starting Thread - 1
Starting Thread - 2
Counter --- 5
Counter --- 4
Counter --- 3
Counter --- 2
Counter --- 1
Thread Thread - 1 exiting.
Counter --- 5
Counter --- 4
Counter --- 3
Counter --- 2
Counter --- 1
Thread Thread - 2 exiting.

2.4 Thread Communication

If you are aware of interprocess communication then it will be easy for


you to understand interthread communication. Interthread
communication is important when you develop an application where two
or more threads exchange some information.

There are three simple methods and a little trick which makes thread
communication possible. All the three methods are listed below −

Sr.No. Method & Description

1 public void wait()

Causes the current thread to wait until another thread invokes the
notify().

2 public void notify()

Wakes up a single thread that is waiting on this object's monitor.

3 public void notifyAll()

Wakes up all the threads that called wait( ) on the same object.
These methods have been implemented as final methods in Object, so
they are available in all the classes. All three methods can be called only
from within a synchronized context.

Example
This examples shows how two threads can communicate
using wait() and notify() method. You can create a complex system
using the same concept.

class Chat {

boolean flag = false;

public synchronized void Question(String msg) {

if (flag) {

try {

wait();

} catch (InterruptedException e) {

e.printStackTrace();

System.out.println(msg);

flag = true;

notify();

public synchronized void Answer(String msg) {


if (!flag) {

try {

wait();

} catch (InterruptedException e) {

e.printStackTrace();

System.out.println(msg);

flag = false;

notify();

class T1 implements Runnable {

Chat m;

String[] s1 = { "Hi", "How are you ?", "I am also doing fine!" };

public T1(Chat m1) {

this.m = m1;

new Thread(this, "Question").start();

public void run() {


for (int i = 0; i < s1.length; i++) {

m.Question(s1[i]);

class T2 implements Runnable {

Chat m;

String[] s2 = { "Hi", "I am good, what about you?", "Great!" };

public T2(Chat m2) {

this.m = m2;

new Thread(this, "Answer").start();

public void run() {

for (int i = 0; i < s2.length; i++) {

m.Answer(s2[i]);

public class TestThread {

public static void main(String[] args) {

Chat m = new Chat();


new T1(m);

new T2(m);

When the above program is complied and executed, it produces the


following result −

Hi
Hi
How are you ?
I am good, what about you?
I am also doing fine!
Great!

2.5 Thread Group

2.6 Thread Priorities

Thread Priorities
Every Java thread has a priority that helps the operating system determine the order in which threads are
scheduled.
Java thread priorities are in the range between MIN_PRIORITY (a constant of 1) and MAX_PRIORITY (a
constant of 10). By default, every thread is given priority NORM_PRIORITY (a constant of 5).
Threads with higher priority are more important to a program and should be allocated processor time before
lower-priority threads. However, thread priorities cannot guarantee the order in which threads execute and
are very much platform dependent.

In a Multi threading environment, thread scheduler assigns processor to a thread based on


priority of thread. Whenever we create a thread in Java, it always has some priority
assigned to it. Priority can either be given by JVM while creating the thread or it can be
given by programmer explicitly.
Accepted value of priority for a thread is in range of 1 to 10. There are 3 static variables
defined in Thread class for priority.
public static int MIN_PRIORITY: This is minimum priority that a thread can have. Value for
this is 1.
public static int NORM_PRIORITY: This is default priority of a thread if do not explicitly
define it. Value for this is 5.
public static int MAX_PRIORITY: This is maximum priority of a thread. Value for this is 10.
Get and Set Thread Priority:
1. public final int getPriority(): java.lang.Thread.getPriority() method returns priority of given
thread.
2. public final void setPriority(int newPriority): java.lang.Thread.setPriority() method changes
the priority of thread to the value newPriority. This method throws IllegalArgumentException if value
of parameter newPriority goes beyond minimum(1) and maximum(10) limit.
Examples of getPriority() and set
// Java program to demonstrate getPriority() and setPriority()
import java.lang.*;

class ThreadDemo extends Thread


{
public void run()
{
System.out.println("Inside run method");
}

public static void main(String[]args)


{
ThreadDemo t1 = new ThreadDemo();
ThreadDemo t2 = new ThreadDemo();
ThreadDemo t3 = new ThreadDemo();

System.out.println("t1 thread priority : " +


t1.getPriority()); // Default 5
System.out.println("t2 thread priority : " +
t2.getPriority()); // Default 5
System.out.println("t3 thread priority : " +
t3.getPriority()); // Default 5

t1.setPriority(2);
t2.setPriority(5);
t3.setPriority(8);

// t3.setPriority(21); will throw IllegalArgumentException


System.out.println("t1 thread priority : " +
t1.getPriority()); //2
System.out.println("t2 thread priority : " +
t2.getPriority()); //5
System.out.println("t3 thread priority : " +
t3.getPriority());//8

// Main thread
System.out.print(Thread.currentThread().getName());
System.out.println("Main thread priority : "
+ Thread.currentThread().getPriority());

// Main thread priority is set to 10


Thread.currentThread().setPriority(10);
System.out.println("Main thread priority : "
+ Thread.currentThread().getPriority());
}
}
Run on IDE

Output:

t1 thread priority : 5

t2 thread priority : 5

t3 thread priority : 5

t1 thread priority : 2

t2 thread priority : 5

t3 thread priority : 8

Main thread priority : 5

Main thread priority : 10

Note:
 Thread with highest priority will get execution chance prior to other threads. Suppose there are
3 threads t1, t2 and t3 with priorities 4, 6 and 1. So, thread t2 will execute first based on
maximum priority 6 after that t1 will execute and then t3.
 Default priority for main thread is always 5, it can be changed later. Default priority for all other
threads depends on the priority of parent thread.
Example:
// Java program to demonstrat ethat a child thread
// gets same priority as parent
import java.lang.*;

class ThreadDemo extends Thread


{
public void run()
{
System.out.println("Inside run method");
}

public static void main(String[]args)


{
// main thread priority is 6 now
Thread.currentThread().setPriority(6);

System.out.println("main thread priority : " +


Thread.currentThread().getPriority());

ThreadDemo t1 = new ThreadDemo();

// t1 thread is child of main thread


// so t1 thread will also have priority 6.
System.out.println("t1 thread priority : "
+ t1.getPriority());
}
}
Run on IDE

Output:

Main thread priority : 6

t1 thread priority : 6
 If two threads have same priority then we can’t expect which thread will execute first. It depends
on thread scheduler’s algorithm(Round-Robin, First Come First Serve, etc)
 If we are using thread priority for thread scheduling then we should always keep in mind that
underlying platform should provide support for scheduling based on thread priority.

2.7 Daemon Thread


+

2.8 Life cycle of Thread


Life Cycle of a Thread
A thread goes through various stages in its life cycle. For example, a thread is born, started, runs, and then
dies. The following diagram shows the complete life cycle of a thread.

Following are the stages of the life cycle −


• New − A new thread begins its life cycle in the new state. It remains in this state until the program starts
the thread. It is also referred to as a born thread.

• Runnable − After a newly born thread is started, the thread becomes runnable. A thread in this state is
considered to be executing its task.

• Waiting − Sometimes, a thread transitions to the waiting state while the thread waits for another thread
to perform a task. A thread transitions back to the runnable state only when another thread signals
the waiting thread to continue executing.

• Timed Waiting − A runnable thread can enter the timed waiting state for a specified interval of time. A
thread in this state transitions back to the runnable state when that time interval expires or when
the event it is waiting for occurs.
Terminated (Dead) − A runnable thread enters the terminated state when it completes its task or
otherwise terminates.

UNIT 3: EXPLAINING NETWORKING IN RELATION TO PROGRAMMING


3.1 Internet Addressing
3.2 InetAddress

3.3 Factory Methods

Address Types

Some IP addresses and some patterns of addresses have special meanings. For
instance, I’ve already mentioned that 127.0.0.1 is the local loopback address. IPv4
addresses in the range 224.0.0.0 to 239.255.255.255 are multicast addresses that
send to several subscribed hosts at once. Java includes 10 methods for testing
whether an InetAddress object meets any of these criteria:

public boolean isAnyLocalAddress()


public boolean isLoopbackAddress()
public boolean isLinkLocalAddress()
public boolean isSiteLocalAddress()
public boolean isMulticastAddress()
public boolean isMCGlobal()
public boolean isMCNodeLocal()
public boolean isMCLinkLocal()
public boolean isMCSiteLocal()
public boolean isMCOrgLocal()

The isAnyLocalAddress() method returns true if the address is a wildcard address,


false otherwise. A wildcard address matches any address of the local system. This is
important if the system has multiple network interfaces, as might be the case on a
system with multiple Ethernet cards or an Ethernet card and an 802.11 WiFi
interface. In IPv4, the wildcard address is 0.0.0.0. In IPv6, this address is
0:0:0:0:0:0:0:0 (a.k.a. ::).
The isLoopbackAddress() method returns true if the address is the loopback address,
false otherwise. The loopback address connects to the same computer directly in the
IP layer without using any physical hardware. Thus, connecting to the loopback
address enables tests to bypass potentially buggy or nonexistent Ethernet, PPP, and
other drivers, helping to isolate problems. Connecting to the loopback address is not
the same as connecting to the system’s normal IP address from the same system. In
IPv4, this address is 127.0.0.1. In IPv6, this address is 0:0:0:0:0:0:0:1 (a.k.a. ::1).

The isLinkLocalAddress() method returns true if the address is an IPv6 link-local


address, false otherwise. This is an address used to help IPv6 networks self-
configure, much like DHCP on IPv4 networks but without necessarily using a server.
Routers do not forward packets addressed to a link-local address beyond the local
subnet. All link-local addresses begin with the eight bytes FE80:0000:0000:0000.
The next eight bytes are filled with a local address, often copied from the Ethernet
MAC address assigned by the Ethernet card manufacturer.

The isSiteLocalAddress() method returns true if the address is an IPv6 site-local


address, false otherwise. Site-local addresses are similar to link-local addresses
except that they may be forwarded by routers within a site or campus but should not
be forwarded beyond that site. Site-local addresses begin with the eight bytes
FEC0:0000:0000:0000. The next eight bytes are filled with a local address, often
copied from the Ethernet MAC address assigned by the Ethernet card manufacturer.

The isMulticastAddress() method returns true if the address is a multicast address,


false otherwise. Multicasting broadcasts content to all subscribed computers rather
than to one particular computer. In IPv4, multicast addresses all fall in the range
224.0.0.0 to 239.255.255.255. In IPv6, they all begin with byte FF. Multicasting will
be discussed in Chapter 13.

The isMCGlobal() method returns true if the address is a global multicast address,
false otherwise. A global multicast address may have subscribers around the world.
All multicast addresses begin with FF. In IPv6, global multicast addresses begin with
FF0E or FF1E depending on whether the multicast address is a well known
permanently assigned address or a transient address. In IPv4, all multicast addresses
have global scope, at least as far as this method is concerned. As you’ll see
in Chapter 13, IPv4 uses time-to-live (TTL) values to control scope rather than
addressing.

The isMCOrgLocal() method returns true if the address is an organization-wide


multicast address, false otherwise. An organization-wide multicast address may have
subscribers within all the sites of a company or organization, but not outside that
organization. Organization multicast addresses begin with FF08 or FF18, depending
on whether the multicast address is a well known permanently assigned address or a
transient address.

The isMCSiteLocal() method returns true if the address is a site-wide multicast


address, false otherwise. Packets addressed to a site-wide address will only be
transmitted within their local site. Site-wide multicast addresses begin with FF05 or
FF15, depending on whether the multicast address is a well known permanently
assigned address or a transient address.

The isMCLinkLocal() method returns true if the address is a subnet-wide multicast


address, false otherwise. Packets addressed to a link-local address will only be
transmitted within their own subnet. Link-local multicast addresses begin with FF02
or FF12, depending on whether the multicast address is a well known permanently
assigned address or a transient address.

The isMCNodeLocal() method returns true if the address is an interface-local


multicast address, false otherwise. Packets addressed to an interface-local address
are not sent beyond the network interface from which they originate, not even to a
different network interface on the same node. This is primarily useful for network
debugging and testing. Interface-local multicast addresses begin with the two bytes
FF01 or FF11, depending on whether the multicast address is a well known
permanently assigned address or a transient address.

3.4 Instance Methods


3.5 TCP/IP Sockets
3.6 URL Connection
3.7 TCP/IP Server Sockets
3.8 Datagrams
UNIT 4: UNDERSTANDING THE ENTERPRISE JAVA BEAN
4.1 Preparing a Class to be a Java Bean
4.2 Creating a Java Bean
4.3 Java Bean Properties
4.4 Types of Beans
4.5 Stateful Session Bean
4.6 Stateless Session Bean
4.7 Entity Bean
UNIT 5UNDERSTAND THE JAVA DATABASE CONNECTIVITY

5.1 Merging Data from Multiple Tables


5.2 Joining
5.3 Manipulating

UNIT 6: APPLYING THE CONCEPTS OF SERVLETS


6. 1 Servlet Overview and Architecture
6.2 Interface Servlet and the Servlet Life Cycle
6.3 Handling HTTP get and post Requests
6.4 Redirecting Request to Other Resources
6.5 Session Tracking
6.6 Cookies
6.7 Session Tracking with Http
UNIT 7: UNDERSTANDING THE BASICS OF JAVASERVER PAGES
7.1 JavaServer Pages Overview
7.2 A First JavaServer Page Example
7.3 Implicit Objects
7.4 Scripting
7.5 Standard Actions
7.6 Directives
7.7 Custom Tag Libraries
UNIT 8: UNDERSTANDING THE CONCEPT OF REMOTE METHOD
INVOCATION
8.1 Remote Interface
8.2 Implementation the Remote Interface
8.3 Compiling and Executing the Server and the Client
UNIT 9: UNDERSTANDING THE PRINCIPLES OF COMMON OBJECT
REQUEST BROKER ARCHITECTURE
9.1 Technical Overview
9.2 Architecture Overview
9.3 CORBA
UNIT 10:APPLYING THE BASICS OF SMART PHONE APPLICATION
DEVELOPMENT
5.1 Android Platform
5.2 Creating Application Template
5.3 Adding Activity
5.4 In tend
5.5 Services to applications
5.6 Using Google Map API

METHODS OF TEACHING

 Lectures
 Group discussions
 Tutorials
 Exercises

ASSESSMENT METHODS

Written assignments, Presentations.

 Continuous Assessment 40%


 Final Examination 60%

Continuous Assessment will comprise:

 Test 20%
 Practical Communication Assignment 20%

PRESCRIBED READINGS
 Deitel P (2007) “ Advanced Java 2 Platform-HOW TO PROGRAM, -
Prentice Hall
RECOMMENDED READINGS
 Goncalves A (2007) “ Beginning Java EE Platform with GlassFish 3
From Novice to professional, - Prentice Hall

Вам также может понравиться