Generics were introduced to C# specifically to solve the boxing problem above, and the compiler/runtime take real advantage of that when the type argument is a value type:
List<int> numbers = new List<int>(); // backed by a real int[] internally — no boxing at all
numbers.Add(1);
numbers.Add(2);
List<object> objects = new List<object>(); // every int added here IS boxed
objects.Add(1);
objects.Add(2);
At the runtime level (this is .NET-specific and worth knowing for a deeper interview), the CLR generates a specialized native implementation of a generic type per distinct value-type argument used (a separate compiled List<int> and List<double>, for instance), while all reference-type instantiations of the same generic (List<string>, List<Point>-the-class) share a single compiled implementation, since they're all just pointer-sized references underneath. This is why generic collections of value types are both type-safe and fast — you get the memory layout of a specialized array without hand-writing one per type.