
JAVA - Week 5PolymorphismStructural PolymorphismPolymorphic FunctionsPolymorphic Data StructuresJava GenericsPolymorphic Data Structures using GenericsHiding a Type VariableExtending SubtypesWildcardsUse of Bounded WildcardsReflectionCreating Class objectUsing the Class objectType ErasureIncorrect Function Overloading
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 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.
xxxxxxxxxx211class Animal {2 public void move() {3 System.out.println("Animals can move");4 }5}6
7class Dog extends Animal {8 public void move() {9 System.out.println("Dogs can walk and run");10 }11}12
13public class Test{14 public static void main(String[] args){15 Animal animal = new Animal();16 Animal dog = new Dog();17 animal.move(); // Output: Animals can move18 dog.move(); // Output: Dogs can walk and run19 20 }21}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.
xxxxxxxxxx241interface Shape {2 public void draw();3}4
5class Circle implements Shape {6 public void draw() {7 System.out.println("Drawing a circle");8 }9}10
11class Square implements Shape {12 public void draw() {13 System.out.println("Drawing a square");14 }15}16public class Test{17 public static void main(String[] args){18 Shape shape1 = new Circle();19 Shape shape2 = new Square();20 shape1.draw(); // Output: Drawing a circle21 shape2.draw(); // Output: Drawing a square22 23 }24}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.
xxxxxxxxxx171class Calculator {2 public int add(int a, int b) {3 return a + b;4 }5
6 public int add(int a, int b, int c) {7 return a + b + c;8 }9}10public class Test{11 public static void main(String[] args){12 Calculator calculator = new Calculator();13 System.out.println(calculator.add(1, 2)); // Output: 314 System.out.println(calculator.add(1, 2, 3)); // Output: 615 16 }17}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.
xxxxxxxxxx431interface Shape {2 public double area();3}4
5class Circle implements Shape {6 private double radius;7
8 public Circle(double radius) {9 this.radius = radius;10 }11
12 public double area() {13 return Math.PI * radius * radius;14 }15}16
17class Rectangle implements Shape {18 private double length;19 private double width;20
21 public Rectangle(double length, double width) {22 this.length = length;23 this.width = width;24 }25
26 public double area() {27 return length * width;28 }29}30
31public class Test {32 public static void printArea(Shape shape) {33 System.out.println("Area: " + shape.area());34 }35
36 public static void main(String[] args) {37 Circle circle = new Circle(5);38 Rectangle rectangle = new Rectangle(4, 6);39
40 printArea(circle);41 printArea(rectangle);42 }43}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.
xxxxxxxxxx61public static void copyArray(Object[] source, Object[] target) {2 int limit = Math.min(src.length,tgt.length);3 for (int i = 0; i < limit; i++) {4 target[i] = source[i];5 }6}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.
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:
xxxxxxxxxx401public class LinkedList {2 private Node head;3
4 private class Node {5 Object data;6 Node next;7
8 public Node(Object data) {9 this.data = data;10 this.next = null;11 }12 }13
14 public void add(Object data) {15 if (head == null) {16 head = new Node(data);17 } else {18 Node current = head;19 while (current.next != null) {20 current = current.next;21 }22 current.next = new Node(data);23 }24 }25
26 public Object get(int index) {27 Node current = head;28 for (int i = 0; i < index; i++) {29 if (current == null) {30 return null;31 }32 current = current.next;33 }34 return current.data;35 }36
37 public boolean remove(int index) {38 //...39 }40}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:
Type information is lost, we need to typecast each element
Data can be homogenous
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:
xxxxxxxxxx91public static int countOccurrences(int[] array, int target) {2 int count = 0;3 for (int i = 0; i < array.length; i++) {4 if (array[i] == target) {5 count++;6 }7 }8 return count;9}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.
xxxxxxxxxx91public static <T> int countOccurrences(T[] array, T target) {2 int count = 0;3 for (int i = 0; i < array.length; i++) {4 if (array[i].equals(target)) {5 count++;6 }7 }8 return count;9}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:
xxxxxxxxxx51String[] stringArray = { "apple", "orange", "banana", "apple" };2System.out.println(countOccurrences(stringArray, "apple"));3
4Double[] doubleArray = { 3.14, 2.71, 3.14, 1.41 };5System.out.println(countOccurrences(doubleArray, 3.14));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.
xxxxxxxxxx71public static <S extends T, T> void copyArray(S[] source, T[] target) {2 int limit = Math.min(src.length,tgt.length);3 for (int i = 0; i < limit; i++) {4 target[i] = source[i];5 }6}7
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.
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.
xxxxxxxxxx341public class LinkedList<T> {2 private Node<T> head;3
4 private static class Node<T> {5 private T data;6 private Node<T> next;7
8 public Node(T data) {9 this.data = data;10 }11 }12
13 public void add(T data) {14 Node<T> newNode = new Node<>(data);15 if (head == null) {16 head = newNode;17 } else {18 Node<T> currentNode = head;19 while (currentNode.next != null) {20 currentNode = currentNode.next;21 }22 currentNode.next = newNode;23 }24 }25
26 public void printList() {27 Node<T> currentNode = head;28 while (currentNode != null) {29 System.out.print(currentNode.data + " ");30 currentNode = currentNode.next;31 }32 System.out.println();33 }34}We can make a LinkedList for only storing Integers as follows:
xxxxxxxxxx71public static void main(String[] args) {2 LinkedList<Integer> myList = new LinkedList<>();3 myList.add(1);4 myList.add(2);5 myList.add(3);6 myList.printList(); // Output: 1 2 37}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.
xxxxxxxxxx121public <T> void add(T data) {2 Node<T> newNode = new Node<>(data);3 if (head == null) {4 head = newNode;5 } else {6 Node<T> currentNode = head;7 while (currentNode.next != null) {8 currentNode = currentNode.next;9 }10 currentNode.next = newNode;11 }12}If S is compatible with T, S[] is compatible with T[].
xxxxxxxxxx151class Fruit{2 //...3}4class Apple extends Fruit{5 //...6}7class Mango extends Fruit{8 //...9}10public class Main{11 public static void main(String[] args){12 Fruit[] fruitArr = new Apple[5];13 //This is valid14 }15}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:
xxxxxxxxxx11fruitArr[0] = new Fruit(); //ErrorSince 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.
xxxxxxxxxx81public static void printList(LinkedList<Fruit> l){2 Fruit current;3 Iterator it = l.getIterator();4 while(it.has_next()){5 current = it.get_next();6 System.out.println(current);7 }8}But as we have seen earlier we can define type variables in order to solve this issue
xxxxxxxxxx31public static <T> void printList(LinkedList<T> l){2 //...3}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:
xxxxxxxxxx51public static void printList(LinkedList<?> list) {2 for (Object obj : list) {3 System.out.println(obj);4 }5}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:
xxxxxxxxxx51public static void printNumbers(LinkedList<? extends Number> list) {2 for (Number num : list) {3 System.out.println(num);4 }5}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:
xxxxxxxxxx41public static void addStrings(LinkedList<? super String> list) {2 list.add("hello");3 list.add("world");4}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.
xxxxxxxxxx21LinkedList<?> l = new LinkedList<String>();2l.add(new Object()) //compiler errorCompiler cannot guarantee type matches
We can use bounded wildcards, for various works for example copying a LinkedList from source to target.
xxxxxxxxxx31public static <? extends T, T> void copy(LinkedList<?> source, LinkedList<T> target){2 //...3}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
Introspection
It allows a program to observe it's own current state
Intercession
It allows a program to modify or alter it's own state and interpretation
We can check whether an object is an instance of a particular class, by the following code:
xxxxxxxxxx51Fruit apple = new Apple();2//...3if (apple instanceof Apple){4 //...5}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.
xxxxxxxxxx91import java.lang.reflect.*;2//...3public static boolean checkEqual(Object o1, Object o2){4 if(o1.getClass() == o2.getClass()){5 return true;6 }else{7 return false;8 }9}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.
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.
Getting the class information of a particular object
xxxxxxxxxx11Class c1 = obj.getClass();Creating class information for a particular class using it's name
xxxxxxxxxx11Class c2 = Class.forName("Fruit");It is possible to create new instances of the class to which the object obj belongs, using the following code.
xxxxxxxxxx21Object o = c1.newInstance(); // creates a new instance of class which obj belongs to2Object newFruit = c2.newInstance(); // creates a new instance of Fruit classWe 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.
xxxxxxxxxx41Class c = obj.getClass();2Constructor[] constructorArr = c.getConstructors();3Method[] methodArr = c.getMethods();4Field[] fieldArr = c.getFields();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.
xxxxxxxxxx21Class params[] = constructorArr[0].getParameterTypes();2//Code to get the list of parameters that the first constructor takesSimilarly, we can invoke methods, set values of fields, and do many more things.
xxxxxxxxxx101//...2Class c = obj.getClass();3Method[] method = c.getMethods();4method[0].invoke(obj, args);5//This invoke methods[0] on obj with arguments args6Field[] field = c.getFields();7field[2].set(obj, value1);8//This sets the value of field[2] in obj to value19Object o = field[1].get(obj);10//This gets the value of field[1] from objJava 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.
xxxxxxxxxx21LinkedList<T> --> LinkedList<Object>2LinkedList<T extends Shape> --> LinkedList<Shape>Since, Java preserves no information about T during runtime, we cannot check if something is an instance of T.
xxxxxxxxxx41if (o instanceof T){2 //...3}4//This is incorrectNow since all the versions of a particular generic class gets promoted to the same type during runtime, the following code will return true.
xxxxxxxxxx51o1 = new LinkedList<String>2o2 = new LinkedList<Date>3if(o1.getClass() == o2.getClass()){ //returns true4 //This will get executed5}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:
xxxxxxxxxx61public void printList(LinkedList<String> l){2 //...3}4public void printList(LinkedList<Integer> l){5 //...6}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.
byte → Byte
short → Short
int → Integer
long → Long
float → Float
double → Double
boolean → Boolean
char → Character
We can convert between basic types and their corresponding Wrapper class as follows:
xxxxxxxxxx31int x = 10;2Integer wrap_x = Integer(x);3int unwrap_x = wrap_x.intValue();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.
xxxxxxxxxx31int x = 10;2Integer wrap_x = x;3int unwrap_x = wrap_x;