Why Not Just Store Object?

Java has a built-in class named Object. It is at the top of the type hierarchy, and every class you write, along with every class in the library, ultimately extends it. (I assume you are familiar with inheritance and know what a class hierarchy is from your earlier CS courses.)

A variable of type Object can therefore hold a reference to an instance of any class. So we could write a single data structure that stores Object and put anything in it:

public class DynamicArray {
  private Object[] arr;
  public void add(Object value) { ... }
  public Object get(int index) { ... }
}

This does solve the duplication problem. One class, and we can use it to store Students, Strings, or anything else. So why not just do that?

The problem shows up when we take something back out. The get method returns an Object, not a Student, so every retrieval needs a cast:

roster.add(new Student(1, "Alice", 3.8));
Student alice = (Student) roster.get(0);  // cast required

And nothing stops us from putting the wrong thing in:

roster.add("oops");                    // compiles fine
Student s = (Student) roster.get(1);   // ClassCastException at runtime

The compiler accepts this code, but the program is broken. We do not find out until the cast fails at runtime, possibly long after the code shipped.

There is a second cost. Look at a method that takes a DynamicArray as a parameter:

public void processRoster(DynamicArray list) {
  // what is actually in here? Students? Strings? A mix?
}

The type DynamicArray carries no information about its contents. So any code that wants to use an element has to check the type at runtime, usually with instanceof.

Storing Object solves the duplication problem, but it gives up type safety. What we want instead is a DynamicArray with a declared element type, so the compiler checks it for us and we never have to cast. That is what generics give us.