Home Week-4 Week-6

JAVA - Week 5

Polymorphism

Polymorphism is a fundamental concept in object-oriented programming (OOP) and is a powerful mechanism that allows objects of different classes to be treated as if they were the same type. It usually refers to the effect of dynamic dispatch, which allow different classes to provide their own implementation of the same method.

Every object knows what it needs to do.

Structural Polymorphism

Structural polymorphism in Java is the ability of different objects to be treated as if they were the same type, even if they are actually different. This is achieved through the use of inheritance, interfaces, and method overloading.

Inheritance allows a new class to be based on an existing class, inheriting its attributes and behaviors. This means that objects of the new class can be treated as if they were objects of the original class, and can be used interchangeably in many situations.

Interfaces provide a way to define a common set of methods that different classes can implement. This means that objects of different classes that implement the same interface can be treated as if they were the same type, even though they are different classes.

Method overloading allows multiple methods with the same name to be defined in a class, each with different parameters. This means that different methods can be called with the same name, depending on the type and number of parameters passed to them.

Polymorphic Functions

Methods which depends on specific capabilities of an object are known as polymorphic functions. It can work with any object as long as it possesses the capability that this method requires in order to work.

Here, the printArea method is a polymorphic method as it can work with objects of any type, as long as area method is defined on that object.

Type Consistency

However, we need to impose certain restrictions in case of some methods. Let's take an example of a polymorphic method to copy an array. It takes a source array and a target array and then copies elements from the source to the target array.

Now, we need to ensure that the source array can be a subtype of target array but not the vice versa. Target array should be type compatible with the source array.

Polymorphic Data Structures

A polymorphic data structure stores values of type Object which allows us to store arbitrary elements in that data structure. A simple example is as follows:

This LinkedList stores data elements which are of type Object, which means it can store data of any type.

The potential issues that may arise as a result of utilizing polymorphic data structures are enumerated below:

Java Generics

Java generics is a feature that enables programmers to create flexible and reusable code. Generics allow classes, interfaces, and methods to be written in a way that is independent of the data types used, while still ensuring type safety at compile time.

This means that a single class or method can be used with different types of data, making code more efficient, and easier to read and maintain. In essence, generics make it possible to write code that is more flexible and less error-prone, without sacrificing performance or type safety.

We can define a type quantifier before return type between angle brackets (<>)

Let's say we want to create a method that counts the number of occurrences of a particular element in an array. We could define a method like this:

This method works fine for arrays consisting of integers, but what if we want to count occurrences of different type of objects such as Strings, double, etc. In that case, we will have to write separate methods for each data type, but that would be repetitive and error-prone.

This is where, we can make use of generics, we can write a single method that works with any type of object by using a generic type parameter.

In this version of the method, the type parameter T represents any type of object. We use the equals method to compare elements and count the occurrences.

Now, we can use this method as follows:

We can use the extends keyword, to put constraints on generic type parameters. Below is an example of a generic method which copies elements from a source array into a target array.

This method defines two type parameters namely T and S, where S must extend T, thereby ensuring that the source array will be compatible with the target array.

Polymorphic Data Structures using Generics

Here's an example of how we can implement the same LinkedList using Java Generics, in order to deal with the problems that can occur using normal polymorphic data structures.

We can make a LinkedList for only storing Integers as follows:

Hiding a Type Variable

We can define new type variable, which hides the type variable that has already been defined. Like modifying the add method like this will result in a new T which is different from the T defined at the class level. Quantifier <T> masks the type parameter T of LinkedList.

Extending Subtypes

If S is compatible with T, S[] is compatible with T[].

In Java, arrays typing is covariant, that means if Apple extends Fruit, then Apple[] extends Fruit[] too. This can cause type errors during runtime as the following code becomes invalid:

Since fruitArr refers to an Apple array, it cannot store objects of type Fruit.

Generic classes are not covariant, that means LinkedList<Apple> is not compatible with LinkedList<Fruit>.

This means that we cannot create a general method like below to print a LinkedList of any type of fruits.

But as we have seen earlier we can define type variables in order to solve this issue

Wildcards

We can notice in the above example that the type variable T is not being used inside the method printList, so instead we can make use of wildcards.

In Java, wildcards are a type of generic parameter that allows us to write more flexible and reusable code. They provide a way to represent an unknown type or a type that is a subtype of a specified type. Wildcards are represented using the ? symbol and can be used in three different forms: ?, ? extends, and ? super.

The first form ? represents an unknown type and can be used in situations where you don't care about the type of the argument or variable. For example, the following method takes a list of unknown type:

This method can accept a LinkedList of any type, but it can only read from it because the type is unknown.

The second form ? extends is used to represent a subtype of a specified type. For example, the following method takes a list of objects that extend Number:

This method can accept a LinkedList of any type that extends Number, such as Integer, Double, or BigDecimal.

The third form ? super is used to represent a supertype of a specified type. For example, the following method takes a list of objects that are supertypes of String:

This method can accept a LinkedList of any type that is a supertype of String, such as Object.

We can define variables of wildcard type, but we need to be careful while assigning values.

Compiler cannot guarantee type matches

Use of Bounded Wildcards

We can use bounded wildcards, for various works for example copying a LinkedList from source to target.

Reflection

The feature of reflection in Java provides the ability to inspect and manipulate the behavior of classes, objects, and their members at runtime, enabling us to examine the current state of a process.

Two components involved in reflection

We can check whether an object is an instance of a particular class, by the following code:

However, we can only perform this check if we know beforehand which type we want to compare our object against. When encountering such situations, we can make use of Introspection to determine the class to which an object belongs.

Presented here is a straightforward function called checkEqual, which accepts two objects as arguments and returns true if they are instances of the same class, and false otherwise.

We cannot make use of instanceof in cases like these, because we don't know which class to compare our object against. We can import the reflection package for Introspection of our objects.

We can extract the class information of any object by using the method getClass(), which is available in the reflection package. This gives us an object of type Class that encodes the class information of the object on which the getClass() method was invoked.

This method gets the class information for o1 and o2, and then returns true if both of them are having the same information and false otherwise.

Creating Class object

To make complete use of Class objects, we should store them in a variable of type Class. We can create Class objects primarily using two ways.

Using the Class object

It is possible to create new instances of the class to which the object obj belongs, using the following code.

We can use this Class object to get more information about the class such as constructors, methods and fields. We have additional classes such as Constructor, Method and Field to introspect them further.

Now, in order to extract the information about constructor, method and field that are present in the class that obj belongs to, we can make use of methods like getConstructors, getMethods and getFields.

We can store the results of these methods in an array of their respective types.

These methods return the public constructors, methods and fields respectively. In order to get the public and private details together, we can use methods like getDeclaredConstructors, getDeclaredMethods and getDeclaredFields.

We can introspect them further for details such as parameters that a constructors takes, etc using the methods present in their respective classes.

Similarly, we can invoke methods, set values of fields, and do many more things.

Type Erasure

Java does not keep different versions of a generic class as separate types during runtime. Instead, at runtime all type variables are promoted to Object or the upper bound if any.

Since, Java preserves no information about T during runtime, we cannot check if something is an instance of T.

Now since all the versions of a particular generic class gets promoted to the same type during runtime, the following code will return true.

Incorrect Function Overloading

Due to type erasure we have to be careful while overloading methods which involves parameters related to type T. We cannot write two methods like this:

Both these functions will have the same method signature after type erasure.

Type Erasure convert LinkedList<T> to LinkedList<Object> but basic types like int, double, char, etc are not compatible with Object, therefore we cannot use these types in place of generic types.

Therefore, we have wrapper classes for each type which is compatible with Object.

We can convert between basic types and their corresponding Wrapper class as follows:

There are similar methods like these for other types like byteValue, doubleValue, and so on.

Autoboxing

Java implicitly converts values between basic types and their corresponding wrapper types.