Generics in Java allow you to create classes, interfaces, and methods that can work with different types of objects. They were introduced in Java 5 to provide compile-time type safety and to eliminate the need for explicit type casting.
The basic syntax for using generics involves placing a type parameter in angle brackets < > after the name of the class or method. For example, the following code defines a generic class named “Box” that can store an object of any type:
public class Box<T> { private T contents; public void setContents(T contents) { this.contents = contents; } public T getContents() { return contents; } }
In this example, the type parameter T is used as a placeholder for the actual type that will be used when an instance of the class is created. When an instance of the Box class is created, the actual type is specified within the angle brackets, like this:
Box<String> stringBox = new Box<String>(); Box<Integer> intBox = new Box<Integer>();
Generics are widely used in Java collections framework classes such as List, Set, and Map. For example, the following code shows how to create a List of strings using generics:
List<String> stringList = new ArrayList<String>(); stringList.add("apple"); stringList.add("banana"); stringList.add("cherry");
Generics in Java provide type safety and enable more reusable and flexible code. They allow you to write code that can work with different types of objects without requiring explicit type casting.
Advantage of Java Generics:
Java Generics provide several advantages, including:
- Type safety: Generics provide compile-time type safety, which means that the compiler can detect errors at compile-time rather than runtime. This makes code more reliable and reduces the likelihood of errors occurring during execution.
- Code reusability: With generics, you can create classes and methods that can work with different types of objects. This means that you can write more generic code that can be reused in different contexts without having to rewrite the code for each specific type.
- Code readability: Generics make code more readable by making the type of objects being used in the code explicit. This can make it easier to understand the code and reduce the likelihood of errors.
- Performance: Generics can improve the performance of code by reducing the need for type casting. Type casting can be a costly operation, and using generics eliminates the need for it in many cases.
- Collection framework enhancements: Java’s collection framework makes extensive use of generics, making it easier to work with collections of objects. Generics provide compile-time type safety, which means that errors can be detected at compile-time rather than at runtime, making collections more reliable and easier to use.
Overall, Java Generics provide a number of benefits that make Java code more reliable, reusable, and readable.
Full Example of Generics in Java:
Here’s a full example of how to use generics in Java:
public class Box<T> { private T contents; public void setContents(T contents) { this.contents = contents; } public T getContents() { return contents; } public static void main(String[] args) { // Create a new Box object that can hold Strings Box<String> stringBox = new Box<>(); // Set the contents of the box to a String stringBox.setContents("Hello, world!"); // Get the contents of the box and print it to the console String contents = stringBox.getContents(); System.out.println(contents); // Create a new Box object that can hold Integers Box<Integer> intBox = new Box<>(); // Set the contents of the box to an Integer intBox.setContents(42); // Get the contents of the box and print it to the console Integer intContents = intBox.getContents(); System.out.println(intContents); } }
In this example, we define a generic class named Box
with a type parameter T
. The Box
class has two methods, setContents
and getContents
, that can be used to set and retrieve the contents of the box. The main
method creates two instances of the Box
class: one that can hold String
objects and one that can hold Integer
objects. The setContents
method is used to set the contents of the boxes, and the getContents
method is used to retrieve the contents and print them to the console. The type parameter T
is replaced with the actual type when an instance of the Box
class is created. In this way, the Box
class can be used to store objects of any type.
Example of Java Generics using Map:
Here’s an example of using Java Generics with the Map interface:
import java.util.HashMap; import java.util.Map; public class GenericMapExample { public static void main(String[] args) { // Create a Map object with Integer keys and String values Map<Integer, String> map = new HashMap<>(); // Add some key-value pairs to the map map.put(1, "Apple"); map.put(2, "Banana"); map.put(3, "Cherry"); // Get the value associated with key 2 and print it to the console String value = map.get(2); System.out.println(value); // Iterate over the map and print out each key-value pair for (Map.Entry<Integer, String> entry : map.entrySet()) { System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue()); } } }
In this example, we create a Map
object with Integer
keys and String
values using Java Generics. We add some key-value pairs to the map using the put
method, and retrieve the value associated with key 2 using the get
method. We then iterate over the map using a for-each loop and print out each key-value pair using the getKey
and getValue
methods of the Map.Entry
interface. The use of generics with the Map
interface ensures that we can only store Integer
keys and String
values in the map, providing compile-time type safety and reducing the likelihood of runtime errors.
Generic class:
A generic class is a class that can work with any data type. It is defined with one or more type parameters that can be replaced with actual types when creating an instance of the class. This allows you to write a class that can be used with different data types, without having to write separate code for each data type.
Here’s an example of a generic class in Java:
public class Pair<T1, T2> { private T1 first; private T2 second; public Pair(T1 first, T2 second) { this.first = first; this.second = second; } public T1 getFirst() { return first; } public T2 getSecond() { return second; } public void setFirst(T1 first) { this.first = first; } public void setSecond(T2 second) { this.second = second; } }
In this example, we define a generic class named Pair
that takes two type parameters, T1
and T2
. The Pair
class has two fields of type T1
and T2
, and a constructor that takes two arguments of types T1
and T2
. The class also has get
and set
methods for each field. The type parameters T1
and T2
can be replaced with actual types when creating an instance of the Pair
class. For example:
Pair<String, Integer> pair = new Pair<>("Hello", 42); String first = pair.getFirst(); Integer second = pair.getSecond();
In this example, we create an instance of the Pair
class with a String
and an Integer
as the type arguments. We then use the getFirst
and getSecond
methods to retrieve the values of the fields. The use of generics with the Pair
class allows us to work with different types of objects using a single class definition.
Type Parameters:
Type parameters are placeholders for types that are used in the definition of generic classes, interfaces, and methods in Java. They allow you to write code that can work with different types of objects without having to rewrite the code for each type. Type parameters are represented by one or more letters enclosed in angle brackets (<>
) following the name of the generic class, interface, or method.
Here’s an example of using type parameters in a generic method:
public class GenericMethods { public static <T> void printArray(T[] array) { for (T element : array) { System.out.println(element); } } public static void main(String[] args) { Integer[] intArray = { 1, 2, 3, 4, 5 }; String[] stringArray = { "Hello", "World" }; System.out.println("Integer array:"); printArray(intArray); System.out.println("String array:"); printArray(stringArray); } }
In this example, we define a generic method named printArray
that takes an array of any type as its argument. The type parameter T
is used to specify the type of the array elements. The printArray
method then iterates over the array and prints each element to the console. We call the printArray
method twice in the main
method, passing in an array of Integer
objects and an array of String
objects, respectively. The use of the type parameter T
allows the printArray
method to work with arrays of any type.
Type parameters can also be used in the definition of generic classes and interfaces, as shown in the previous example of a generic class. When a class or interface is defined with type parameters, the type parameters can be used as the types of fields, method arguments, and return types within the class or interface. The type parameters can then be replaced with actual types when creating an instance of the generic class or interface.
Generic Method:
A generic method is a method that can work with any data type. It is defined with one or more type parameters that can be replaced with actual types when invoking the method. This allows you to write a method that can be used with different data types, without having to write separate code for each data type.
Here’s an example of a generic method in Java:
public static <T> int countOccurrences(T[] array, T value) { int count = 0; for (T element : array) { if (element.equals(value)) { count++; } } return count; }
In this example, we define a generic method named countOccurrences
that takes an array of type T
and a value of type T
as its arguments. The method uses the equals
method to compare each element of the array to the given value and counts the number of occurrences. The method returns the count as an integer. The type parameter T
can be replaced with an actual type when invoking the method. For example:
Integer[] intArray = { 1, 2, 3, 4, 5 }; int count = countOccurrences(intArray, 3); System.out.println("Occurrences of 3: " + count); String[] stringArray = { "foo", "bar", "baz", "foo" }; count = countOccurrences(stringArray, "foo"); System.out.println("Occurrences of 'foo': " + count);
In this example, we create an array of Integer
objects and an array of String
objects. We then call the countOccurrences
method twice, passing in the arrays and a value to count. The use of the type parameter T
allows the countOccurrences
method to work with arrays of any type.
Generic methods are especially useful when working with collections, where you may want to perform operations on the collection that are independent of the specific type of object in the collection. By using a generic method, you can write code that is more flexible and reusable.
Wildcard in Java Generics:
In Java Generics, the wildcard is denoted by the ?
character and is used to represent an unknown type. Wildcards are commonly used in situations where the type of an object is not known or is irrelevant. There are two types of wildcards: the upper-bounded wildcard and the lower-bounded wildcard.
The upper-bounded wildcard is used to specify that a type argument must be a subtype of a certain type. It is denoted by the <? extends T>
syntax, where T
is the upper bound. For example:
public static double sumOfList(List<? extends Number> list) { double sum = 0.0; for (Number n : list) { sum += n.doubleValue(); } return sum; }
In this example, the sumOfList
method takes a List
of any subclass of Number
, such as Integer
, Double
, or Long
, as its argument. The method then iterates over the list and calculates the sum of the elements. The use of the upper-bounded wildcard <? extends Number>
allows the method to work with any subclass of Number
.
The lower-bounded wildcard is used to specify that a type argument must be a supertype of a certain type. It is denoted by the <? super T>
syntax, where T
is the lower bound. For example:
public static void addNumbers(List<? super Integer> list) { list.add(1); list.add(2); list.add(3); }
In this example, the addNumbers
method takes a List
of any superclass of Integer
, such as Number
or Object
, as its argument. The method then adds the integers 1, 2, and 3 to the list. The use of the lower-bounded wildcard <? super Integer>
allows the method to add any subclass of Integer
to the list, such as Integer
, Short
, or Byte
.
Wildcard types can also be used as type arguments when creating objects of generic classes or invoking generic methods. The use of wildcards allows you to write more flexible and reusable code that can work with different types of objects.
Upper Bounded Wildcards:
An upper bounded wildcard is a wildcard in Java Generics that allows you to specify a range of types that a type parameter can accept. It is denoted by the <? extends T>
syntax, where T
is the upper bound. The upper bound specifies the upper limit of the type parameter.
Here’s an example that demonstrates the use of an upper bounded wildcard:
public static double sumOfList(List<? extends Number> list) { double sum = 0.0; for (Number n : list) { sum += n.doubleValue(); } return sum; }
In this example, the sumOfList
method takes a List
of any subclass of Number
, such as Integer
, Double
, or Long
, as its argument. The method then iterates over the list and calculates the sum of the elements. The use of the upper-bounded wildcard <? extends Number>
allows the method to work with any subclass of Number
.
The upper bounded wildcard is useful when you want to write code that can work with a range of related types, without knowing the exact type in advance. For example, if you have a method that needs to accept a list of numbers, but you don’t care if the numbers are integers or doubles, you can use an upper bounded wildcard to allow the method to accept both types.
It’s important to note that you can’t add any elements to a collection that uses an upper bounded wildcard. This is because the actual type of the collection is unknown, so you can’t guarantee that the element you’re adding is of the correct type. However, you can read elements from the collection, because you know that they are of a type that is a subclass of the upper bound.
In summary, upper bounded wildcards are a powerful feature of Java Generics that allow you to write more flexible and reusable code. By specifying a range of types that a type parameter can accept, you can write code that can work with a variety of related types, without knowing the exact type in advance.
Unbounded Wildcards:
In Java Generics, an unbounded wildcard is a wildcard that represents an unknown type. It is denoted by the <?>
syntax, and it can be used when you don’t care about the actual type of the parameter.
Here’s an example that demonstrates the use of an unbounded wildcard:
public static void printList(List<?> list) { for (Object obj : list) { System.out.print(obj + " "); } System.out.println(); }
In this example, the printList
method takes a List
of any type as its argument. The method then iterates over the list and prints each element to the console. The use of the unbounded wildcard <?>
allows the method to accept a list of any type, without knowing the actual type in advance.
Unbounded wildcards are useful when you don’t care about the actual type of the parameter, but you still want to be able to read elements from the collection. You can’t add any elements to a collection that uses an unbounded wildcard, because you don’t know the actual type of the collection. However, you can read elements from the collection, because you know that they are of type Object
.
Unbounded wildcards are also useful when you’re working with generic methods that have multiple type parameters. In this case, you can use an unbounded wildcard to represent an unknown type for one or more of the type parameters.
In summary, unbounded wildcards are a powerful feature of Java Generics that allow you to write more flexible and reusable code. By representing an unknown type, you can write code that can work with a variety of types, without knowing the actual type in advance.
Example of Unbounded Wildcards:
Here’s an example that demonstrates the use of an unbounded wildcard in Java Generics:
public static void printList(List<?> list) { for (Object obj : list) { System.out.print(obj + " "); } System.out.println(); } public static void main(String[] args) { List<Integer> intList = Arrays.asList(1, 2, 3); List<String> strList = Arrays.asList("hello", "world"); List<Double> doubleList = Arrays.asList(1.5, 2.5, 3.5); printList(intList); // prints: 1 2 3 printList(strList); // prints: hello world printList(doubleList); // prints: 1.5 2.5 3.5 }
In this example, the printList
method takes a List
of any type as its argument, using an unbounded wildcard <?>
. The method then iterates over the list and prints each element to the console.
The main
method creates three different lists of different types (Integer
, String
, and Double
) and passes them as arguments to the printList
method. Because the printList
method uses an unbounded wildcard, it can accept a list of any type, without knowing the actual type in advance.
This example demonstrates the flexibility of using an unbounded wildcard in Java Generics. By using an unbounded wildcard, you can write code that can work with a variety of types, without having to write separate code for each type.
Lower Bounded Wildcards:
In Java Generics, a lower bounded wildcard is a wildcard that represents a range of unknown types, where the unknown types are either the type parameter itself or any of its superclasses. It is denoted by the <? super T>
syntax, where T
is the type parameter.
Here’s an example that demonstrates the use of a lower bounded wildcard:
public static void addNumbers(List<? super Integer> list) { for (int i = 1; i <= 10; i++) { list.add(i); } } public static void main(String[] args) { List<Number> numberList = new ArrayList<>(); addNumbers(numberList); System.out.println(numberList); // prints: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] }
In this example, the addNumbers
method takes a List
of any type that is a superclass of Integer
, using a lower bounded wildcard <? super Integer>
. The method then adds integers from 1 to 10 to the list.
The main
method creates an empty List
of Number
, which is a superclass of Integer
. The addNumbers
method is called with this list as its argument. Because the addNumbers
method uses a lower bounded wildcard, it can accept a list of any type that is a superclass of Integer
, including Number
. The method adds integers to the list, which can be of any type that is a superclass of Integer
, including Number
.
This example demonstrates the use of a lower bounded wildcard to specify a range of types that are acceptable as method arguments. By using a lower bounded wildcard, you can write code that is more flexible and can accept a wider range of input types.
Example of Lower Bound Wildcard:
Here’s an example that demonstrates the use of a lower bounded wildcard in Java Generics:
public static void addNumbers(List<? super Integer> list) { for (int i = 1; i <= 10; i++) { list.add(i); } } public static void main(String[] args) { List<Number> numberList = new ArrayList<>(); addNumbers(numberList); // add integers to Number list System.out.println(numberList); // prints: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] List<Integer> integerList = new ArrayList<>(); addNumbers(integerList); // add integers to Integer list System.out.println(integerList); // prints: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] List<Object> objectList = new ArrayList<>(); addNumbers(objectList); // add integers to Object list System.out.println(objectList); // prints: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] }
In this example, the addNumbers
method takes a List
of any type that is a superclass of Integer
, using a lower bounded wildcard <? super Integer>
. The method then adds integers from 1 to 10 to the list.
The main
method creates three different lists of different types (Number
, Integer
, and Object
) and passes them as arguments to the addNumbers
method. Because the addNumbers
method uses a lower bounded wildcard, it can accept a list of any type that is a superclass of Integer
, including Number
, Integer
, and Object
.
The method adds integers to each of the lists, and the resulting lists contain integers only. The numberList
contains integers as well as other numbers, because it is a List
of Number
. The integerList
contains only integers, because it is a List
of Integer
. The objectList
contains integers as well as other objects, because it is a List
of Object
.
This example demonstrates the flexibility of using a lower bounded wildcard in Java Generics. By using a lower bounded wildcard, you can write code that can accept a wide range of types, without having to write separate code for each type.