J2SE 5.0, codenamed Tiger, was released on September 30, 2004. Originally numbered 1.5 following the internal versioning scheme, the number was changed to “better reflect the maturity, stability, scalability, and security level of J2SE.” This release introduced several significant new language features, developed under JSR 176.

Generic

Using generics in code brings many benefits:

  1. Strong type checking at compile time
  2. Elimination of type casts
    Without generics:
    1
    2
    3
    List list = new ArrayList();
    list.add("hello");
    String s = (String) list.get(0);
    With generics:
    1
    2
    3
    List<String> list = new ArrayList<String>();
    list.add("hello");
    String s = list.get(0);
  3. Implementation of generic algorithms, reducing code redundancy

Enhanced for Loop

Before Java 5.0, iterating over arrays required a traditional for loop:

1
2
3
for (int i = 0; i < array.length; i++) {
item = array[i]
}

or

1
2
3
for (int i = 0; i < list.size(); i++) {
item = list.get(i)
}

or

1
2
3
for (Iterator<Object> it = list.iterator(); it.hasNext();) {
item = it.next()
}

Starting from Java 5.0, both arrays and collections can be iterated using a unified for-each loop:

1
2
3
for (item : arrayOrList) {
// ...
}

The benefit of for-each is that without JIT enabled, for-each offers noticeable performance improvements over a traditional for loop. With JIT enabled, the difference is negligible. But as Java developers, we use Java to eliminate platform differences. Unless there are special performance requirements, we should prefer for-each. So how does for-each unify the iteration of arrays and collections?

1
2
3
4
public static void main(String[] args) {
for (String arg : args) {
}
}

The compiler generates the following bytecode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 0: aload_0
1: astore_1
2: aload_1
3: arraylength
4: istore_2
5: iconst_0
6: istore_3
7: iload_3
8: iload_2
9: if_icmpge 23
12: aload_1
13: iload_3
14: aaload
15: astore 4
17: iinc 3, 1
20: goto 7
23: return

Decompiling the generated bytecode gives us:

1
2
3
4
5
6
7
8
public static void main(String[] var0) {
String[] var1 = var0;
int var2 = var0.length;

for(int var3 = 0; var3 < var2; ++var3) {
String var10000 = var1[var3];
}
}

As we can see, the compiler optimized the for loop by extracting the array length access into a variable outside the loop body. So what about iterating collections with for-each? Take this code as an example:

1
2
3
4
public static void main(String[] args) {
for (String arg : Arrays.asList(args)) {
}
}

The compiler generates the following bytecode:

1
2
3
4
5
6
7
8
9
10
11
12
13
 0: aload_0
1: invokestatic #2 // Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
4: invokeinterface #3, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
9: astore_1
10: aload_1
11: invokeinterface #4, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
16: ifeq 32
19: aload_1
20: invokeinterface #5, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
25: checkcast #6 // class java/lang/String
28: astore_2
29: goto 10
32: return

Decompiled:

1
2
3
4
5
public static void main(String[] var0) {
String var2;
for(Iterator var1 = Arrays.asList(var0).iterator(); var1.hasNext(); var2 = (String)var1.next()) {
}
}

So for-each uses Iterator when iterating collections, and an optimized traditional for loop when iterating arrays.

Autoboxing / Unboxing

Autoboxing & Unboxing automatically performs implicit conversions between primitive types and their corresponding wrapper classes, eliminating redundant code. For example:

1
Integer a = 100;

The compiler automatically generates the following bytecode:

1
2
3
   bipush        100
-> invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
astore_1

From the bytecode, we can see that autoboxing is essentially the compiler converting 100 to Integer.valueOf(100). Unboxing automatically calls Integer.intValue(). For example:

1
int a = new Integer(100);

The compiler generates:

1
2
3
4
5
6
   new           #2                  // class java/lang/Integer
dup
bipush 100
invokespecial #3 // Method java/lang/Integer."<init>":(I)V
-> invokevirtual #4 // Method java/lang/Integer.intValue:()I
istore_1

Typesafe Enum

Before Java 5.0, there was no real enum class. Enumerations were typically represented using int values, which brought several problems:

  1. Not type-safe – any int value could be used as a parameter, and the compiler had no way to validate it
  2. No namespace – only variable prefixes could distinguish enums, easily leading to naming conflicts
  3. Fragile references – since int enums are typically constants inlined by the compiler, changing enum values or inserting new ones would require recompilation of all code using them
  4. Log-unfriendly – int values printed in logs convey no meaning

Starting from Java 5.0, you can define typesafe enums with the enum keyword:

1
2
3
public enum Color {
RED, GREEN, BLUE
}

Long ago, the official Android Performance Tips had a section called Avoid Enums Where You Only Need Ints, recommending against using Enum because it consumed more memory. This section was later removed. The reason was tied to Android‘s Runtime – early Android used Dalvik, which was weak in memory allocation, hence the recommendation. Starting from Android 5.0, which uses ART, the memory overhead of Enum became negligible.

Varargs

Varargs must be the last parameter of a method:

1
2
public void printf(String format, Object... args) {
}

What’s the difference between Object... and Object[]? Let’s look at the bytecode:

1
2
3
4
5
6
7
8
public void printf(java.lang.String, java.lang.Object...);
descriptor: (Ljava/lang/String;[Ljava/lang/Object;)V
flags: ACC_PUBLIC, ACC_VARARGS
Code:
stack=0, locals=3, args_size=3
0: return
LineNumberTable:
line 11: 0

It turns out args is actually Object[]Object... is merely syntactic sugar. What happens if we declare another method with the same name but Object[] as the last parameter?

1
2
3
4
5
6
7
class VarArgs {
public void printf(String format, Object... args) {
}

public void printf(String format, Object[] args) {
}
}

As expected, the compiler throws an error:

1
2
3
4
VarArgs.java:5: error: cannot declare both printf(String,Object[]) and printf(String,Object...) in VarArgs
public void printf(String format, Object[] args) {
^
1 error

Static Import

Before Java 5.0, accessing static members of a class required qualifying them with the class name:

1
double r = Math.cos(Math.PI * theta);

A common workaround was defining static members in an interface and inheriting from it, but this was never a good idea. A class’s use of another class’s static members is an implementation detail. When a class implements an interface, the interface’s members become part of that class’s public API – implementation details should not leak into the public API. To properly solve this, Java 5.0 introduced static imports:

1
2
3
4
import static java.lang.Math.PI;
import static java.lang.Math.cos;

double r = cos(PI * theta);

Annotation

Many APIs require substantial boilerplate code. For example, writing a JAX-RPC web service requires both an interface and its implementation class, leading to redundant boilerplate. Before Java 5.0, Java only provided limited ad-hoc annotations such as @deprecated. Starting from 1.5, Java added the ability to define custom annotations and provided APT for compile-time annotation processing.

How did Java support Annotation without changing the class file structure? It comes down to the ClassFile structure:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}

The ClassFile structure ends with an attribute_info array. See the JVM specification. Annotation exists in the class file as a form of attribute_info. According to the JVM specification, Java 5.0 supports the following forms:

  1. RuntimeVisibleAnnotations - annotations visible at runtime
  2. RuntimeInvisibleAnnotations - annotations invisible at runtime
  3. RuntimeVisibleParameterAnnotations - parameter annotations visible at runtime
  4. RuntimeInvisibleParameterAnnotations - parameter annotations invisible at runtime
  5. AnnotationDefault - default values for annotation methods

Whether an annotation is visible at runtime depends on its @RetentionPolicy. From the Java source code, @RetentionPolicy has three values:

  1. SOURCE - retain the annotation in source code only
  2. CLASS - retain the annotation in the class file
  3. RUNTIME - retain the annotation at runtime

The mapping between RetentionPolicy and annotation visibility:

RetentionPolicy Visibility
SOURCE RuntimeInvisible
CLASS RuntimeInvisible
RUNTIME RuntimeVisible

So if you want to access a custom Annotation at runtime, you must declare its RetentionPolicy as RetentionPolicy.RUNTIME. Otherwise, it will not be accessible at runtime.

For more details, see: https://docs.oracle.com/javase/1.5.0/docs/guide/language/