Generics in Java was never this EASY! — A Complete Beginner’s Guide!
Code Craft Club
Posted on September 11, 2023
Imagine you have a collection of jars, all identical in shape and size, but each one with a unique label. Now, these jars can be used to store a variety of items, from candies to spices, without any confusion. The label on each jar is the key — it tells you exactly what should go inside. Just like these labeled jars simplify your storage needs, Generics in Java are like labels for your code, making it easy to work with multiple data types while keeping things organized.
What Are Generics?
Generics in Java are like those labels on your jars, but for your code. They allow you to create classes, methods, and interfaces that can work with different data types while ensuring type safety. These labels (or type parameters
) specify what kind of data each container can hold, just like the labels on your jars specify what goes inside.
The Need for Generics
Imagine if your jars had no labels. You’d have to open each one to figure out what’s inside. It would be a chaotic mess! Without Generics, your code can end up in a similar mess, with lots of redundancy. You might need to create separate classes for each data type, which is inefficient and prone to errors. Generics come to the rescue, allowing you to create a single, versatile container that can hold various types of data, just like those labeled jars help keep your kitchen organized.
Type Parameters and Representation
In Java, generics use type parameters, which are placeholders for specific data types. These type parameters can be represented in two common ways:
Single Letter Convention: Typically, single uppercase letters are used to distinguish from regular class or interface names. The most commonly used parameters are
E
- Element (used extensively by the Java Collections Framework)
K
- Key
N
- Number
T
- Type
V
- Value
They don't carry any special meaning; they're just placeholders.
For example, the classjava.util.HashMap<K, V>
has two type parameters,K
andV
, representing the type of the keys and values, respectively, stored in the map. The interfacejava.util.List<E>
has a single type parameterE
representing the type of the elements stored in the list.Descriptive Names: Sometimes, more descriptive names like
KeyType
orValueType
are used for type parameters, especially when clarity is important, just as you might use labels with descriptive names for your jars.
Generic Classes
Now, let’s delve into generic classes with code and explanations.
Syntax of a Generic Class -
class ClassName<T1, T2, ..., Tn> {
// block of code
}
Here, T1, T2, …, Tn are the comma-separated type variables.
Let’s understand with an example:
class Box<T> {
private T contents;
public Box(T contents) {
this.contents = contents;
}
public void printDataType() {
System.out.println("Type: " + this.contents.getClass().getSimpleName());
}
}
class Main{
public static void main(String[] args) {
Box<Integer> integerBox = new Box<>(42);
integerBox.printDataType();
Box<String> stringBox = new Box<>("Hello, Generics!");
stringBox.printDataType();
}
}
// Output:
// Type: Integer
// Type: String
In this code, we've defined a generic class Box<T>
that can hold any type of object, where T
is our type parameter. The printDataType
method prints the type of the contents stored in the Box
using getClass().getSimpleName()
.
In the main
method, we create instances of Box
for both Integer
and String
types and call the printDataType
method on each of them. This should print the respective types of the contents stored in the Box
.
Analogy -
Just like your labeled jars can hold different items, the
Box
class can hold different data types. The type parameterT
acts like the label, ensuring that you only put compatible items inside.
Generic Methods
Now, let’s delve into generic methods within a generic class.
Syntax of a Generic Method:
<T1, T2, ..., Tn> returnType methodName(parameters) {
// block of code
}
// Example Syntax -
//Example 1
public <T> void methodName(T obj) {
// block of code
}
//Example 2
public static <T, N> T methodName(T obj, N num) {
// block of code
}
Let’s understand with an example now:
class Box<T> {
private T contents;
public Box(T contents) {
this.contents = contents;
}
public T getContents(){ //method returns T type
return this.contents;
}
}
class Main{
public static void main(String[] args) {
Box<Integer> integerBox = new Box<>(42);
System.out.println("Method Returned:" + integerBox.getContents());
Box<String> stringBox = new Box<>("Hello, Generics!");
System.out.println("Method Returned:" + stringBox.getContents());
}
}
// Output -
// Method Returned:42
// Method Returned:Hello, Generics!
In this code, we have a generic class Box<T>
with a constructor that accepts an object of type T
and a getContents
method that returns an object of type T
.
The integerBox
object is created with an Integer type parameter and the stringBox
object is created with a String type parameter.
In the main()
method, we are calling the getContents()
method on both objects and displayed the returned values on the console.
Generic Interfaces
Similar to creating Generic Classes and Generic Methods, we can also create interfaces in Java that can be used with any type of data by using generics. This type of method is known as a Generic Interface.
Syntax of a Generic Interface:
interface InterfaceName<T1, T2, ..., Tn> {
// block of code
}
class ClassName<T1, T2, ..., Tn> implements InterfaceName<T1, T2, ..., Tn> {
// block of code
}
// Example Syntax -
interface Pair<K, V> {
K getKey();
V getValue();
}
Pair
is a generic interface defining two type parameters, K
and V
.
Implementing classes specify the actual types:
interface Pair<K, V> {
K getKey();
V getValue();
}
class OrderedPair<K, V> implements Pair<K, V> {
private K key;
private V value;
public OrderedPair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
}
class Main{
public static void main(String[] args){
OrderedPair<String, Integer> obj = new OrderedPair<>("Mobile No", 999999999);
System.out.println("Key: " + obj.getKey());
System.out.println("Value: " + obj.getValue());
}
}
// Output -
// Key: Mobile No
// Value: 999999999
In this code, we have defined a generic interface Pair<K, V>
with two type parameters, K
and V
, which define methods getKey
and getValue
. Then, we have implemented this interface with the OrderedPair<K, V>
class, where K
and V
are specific types (in this case, String
and Integer
).
In the main
method, we create an instance of OrderedPair<String, Integer>
with values "Mobile No"
and 999999999
.
Bounded Types
Now, imagine if you wanted to limit the types that can be used with generics, just like specifying that a jar is for storing only chocolates. This is where bounded types come in.
As we saw above of how we can create generic classes, interfaces, and methods that can work with any data type, however, sometimes we may want to limit the data types that can be used with a generic to a specific set of types. In these cases, we can use bounded types by specifying the upper bound type parameter with the extends keyword.
Syntax of a Bounded Types:
<T extends A>
Here, the type parameter T
can only accept data types that are of type A
or any classes or interfaces that extends A
.
Let’s understand with an example:
class NumberContainer<T extends Number> {
private T value;
public NumberContainer(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
class Main{
public static void main(String[] args) {
NumberContainer<Integer> intContainer = new NumberContainer<>(42);
Integer intValue = intContainer.getValue(); // Works fine
System.out.println(intValue);
NumberContainer<Double> doubleContainer = new NumberContainer<>(3.14);
Double doubleValue = doubleContainer.getValue(); // Works fine
System.out.println(doubleValue);
}
}
// Output -
// 42
// 3.14
In this example, NumberContainer
is a generic class that can hold any type that extends the Number
class, which includes numeric types such as Integer
, Double
, and Float
.
We create NumberContainer
instances for Integer
and Double
types and successfully store and retrieve numeric values from them.
Failing Scenario in Bounded Types
Now, let’s see a failing scenario for the above example:
public static void main(String[] args) {
NumberContainer<String> stringContainer = new NumberContainer<>("Hello"); // Compilation error!
}
In this scenario, we attempt to create a NumberContainer
for the String
type, which is not a numeric type. This results in a compilation error because it violates the constraint that the type must extend Number
.
So, in this example, bounded types ensure that the container only works with numeric types and prevents non-numeric types from being stored in it.
Conclusion
In summary, generics in Java are like labeled jars, making it easy to understand, organize, and work with multiple data types in your code. Whether you're working with classes, methods, or interfaces, generics allow you to create more versatile and reliable code. So, just like those labeled jars in your kitchen, generics help you keep your Java code neatly organized and easy to grasp.
Closing Thoughts:
Thank you for reading our blog. We appreciate your time and interest in our content. If you have any questions, feedback, or topics you’d like us to cover in our future articles, please feel free to leave a comment below. Your input is valuable, and it helps us create content that matters to you.
Stay connected with us for more insightful articles on various topics related to technology, programming, and much more.
Happy coding!
Posted on September 11, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.