Both C# and Java provide a mechanism for creating strongly typed data structures without knowing the specific types at compile time. Prior to the existence of the Generics feature set, this capability was achieved by specifying the type of the objects within the data structure as
Object
then casting to specific types at runtime. This technique had several drawbacks including lack of type safety, poor performance and code bloat.
The following code sample shows how one would calculate the sum of all the integers in a collection using generics and using a collection of Objects so that both approaches can be compared.
C# Code
using System;
using System.Collections;
using System.Collections.Generic;
class Test{
public static Stack GetStackB4Generics(){
Stack s = new Stack();
s.Push(2);
s.Push(4);
s.Push(5);
return s;
}
public static Stack<int> GetStackAfterGenerics(){
Stack<int> s = new Stack<int>();
s.Push(12);
s.Push(14);
s.Push(50);
return s;
}
public static void Main(String[] args){
Stack s1 = GetStackB4Generics();
int sum1 = 0;
while(s1.Count != 0){
sum1 += (int) s1.Pop(); //cast
}
Console.WriteLine("Sum of stack 1 is " + sum1);
Stack<int> s2 = GetStackAfterGenerics();
int sum2 = 0;
while(s2.Count != 0){
sum2 += s2.Pop(); //no cast
}
Console.WriteLine("Sum of stack 2 is " + sum2);
}
}
Java Code
import java.util.*;
class Test{
public static Stack GetStackB4Generics(){
Stack s = new Stack();
s.push(2);
s.push(4);
s.push(5);
return s;
}
public static Stack<Integer> GetStackAfterGenerics(){
Stack<Integer> s = new Stack<Integer>();
s.push(12);
s.push(14);
s.push(50);
return s;
}
public static void main(String[] args){
Stack s1 = GetStackB4Generics();
int sum1 = 0;
while(!s1.empty()){
sum1 += (Integer) s1.pop(); //cast
}
System.out.println("Sum of stack 1 is " + sum1);
Stack<Integer> s2 = GetStackAfterGenerics();
int sum2 = 0;
while(!s2.empty()){
sum2 += s2.pop(); //no cast
}
System.out.println("Sum of stack 2 is " + sum2);
}
}
Although similar in concept to templates in C++, the Generics feature in C# and Java is not implemented similarly. In Java, the generic functionality is implemented using
type erasure. Specifically the generic type information is present only at compile time, after which it is erased by the compiler and all the type declarations are replaced with
Object
. The compiler then automatically inserts casts in the right places. The reason for this approach is that it provides total interoperability between generic code and legacy code that doesn't support generics. The main problem with type erasure is that the generic type information is not available at run time via reflection or run time type identification. Another consequence of this approach is that generic data structures types must always be declared using objects and not primitive types. Thus one must create
Stack<Integer>
instead of
Stack<int>
when working integers.
In C#, there is explicit support for generics in the .NET runtime's instruction language (IL). When the generic type is compiled, the generated IL contains place holders for specific types. At runtime, when an initial reference is made to a generic type (e.g.
List<int>
) the system looks to see if anyone already asked for the type or not. If the type has been previously requested, then the previously generated specific type is returned. If not, the JIT compiler instantiates a new type by replacing the generic type parameters in the IL with the specific type (e.g. replacing
List<T>
with
List<int>
). It should be noted that if the requested type is a reference type as opposed to a value type then the generic type parameter is replaced with
Object
. However there is no casting done internally by the .NET runtime when accessing the type.
In certain cases, one may need create a method that can operate on data structures containing any type as opposed to those that contain a specific type (e.g. a method to print all the objects in a data structure) while still taking advantage of the benefits of strong typing in generics. The mechanism for specifying this in C# is via a feature called
generic type inferencing while in Java this is done using
wildcard types. The following code samples show how both approaches lead to the same result.
C# Code
using System;
using System.Collections;
using System.Collections.Generic;
class Test{
//Prints the contents of any generic Stack by
//using generic type inference
public static void PrintStackContents<T>(Stack<T> s){
while(s.Count != 0){
Console.WriteLine(s.Pop());
}
}
public static void Main(String[] args){
Stack<int> s2 = new Stack<int>();
s2.Push(4);
s2.Push(5);
s2.Push(6);
PrintStackContents(s2);
Stack<string> s1 = new Stack<string>();
s1.Push("One");
s1.Push("Two");
s1.Push("Three");
PrintStackContents(s1);
}
}
Java Code
import java.util.*;
class Test{
//Prints the contents of any generic Stack by
//specifying wildcard type
public static void PrintStackContents(Stack<?> s){
while(!s.empty()){
System.out.println(s.pop());
}
}
public static void main(String[] args){
Stack <Integer> s2 = new Stack <Integer>();
s2.push(4);
s2.push(5);
s2.push(6);
PrintStackContents(s2);
Stack<String> s1 = new Stack<String>();
s1.push("One");
s1.push("Two");
s1.push("Three");
PrintStackContents(s1);
}
}
Both C# and Java provide mechanisms for specifying constraints on generic types. In C# there are three types of constraints that can be applied to generic types
- A derivation constraint indicates to the compiler that the generic type parameter derives from a base type such an interface or a particular base class
- A default constructor constraint indicates to the compiler that the generic type parameter exposes a public default constructor
- A reference/value type constraint constrains the generic type parameter to be a reference or a value type.
In Java, only the derivation constraint is supported. The following code sample shows how constraints are used in practice.
C# Code
using System;
using System.Collections;
using System.Collections.Generic;
public class Mammal {
public Mammal(){;}
public virtual void Speak(){;}
}
public class Cat : Mammal{
public Cat(){;}
public override void Speak(){
Console.WriteLine("Meow");
}
}
public class Dog : Mammal{
public Dog(){;}
public override void Speak(){
Console.WriteLine("Woof");
}
}
public class MammalHelper<T> where T: Mammal /* derivation constraint */,
new() /* default constructor constraint */{
public static T CreatePet(){
return new T();
}
public static void AnnoyNeighbors(Stack<T> pets){
while(pets.Count != 0){
Mammal m = pets.Pop();
m.Speak();
}
}
}
public class Test{
public static void Main(String[] args){
Stack<Mammal> s2 = new Stack<Mammal>();
s2.Push(MammalHelper<Dog>.CreatePet());
s2.Push(MammalHelper<Cat>.CreatePet());
MammalHelper<Mammal>.AnnoyNeighbors(s2);
}
}
Java Code
import java.util.*;
abstract class Mammal {
public abstract void speak();
}
class Cat extends Mammal{
public void speak(){
System.out.println("Meow");
}
}
class Dog extends Mammal{
public void speak(){
System.out.println("Woof");
}
}
public class Test{
//derivation constraint applied to pets parameter
public static void AnnoyNeighbors(Stack<? extends Mammal> pets){
while(!pets.empty()){
Mammal m = pets.pop();
m.speak();
}
}
public static void main(String[] args){
Stack<Mammal> s2 = new Stack<Mammal>();
s2.push(new Dog());
s2.push(new Cat());
AnnoyNeighbors(s2);
}
}
C# also includes the
default
operator which returns the default value for a type. The default value for reference types is
null
, and the default value for value types (such as integers, enum, and structures) is a zero whitewash (filling the structure with zeros). This operator is very useful when combined with generics. The following code sample excercises the functionality of this operator.
C# Code
using System;
public class Test{
public static T GetDefaultForType(){
return default(T); //return default value of type T
}
public static void Main(String[] args){
Console.WriteLine(GetDefaultForType<int>());
Console.WriteLine(GetDefaultForType<string>());
Console.WriteLine(GetDefaultForType<float>());
}
}