Generics & Metaprogramming
Type Systems Deep Dive
What are Generics?
Generics are a feature of statically-typed programming languages that allow you to define classes, interfaces, and methods with a placeholder for a type. This allows you to write code that works with any data type in a type-safe manner.
The core idea is to create components that are highly reusable across different data types without sacrificing the safety and compile-time checking of a static type system.
The Problem Generics Solve: Imagine you want to create a list container. Without generics, you have two bad options:
- Create a specific list for each type:
IntList
,StringList
,UserList
, etc. This leads to massive code duplication. - Create a list that holds a generic
Object
type: This sacrifices type safety. When you retrieve an item from the list, you get back anObject
and have to manually cast it back to its original type. This is error-prone, as you could accidentally add aString
to a list ofIntegers
and only find out at runtime when you get aClassCastException
.
The Solution: Generics
With generics, you can define a single List<T>
class, where T
is a type parameter. When you use the list, you specify the concrete type you want to store.
// A single, generic List class. 'T' is the type parameter.
// List<T>
// Using the generic list with specific types.
List<String> names = new ArrayList<>(); // A list that can ONLY hold Strings.
names.add("Alice");
// names.add(123); // COMPILE-TIME ERROR! The list is type-safe.
String name = names.get(0); // No cast needed. The compiler knows it's a String.
List<Integer> numbers = new ArrayList<>(); // A list that can ONLY hold Integers.
numbers.add(10);
// numbers.add("hello"); // COMPILE-TIME ERROR!
int number = numbers.get(0); // No cast needed.
// C# uses the same syntax for generics.
List<string> names = new List<string>();
names.Add("Alice");
// names.Add(123); // COMPILE-TIME ERROR!
string name = names[0]; // No cast needed.
Generic Methods
You can also create generic methods that can operate on different types.
// A generic method to print any array.
// The <T> before the return type declares it as a generic method.
public <T> void printArray(T[] inputArray) {
for (T element : inputArray) {
System.out.printf("%s ", element);
}
System.out.println();
}
// Usage:
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray(intArray); // Works with Integer array
printArray(stringArray); // Works with String array
// C# has a similar syntax for generic methods.
public void PrintArray<T>(T[] inputArray) {
foreach (T element in inputArray) {
Console.Write($"{element} ");
}
Console.WriteLine();
}
What is Metaprogramming?
Metaprogramming is the concept of writing code that writes or manipulates other code. It's a program's ability to treat code as its data, allowing it to read, analyze, modify, or even generate code at runtime or compile-time.
Generics can be seen as a simple form of compile-time metaprogramming, as they generate specialized versions of a class for a given type. However, metaprogramming encompasses a much broader set of more powerful (and more complex) techniques.
Common Forms of Metaprogramming
1. Reflection
Reflection is the ability of a program to examine and modify its own structure and behavior at runtime. You can inspect a class to find out its methods, fields, and constructors, and you can invoke them dynamically.
- Use Cases: Frameworks like Spring (Java) and Django (Python) use reflection heavily for dependency injection, serialization (JSON/XML), and creating Object-Relational Mappers (ORMs).
- Warning: Reflection is powerful but slow and can break encapsulation. It should be used carefully.
// Java Reflection
MyClass obj = new MyClass();
// Get the class object
Class<?> clazz = obj.getClass();
// Get a method by its name (even if it's private!)
Method method = clazz.getDeclaredMethod("myPrivateMethod", String.class);
// Make it accessible
method.setAccessible(true);
// Invoke it dynamically
method.invoke(obj, "dynamic call");
# Python Reflection (using getattr, setattr)
class MyClass:
def greet(self):
print("Hello")
obj = MyClass()
method_name = "greet"
# Check if the object has the method
if hasattr(obj, method_name):
# Get the method object
method = getattr(obj, method_name)
# Call it
method() # Prints "Hello"
2. Decorators (Python) / Attributes (C#) / Annotations (Java)
These are a form of metaprogramming that allows you to add metadata or behavior to functions or classes in a declarative way.
A decorator is a function that takes another function and extends its behavior without explicitly modifying it.
# Python Decorator
def log_execution(func):
def wrapper(*args, **kwargs):
print(f"Calling function '{func.__name__}'...")
result = func(*args, **kwargs)
print(f"Finished function '{func.__name__}'.")
return result
return wrapper
@log_execution
def say_hello(name):
print(f"Hello, {name}")
say_hello("Alice")
# Output:
# Calling function 'say_hello'...
# Hello, Alice
# Finished function 'say_hello'.
// Java Annotations are used by frameworks to add behavior.
// This is not a direct equivalent, but shows the declarative metadata style.
// A testing framework like JUnit would use reflection to find and run this method.
@Test
public void myTestMethod() {
// ... test logic
}
3. Macros
Macros are a more powerful form of compile-time metaprogramming. They are code that runs during compilation and generates new code that is then integrated into the program. This allows you to extend the syntax of the language itself.
- Languages: Rust and Lisp are famous for their powerful macro systems.
Summary for Interviews
- Generics allow you to write type-safe, reusable components by using a placeholder (
<T>
) for a type. This avoids code duplication and the dangers of casting from a genericObject
type. It's a fundamental tool for building collections and robust, reusable algorithms in statically-typed languages. - Metaprogramming is the concept of code that operates on other code. It's a powerful, advanced technique for creating flexible and dynamic frameworks.
- Key forms of metaprogramming include:
- Reflection: Inspecting and modifying code at runtime. Powerful but slow and complex. Used in dependency injection and serialization frameworks.
- Decorators/Annotations: Declaratively adding metadata or behavior to code. Common in web frameworks, testing, and logging.
- Macros: Code that runs at compile-time to generate new code. The most powerful form, allowing you to extend the language's syntax.
- Generics can be considered a simple, safe form of metaprogramming. More advanced techniques like reflection and macros offer more power but also come with greater complexity and risk.