This document summarizes a Java hands-on workshop covering containers, I/O, reflection, generics and other Java concepts. The workshop included examples and exercises on topics like lists, sets, maps, iterators, object creation, garbage collection, the File class, readers/writers and more. Code examples were provided on GitHub for attendees to reference.
2. Examples & Exercises
The code for examples and exercises
related to the workshop can be found at
https://github.com/arpoladia/java-hands-on-workshop
3. Credits
Thinking In Java by Bruce Eckel
(http://www.mindviewinc.com/Books/TIJ4/)
Strongly recommend all Java programmers
to buy and read this book cover to cover
6. Basics
• There are two distinct concepts in containers represented as
interfaces – ‘Collection’ which is a sequence of individual
elements & ‘Map’ which is a group of key value pairs.
• There are three basic types of Collections - A ‘List’ holds elements
in the way they were inserted, a ‘Set’ cannot have duplicate
elements while a ‘Queue’ produces elements in a specific order.
• Java container classes are predefined generic classes. They also
resize themselves automatically, which gives a lot of flexibility.
7. List
• An ‘ArrayList’ excels at randomly accessing elements but is slower when
inserting and removing elements in the middle of the list.
• A ‘LinkedList’ provides fast sequential access and inexpensive insertions and
deletions in the middle of the list but is slower for random access.
• Some common methods with Lists are – contains(), add(), addAll(), remove(),
indexOf(), subList(), clear().
• LinkedList has extra methods which also allow it to be used as a stack or a
queue – getFirst(), addFirst(), removeFirst(), addLast(), removeLast().
• Example: ListOperations
8. Set
• A Set only allows one instance of each value i.e. it prevents
duplication.
• A HashSet uses a hashing function for lookup speed at the sacrifice of
order.
• A TreeSet keeps its elements sorted – either by the provided ordering
(through a Comparator) or by their natural ordering.
• A LinkedHashSet also uses hashing for lookup speed, but appears to
maintain elements in insertion order using an internal LinkedList.
• Example: SetTypes
9. Map
• A Map allows you associate a value with a key. Maps do not allow duplicate
keys. However, different keys can be associated to the same value.
• As we saw in Set, similarly we have HashMap, LinkedHashMap & TreeMap.
• Some common methods – get(), put(), putAll(), containsKey(),
containsValue(), keySet() etc.
• Maps (like Collections) can be easily expanded to multiple dimensions by
combining containers to create powerful data structures. For example –
Map<Brand, List<Product>>, Map<Person, Map<Brand, List<Product>>> &
so on.
• Example: MapTypes
10. Exercise
• Implement a program that uses Java’s Random class to generate
random integers between 0 to 10 and counts how many times
each integer was generated. Test the randomness for a varying
number of iterations.
11. Iterator
• Iterator is a light weight object used to move through a sequence without
worrying about its underlying structure or size. You can ask a collection to
hand you an iterator using iterator().
• You can check if the sequence has more objects by calling hasNext() on the
iterator. You can get the next object in the sequence by calling next().
• You can remove the last element returned by the iterator with remove().
• ListIterator is a more powerful subtype of Iterator produced only by List
classes that allows moving in both directions. It can also replace the last
element visited with set().
• Example: ListIteration
12. Container Taxonomy
Java SE 5 added –
• The Queue interface and its
implementations PriorityQueue & various
flavours of BlockingQueue.
• A ConcurrentMap interface and its
implementation ConcurrentHashMap for
use in concurrency.
• CopyOnWriteArrayList &
CopyOnWriteArraySet, also for use in
concurrency.
• EnumSet & EnumMap, special
implementations designed for use with
enums.
13. The equals() Method
A proper equals() must satisfy the following five conditions –
• Reflexive: For any x, x.equals(x) should return true.
• Symmetric: For any x & y, x.equals(y) should return true if and only if
y.equals(x) returns true.
• Transitive: For any x, y & z, if x.equals(y) returns true and y.equals(z) returns
true, then x.equals(z) should return true.
• Consistent: For any x & y, multiple invocations of x.equals(y) should
consistently return true or consistently return false, provided theirs states
have not changed.
• Null-check: For any non-null x, x.equals(null) should return false.
14. Hashing And The hashcode() Method
• Hashed data structures internally use an array of ‘buckets’. From the key, a number
is derived that indexes into the array (points to a bucket). This number is the hash
code, produced by the hashcode() method.
• Each bucket in the array holds a list of map entries. These entries are searched in a
linear fashion using the equals() method.
• So instead of searching through all the entries, you quickly jump to a bucket where
you only have to compare a few entries, which is much faster.
• The hashcode() method must be fast, consistent and should result in an even
distribution of values. If the values tend to cluster, then the HashMap will be
heavily loaded in some buckets and performance will suffer.
• Example: EqualsAndHashcode
15. Choosing Between Implementations
• Choosing between Lists – ArrayList, LinkedList, CopyOnWriteArrayList
• Choosing between Sets – HashSet, LinkedHashSet, TreeSet, CopyOnWriteArraySet
• Choosing between Maps – HashMap, LinkedHashMap, TreeMap, ConcurrentHashMap
• Example: ListPerformance, SetPerformance, MapPerformance
• Hashed containers performance – capacity and load factor.
• ‘Load factor’ is size/capacity. When the load factor is reached, the container expands by
roughly doubling the number of buckets and redistributing the existing objects into the
new set of buckets. This is called ‘rehashing’.
• Lower load factor means faster lookups but requires more space, while higher load factor
decreases the space required but increases the lookup cost.
17. Exercise
• Write a program that takes a sentence and returns the list of
unique words in the sentence, sorted alphabetically ignoring
case. It is expected that the same sentence may be passed
several times to the program. Since the operation can be time
consuming, the program should cache the results, so that when it
is given a sentence previously encountered, it will simply retrieve
the cached result.
19. Object Creation
• The first time an object of type T is created OR the first time a static method or
static field of class T is accessed, the Java interpreter locates T.class by searching
the classpath.
• As T.class is loaded, all of its static initializers are run. Static initialization take place
only once – during the first load.
• When you create a new T(), storage for a T object is allocated on the heap and
wiped to zero, automatically setting all primitives to their defaults and all
references to null.
• Next, any initializations that occur during field definition are executed. Finally, the
constructor is executed.
• Example: ObjectCreation
20. Garbage Collection
• Java heap is like a conveyor belt that moves forward every time
you allocate a new object, making it remarkably rapid.
• GC is based on the idea that all live objects must ultimately trace
back to a reference on the stack or in static storage.
• GC starts in the stack & static storage and walks through all the
references to find the live objects. For each reference, it traces
into that object and follows all it’s references and so on
(recursively) until it has found the entire web of live objects.
21. GC – Stop & Copy
• The program is first stopped, then each live object is copied from one
heap to another leaving behind all the garbage.
• In addition, as live objects are copied they are packed end-to-end
thereby compacting the new heap. Also, all the references that point
to the object are updated.
• One disadvantage is that it requires twice as much memory as
needed.
• Another disadvantage is that even if your program is generating little
to no garbage, the entire memory is copied which is wasteful.
22. GC – Mark & Sweep
• Each time a live object is found, it is ‘marked’ by setting a flag
in it, but nothing is collected yet.
• Only when the marking process is finished, the sweep
occurs. During the sweep, dead objects are released.
However, no copying happens – fragmented heap is
compacted by shuffling objects around.
• Only blocks created since last GC are compacted.
Periodically, a full sweep is made – all blocks are compacted.
23. GC – Adaptive Optimization
• JVM monitors the efficiency of GC.
• If it becomes a waste of time due to long-lived objects, JVM switches to
mark-and-sweep policy.
• Similarly, JVM tracks how successful mark-and-sweep is.
• If the heap starts to become fragmented, the JVM switches to stop-
and-copy policy.
• This is the ‘adaptive’ part of garbage collection in a JVM.
• Example: GarbageCollection (with different heap sizes).
25. The Class Object
• Run-time type information (RTTI) allows to discover and use type
information while a program is running.
• Type information is represented at runtime through a special object
called Class object. The Class object is used to create all regular
objects of your class.
• There is one Class object for each class that is a part of your program.
• A class object is just like any other object, so you can get a reference
to it. You can also constrain the type using generics.
• Example: ClassObject
26. Class Object Information
• getName() – fully qualified name, including the package
• getSimpleName() – only the class name, excluding the package
• getPackage() – get the package this type belongs to
• isInterface() – whether this type is an interface
• getInterfaces() – get the interfaces implemented by this type
• getSuperClass() – get the superclass of this type
• Example: ClassObjectInfo
27. Reflection
• Suppose you are given a reference to an object whose class is not
available at compile time. Or you need to create and execute objects
on a remote platform, across a network. These problems are solved
using reflection.
• The Class class supports reflection, in conjuction with java.lang.reflect
library which contains Field, Method, Constructor etc. You can use
Constructor to create new objects, Field to read and modify fields,
method to discover and invoke methods etc.
• Example: Reflection
28. A Note On Reflection
• You can change values of private fields by using setAccessible().
• You can even reach in and call private methods, using
setAccessible().
• Even private inner and anonymous classes cannot hide from
reflection.
• However, final fields are safe from change. You can try to change
the value using reflection and no error will be thrown, but nothing
happens – the value is not changed.
29. Exercise
• Create a class without any constructors. Verify that the compiler
creates a default constructor using reflection. Create a class that
has a single constructor that takes a single argument. Verify that
the compiler does not create any default constructor using
reflection.
• Create an ArrayList, print its size, add an element to it and again
print its size, performing all the above operations using
reflection.
31. Parameterized Types
• Generics implements the concept of parameterized types, which allows you
to write a class / interface that can work with multiple types.
• When you create an instance of a parameterized type, casts are taken care
of and type correctness is ensured at compile type.
• The most compelling example of generics are containers – List<>, Map<>,
Set<> etc.
• You substitute the actual type when you use the class / interface. You can
then only work with objects of the actual type or its subtypes.
• Example: TwoTuple, ThreeTuple
32. Generic Methods
• You can also parameterize methods within a class. The class itself
may or may not be generic – both are independent.
• To define a generic method, place a generic parameter list before
the return type – public <T> void add(T object) { }
• With generic method, you generally do not need to specify the
parameter type – the compiler can figure that out for you from
the arguments you pass. This is called ‘type argument inference’.
• Example: SetUtils, TupleUtils
33. Exercise
• Implement RandomList, a special type of parameterized list that
randomly selects one of its elements each time you call select().
• Implement a parameterized method that converts any given list
into a RandomList.
34. Erasure
• Although you can say ArrayList.class, you cannot say
ArrayList<String>.class. The truth is that there is no information about
generic parameter types available at runtime.
• Generics are implemented using ‘erasure’ – any type information is
erased when you use a generic. So List<String> & List<Integer> are in
fact, the same at runtime! Both are erased to their raw form List.
• Generic types are present only during static type checking – after
which they are erased by replacing with a non-generic type. List<T>
gets erased to List, T gets erased to Object and so on.
35. Erasure
• Because erasure removes type information, what matters at runtime are
the boundaries – the points where an object enters and leaves a method.
• These are the points where all the magic happens in generics during
compile time – extra type checks for the incoming values and inserted casts
for the outgoing values.
• Anything that requires knowledge of exact type at runtime will not work
with generics. For example - if (arg instanceof T), T var = new T(), T[] arr =
new T[10] - all will not work.
• Example: Erasure
36. Bounds
• Bounds allow you to place constraints on the parameter types
that can be used with generics. An important effect is that you
can call methods that are in your bound types.
• Bounds are defined using ‘extends’. The class must come first
and then the interfaces. You can have only concrete class but
multiple interfaces. For example - class Solid<T extends Class1 &
IF1 & IF2 & IF3>
• Example: Bounds
37. Exercise
• Implement a generic class Value that stores a value and keeps a
track of how many times the value is accessed by calling
getValue(). Implement a MFUList (most frequently used list) that
holds a list of Values and returns the most frequently used value
(first found by index) on calling mfuValue().
39. The File class
• The ‘File’ class can represent either a File or a Directory. The name is a
bit misleading as what it actually represents is a path on the file
system.
• Common methods –
• getPath(), getAbsolutePath(), getName(), getParent()
• exists(), canRead(), canWrite(), lastModified(), isFile(), isDirectory()
• renameTo(File), mkdir(), mkdirs(), delete()
• list()
• Example: FileInformation
40. Exercise
• Write a program that searches a given directory and lists all the
files and directories under it whose name contains the given
search query.
41. Reader & Writer
• InputStream & OutputStream (old) provide functionality in the form of
byte-oriented IO, whereas Reader & Writer provide Unicode-
compliant, character-based IO. They are also faster than the old
classes.
• Commonly used are FileReader, FileWriter, StringReader, StringWriter,
PipedReader, PipedWriter, BufferedReader, BufferedWriter.
• You’ll almost always want to wrap your reader & writer with a
BufferedReader & a BufferedWriter respectively – buffering tends to
dramatically increase the performance of IO operations.
• Example: FileReaderWriter
42. Exercise
• Write a program that reads an input file, counts the occurrence
of each character in the file, and writes these counts to an output
file.
43. Process Control
• Sometimes you may need to execute other operating system
programs / commands from inside Java. This is done using
java.lang.ProcessBuilder.
• The output of that program / command is available through
Process.getInputStream().
• Any program / command that you can execute from the terminal
can also be executed in this way.
• Example: OSCommand
44. NIO – New I/O
• The new I/O library, java.nio.*, introduced in JDK1.4 has one goal – speed. The old I/O
packages have been reimplemented using nio to take advantage of this speed, so you
will benefit even if you don’t explicitly use nio. The speed comes from using structures
that are closer to the operating system’s way of performing I/O – channels and buffers.
• Think of it as a coal mine – the channel is the mine containing coal (the data) and buffer
is the cart that you send into the mine. The cart comes back full of coal, you get the
coal from the cart. That is, you don’t interact directly with the channel but with the
buffer. The channel either pulls data from or puts data into the buffer.
• The buffer contains plain bytes and to turn them into characters, we must either
encode (while writing) or decode (while reading) – this is done using Charset.
• Example: FileChannelOperation
45. Exercise
• Modify the earlier program that reads an input file, counts the
occurrence of each character in the file, and writes these counts
to an output file using NIO library.
46. File Locking
• File locking allows you to synchronize access to a file as a shared
resource.
• The file locks are visible to and respected by the other operating
system processes because Java file locking mechanism maps directly
to the operating system’s native locking facility.
• You get a lock by calling tryLock() or lock() on a FileChannel. tryLock() is
non-blocking (it returns immediately) whereas lock() blocks until the
lock is acquired or the channel is closed. A lock is released using
FileLock.release().
• Example: FileLocking
48. Introduction to concurrency
• A program can be divided into multiple tasks distributed across
processors, which can dramatically improve throughput.
• One way to implement concurrency is through OS processes. Java
uses threading which creates tasks within the single OS process
represented by the executing process instead of forking additional
external processes.
• Each task is driven by a thread of execution. A thread is a single
sequential flow of control within a process.
• Java’s threading in pre-emptive, which means a scheduling mechanism
provides CPU time slices for each thread.
49. Basic Threading
• To define a task, simply implement Runnable and write a run() method to
make the task do your bidding. A task’s run() method usually has some loop
that continues until the task is no longer necessary, so you must establish
the condition on which to break out of the loop.
• To activate threading behaviour, you must attach the task to a Thread.
• You can optionally call Thread.yield() to yield control – it is basically a
message to the thread scheduler that says “I’ve finished my important parts,
now is a good time to switch to another task for a while”.
• To make the thread sleep or pause for a while, you can call TimeUnit.sleep().
• Example: BasicThreading
50. Executors
• Executors allows you to manage the execution of asynchronous tasks without
having to explicitly manage the lifecycle of threads. They are the preferred way
of working with tasks since its introduction in Java SE5.
• The CachedThreadPool creates one thread per task, whereas the
FixedThreadPool uses a fixed pool of threads to execute the submitted tasks.
• A SingleThreadExecutor is like a pool but with only one thread – it serializes
tasks and completes them in the order they were submitted.
• A call to ExecutorService.shutdown() prevents new tasks from being submitted
to it. All tasks that have already been submitted will be completed first and
then the program will exit.
• Example: UsingExecutors
51. Returning values from tasks
• If you want the task to produce a return value when it’s done, you can
implement Callable (instead of Runnable) and the call() method (instead of
run() method).
• Callable is a generic with a type parameter representing the return value
from call(). Such tasks are invoked using ExecutorService.submit(). The
submit produces a Future object parameterized to the same return type.
• You can query the Future with isDone() to see if the task has completed, and
call get() to fetch the result once the task is completed.
• Example: CallableTasks
52. Exercise
• Write a task which given a positive integer ‘n’, calculates and
returns the factorial of ‘n’ (factorial of a positive integer ‘n’ is the
product of n and all the positive integers less than n).
• Write a program which calculates and displays the factorial of the
first fifteen positive integers using a fixed pool of three threads.
53. synchronized keyword
• For concurrency to work, you need to prevent two tasks from accessing the same
resource during critical periods by putting a lock on it when a task is using it.
• One way is using ‘synchronized’ keyword. All objects automatically contain a single
lock called monitor. When you call a synchronized method, that object is locked
and no other synchronized method of that object can be executed until the lock is
released.
• It is important to make fields private otherwise synchronized keyword cannot
prevent another task from accessing a field directly.
• Every method that accesses a critical shared resource must be synchronized.
• Example: SynchronizingAndLockingAccess
54. Lock objects
• The java.util.concurrent library also contains explicit locking mechanism via
the use of Lock objects.
• The Lock object must be explicitly created, locked and unlocked.
• Right after you call lock(), you must place a try-finally block with the unlock()
in the finally clause – this is the only way to guarantee the lock is always
released.
• With the synchronized keyword, you can’t try and fail to acquire a lock, or try
to acquire a lock for a certain amount of time before giving up – this is
possible only using explicit Lock objects using the tryLock() method.
• Example: SynchronizingAndLockingAccess
55. volatile keyword
• The volatile keyword ensure visibility across the application. If
you declare a field as volatile, this means that as soon a write
occurs for that field, all reads will see the change.
• You should make a field volatile if that field could be
simultaneously accessed by multiple tasks, and at least one of
those accesses is a write.
• Volatile does not work if the value of a field depends on its
previous value (such a incrementing a counter) – you need to
synchronize in such cases.
56. Exercise
• Implement a generic class ConcurrentArrayList which is safe to
use in a multi-threaded environment. It should internally use an
ArrayList for storage and have five methods - get(index), add(),
remove(index), clear(), size().
57. Thread states
• A Thread can be in any of four states –
New : A thread remains in this state momentarily during creation when it performs
initialization. At this point it becomes eligible to receive CPU time. The thread
scheduler then transitions it to runnable or blocked state.
Runnable: This means a thread can be run when CPU cycles are available for it. It
might or might not be running, but nothing is preventing it from being run by the
scheduler i.e. it is not dead or blocked.
Blocked: The thread is prevented from being run due to some reason. It won’t
perform any operations until it re-enters the runnable state. The thread scheduler
simply skips threads in blocked state while allotting CPU time.
Dead: A thread in dead or terminated state is no longer runnable and will not
receive any CPU time, its task has completed.
58. Becoming blocked
• A thread can become blocked for the following reasons –
You’ve put it to sleep by calling sleep(), in which case it will
not be run for the specified time.
It is waiting for some I/O operation to complete.
It is trying to acquire a lock on another object (either by
calling a synchronized method or by trying lock() method)
and that lock is not available because it has already been
acquired by another task.