DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workkloads.

Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • How to Convert XLS to XLSX in Java
  • Recurrent Workflows With Cloud Native Dapr Jobs
  • Java Virtual Threads and Scaling
  • Java’s Next Act: Native Speed for a Cloud-Native World

Trending

  • GDPR Compliance With .NET: Securing Data the Right Way
  • A Guide to Developing Large Language Models Part 1: Pretraining
  • Hybrid Cloud vs Multi-Cloud: Choosing the Right Strategy for AI Scalability and Security
  • Optimize Deployment Pipelines for Speed, Security and Seamless Automation
  1. DZone
  2. Coding
  3. Java
  4. An Introduction to Generics in Java – Part 5

An Introduction to Generics in Java – Part 5

By 
MD Sayem Ahmed user avatar
MD Sayem Ahmed
·
Jul. 29, 13 · Interview
Likes (5)
Comment
Save
Tweet
Share
32.0K Views

Join the DZone community and get the full member experience.

Join For Free

This is a continuation of an introductory discussion on generics, previous parts of which can be found here.

In this post I will focus on type parameter bounds and their usage.

Bounded Type Parameters

When a generic type is compiled, all occurrences of a type parameter are removed by the compiler and replaced by a concrete type. The compiler also generates appropriate casting needed for type safety by itself during this procedure. This concrete type is typically Object, but compiler can use other types as well. This process is called Type Erasure and will be explained in a future post. For the time being, all we need to understand is that the type information of generic types are lost once they are compiled. For this reason, if we want to access a method/property using a type parameter, we’ll typically be able to access those ones that are defined in the class Object (I am oversimplifying here as we’ll be able to access other methods/properties as well if we use a bound, which we will discuss here in this post). For example, take a look at the following code snippet –

public class MyGenericClass<E> {
    private E prop;
 
    public MyGenericClass(E prop) {
        this.prop = prop;
    }
 
    public E getProp() {
        return this.prop;
    }
 
    public void printProp() {
 
        //OK, because toString is defined within Object
        System.out.println(this.prop.toString());
    }
 
    public int getValue() {
        /**
         * NOT OK, because Object doesn’t have
         * compareTo method. Compile-time Error.
         */
        return this.prop.intValue();
    }
}

After compiling the above class, I get the following message –

MyGenericClass.java:37: error: cannot find symbol

return this.prop.intValue();

^

symbol:   method intValue(E)

location: variable prop of type E

where E is a type-variable:

E extends Object declared in class MyGenericClass

1 error

Although the message seems cryptic, the reason behind this is the one that I’ve stated above – when this type is compiled the type parameter E here will be replaced by Object by the compiler, and it doesn’t have intValue method.

So the problem occurs here because the compiler is using Object to replace the type parameters. If I could somehow tell the compiler to use other types during this erasure which has an intValue (Number, for example) method, then this error would have been resolved.

This is exactly what parameter bounds do. By using a type as a bound on a type parameter, I can instruct compiler to use that type during the erasure in place of Object, and then I can easily access the methods/properties defined in that type.

The general syntax for specifying a type parameter bound is as follows –

public class MyGenericClass
    <E extends MyBoundType,
     T extends AnotherBoundType,
     .......>

This also tells the compiler that when a type argument is passed during an instance creation of this generic type, it will be a subtype of MyBoundType, so it can safely let us access the methods that are defined in that type using the type parameter E. If any other type is passed, then the compiler will issue a compile-time error. The extends keyword specify the bound relation between E and MyBoundType. We will use the same keyword even if E is bounded by an interface type, that is, if MyBoundType is an interface. Here extends means both classical extends and implements.

So, if we use Number as our parameter bound for our last example, the error message will be gone because now the compiler will use Number to erase type parameter E, and it has an intValue method defined in it -

public class MyGenericClass<E extends Number> {
    private E prop;
 
    public MyGenericClass(E prop) {
        this.prop = prop;
    }
 
    public E getProp() {
        return this.prop;
    }
 
    public void printProp() {
        // OK, because toString is defined within Object
        System.out.println(this.prop.toString());
    }
 
    public int getValue() {
        // Now it compiles just fine!
        return this.prop.intValue();
    }
}

This code will now compile just fine.

Remember our generic Insertion Sort algorithm from the first part of the series? We had declared it like this –

public class InsertionSort<E extends Comparable<E>>

This told the compiler that the type arguments that will be passed here will all implement the Comparable interface, so they will have a compareTo method. As a result, compiler allowed us to do this inside the sort method –

for (int i = 1; i <= elements.length - 1; i++) {
    E valueToInsert = elements[i];
    int holePos = i;
 
    /**
     * See how we are calling compareTo method
     * on the type parameter?
     */
    while (holePos > 0 &&
            valueToInsert.compareTo(elements[holePos - 1]) < 0) {
        elements[holePos] = elements[holePos - 1];
        holePos--;
    }
 
    elements[holePos] = valueToInsert;
}

This example also shows that we can pass another generic type as a type parameter bound. In fact we can use all Classes, Interfaces and Enums and even another type parameter as a bound. Only primitive and array types are not allowed as a bound.

Multiple and Recursive Bounds

We can define multiple bounds on a single parameter. In this case we use the & operator to list them in the following way -

public class MyGenericClass<E extends MyBoundClass & MyBoundInterface>

This tells the compiler that the type argument that is passed will be a subtype of MyBoundClass and implements MyBoundInterface. So, we will be able to access all the methods/properties defined in those types.

The Java Language Specification requires us to list the class bound first, otherwise the compiler will throw an error. For example, the following will throw an error –

/**
 * Will throw an error because we are not
 * listing the class bound first.
 */
public class MyGenericClass
    <E extends MyBoundInterface
     & MyBoundClass>

We can also declare recursive bounds, so that a bound can depend on itself too. Consider our sorting example from first part of the series. We declared it like this –

public class InsertionSort<E extends Comparable<E>>

Here, the bound is recursive because E itself depends upon E (the one that is supplied to Comparator). If we passInteger as a type argument when creating an instance of InsertionSort, then the type argument to Comparable will beInteger too.

We can also declare mutually recursive bounds like this –

public class MyGenericClass
    <T extends FirstType<T, U>,
     U extends SecondType<T, U>>

Java Enum Declaration

Let us now consider an example from the Java API itself. We all know that enumerations in Java are all objects of a class, and that class extends the Enum class. The declaration of that class looks like this –

public abstract class Enum<E extends Enum<E>>
    implements Comparable<E>, Serializable

Beginners in Java Generics find the first line very confusing. Before explaining the reasoning behind this weird declaration, let us explore an example.

Suppose that we are going to build a software system which will have various types of vehicles (a vehicle simulation system, perhaps?). The vehicles will have a name and a length. We also want to compare vehicles with each other based on their lengths. An approach for building the vehicle classes might be something like this –

public abstract class Vehicle {
    private String name;
    private double length;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public double getLength() {
        return length;
    }
 
    public void setLength(double length) {
        this.length = length;
    }
}
 
public class Car extends Vehicle implements Comparable<Car> {
    public int compareTo(Car anotherCar) {
        double thisLength = this.getLength();
        double thatLength = anotherCar.getLength();
 
        if (thisLength > thatLength)
            return 1;
        else if (thisLength < thatLength)
            return -1;
 
        return 0;
    }
 
    // other methods and properties
}
 
public class Bus extends Vehicle implements Comparable<Bus> {
    public int compareTo(Bus anotherBus) {
        double thisLength = this.getLength();
        double thatLength = anotherBus.getLength();
 
        if (thisLength > thatLength)
            return 1;
        else if (thisLength < thatLength)
            return -1;
 
        return 0;
    }
 
    // other methods and properties
}

The problem of the above implementation is pretty obvious – even though the comparing logics are almost the same among all the subtypes of Vehicle, it’s duplicated in all of them. This creates a maintenance problem as now changing the comparison logic forces us to change the code in many places.

To remedy this, we can remove the comparison from the subtypes and move it up in the Vehicle. To do this, we will rewrite those classes as follows –

public abstract class Vehicle implements Comparable<Vehicle> {
    private String name;
    private double length;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public double getLength() {
        return length;
    }
 
    public void setLength(double length) {
        this.length = length;
    }
 
    public int compareTo(Vehicle anotherVehicle) {
        double thisLength = this.getLength();
        double thatLength = anotherVehicle.getLength();
 
        if (thisLength > thatLength)
            return 1;
        else if (thisLength < thatLength)
            return -1;
 
        return 0;
    }
}
 
public class Car extends Vehicle {
    // car-specific methods and properties
}
 
public class Bus extends Vehicle {
    // bus-specific methods and properties
}

This approach has also a problem. The above implementation will allow us to compare a car with a bus without any errors –

Car car = new Car();
car.setName("Toyota");
car.setLength(2);
 
Bus bus = new Bus();
bus.setName("Volvo");
bus.setLength(4);
 
car.compareTo(bus);    // No error

This is certainly a problem, since comparing a bus with a car should not be done using the same logic that is used to compare a car with a car.

How can we solve this? How can we reuse the comparison logic among all the subtypes, while at the same time raising error flags at compile time whenever someone tries to compare two incompatible types?

In the last example the problem occurred because the compareTo method has a parameter which is of type Vehicle. As a result we were able to pass any subtypes of Vehicle to it, like the way we passed a bus to compare with a car. Let’s try to change the type of this parameter so that now this kind of mixing generates an error. If we want to allow the comparison of a car only with a car, then the argument to the compareTo method must be of type Car. If we change it to Car, we will also need to change the type argument that we are passing to Comparable in the Vehicle class declaration –

public abstract class Vehicle implements Comparable<Car> {
    // other methods and properties
 
    public int compareTo(Car car) {
        // method implementation
    }
}

But then this will not allow us to compare any other types. We will not be able to compare a bus with another bus. To allow this, we will need to change the parameter to be of type Bus. If we declare a new subtype named Cycle, we will also need this method to support this type too! So we can see that the parameter type of this compare method should vary if we need to enforce compatible comparison.

From the above discussion it’s clear that we need to parameterize the parameter type of the compareTo method, and in turn, parameterize the Vehicle class itself. If we do this, we will then be able to pass Car, Bus, and Cycle etc. as its type argument, which in turn will be used as the parameter type of the compare method. In general, after we declare Vehicleas a generic type, all of its subtypes will pass themselves as a type argument while extending from it, so that the parameter type of this compareTo method matches their type –

public abstract class Vehicle<E> implements Comparable<E> {
    // other methods and properties
 
    public int compareTo(E vehicle) {
        // method implementation
    }
}
 
/**
* Now this class’s compareTo version will take a Car type
* as its argument.
*/
public class Car extends Vehicle<Car> {
    // car specific method and properties
}
 
/**
* Now this class’s compareTo version will take a Bus type
* as its argument.
*/
public class Bus extends Vehicle<Bus> {
    // bus specific method and properties
}
 
/**
* Doing something like this will now generate a
* compile-time error.
*/
car.compareTo(bus);

This approach solves our last problem that we were facing, but introduces a new one. After converting Vehicle a generic type and using the type parameter as the parameter type of the compare method, it looks like this –

public int compareTo(E anotherVehicle) {
    double thisLength = this.getLength();
 
    // Now the following line is an error.
    double thatLength = anotherVehicle.getLength();
 
    if (thisLength > thatLength)
        return 1;
    else if (thisLength < thatLength)
        return -1;
 
    return 0;
}

Since we didn’t put any bound on the type parameter, and Object class doesn’t have a getLength method, compiler will generate an error. We get to call this method on an object of type E only if it’s bounded by Vehicle itself, because then compiler will know that objects of this type will have this method. So our compare method will work only if E is bounded by Vehicle itself! After this modification, the classes look like below –

public abstract class Vehicle<E extends Vehicle<E>>
        implements Comparable<E> {
 
    private String name;
    private double length;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public double getLength() {
        return length;
    }
 
    public void setLength(double length) {
        this.length = length;
    }
 
    public int compareTo(E anotherVehicle) {
        double thisLength = this.getLength();
        double thatLength = anotherVehicle.getLength();
 
        if (thisLength > thatLength)
            return 1;
        else if (thisLength < thatLength)
            return -1;
 
        return 0;
    }
}
 
public class Car extends Vehicle<Car> {
    // Car-specific properties and methods
}
 
public class Bus extends Vehicle<Bus> {
    // Bus-specific properties and methods
}
 
// and in main
Car car = new Car();
car.setName("Toyota");
car.setLength(2);
 
Bus bus = new Bus();
bus.setName("Volvo");
bus.setLength(4);
 
car.compareTo(car);       // Works as expected
car.compareTo(bus);       // compile-time error

Even with the above example, a certain kind of type mixing is possible. Rather than discussing it here, I am going to leave it to you to figure it out. If you can’t, check out the next post of this series!

I guess now you know why the Enum class is declared in that way. This kind of recursive bound allows us to write methods in a supertype which will take its subtypes as its arguments, or return them as return value. I encourage you to check out the source code of the Enum class to find out these methods.

That’s it for today. Stay tuned for the next post!

Resources

  1. Java Generics and Collections
  2. Java Generics FAQs by Angelika Langer






Java (programming language)

Published at DZone with permission of MD Sayem Ahmed, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • How to Convert XLS to XLSX in Java
  • Recurrent Workflows With Cloud Native Dapr Jobs
  • Java Virtual Threads and Scaling
  • Java’s Next Act: Native Speed for a Cloud-Native World

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!