Java Language Nuances and Intricacies
Java is complete and thorough as regards of implemented features. Each construct or mechanism is implemented full-blown considering many cases. Therefore complicated issues are addressed within itself elaborately. Delving into those elaborations is an enabler for more effective and advanced command of Java language. However they may not be obvious in daily development routine even for experienced developers. Once in a while, someone encounters a compiler error or weird run-time behavior and becomes aware of them. For that, it is worthwhile examining them to have a complete sense. In addition, examining those are quite beneficial for preparing for Oracle’s Java certification, since the certification exam is mostly about rules and properties of the language to a large extent.
In this article, nuances and intricacies about 10 features of Java language are presented. These are comprised of rules and properties pertaining to language constructs, mechanisms. It is intended to demonstrate specifics and edge cases for each issue. As a software engineer having an extensive experience on Java, I have chosen ones seemed interesting by trying to cover different parts of the language.
Initializer Blocks
A member variable can be initialized in a declaration statement, initializer block or constructor. It could be relatively complicated when inheritance, inner classes and mixed usages are taken into account. There are rules applying to initialization.
- Static initializers are run in class loading phase. Instance initializers are run while an object is being constructed.
- Initialization in declaration and initializer blocks are executed in the order they appeared in the code. There could be multiple initializer blocks and they can be interleaved with declaration statements.
- Constructor block is executed after initializer blocks.
- Initializers of parent classes are executed before child.
- Related with class loading, static initializers of outer classes are executed before static initializers of static nested classes. However, there is a special case here. If a static member variable of a nested class is assigned both in outer and nested class initializers, assignment in that of nested class takes place first.
When all these rules are combined, difficult cases can occur. Let’s look at a few examples.
The output of executing main method in the above code will be:
Static Initializer Block
Initializer Block
Constructor Block
The output of executing main method in the above code will be:
staticMemberA: 5
staticMemberB: 10
staticMemberC: 20
instanceMemberA: 104
instanceMemberB: 201
instanceMemberC: 302
instanceMemberD: 402
The output of executing main method in the above code will be:
Parent Init
Outer Init
Inner Init
classMemberP1: 1
classMemberP2: 20
classMemberP3: 300
classMemberO1: 60
classMemberO2: 70
classMemberO2: 800
Inner1.classMemberI1: 400
Inner2.classMemberI2: 500
Overloaded Method Resolution
Resolution of an overloaded method can be tricky. Multiple overloaded methods might be suitable for a parameter list. For example, in case of numeric types, mechanisms such as numeric promotion, auto-boxing, var-args, inheritance could be applied to parameters to match target types. Otherwise, for reference types, overloaded methods for both parent and child classes may be present. Since ambiguity has no place in a programming language, there are rules to resolve that problem.
When no exact match is found, the first rule is that method having the least implicit type conversion steps is selected. For example, int parameter can be applied both to long and Number parameter types. However, only numeric promotion is necessary to obtain long of int, whereas boxing and generalization should be applied for Number type. Therefore, method with long type is selected.
Second rule is selecting the method with most specific/closest parameter types. For example, regarding a short parameter, int is closer to it than long parameter type. As another example, regarding ArrayList parameter, List type is closer to it than Collection parameter type.
As the last rule, even ambiguity persists after first two , the following order is applied to obtain target parameters: numeric promotion, auto-boxing, inheritance and var-args. Since var-args is a relatively new language feature (since Java 5), it takes the lower priority.
The output of executing main method in the above code will be:
short param
long param
Object param
Object param
Hiding, Shadowing and Obscuring
If a member of a class becomes invisible via inheritance, it is called member hiding. Three member types can be hidden: static methods, static variables and instance variables. Instance methods cannot be hidden, they are overridden. Static and instance variables can cause hiding for each other as well. Hidden members can be accessed via super keyword within the subclass, otherwise via type-casting from outside.
The output of executing main method in the above code will be:
10
20
30
Child
1
2
3
Parent
On the other hand, if a member becomes invisible because of scoping, it is member shadowing. Member shadowing can occur in methods or inner classes. When a local variable is defined with the same name as an instance variable, instance variable is shadowed. It can be accessed using this keyword. Also when any member with the same name/signature is defined within an inner class as a member or local variable, this also causes shadowing. Member in the outer element can be accessed using ClassName.this notation. Even in case of many nested levels, members belong to a certain nested class can be accessed using this statement.
The output of executing main method in the above code will be:
local intMember: 10
instance intMember: 1
inner local intMember: 101
inner instance intMember: 100
outer instance intMember: 1
inner instance method: 20
outer instance method: 2
Obscuring is a problem occurring when a variable name is declared the same as a class name. Due to precedence rules, the class may not be accessed directly but via fully qualified name. The following code will cause compile error.
String Pool Usage
Since String objects are immutable, string pool is in action to achieve better efficiency. There are also confusing cases about string pool usage. It is straightforward to know that string literals are used from the pool. What about other cases such as creating strings using String class or producing strings as a result of operations? The key rule here is that only compile time constant expressions are obtained from string pool. Let’s take a look at examples:
The output of executing main method in the above code will be:
true
false
true
false
false
false
false
true
String a and b are assigned to a string literal, therefore they are the same objects. Apart from that, only string d is assigned to a compile time constant expression with concat operator in declaration.
String class has also intern method to return the equivalent object from the pool, or add to the pool as a new object if doesn’t exist.
The output of executing main method in the above code will be:
false
false
true
Interface Methods
Prior to Java 8, interfaces could only have public abstract methods. As of Java 8, public default and public static methods are involved. Even further, from Java 9 and onward, private and private static methods can be part of interfaces.
Newly-allowed methods are used as they are for other classes. However, there is a complicated case about default methods. It is actually a multiple inheritance problem and occurs when multiple interfaces are implemented. In this case, if the same default methods exists more than once in implemented methods, that method must be overridden by the subclass. But that doesn’t happen when the same method exists in an inherited class.
Also these conflicting default method declarations are reachable in the subclass using following syntax:
Another point is that a public static method in the interface can be used only with the name of the interface in the subclass, whereas class names aren’t necessary when static methods from parent classes are used.
try-with-resources Statement
try-with-resources statement is a convenient way of accessing resources requiring to be released after use. It also has a few complicated use cases handled by Java language.
What if there are dependencies between resources? In that case resources must be released in reverse order they obtained. That is what is done with try-with-resources statement with multiple resources. Actually it works like nested statements.
Another case occurs based on the fact that closing resources may also raise an exception that can be handled in one of the catch blocks. However, an exception also may be thrown inside of try block. In that case, we would have two exceptions to handle. Suppressed exception notion takes place here. The exception thrown in try block will be handled mainly with the exception occurred when closing the resource attached as a suppressed exception.
The output of executing main method in the above code will be like this:
ArithmeticException occured.
java.lang.ArithmeticException: / by zero
at javanuances/org.example.AccessResource.main(TryWithResources.java:34)
Suppressed: java.lang.Exception: Cannot close resource
at javanuances/org.example.Resource1.close(TryWithResources.java:27)
at javanuances/org.example.AccessResource.main(TryWithResources.java:33)
Generics Type Erasure
Since type parameters aren’t available in runtime due to type erasure in Java, some operations cannot be done with them such as creating new object, creating array or type check with instanceof operator.
Besides, overloading with different type parameters is not possible because of type erasure again. With this view point, overriding with different type parameters may seem possible but it is also not allowed. The following snippet exemplifies illegal overloading and overriding:
Type parameters cannot be obtained using reflection as expected. And a class object with generic type is not available as well like Generic1<Integer>.class.
Nested Class Rules
Nested classes can be declared in four forms: inner class, static nested class, local class, anonymous class. Inner classes are member classes attached to instances whereas static nested classes are member classes attached to class. Local classes are declared in methods or initializer blocks. Anonymous classes are special type of local classes that are created during an assignment only to extend a class or implement a method anonymously.
Permitted modifiers and member types may vary according to the type of nested class and this could be puzzling. These are the rules that apply:
- Inner classes and static nested classes may take all access modifiers as well as final and static modifier. However local classes may take abstract and final modifiers whereas anonymous classes may not take any of them.
- Static nested classes may have all types of members such as static/instance variables or methods. On the other hand inner classes, local classes and anonymous classes may only have instance variables/methods or static final variables.
- Nested classes except anonymous classes can implement interfaces or extend any class, but anonymous classes cannot.
- Interfaces cannot have inner classes but can have public static nested classes.
- Only static nested interfaces can be declared within any class or interface.
Annotation Targets
When creating an annotation, usage of annotations can be restricted using @Target annotation in a detailed manner. It takes a value of ElementType enumeration. Elements of ElementType are:
TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, ANNOTATION_TYPE, TYPE_PARAMETER, TYPE_USE, PACKAGE, MODULE
Names of the most ElementType values convey the scope in direct manner. They are applied to any declaration of covered element. For instance, TYPE applies to any type declaration including classes, interfaces, enums and annotations. Also some of the element types overlap, such as METHOD and CONSTRUCTOR.
However, TYPE_USE target element type is relatively complex. It applies everywhere a declared type is used. According to Java language specification (https://docs.oracle.com/javase/specs/jls/se11/html/jls-4.html#jls-4.11), there are 16 cases where types are used. It covers most of the other ElementType values. However, there are a few exceptions. For example, it cannot be used with methods without a return type. In addition to those, it can be used in extends/implements clauses, type casts, object construction etc.
By default, element type is union of all element types except for TYPE_USE. Therefore it should always be explicitly given.
Module System
A module is a set of related java packages containing types, data files and static resources. It is introduced to promote encapsulation and modularity as of Java 9. However, module concept alters behavior of Java in a few ways.
Firstly, types in packages that are not exposed by a module cannot be accessed by other packages even via reflection. Without module system, all members of all types could be accessed via reflection including private ones. Packages can be exposed using exporting or opening statements: exports, exports … to, open, opens, opens … to. Exports statement allows access to the types both in compile time and run-time whereas opens statement only allows in runtime. That means opens statement only allows accessing a type via reflection.
Split packages are not allowed with module system. That means a specific package can only be exported by only one module. If none of the modules exports the same package, it doesn’t create a problem. If either or both of them exports the package, that will cause compilation error. The following scenario, module declarations cause a compile error.
./moduleB/src/main/java/com/company/app/partialpackage/ModuleBClass.java:1: error: package exists in another module: mod1
package com.company.app.partialpackage;
Cyclic dependencies are also not allowed within modules.
./moduleC/src/main/java/module-info.java:3: error: cyclic dependence involving moduleB
requires moduleB;
^
./moduleA/src/main/java/module-info.java:3: error: cyclic dependence involving moduleC
requires moduleC;
^
./moduleB/src/main/java/module-info.java:3: error: cyclic dependence involving moduleA
requires moduleA;
^
Conclusion
Several nuances and intricate parts of Java are explained and demonstrated through this article. There could be found more of them in the language when it is examined and dug in. Other specifics can be deducted by inspecting Java language specification and Java docs. They are useful for fully and comfortably harnessing of language and satisfying our analytical minds. Let’s keep up that inquisitive and sharp mind in programming!