Pages

What are generics in Java?

I must say that you will learn a lot! Not only this lesson, but also the next few lessons, will be devoted to generics. What are generics in Java? - 1 So, if you're interested in generics, today's you're lucky day: you will learn a lot about the features of generics. And if not, resign yourself and relax! :) This is a very important topic, and you need to know it. Let's start with the simple: the "what" and the "why". What are generics? Generics are types that have a parameter. When creating a generic type, you specify not only a type, but also the data type that it will work with. I'm guessing the most obvious example has already come to your mind: ArrayList! This is how we usually create one in a program:
import java.util.ArrayList;
import java.util.List;

public class Main {

   public static void main(String[] args) {

       List<String> myList1 = new ArrayList<>();
       myList1.add("Test String 1");
       myList1.add("Test String 2");
   }
}
As you might guess, a feature of this list is that we can't stuff everything into it: it works exclusively with String objects. Now let's take a little digression into Java's history and try to answer the question "why?" To do this, we'll write our own simplified version of the ArrayList class. Our list only knows how to add data to and retrieve data from an internal array:
public class MyListClass {

   private Object[] data;
   private int count;

   public MyListClass() {
       this.data = new Object[10];
       this.count = 0;
   }

   public void add(Object o) {
       this.data[count] = o;
       count++;
   }

   public Object[] getData() {
       return data;
   }
}
Suppose we want our list to only store Integers. We're not using a generic type. We don't want to include an explicit "instanceof Integer" check in the add() method. If we did, our whole class would be suitable only for Integer, and we would have to write a similar class for every other data type in the world! We'll rely on our programmers, and just leave a comment in the code to ensure that they don't add anything we don't want:
// Use this class ONLY with the Integer data type
public void add(Object o) {
   this.data[count] = o;
   count++;
}
One of the programmers missed this comment and inadvertently put several Strings in a list of numbers and then calculated their sum:
public class Main {

   public static void main(String[] args) {

       MyListClass list = new MyListClass();
       list.add(100);
       list.add(200);
       list.add("Lolkek");
       list.add("Shalala");

       Integer sum1 = (Integer) list.getData()[0] + (Integer) list.getData()[1];
       System.out.println(sum1);

       Integer sum2 = (Integer) list.getData()[2] + (Integer) list.getData()[3];
       System.out.println(sum2);
   }
}
Console output:
300
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
      at Main.main (Main.java:14)
What's the worst part of this situation? Certainly not the carelessness of the programmer. The worst part is that incorrect code ended up in an important place in our program and compiled successfully. Now we'll encounter the bug not while writing code, but only during testing (and this is the best case scenario!). Fixing bugs in later stages of development costs much more — both in terms of money and time. This is precisely where generics benefit us: a generic class lets the unlucky programmer detect the error right away. The program simply won't compile!
import java.util.ArrayList;
import java.util.List;

public class Main {

   public static void main(String[] args) {

       List<Integer> myList1 = new ArrayList<>();

       myList1.add(100);
       myList1.add(100);
       myList1.add ("Lolkek"); // Error!
       myList1.add("Shalala"); // Error!
   }
}
The programmer immediately realizes his or her mistake and instantly gets better. By the way, we didn't have to create our own List class to see this sort of error. Simply remove the angle brackets and type (<Integer>) from an ordinary ArrayList!
import java.util.ArrayList;
import java.util.List;

public class Main {

   public static void main(String[] args) {

      List list = new ArrayList();

      list.add(100);
      list.add(200);
      list.add("Lolkek");
      list.add("Shalala");

       System.out.println((Integer) list.get(0) + (Integer) list.get(1));
       System.out.println((Integer) list.get(2) + (Integer) list.get(3));
   }
}
Console output:
300
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
     at Main.main(Main.java:16)
In other words, even using Java's "native" mechanisms, we can make this sort of mistake and create an unsafe collection. However, if we paste this code into an IDE, we get a warning: "Unchecked call to add(E) as a member of raw type of java.util.List" We're told that something may go wrong when adding an item to a collection that lacks a generic type. But what does the phrase "raw type" mean? A raw type is a generic class whose type has been removed. In other words, List myList1 is a raw type. The opposite of a raw type is a generic type — a generic class with an indication of the parameterized type(s). For example, List<String> myList1. You might ask why the language allows the use of raw types? The reason is simple. Java's creators left support for raw types in the language in order to avoid creating compatibility problems. By the time Java 5.0 was released (generics first appeared in this version), a lot of code had already been written using raw types. As a result, this mechanism is still supported today. We have repeatedly mentioned Joshua Bloch's classic book "Effective Java" in the lessons. As one of the creators of the language, he didn't skip raw types and generic types in his book. What are generics in Java? - 2Chapter 23 of the book has a very eloquent title: "Don't use raw types in new code" This is what you need to remember. When using generic classes, never turn a generic type into a raw type.

Generic methods

Java lets you parameterize individual methods by creating so-called generic methods. How are such methods helpful? Above all, they are helpful in that they let you work with different types of method parameters. If the same logic can be safely applied to different types, a generic method can be a great solution. Consider this a very simple example: Suppose we have a some list called myList1. We want to remove all values from the list and fill all the empty spaces with new values. Here's what our class looks like with a generic method:
public class TestClass {

   public static <T> void fill(List<T> list, T val) {
       for (int i = 0; i < list.size(); i++)
           list.set(i, val);
   }

   public static void main(String[] args) {

       List<String> strings = new ArrayList<>();
       strings.add("Old String 1");
       strings.add("Old String 2");
       strings.add("Old String 3");

       fill(strings, "New String");

       System.out.println(strings);

       List<Integer> numbers = new ArrayList<>();
       numbers.add(1);
       numbers.add(2);
       numbers.add(3);

       fill(numbers, 888);
       System.out.println(numbers);
   }
}
Pay attention to the syntax. It looks a bit unusual:
public static <T> void fill(List<T> list, T val)
We write <T> before the return type. This indicates that we're dealing with a generic method. In this case, the method accepts 2 parameters as an input: a list of T objects and another separate T object. By using <T>, we parameterize the method's parameter types: we can't pass in a list of Strings and an Integer. A list of Strings and a String, a list of Integers and an Integer, a list of our own Cat objects and another Cat object — that's what we need to do. The main() method illustrates how the fill() method can be easily used to work with different types of data. First, we use the method with a list of Strings and a String as input, and then with a list of Integers and an Integer. Console output:
[New String, New String, New String] [888, 888, 888]
Imagine if we didn't have generic methods and needed the logic of the fill() method for 30 different classes. We would have to write the same method 30 times for different data types! But thanks to generic methods, we can reuse our code! :)

Generic classes

You aren't limited to the generic classes provided in the standard Java libraries — you can create your own! Here's a simple example:
public class Box<T> {

   private T t;

   public void set(T t) {
       this.t = t;
   }

   public T get() {
       return t;
   }

   public static void main(String[] args) {

       Box<String> stringBox = new Box<>();

       stringBox.set("Old String");
       System.out.println(stringBox.get());
       stringBox.set("New String");

       System.out.println(stringBox.get());

       stringBox.set(12345); // Compilation error!
   }
}
Our Box<T> class is a generic class. Once we assign a data type (<T>) during creation, we are no longer able to place objects of other types in it. This can be seen in the example. When creating our object, we indicated that it would work with Strings:
Box<String> stringBox = new Box<>();
And in the last line of code, when we try to put the number 12345 inside the box, we get a compilation error! It's that easy! We've created our own generic class! :)

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.