Advanced JAVA Tutorial for Beginners & Experts – A Perfect Guide to Refer
Last updated on 10th Jul 2020, Blog, Tutorials
In this Advanced Java Programming training course, expert content provider Infinite Skills builds on the beginners Java course, and goes deeper into programming topics that help you to understand these more advanced Java concepts. Designed for the more experienced Java developer, you should have a good working knowledge of the Java programming language before going through this tutorial.
Some of the advanced topics that you will cover in this Advanced Java Tutorial includes; generic programming, sequential and associative data structures, classic data structures, sorting and searching, exception handling, database programming with JDBC, networking programming GUI development using Swing and an overview of Multithreading. You will also explore Java Applets, web applications (Servlets), advanced input and output classes, more advanced strings, regular expressions, Java graphics, and finally, closing off with a look at using Eclipse.
By the conclusion of this training course, you will have a clear understanding of each of the topics of Advanced Java Programming, which will allow you to go more in-depth with the concepts of your choice. Working files are included to allow you to learn the concepts using the same files that the author does throughout this computer based training course.
Java programming language, originated in Sun Microsystems and released back in 1995, is one of the most widely used programming languages in the world, according to TIOBE Programming Community Index. Java is a general-purpose programming language. It is attractive to software developers primarily due to its powerful library and runtime, simple syntax, rich set of supported platforms (Write Once, Run Anywhere – WORA) and awesome community. In this tutorial we are going to cover advanced Java concepts, assuming that our readers already have some basic knowledge of the language. It is by no means a complete reference, rather a detailed guide to move your Java skills to the next level.
Java is object-oriented language and as such the creation of new class instances (objects) is, probably, the most important concept of it. Constructors are playing a central role in new class instance initialization and Java provides a couple of favors to define them.
Implicit (Generated) Constructor
Java allows to define a class without any constructors but it does not mean the class will not have any.
Constructors without Arguments
The constructor without arguments (or no-arg constructor) is the simplest way to do Java compiler’s
Constructors with Arguments
The constructors with arguments are the most interesting and useful way to parameterize new class instances creation.
Java has yet another way to provide initialization logic using initialization blocks. This feature is rarely used but it is better to know it exists.
Java provides certain initialization guarantees which developers may rely on. Uninitialized instance and class (static) variables are automatically initialized to their default values.
Constructors are subject to Java visibility rules and can have access control modifiers which determine if other classes may invoke a particular constructor.
Java (and JVM in particular) uses automatic garbage collection. To put it simply, whenever new objects are created, the memory is automatically allocated for them. Consequently, whenever the objects are not referenced anymore, they are destroyed and their memory is reclaimed. Java garbage collection is generational and is based on assumption that most objects die young (not referenced anymore shortly after their creation and as such can be destroyed safely). Most developers used to believe that objects creation in Java is slow and instantiation of the new objects should be avoided as much as possible. In fact, it does not hold true: the objects creation in Java is quite cheap and fast. What is expensive though is an unnecessary creation of long-lived objects which eventually may fill up old generation and cause stop-the-world garbage collection.
So far we have talked about constructors and objects initialization but have not actually mentioned anything about their counterpart: objects destruction. That is because Java uses garbage collection to manage objects lifecycle and it is the responsibility of garbage collector to destroy unnecessary objects and reclaim the memory.
So far we have looked through class instance construction and initialization. But Java also supports class-level initialization constructs called static initializers. There are very similar to the initialization blocks except for the additional static keyword. Please notice that static initialization is performed once per class-loader.
Over the years a couple of well-understood and widely applicable construction (or creation) patterns have emerged within Java community. We are going to cover the most famous of them: singleton, helpers, factory and dependency injection (also known as inversion of control).
Singleton is one of the oldest and controversial patterns in software developer’s community. Basically, the main idea of it is to ensure that only one single instance of the class could be created at any given time. Being so simple however, singleton raised a lot of the discussions about how to make it right and, in particular, thread-safe.
The utility or helper classes are quite popular pattern used by many Java developers. Basically, it represents the non-instantiable class (with constructor declared as private), optionally declared as final (more details about declaring classes as final will be provided in part 3 of the tutorial, How to design Classes and Interfaces) and contains static methods only.
Factory pattern is proven to be extremely useful technique in the hands of software developers. As such, it has several flavors in Java, ranging from factory method to abstract factory. The simplest example of factory pattern is a static method which returns new instance of a particular class (factory method).
Dependency injection (also known as inversion of control) is considered as a good practice for class designers: if some class instance depends on the other class instances, those dependencies should be provided (injected) to it by means of constructors (or setters, strategies, etc.) but not created by the instance itself.
In object-oriented programming, the concept of interfaces forms the basics of contract-driven (or contract-based) development. In a nutshell, interfaces define the set of methods (contract) and every class which claims to support this particular interface must provide the implementation of those methods: a pretty simple, but powerful idea. Many programming languages do have interfaces in one form or another, but Java particularly provides language support for that
Marker Interfaces Marker interfaces are a special kind of interfaces which have no methods or other nested constructs defined. We have already seen one example of the marker interface in part 2 of the tutorial Using methods common to all objects, the interface Cloneable.
Functional interfaces, default and static methods
With the release of Java 8, interfaces have obtained new very interesting capabilities: static methods, default methods and automatic conversion from lambdas (functional interfaces). In section Interfaces we have emphasized on the fact that interfaces in Java can only declare methods but are not allowed to provide their implementations. With default methods it is not true anymore: an interface can mark a method with the default keyword and provide the implementation for it.
Abstract classes Another interesting concept supported by Java language is the notion of abstract classes. Abstract classes are somewhat similar to the interfaces in Java 7 and very close to interfaces with default methods in Java 8. By contrast to regular classes, abstract classes cannot be instantiated but could be subclassed (please refer to the section Inheritance for more details). More importantly, abstract classes may contain abstract methods: the special kind of methods without implementations, much like interfaces do.
Immutability is becoming more and more important in the software development nowadays. The rise of multi-core systems has raised a lot of concerns related to data sharing and concurrency (in the part 9, Concurrency best practices, we are going to discuss in details those topics). But the one thing definitely emerged: less (or even absence of) mutable state leads to better scalability and simpler reasoning about the systems. Unfortunately, the Java language does not provide strong support for class immutability. However using a combination of techniques it is possible to design classes which are immutable. First and foremost, all fields of the class should be final. It is a good start but does not guarantee immutability alone.
In the pre-Java 8 era, anonymous classes were the only way to provide in-place class definitions and immediate instantiations. The purpose of the anonymous classes was to reduce boilerplate and provide a concise and easy way to represent classes as expressions.
We have already talked a bit about Java visibility and accessibility rules in part 1 of the tutorial, How to design Classes and Interfaces. In this part we are going to get back to this subject again but in the context of subclassing.
Inheritance is one of the key concepts of object-oriented programming, serving as a basis of building class relationships. Combined together with visibility and accessibility rules, inheritance allows designing extensible and maintainable class hierarchies. Conceptually, inheritance in Java is implemented using subclassing and the extends keyword, followed by the parent class. The subclass inherits all of the public and protected members of its parent class. Additionally, a subclass inherits the package private members of the parent class if both reside in the same package. Having said that, it is very important no matter what you are trying to design, to keep the minimal set of the methods which class exposes publicly or to its subclasses. For example, let us take a look on a class Parent and its subclass Child to demonstrate different visibility levels and their effect.
Best Advanced Java Training from MNC Experts
- Instructor-led Sessions
- Real-life Case Studies
In contrast to C++ and some other languages, Java does not support multiple inheritance: in Java every class has exactly one direct parent (with Object class being on top of the hierarchy as we have already known from part 2 of the tutorial, Using methods common to all objects). However, the class may implement multiple interfaces and as such, stacking interfaces is the only way to achieve (or mimic) multiple inheritance in Java.
Inheritance and composition
Fortunately, inheritance is not the only way to design your classes. Another alternative, which many developers consider being better than inheritance, is composition. The idea is very simple: instead of building class hierarchies, the classes should be composed from other classes.
The concept of encapsulation in object-oriented programming is all about hiding the implementation details (like state, internal methods, etc.) from the outside world. The benefits of encapsulation are maintainability and ease of change. The less intrinsic details classes expose, the more control the developers have over changing their internal implementation, without the fear to break the existing code (a real problem if you are developing a library or framework used by many people).
Generics and interfaces
In contrast to regular interfaces, to define a generic interface it is sufficient to provide the type (or types) it should be parameterized with.
Generics and classes
Similarly to interfaces, the difference between regular and generic classes is only the type parameters in the class definitions.
Generics and methods
We have already seen a couple of generic methods in the previous sections while discussing classes and interfaces. However, there is more to say about them. Methods could use generic types as part of arguments declaration or return type declaration.
Limitation of generics
Being one of the brightest features of the language, generics unfortunately have some limitations, mainly caused by the fact that they were introduced quite late into already mature language. Most likely, more thorough implementation required significantly more time and resources so the trade-offs had been made in order to have generics delivered in a timely manner.
Generics, wildcards and bounded types
So far we have seen the examples using generics with unbounded type parameters. The extremely powerful ability of generics is imposing the constraints (or bounds) on the type they are parameterized with using the extends and super keywords. The extends keyword restricts the type parameter to be a subclass of some other class or to implement one or more interfaces.
Generics and type inference
When generics found their way into the Java language, they blew up the amount of the code developers had to write in order to satisfy the language syntax rules.
Generics and annotations
Although we are going to discuss the annotations in the next part of the tutorial, it is worth mentioning that in the pre-Java 8 era the generics were not allowed to have annotations associated with their type parameters. But Java 8 changed that and now it becomes possible to annotate generics type parameters at the places they are declared or used.
Accessing generic type parameters
As you already know from the section Limitation of generics, it is not possible to get the class of the generic type parameter. One simple trick to work-around that is to require additional argument to be passed, Class< T >, in places where it is necessary to know the class of the type parameter T.
When to use generics
Despite all the limitations, the value which generics add to the Java language is just enormous. Nowadays it is hard to imagine that there was a time when Java had no generics support. Generics should be used instead of raw types (Collection< T > instead of Collection, Callable< T > instead of Callable, . . . ) or Object to guarantee type safety, define clear type constraints on the contracts and algorithms, and significantly ease the code maintenance and refactoring. However, please be aware of the limitations of the current implementation of generics in Java, type erasure and the famous implicit boxing and unboxing for primitive types. Generics are not a silver bullet solving all the problems you may encounter and nothing could replace careful design and thoughtful thinking. It would be a good idea to look at some real examples and get a feeling how generics make a Java developer’s life easier.
Method signatures As we already know very well, Java is an object-oriented language. As such, every method in Java belongs to some class instance (or a class itself in case of static methods), has visibility (or accessibility) rules, may be declared abstract or final, and so on. However, arguably the most important part of the method is its signature: the return type and arguments, plus the list of checked exceptions which method implementation may throw (but this part is used less and less often nowadays).
Every method has its own implementation and purpose to exist. However, there are a couple of general guidelines which really help writing clean and understandable methods. Probably the most important one is the single responsibility principle: try to implement the methods in such a way, that every single method does just one thing and does it well. Following this principle may blow up the number of class methods, so it is important to find the right balance. Another important thing while coding and designing is to keep method implementations short (often just by following single responsibility principle you will get it for free). Short methods are easy to reason about, plus they usually fit into one screen so they could be understood by the reader of your code much faster. The last (but not least) advice is related to using return statements. If a method returns some value, try to minimize the number of places where the return statement is being called (some people go even further and recommend to use just single return statement in all cases). More return statements method has, much harder it becomes to follow its logical flows and modify (or refactore) the implementation.
The technique of methods overloading is often used to provide the specialized version of the method for different argument types or combinations. Although the method name stays the same, the compiler picks the right alternative depending on the actual argument values at the invocation point (the best example of overloading is constructors in Java: the name is always the same but the set of arguments is different) or raises a compiler error if none found.
We have talked a lot about method overriding in part 3 of the tutorial, How to design Classes and Interfaces. In this section, when we already know about method overloading, we are going to show off why using @Override annotation is so important. Our example will demonstrate the subtle difference between method overriding and overloading in the simple class hierarchy.
Inlining is an optimization performed by the Java JIT (just-in-time) compiler in order to eliminate a particular method call and replace it directly with method implementation. The heuristics JIT compiler uses are depending on both how often a method is being invoked and also on how large it is. Methods that are too large cannot be inlined effectively. Inlining may provide significant performance improvements to your code and is yet another benefit of keeping methods short as we already discussed in the section Method body.
Recursion in Java is a technique where a method calls itself while performing calculations.
Immutability is taking a lot of attention these days and Java is not an exception. It is well-known that immutability is hard in Java but this does not mean it should be ignored. In Java, immutability is all about changing internal state. As an example, let us take a look on the JavaBeans specification (http://docs.oracle.com/javase/tutorial/javabeans/). It states very clearly that setters may modify the state of the containing object and that is what every Java developer expects. However, the alternative approach would be not to modify the state, but return a new one every time. It is not as scary as it sounds and the new Java 8 Date/Time API (developed under JSR 310: Date and Time API umbrella) is a great example of that.
In Java, specifically if you are developing some kind of library or framework, all public methods should be documented using the Javadoc tool (http://www.oracle.com/technetwork/articles/java/index-jsp-135444.html). Strictly speaking, nothing enforces you to do that, but good documentation helps other developers to understand what a particular method is doing, what arguments it requires, which assumptions or constraints its implementation has, what types of exceptions and when could be raised and what the return value (if any) could be (plus many more things).
Method Parameters and Return Values
Documenting your methods is a great thing, but unfortunately it does not prevent the use cases when a method is being called using incorrect or unexpected argument values. Because of that, as a rule of thumb all public methods should validate their arguments and should never believe that they are going to be specified with the correct values all the time (the pattern better known as sanity checks).
Methods as API entry points
Even if you are just a developer building applications within your organization or a contributor to one of the popular Java framework or library, the design decisions you are taking play a very important role in a way how your code is going to be used. While the API design guidelines are worth of several books, this part of the tutorial touches many of them (as methods become the API entry points) so a quick summary would be very helpful:
- Use meaningful names for methods and their arguments (Method signatures)
- Try to keep the number of arguments to be less than 6 (section Method signatures)
- Keep your methods short and readable (section Method body and Inlining)
- Always document your public methods, including preconditions and examples if it makes sense (section Method Documentation)
- Always perform argument validation and sanity checks (section Method Parameters and Return Values)
- Try to escape null as method return value (section Method Parameters and Return Values)
- Whenever it makes sense, try to design immutable methods (which do not affect the internal state, section Immutability)
- Use visibility and accessibility rules to hide the methods which should not be public (part 3 of the tutorial, How to design Classes and Interfaces)
General programming guidelines:
In the part 3 of the tutorial, How to design Classes and Interfaces, we have discussed how the visibility and accessibility could be applied to class and interface members, limiting their scope. However we have not discussed yet the local variables, which are used within method implementations. In the Java language, every local variable, once declared, has a scope. The variable becomes visible from the place it is declared to the end of the method (or code block) it is declared in. As such, there is only one single rule to follow: declare the local variable as close to the place where it is used as possible.
Class fields and local variables
Every method in Java belongs to some class (or some interface in case of Java 8 and the method is declared as default). As such, there is a probability of name conflicts between local variables used in method implementations and class members. The Java compiler is able to pick the right variable from the scope although it might not be the one developer intended to use. The modern Java IDEs do a tremendous work in order to hint to the developers when such conflicts are taking place (warnings, highlightings, . . . ) but it is still better to think about that while developing.
Method arguments and local variables
Another trap, which quite often inexperienced Java developers fall into, is using method arguments as local variables. Java allows to reassign non-final method arguments with a different value (however it has no effect on the original value whatsoever).
Boxing and unboxing
Boxing and unboxing are all the names of the same technique used in Java language to convert between primitive types (like int, long, double) to respective primitive type wrappers (like Integer, Long, Double). In the part 4 of the tutorial, How and when to use Generics, we have already seen it in action while talking about primitive type wrappers as generic type parameters. Although the Java compiler tries to do its best to hide those conversions by performing autoboxing, sometimes it make things worse and leads to unexpected results.
In the part 3 of the tutorial, How to design Classes and Interfaces, we have discussed interfaces and contract-based development, emphasizing a lot on the fact that interfaces should be preferred to concrete classes wherever possible. The intent of this section is to convince you one more time to consider interfaces first by showing off real examples. Interfaces are not tied to any particular implementation (with default methods being an exception). They are just contracts and as such they provide a lot of freedom and flexibility in the way contracts could be fulfilled. This flexibility becomes increasingly important when the implementation involves external systems or services.
Strings are one of the most widely used types in Java and, arguably, in most of the programming languages. The Java language simplifies a lot the routine operations over strings by natively supporting the concatenations and comparison. Additionally, the Java standard library provides many different classes to make strings operations efficient and that is what we are going to discuss in this section. In Java, strings are immutable objects, represented in UTF-16 format. Every time you concatenate the strings (or perform any operation which modifies the original string) the new instance of the String class is created. Because of this fact, the concatenation operations may become very ineffective, causing the creation of many intermediate string instances (generally speaking, generating garbage).
Java as a language does not force the developers to strictly follow any naming conventions, however the community has developed a set of easy to follow rules which make the Java code looking uniformly across standard library and any other Java project in the wild.
- package names are typed in lower case: org.junit, com.fasterxml.jackson, javax.json • class, enum, interface or annotation names are typed in capitalized case: StringBuilder, Runnable, @Override
- method or field names (except static final) are typed in camel case: isEmpty, format, addAll • static final field or enumeration constant names are typed in upper case, s*eparated by underscore* _’:`LOG, MIN_RA DIX, INSTANCE
- local variable and method arguments names are typed in camel case: str, newLength, minimumCapacity
- generic type parameter names are usually represented as one character in upper case: T, U, E
By following these simple conventions the code you are writing will look concise and indistinguishable from any other library or framework, giving the impression it was authored by the same person (one of those rare cases when conventions are really working).
No matter what kind of Java projects you are working on, the Java standard libraries are your best friends. Yes, it is hard to disagree that they have some rough edges and strange design decisions, nevertheless in 99% it is a high quality code written by experts. It is worth learning. Every Java release brings a lot of new features to existing libraries (with some possible deprecation of the old ones) as well as adds many new libraries. Java 5 brought new concurrency library reconciled under java.util.concurrent package. Java 6 delivered (a bit less known) scripting support (javax.script package) and the Java compiler API (under javax. tools package). Java 7 brought a lot of improvements into java.util.concurrent, introduced new I/O library under java.nio.file package and dynamic languages support with java.lang.invoke package. And finally, Java 8 delivered a long-awaited date/time API hosted under java.time package. Java as a platform is evolving and it is extremely important to keep up with this evolution. Whenever you are considering to introduce third-party library or framework into your project, make sure the required functionality is not already present in the Java standard libraries (indeed, there are many specialized and high-performance algorithm implementations which outperforms the ones from standard libraries but in most of the cases you do not really need them).
Immutability is all over the tutorial and in this part it stays as a reminder: please take immutability seriously. If a class you are designing or a method you are implementing could provide the immutability guarantee, it could be used mostly everywhere without the fear of concurrent modifications. It will make your life as a developer easier (and hopefully lives of your teammates as well).
Test-driven development (TDD) practices are extremely popular in Java community, raising the quality bar of the code being written. With all those benefits which TDD brings on the table, it is sad to observe that Java standard library does not include any test framework or scaffolding as of today.
Java offers the real possibility that most programs can be written in a type-safe language. However, for Java to be broadly useful, it needs to have more expressive power than it does at present.
This paper addresses one of the areas where more power is needed. It extends Java with a mechanism for parametric polymorphism, which allows the definition and implementation of generic abstractions. The paper gives a complete design for the extended language. The proposed extension is small and conservative and the paper discusses the rationale for many of our decisions. The extension does have some impact on other parts of Java, especially Java arrays, and the Java class library.
The paper also explains how to implement the extensions. We first sketched two designs that do not change the JVM, but sacrifice some space or time performance. Our implementation avoids these performance problems. We had three main goals: to allow all instantiations to share the same bytecodes (avoiding code blowup), to have good performance when using parameterized code, and to have little impact on the performance of code that does not use parameterization.
The implementation discussed in Section 3 meets these goals. In that section, we described some small extensions to the virtual machine specification that are needed to support parameterized abstractions; we also described the designs of the bytecode verifier and interpreter, and the runtime structures they rely on.
Preliminary performance results from our implementation of the extended bytecode interpreter show roughly a 2% penalty for the presence of parameterized code, but a speedup for parameterized code of 17%, by eliminating runtime checks. We expect that some simple performance tuning can improve these results.