Питання 20 · Розділ 20

Що станеться при спробі створити екземпляр дженерик-типу через new T()

Structured Java interview answer with junior, middle, and senior-level explanation.

Мовні версії: English Russian Ukrainian

🟢 Junior Level

Не можна створити екземпляр дженерик-типу через new T(). Це помилка компіляції.

public class Box<T> {
    private T value;

    public Box() {
        value = new T();  // ❌ compilation error: "cannot instantiate T"
    }
}

Чому: Через type erasure компілятор не знає, який тип ховається за T в runtime. Після компіляції T перетворюється на Object, а new Object() — не те, що ви хочете.

Рішення:

// Рішення 1: Передача Class<T>
public class Box<T> {
    private T value;

    public Box(Class<T> type) throws Exception {
        value = type.getDeclaredConstructor().newInstance();
    }
}

// Рішення 2: Фабрика (Supplier)
public class Box<T> {
    private T value;

    public Box(Supplier<T> factory) {
        value = factory.get();
    }
}

// Використання
Box<String> box1 = new Box<>(String.class);
Box<String> box2 = new Box<>(() -> "default");

🟡 Middle Level

Чому не можна?

Type erasure:

public class Box<T> {
    public Box() {
        value = new T();  // ❌ що це означає після erasure?
    }
}

// Після компіляції:
public class Box {
    public Box() {
        value = new Object();  // T -> Object
        // Але це не те, що хотів розробник!
    }
}

Проблема: Немає інформації про конструктор T. Може у T немає no-arg конструктора?

Робочі рішення

**1. Class підхід:**

public class Factory<T> {
    private final Class<T> type;

    public Factory(Class<T> type) {
        this.type = type;
    }

    public T create() throws Exception {
        return type.getDeclaredConstructor().newInstance();
    }
}

Factory<String> stringFactory = new Factory<>(String.class);
String s = stringFactory.create();  // ""

2. Supplier підхід:

public class Box<T> {
    private final Supplier<T> factory;

    public Box(Supplier<T> factory) {
        this.factory = factory;
    }

    public T create() {
        return factory.get();
    }
}

Box<User> userBox = new Box<>(User::new);
User user = userBox.create();

3. Abstract factory method:

public abstract class Creator<T> {
    protected abstract T createInstance();

    public T createAndProcess() {
        T instance = createInstance();
        // process
        return instance;
    }
}

public class UserCreator extends Creator<User> {
    @Override
    protected User createInstance() {
        return new User();
    }
}

Типові помилки

  1. Спроба обійти через cast:
    public class Box<T> {
     public T create() {
         return (T) new Object();  // ⚠️ unchecked cast
         // Працює тільки якщо T = Object
         // ClassCastException для інших типів
     }
    }
    
  2. Reflection без перевірки: ```java public T create(Class type) throws Exception { return type.newInstance(); // deprecated в Java 9+ }

// ✅ Краще public T create(Class type) throws Exception { return type.getDeclaredConstructor().newInstance(); }


---

## 🔴 Senior Level

### Internal Implementation

**Чому компілятор забороняє:**

JLS 15.9:

  • new T() неможливо, бо T — type variable
  • Type variables не мають constructor metadata в class file.
  • JVM буквально не може emit-ити new інструкцію для T.
  • Після erasure T -> Object або bound ```

Reflection підхід:

public T create(Class<T> type) {
    try {
        Constructor<T> ctor = type.getDeclaredConstructor();
        return ctor.newInstance();
    } catch (NoSuchMethodException e) {
        // У T немає no-arg конструктора
        throw new RuntimeException(e);
    }
}

Архітектурні Trade-offs

Підходи до створення T:

Підхід Плюси Мінуси
**Class** Type-safe, просто Потрібен no-arg constructor
**Supplier** Гнучкий, будь-який конструктор Потрібно передати фабрику
Abstract method OOP стиль Потрібен підклас
Unchecked cast Просто Unsafe, ClassCastException risk

Edge Cases

1. Bounded type з no-arg constructor:

public class Box<T extends Runnable> {
    public T create(Class<T> type) throws Exception {
        // T повинен мати no-arg constructor
        return type.getDeclaredConstructor().newInstance();
    }
}

2. Constructor з параметрами:

public T create(Class<T> type, String name) throws Exception {
    return type.getConstructor(String.class)
        .newInstance(name);
}

3. Generic array creation:

// Теж не можна
public class Box<T> {
    private T[] items = new T[10];  // ❌ error

    // ✅ Рішення
    private T[] items = (T[]) new Object[10];
}

Продуктивність

Спосіб створення T:
- new T(): N/A (не можна)
- Class.newInstance(): ~50-100 ns (deprecated)
// Приблизні значення. Залежать від JVM, optimization level, inlining.
- Constructor.newInstance(): ~100-200 ns
- Supplier.get(): ~1-5 ns (звичайний виклик)
- Unchecked cast: ~1 ns (але unsafe)

Supplier — найшвидший runtime approach

Production Experience

Spring Bean Factory:

@Component
public class EntityFactory {
    public <T> T create(Class<T> type) {
        return applicationContext.getBean(type);
    }
}

// Використання
User user = entityFactory.create(User.class);

Builder pattern:

public class Builder<T> {
    private final Supplier<T> factory;
    private final Map<String, Object> properties = new HashMap<>();

    public Builder(Supplier<T> factory) {
        this.factory = factory;
    }

    public Builder<T> with(String name, Object value) {
        properties.put(name, value);
        return this;
    }

    public T build() {
        T instance = factory.get();
        // inject properties
        return instance;
    }
}

User user = new Builder<>(User::new)
    .with("name", "John")
    .with("age", 25)
    .build();

Best Practices

// ✅ Supplier для гнучкості
public class Box<T> {
    private final Supplier<T> factory;
    public Box(Supplier<T> factory) { this.factory = factory; }
    public T create() { return factory.get(); }
}

// ✅ Class<T> для простих випадків
public T create(Class<T> type) {
    return type.getDeclaredConstructor().newInstance();
}

// ✅ Abstract factory method
protected abstract T createInstance();

// ❌ new T()
// ❌ Unchecked cast без необхідності
// ❌ Class.newInstance() (deprecated)

🎯 Шпаргалка для співбесіди

Обов’язково знати:

  • new T() — compilation error: “cannot instantiate T”
  • Причина: type erasure — після компіляції T -> Object, JVM не знає конструктор T
  • Рішення 1: Class<T>type.getDeclaredConstructor().newInstance()
  • Рішення 2: Supplier<T>factory.get() — найгнучкіший і найшвидший підхід
  • Рішення 3: Abstract factory method — підклас реалізує створення
  • Class.newInstance() deprecated в Java 9+, використовувати getDeclaredConstructor().newInstance()

Часті уточнюючі запитання:

  • Чому не можна new T()? — Type erasure: T замінюється на Object, немає інформації про конструктор
  • **Який підхід кращий — Class чи Supplier?** — Supplier гнучкіший і швидший (~1-5 ns vs ~100-200 ns)
  • Чи можна створити T з параметрами через Class? — Так: type.getConstructor(String.class).newInstance(name)
  • Що таке TypeReference патерн? — Jackson style: new TypeReference<User>() {} для отримання generic type

Червоні прапорці (НЕ говорити):

  • ❌ “new T() працює через unchecked cast” — Це compilation error, не warning
  • ❌ “Class.newInstance() — найкращий спосіб” — Deprecated в Java 9+, використовувати getDeclaredConstructor
  • ❌ “Supplier повільніший за Class” — Supplier швидший (~1-5 ns vs ~100-200 ns)
  • ❌ “Можна створити T через reflection без Class” — Потрібен Class або Supplier

Пов’язані теми:

  • [[11. Що таке дженерики (Generics) в Java]]
  • [[13. Що таке type erasure (стирання типів)]]
  • [[14. Чи можна створити масив дженерик-типу]]
  • [[15. Що таке bounded type parameters]]