Module-4
Module-4
Generics Concept: General Form of a Generic Class, Bounded Types, Generic Class
Hierarchy, Generic Interfaces, Restrictions in Generics. Introduction to Lambda expression,
Block Lambda Expressions, Generic Functional Interfaces. Passing lambda expressions as
arguments - Lambda expressions and exceptions, Lambda expressions and variable capture.
Introduction to Generics
Introduced by JDK 5, generics changed Java in two important ways. First, it added a new
syntactical element to the language. Second, it caused changes to many of the classes and
methods in the core API. Through the use of generics, it is possible to create classes,
interfaces, and methods that will work in a type-safe manner with various kinds of data. With
generics, you can define an algorithm once, independently of any specific type of data, and
then apply that algorithm to a wide variety of data types without any additional effort.
The term generics means parameterized types. Parameterized types are important because
they enable you to create classes, interfaces, and methods in which the type of data upon
which they operate is specified as a parameter. A class, interface, or method that operates on
a parameterized type is called generic.
Generics added the type safety that was lacking. With generics, all casts are automatic and
implicit. Thus, generics expanded your ability to reuse code and let you do so safely and
easily. When declaring an instance of a generic type, the type argument passed to the type
parameter must be a class type. You cannot use a primitive type, such as int or char.
2. We can hold only a single type of object in generics. It doesn’t allow to store other
objects.
5. It is checked at compile time so the problem will not occur at runtime. The good
programming strategy says it is far better to handle the problem at compile time than
runtime.
Example:
List<String> list = new ArrayList<String>();
list.add("hello");
list.add(32); //Compile Time Error
private K key;
private V value;
Here in this example, we can use the object or instance of this class as many times with
different Parameters as T type. If we want the data to be of int type, the T can be replaced
with Integer, and similarly for String, Character, Float, or any user-defined type. The
declaration of a generic class is almost the same as that of a non-generic class except the
class name is followed by a type parameter section. The type parameter section of a generic
class can have one or more type parameters separated by commas.
All generic method declarations have a type parameter section indicated by angle
brackets <> that precedes the method’s return type.
Each type parameter section can contain one or more type parameters separated by
commas. A type parameter or a type variable is an identifier that specifies a generic
type name.
The type parameters can be used to declare the return type which is known as actual
type arguments.
A generic method’s body is declared like that of any non-generic method. The point to
be noted is that type parameter can represent only reference types, and not primitive
types (like int, double, and char).
A bounded type parameter is a generic type that is restricted to a specific subset of types. It
can be defined using the ‘extends’ keyword followed by the upper bound. The upper bound
can be a class or an interface, and it indicates that the generic type must be a subclass of the
specified class or implement the specified interface.
The syntax for defining a bounded type parameter is as follows:
class Shape {
// ...
}
class Circle extends Shape {
// ...
}
class Rectangle extends Shape {
// ...
}
Now, let's define a generic class called ShapeContainer that can hold objects of various
shapes:
By using bounded types, we can enforce compile-time type safety and ensure that only shape-
related operations can be performed on objects stored in ShapeContainer. For instance, if we
try to add a string or any other unrelated type, the compiler will generate an error.
Bounded Type with Multiple Bounds
Java also supports bounded types with multiple bounds, where a type parameter must satisfy
multiple constraints. Multiple bounds are specified using the & symbol, followed by the
interface or class names.
Consider the following example of a generic method that compares two objects:
class BoundedTypeExample {
public static <T extends Comparable<T> & MyInterface> int compare(T obj1, T obj2) {
return obj1.compareTo(obj2);
}
}
In this example, the generic method compare has a bounded type parameter T with two
bounds: Comparable<T> and MyInterface. It ensures that the type parameter T must
implement the Comparable interface and extend the MyInterface interface.
By using bounded types with multiple bounds, we can leverage the capabilities of multiple
interfaces or classes to provide more flexible and powerful functionality.
BoundedTypeExample.java
// Shape hierarchy
abstract class Shape {
public abstract void draw();
}
class Circle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a circle");
}
}
class Rectangle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a rectangle");
}
}
// Generic class with bounded type parameter
class ShapeContainer<T extends Shape> {
private T shape;
public void addShape(T shape) {
this.shape = shape;
}
public T getShape() {
return shape;
}
}
// Main class
public class BoundedTypeExample {
public static void main(String[] args) {
// Create a ShapeContainer for Circles
ShapeContainer<Circle> circleContainer = new ShapeContainer<>();
Circle circle = new Circle();
circleContainer.addShape(circle);
// Get the shape from the container
Circle retrievedCircle = circleContainer.getShape();
retrievedCircle.draw();
// Create a ShapeContainer for Rectangles
ShapeContainer<Rectangle> rectangleContainer = new ShapeContainer<>();
Rectangle rectangle = new Rectangle();
rectangleContainer.addShape(rectangle);
// Get the shape from the container
Rectangle retrievedRectangle = rectangleContainer.getShape();
retrievedRectangle.draw();
// Try to add a String to the ShapeContainer (results in a compile-time error)
// ShapeContainer<String> stringContainer = new ShapeContainer<>(); // Compile-time e
rror
// stringContainer.addShape("Invalid Shape"); // Compile-time error
}
}
Generic Class Hierarchy
Generic classes can be part of a class hierarchy in just the same way as a non-generic class.
Thus, a generic class can act as a superclass or be a subclass. The key difference between
generic and non-generic hierarchies is that in a generic hierarchy, any type arguments needed
by a generic superclass must be passed up the hierarchy by all subclasses. This is similar to
the way that constructor arguments must be passed up a hierarchy.
Hierarchical classifications are allowed by Inheritance. Superclass is a class that is
inherited. The subclass is a class that does inherit. It inherits all members defined by super-
class and adds its own, unique elements. These uses extends as a keyword to do so.
Sometimes generic class acts like super-class or subclass. In Generic Hierarchy, all sub-
classes move up any of the parameter types that are essential by super-class of generic in
the hierarchy. This is the same as constructor parameters being moved up in a hierarchy.
Example 1: Generic super-class
// Helper class 2
// Class 2 - Child class
class Generic2<T, V> extends Generic1<T> {
// Member variable of child class
V obj2;
Generic2(T o1, V o2)
{
// Calling super class using super keyword
super(o1);
obj2 = o2;
}
class GFG {
public static void main(String[] args)
{
Gen<String> w = new Gen<String>("Hello", 2024);
System.out.println(w.getobj() + " " + w.getval());
}
}
Output
Hello 2024
Generic Interfaces
Generic Interfaces in Java are the interfaces that deal with abstract data types. Interface
help in the independent manipulation of java collections from representation details. They
are used to achieving multiple inheritance in java forming hierarchies. They differ from the
java class. These include all abstract methods only, have static and final variables only. The
only reference can be created to interface, not objects. Unlike class, these don’t contain any
constructors, instance variables. This involves the “implements” keyword. These are similar
to generic classes.
We can create a generic interface by specifying the <T> after the interface name. The syntax
is given below.
Interface InterfaceName<T>
{
//members
}
// Class 1 - Sub-class
// class extending Comparable and implementing interface
class MyClass<T extends Comparable<T> >
implements MinMax<T> {
Java
Output
Minimum value: 2
Maximum value: 8
Output explanation: Here interface is declared with type parameter T, and its upper bound
is Comparable which is in java.lang. This states how objects are compared based on type of
objects. Above T is declared by MyClass and further passed to MinMax as MinMax needs a
type that implements Comparable and implementing class(MyClass) should have same
bounds.
Restrictions in Generics
There are a few restrictions associated with the usage of generics that are mentioned below:
1. In Java, generic types are compile time entities. The runtime execution is possible only if it
is used along with raw type.
3. For the instances of generic class throw and catch instances are not allowed.
For example:
public class Test<T> extends Exception
{
//code // Error: can't extend the Exception class
}
We cannot create generic exception classes and cannot extend Throwable (which is
superior to all exception classes in the exception class hierarchy).
4. Instantiation of generic parameter T is not allowed.
For example:
new T() //Error
new T[10]
6. Static fields and static methods with type parameters are not allowed.
GenType(T data)
{
// parameterized constructor
this.data = data;
}
// main function
public static void main(String[] args)
{
GenType<Integer> gt = new GenType<>(10);
System.out.println(gt.getData());
}
}
Output
10
2. Restriction on using static members
It is illegal for the static members (variables, methods) to use a type parameter declared by
the enclosing class. We will use the above example in this case to make the variable and
method static, this also leads to compile time error. Let’s see in detail what the compiler
says:
Example:
GenType() {}
GenType(T data)
{
// parameterized constructor
this.data = data;
}
Output
Hello.How are you!!
Example:
// Java Program to implement Generic array Restrictions
import java.util.Arrays;
System.out.println(Arrays.toString(obj1.getArray()));
// illegal to create an array of type-specific generic references.
}
}
Output
value: 10
[1, 2, 3, 4, 5]
4. Primitive data types are not used with the generic types
We will get the compilation error if we use the primitive data types at the time of object
creation. The following code demonstrates the situation:
Example:
// Java Program to implement
// Primitive data types are not
// used with the generic types
import java.io.*;
// Driver Class
class Box<T> {
private T data;
Box(T data) { this.data = data; }
T getData() { return data; }
public static void main(String[] args)
{
Box<Integer> b1 = new Box<Integer>(10);
// use of wrapper classes
Box<String> b2 = new Box<String>("Geek For Geeks");
System.out.println("value: " + b1.getData());
System.out.println("value: " + b2.getData());
}
}
Output
value: 10
value: Geek For Geeks
Output:
Error: Could not find or load main class Box
Caused by: java.lang.ClassNotFoundException: Box
Lambda expression is a new and important feature of Java which was included in Java SE 8.
A lambda expression is a short block of code which takes in parameters and returns a value.
Lambda expressions are similar to methods, but they do not need a name and they can be
implemented right in the body of a method.
Syntax:
The simplest lambda expression contains a single parameter and an expression:
(argument-list) -> {body} (or) parameter -> expression
System.out.println("Hello World");
}
To convert this method to a lambda expression, remove the access specifier “public”, return
type “void” and method name “hello” and write it as :
Here is another method example that adds two numbers and returns their sum :
int add(int x, int y){
return x+y;
}
The lambda expressions that have a single expression in the body are known an Expression
lambdas. Java also supports second type of lambda expression where the lambda expression
body contains a block of code rather than just a single statement. This type of lambda
expression are called Block lambdas.
Block Lambda contains many operations that work on lambda expressions as it allows the
lambda body to have many statements. This includes variables, loops, conditional
statements like if, else and switch statements, nested blocks, etc. This is created by
enclosing the block of statements in lambda body within braces {}. This can even have a
return statement i.e. return value.
In the block lambda expression, we must explicitly use a return statement to return a
value.
A lambda block contains multiple statements.
It expands the type of operations that can be performed with a lambda expression.
The multiple statements are enclosed within braces {}.
In a block lambda expression, a return statement must be explicitly used to return a value.
The lambda function body can contain expressions over multiple lines if encased in curly
braces.
The FindFact functional interface declares the factorial method. This method takes an integer
argument and returns an integer value.
The return statement just returns the control from the lambda; it does not cause an enclosing
method to return.
Functional Interfaces
A functional interface is an interface that contains only one abstract method. They can
have only one functionality to exhibit. From Java 8 onwards, lambda expressions can be
used to represent the instance of a functional interface. A functional interface can have any
number of default methods.
In Functional interfaces, there is no need to use the abstract keyword as it is optional to use
the abstract keyword because, by default, the method defined inside the interface is abstract
only. We can also call Lambda expressions as the instance of functional interface .
// Java program to demonstrate lambda expressions to
// implement a user defined functional interface.
@FunctionalInterface
interface Square {
int calculate(int x);
}
class Test {
public static void main(String args[])
{
int a = 5;