Java Generics were introduced with Java 1.5. The idea of new language features is to never break preceding versions. We have to keep in mind that Generics are a type-safety feature for the language/developer.
With that two new types were introduced parameterized types
and type variables
.
The JLS 4.3 Reference Types and Values proposes the following syntax for TypeArgument
and TypeVariable
.
ReferenceType:
ClassOrInterfaceType
TypeVariable
ArrayType
ClassOrInterfaceType:
ClassType
InterfaceType
ClassType:
TypeDeclSpecifier TypeArgumentsopt
InterfaceType:
TypeDeclSpecifier TypeArgumentsopt
TypeDeclSpecifier:
TypeName
ClassOrInterfaceType . Identifier
TypeName:
Identifier
TypeName . Identifier
TypeVariable:
Identifier
ArrayType:
Type [ ]
The examples are like these
Vector<String>
Seq<Seq<A>>
Seq<String>.Zipper<Integer>
Collection<Integer>
Pair<String,String>
and for parameterized types
Vector<String> x = new Vector<String>();
Vector<Integer> y = new Vector<Integer>();
return x.getClass() == y.getClass();
Whenever no bound is given it will assume it as an java.lang.Object
and with type erasure it will be e.g. Vector<Object>
so it's backwards compatible with previous versions of Java.
The syntax for Generic Methods where the Class itself is not generic have the following syntax.
From JLs 8.4 Method Declarations
MethodDeclaration:
MethodHeader MethodBody
MethodHeader:
MethodModifiersopt TypeParametersopt Result MethodDeclarator Throwsopt
MethodDeclarator:
Identifier ( FormalParameterListopt )
An example looks like this
public class GenericMethod {
public static <T> T aMethod(T anObject) {
return anObject;
}
public static void main(String[] args) {
String greeting = "Hi";
String reply = aMethod(greeting);
}
}
Which results with type erasure to
public class GenericMethod {
public static Object aMethod(Object anObject) {
return anObject;
}
public static void main(String[] args) {
String greeting = "Hi";
String reply = (String) aMethod(greeting);
}
}
And once again it is backwards compatible to previous Java versions.
See both proposal papers for more in-depth reasoning
Adding Generics to the Java Programming Language:
Participant Draft Specification
Specialization of Java Generic Types
About the technical part. The steps to create a Java program is to compile the .java
file. One would do that with the javac
command to generate the classfiles. The JavacParser
parses the whole file with the above specification and generates the bytecode. See here for the JavacParser source code.
Let's take the following Test.java file
class Things{}
class Stuff<T>{
T t;
public <U extends Things> U doStuff(T t, U u){
return u;
};
public <T> T doStuff(T t){
return t;
};
}
To keep it backwards compatible the JVM did not change it's previous attributes for the class files. They added a new attribute and named it Signature
. From the propsal paper
When used as an attribute of a method or field, a signature gives the
full (possibly generic) type of that method or field. When used as a
class attribute, a signature indicates the type parameters of the
class, followed by its supertype, followed by all its interfaces. The
type syntax in signatures is extended to parameterized types and type
variables. There is also a new signature syntax for formal type
parameters. The syntax extensions for signature strings are as
follows:
The JVM Spec 4.3.4 define the following syntax
MethodTypeSignature:
FormalTypeParametersopt (TypeSignature*) ReturnType ThrowsSignature*
ReturnType:
TypeSignature
VoidDescriptor
ThrowsSignature:
^ ClassTypeSignature
^ TypeVariableSignature
By disassembling the Test.class
file with javap -v
we get the following:
class Stuff<T extends java.lang.Object> extends java.lang.Object
minor version: 0
major version: 52
flags: ACC_SUPER
Constant pool:
#1 = Methodref #3.#20 // java/lang/Object."<init>":()V
#2 = Class #21 // Stuff
#3 = Class #22 // java/lang/Object
#4 = Utf8 t
#5 = Utf8 Ljava/lang/Object;
#6 = Utf8 Signature
#7 = Utf8 TT;
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 doStuff
#13 = Utf8 (Ljava/lang/Object;LThings;)LThings;
#14 = Utf8 <U:LThings;>(TT;TU;)TU;
#15 = Utf8 (Ljava/lang/Object;)Ljava/lang/Object;
#16 = Utf8 <T:Ljava/lang/Object;>(TT;)TT;
#17 = Utf8 <T:Ljava/lang/Object;>Ljava/lang/Object;
#18 = Utf8 SourceFile
#19 = Utf8 Test.java
#20 = NameAndType #8:#9 // "<init>":()V
#21 = Utf8 Stuff
#22 = Utf8 java/lang/Object
{
T t;
descriptor: Ljava/lang/Object;
flags:
Signature: #7 // TT;
Stuff();
descriptor: ()V
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 4: 0
public <U extends Things> U doStuff(T, U);
descriptor: (Ljava/lang/Object;LThings;)LThings;
flags: ACC_PUBLIC
Code:
stack=1, locals=3, args_size=3
0: aload_2
1: areturn
LineNumberTable:
line 8: 0
Signature: #14 // <U:LThings;>(TT;TU;)TU;
public <T extends java.lang.Object> T doStuff(T);
descriptor: (Ljava/lang/Object;)Ljava/lang/Object;
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=2
0: aload_1
1: areturn
LineNumberTable:
line 11: 0
Signature: #16 // <T:Ljava/lang/Object;>(TT;)TT;
}
Signature: #17 // <T:Ljava/lang/Object;>Ljava/lang/Object;
SourceFile: "Test.java"
The method
public <U extends Things> U doStuff(T t, U u){
return u;
};
translates to the Signature of to indicate it's a generic method
Signature: #14 // <U:LThings;>(TT;TU;)TU;
If we used a non-generic class for previous Java 1.5 versions e.g.
public String doObjectStuff(Object t, String u){
return u;
}
would translate to
public java.lang.String doObjectStuff(java.lang.Object, java.lang.String);
descriptor: (Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=3, args_size=3
0: aload_2
1: areturn
LineNumberTable:
line 12: 0
The only difference between both are that one has Signature
attribute field indicating that it is indeed a generic method while the other previous Java 1.5 versions does not have it. But both have the same descriptor
attribute
Non-Generic method
descriptor: (Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/String;
Generic method
descriptor: (Ljava/lang/Object;LThings;)LThings;
Which makes it backwards compatible. So the answer would be as you suggested
"the language designers just made it that way"
with the addition of
"the language designers just made it that way, to make it backwards compatible while not adding much code"
EDIT: About the comment that it should be easy to handle the different syntax I found a passage in the book Java Generics and Collections by Philip Wadler, Maurice Naftalin
Generics in Java resemble templates in C++. There are just two important things to bear in mind about the relationship between Java generics and C++ templates: syntax and semantics. The syntax is deliberately similiar and the semantics are deliberately different.
Syntactically, angle brackets were chosen because they are familiar to C++ users, and because square brackets would be hard to parse. However, there is one difference in syntax. In C++, nested parameters require extra spaces, so you see things like this: List< List > [...] etc.
See here