Generics - обобщения, обобщенные типы и методы. Возможность создать код (класс или метод) без привязки к конкретным тыпам данных. Такая реализация имеет обобщенный параметр типа, который используется в реализации и мы должны определнить этот параметр конкретным типом в момент создания класса.
Сделаем обобщенный класс для хранения экземпляра объекта. Наш контейнер должен уметь предоставить доступ к id хранимого объекта.
class MyСontainer<T> {
private T value;
public MyСontainer(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public int getId() throws IllegalStateException {
if (value == null) throw new IllegalStateException("Container shoujld be initialized.");
return value.getId();
}
}
var wrappedObject = new MyСontainer<SomeObject>(someObject);
System.out.println("object id = " + wrappedObject.getId());
Здесь T
- это наш обобщенный тип, определяемый как SomeObject
при создании инстанса MyСontainer
.
Java: нельзя использовать примитивные типы как параметр обобщенной перализации
Еще один пример с вариантами использования
class Base {}
class DomainObject extends Base {}
interface MyInterface<T extends Base> {}
class MyClass implements MyInterface<DomainObject> {
};
class MySecondClass<T extends Base> {
};
var x = new MyClass();
var x = new MySecondClass<DomainObject>();
interface MyInterface
{
int getId();
}
class MyСontainer<T>
{
private T value { get; init; }
public MyСontainer(T value)
{
this.value = value;
}
public int getId()
{
if (value == null) throw new InvalidOperationException("Container shoujld be initialized.");
return ((MyInterface) value).getId();
}
}
var wrappedObject = new MyСontainer<SomeObject>(someObject);
Console.WriteLine($"object id = {wrappedObject.getId()}");
C#: можно использовать int, string, double, bool как параметр обобщенной реализации
Больше примеров C#
class Base { }
class DomainObject : Base { }
interface MyCustomInterface<out T, in Q>
{
T GetMessage(Q message);
}
class MyCustomList1 : MyCustomInterface<string, string>
{
public string GetMessage(string message)
{
return message;
}
}
class MyCustomList2<T> : MyCustomInterface<T, T>
{
public T GetMessage(T message)
{
return message;
}
}
var x = new MyCustomList1();
var y = new MyCustomList2<string>();
Если не указывать конкретный тип при создании инстанса, например так new MyСontainer<>(someObject);
, то мы получим так называемый diamond синтакс. Применяя diamond синтакс мы пологаемся на механизм компилятора, который определит требуемый тип по левой части выражения.
The Diamond - это <>
в MyСontainer<>
.
Такой (diamond) синтаксис следует использовать всегда так как использование например new MyСontainer(someObject);
приведит к менее надежному выводу типов в Java.
В некоторых случаях Java может вывести тип самостоятельно
var wrappedObject = new MyСontainer<SomeObject>(someObject);
MyСontainer<SomeObject> wrappedObject = new MyСontainer<>(someObject);
MyСontainer<SomeObject> wrappedObject = new MyСontainer(someObject);
var wrappedObject = new MyСontainer<>(someObject);
MyСontainer wrappedObject = new MyСontainer<>(someObject);
В C# эта "фича" не поддерживатеся. C# более строг в плане типизации ;)
Ковариантность: upper bounded, позволяет использовать более конкретный тип, чем заданный изначально
В C# ковариантность реализуется с использованием out T
.
В Java ковариантность реализуется с использованием ? extends Number
.
Контравариантность: lower bounded, позволяет использовать более универсальный тип, чем заданный изначально
В C# ковариантность реализуется с использованием in T
.
В Java ковариантность реализуется с использованием ? super Number
.
Инвариантность: unbounded, позволяет использовать только заданный тип
Если просто о сложном, то - возможностью использовать тип, который находится ниже или выше в иерархии наследования.
Пример для Java
class Base {}
class DomainObject extends Base {}
// ковариантность. можем использовать Base и его потомки
class MyCustomList<T extends Base> {}
// контравариантность. можем использовать DomainObject и его предков
// class MyCustomList<T super DomainObject> {} - не допустимо для переменной типа
// инвариантность. можем использовать только DomainObject
class MyCustomList<T> {}
Пример для C# немного отличается, так как в C# определение ковариант или контравариант происходит только при определении класса или интерфейса.
В C# только interface и delegate могуть иметь ковариантность/контравариантность определения типов классы должны быть с инвариантными параметрами тапов
class Base {}
class DomainObject : Base {}
// ковариантность. можем использовать Base и его потомки
interface MyCustomList1<out T> {}
class MyCustomList<T> : MyCustomList1<T>
{
}
// контравариантность. можем использовать DomainObject и его предков
interface MyCustomList2<in T> {}
// инвариантность. можем использовать только DomainObject или Base
class MyCustomList3<T> {}
В Java
<T extends Comparable<T>>
В C#
<T extends Comparable<T>>
В Java
<SomeOne & SomethingElse>
В момент создания инстанса мы должны определить какой тип будет присвоен переменной типа T.
new List<DomainObject>()
.
В Java у нас есть возможность определить тип не строго. Мы можем сказать, например, что в нашей коллекции можно использоваться все классы унаследованные от базового и сам базовый класс.
class Base {}
class DomainObject extends Base {}
// ковариантность. можем использовать Base и его потомки
var pool = new ArrayList<? extends Base>();
// контравариантность. можем использовать DomainObject и его предков
var pool = new ArrayList<? super DomainObject>();
// инвариантность. можем использовать только DomainObject
var pool = new ArrayList<DomainObject>();
Java: из списка
List<? super T>
можно только читать и толькоObject
Java: в списов
List<? extends Number>
нельзя ничего добавить, кромеnull
Java:
List<?>
означаетList<? extends Object>
, так как<?>
- это указание возможности подставлять любой допустимый тип, а 'любой допустимый' тип наследуется отObject
. PECS говорит о том что из<?>
так как это? extends Object
- можно только читать.PECS — Producer Extends Consumer Super:
<T> void move(List<? super T> dest, List<? extends T> src
В С# нет механизмов похожих на Wildcard в Java и в этом плане является более строгим.
Применяя дженерики работа с коллекциями выходит на новый уровень. Теперь (да, уже давно :)) мы можем иметь одну коллекцию которая может работать с разными типами. При создании коллекции мы можем указать с каким типом или типами будет работать коллекция.
Для Java
List<Integer> ints = new ArrayList<Integer>();
// or, for list of strings
List<String> ints = new ArrayList<String>();
// or
List<MyClass> ints = new ArrayList<MyClass>();
// or more complex example
List<Pair<String, Double>> ints = new ArrayList<Pair<String, Double>>();
В C# пакет с обобщенными коллекциями находится тут System.Collections.Generic
, docs
var list = new List<int>();
list.Add(1);
list.Add(2);
Для Java, обощенный метод будет выглядеть так
public static class Util {
public static <T> T toDoSomething(Object obj) {
return (T) obj;
}
}
Util.<String>toDoSomething(element)
Обобщенные методы в C#
T Add<T>(T a, T b)
{
return a + b;
}
// can be used as
int x = Add<int>(1, 2);
Затирание типов или удаление информации о типах на этапе компиляции.
Java компилирует generics в Object и в рантайм все обобщенные коллекции будут работать с Object и иметь соответствующие касты.
В C# такой механизм не используется. В нем нет необходимости так как C# runtime (dotnet) умеет работать с дженериками и предоставлять соответствующие гарантии.