Перейти к содержимому

Что такое generics в java

  • автор:

Что такое generics в java

Обобщения или generics (обобщенные типы и методы) позволяют нам уйти от жесткого определения используемых типов. Рассмотрим проблему, в которой они нам могут понадобиться.

Допустим, мы определяем класс для представления банковского счета. К примеру, он мог бы выглядеть следующим образом:

class Account < private int id; private int sum; Account(int id, int sum)< this.id = id; this.sum = sum; >public int getId() < return id; >public int getSum() < return sum; >public void setSum(int sum) < this.sum = sum; >>

Класс Account имеет два поля: id — уникальный идентификатор счета и sum — сумма на счете.

В данном случае идентификатор задан как целочисленное значение, например, 1, 2, 3, 4 и так далее. Однако также нередко для идентификатора используются и строковые значения. И числовые, и строковые значения имеют свои плюсы и минусы. И на момент написания класса мы можем точно не знать, что лучше выбрать для хранения идентификатора — строки или числа. Либо, возможно, этот класс будет использоваться другими разработчиками, которые могут иметь свое мнение по данной проблеме. Например, в качестве типа id они захотят использовать какой-то свой класс.

И на первый взгляд мы можем решить данную проблему следующим образом: задать id как поле типа Object, который является универсальным и базовым суперклассом для всех остальных типов:

public class Program < public static void main(String[] args) < Account acc1 = new Account(2334, 5000); // id - число int acc1Id = (int)acc1.getId(); System.out.println(acc1Id); Account acc2 = new Account("sid5523", 5000); // id - строка System.out.println(acc2.getId()); >> class Account < private Object id; private int sum; Account(Object id, int sum)< this.id = id; this.sum = sum; >public Object getId() < return id; >public int getSum() < return sum; >public void setSum(int sum) < this.sum = sum; >>

В данном случае все замечательно работает. Однако тогда мы сталкиваемся с проблемой безопасности типов . Например, в следующем случае мы получим ошибку:

Account acc1 = new Account("2345", 5000); int acc1Id = (int)acc1.getId(); // java.lang.ClassCastException System.out.println(acc1Id);

Проблема может показаться искуственной, так как в данном случае мы видим, что в конструктор передается строка, поэтому мы вряд ли будем пытаться преобразовывать ее к типу int. Однако в процессе разработки мы можем не знать, какой именно тип представляет значение в id, и при попытке получить число в данном случае мы столкнемся с исключением java.lang.ClassCastException.

Писать для каждого отдельного типа свою версию класса Account тоже не является хорошим решением, так как в этом случае мы вынуждены повторяться.

Эти проблемы были призваны устранить обобщения или generics. Обобщения позволяют не указывать конкретный тип, который будет использоваться. Поэтому определим класс Account как обобщенный:

class Account < private T id; private int sum; Account(T id, int sum)< this.id = id; this.sum = sum; >public T getId() < return id; >public int getSum() < return sum; >public void setSum(int sum) < this.sum = sum; >>

С помощью буквы T в определении класса class Account мы указываем, что данный тип T будет использоваться этим классом. Параметр T в угловых скобках называется универсальным параметром , так как вместо него можно подставить любой тип. При этом пока мы не знаем, какой именно это будет тип: String, int или какой-то другой. Причем буква T выбрана условно, это может и любая другая буква или набор символов.

После объявления класса мы можем применить универсальный параметр T : так далее в классе объявляется переменная этого типа, которой затем присваивается значение в конструкторе.

Метод getId() возвращает значение переменной id, но так как данная переменная представляет тип T, то данный метод также возвращает объект типа T: public T getId() .

Используем данный класс:

public class Program < public static void main(String[] args) < Accountacc1 = new Account("2345", 5000); String acc1Id = acc1.getId(); System.out.println(acc1Id); Account acc2 = new Account(2345, 5000); Integer acc2Id = acc2.getId(); System.out.println(acc2Id); > > class Account < private T id; private int sum; Account(T id, int sum)< this.id = id; this.sum = sum; >public T getId() < return id; >public int getSum() < return sum; >public void setSum(int sum) < this.sum = sum; >>

При определении переменной даннного класса и создании объекта после имени класса в угловых скобках нужно указать, какой именно тип будет использоваться вместо универсального параметра. При этом надо учитывать, что они работают только с объектами, но не работают с примитивными типами. То есть мы можем написать Account , но не можем использовать тип int или double, например, Account . Вместо примитивных типов надо использовать классы-обертки: Integer вместо int, Double вместо double и т.д.

Например, первый объект будет использовать тип String, то есть вместо T будет подставляться String:

Account acc1 = new Account("2345", 5000);

В этом случае в качестве первого параметра в конструктор передается строка.

А второй объект использует тип int (Integer):

Account acc2 = new Account(2345, 5000);

Обобщенные интерфейсы

Интерфейсы, как и классы, также могут быть обобщенными. Создадим обобщенный интерфейс Accountable и используем его в программе:

public class Program < public static void main(String[] args) < Accountableacc1 = new Account("1235rwr", 5000); Account acc2 = new Account("2373", 4300); System.out.println(acc1.getId()); System.out.println(acc2.getId()); > > interface Accountable < T getId(); int getSum(); void setSum(int sum); >class Account implements Accountable < private String id; private int sum; Account(String id, int sum)< this.id = id; this.sum = sum; >public String getId() < return id; >public int getSum() < return sum; >public void setSum(int sum) < this.sum = sum; >>

При реализации подобного интерфейса есть две стратегии. В данном случае реализована первая стратегия, когда при реализации для универсального параметра интерфейса задается конкретный тип, как например, в данном случае это тип String. Тогда класс, реализующий интерфейс, жестко привязан к этому типу.

Вторая стратегия представляет определение обобщенного класса, который также использует тот же универсальный параметр:

public class Program < public static void main(String[] args) < Accountacc1 = new Account("1235rwr", 5000); Account acc2 = new Account("2373", 4300); System.out.println(acc1.getId()); System.out.println(acc2.getId()); > > interface Accountable < T getId(); int getSum(); void setSum(int sum); >class Account implements Accountable < private T id; private int sum; Account(T id, int sum)< this.id = id; this.sum = sum; >public T getId() < return id; >public int getSum() < return sum; >public void setSum(int sum) < this.sum = sum; >>

Обобщенные методы

Кроме обобщенных типов можно также создавать обобщенные методы, которые точно также будут использовать универсальные параметры. Например:

public class Program< public static void main(String[] args) < Printer printer = new Printer(); String[] people = ; Integer[] numbers = ; printer.print(people); printer.print(numbers); > > class Printer < public void print(T[] items) < for(T item: items)< System.out.println(item); >> >

Особенностью обобщенного метода является использование универсального параметра в объявлении метода после всех модификаторов и перед типом возвращаемого значения.

public void print(T[] items)

Затем внутри метода все значения типа T будут представлять данный универсальный параметр.

При вызове подобного метода перед его именем в угловых скобках указывается, какой тип будет передаваться на место универсального параметра:

printer.print(people); printer.print(numbers);

Использование нескольких универсальных параметров

Мы можем также задать сразу несколько универсальных параметров:

public class Program < public static void main(String[] args) < Accountacc1 = new Account("354", 5000.87); String sum = acc1.getSum(); System.out.printf("Id: %s Sum: %f \n", id, sum); > > class Account < private T id; private S sum; Account(T id, S sum)< this.id = id; this.sum = sum; >public T getId() < return id; >public S getSum() < return sum; >public void setSum(S sum) < this.sum = sum; >>

В данном случае тип String будет передаваться на место параметра T, а тип Double — на место параметра S.

Обобщенные конструкторы

Конструкторы как и методы также могут быть обобщенными. В этом случае перед конструктором также указываются в угловых скобках универсальные параметры:

public class Program < public static void main(String[] args) < Account acc1 = new Account("cid2373", 5000); Account acc2 = new Account(53757, 4000); System.out.println(acc1.getId()); System.out.println(acc2.getId()); >> class Account< private String id; private int sum; Account(T id, int sum) < this.id = id.toString(); this.sum = sum; >public String getId() < return id; >public int getSum() < return sum; >public void setSum(int sum) < this.sum = sum; >>

В данном случае конструктор принимает параметр id, который представляет тип T. В конструкторе его значение превращается в строку и сохраняется в локальную переменную.

Дженерики Java

Дженерики (или обобщения) — это параметризованные типы.

Параметризованные типы позволяют объявлять классы, интерфейсы и методы, где тип данных, которыми они оперируют, указан в виде параметра. Используя дженерики, можно создать единственный класс, например, который будет автоматически работать с разными типами данных.

Классы, интерфейсы или методы, имеющие дело с параметризованными типами, называются параметризованными или обобщениями, параметризованными (обобщенными) классами или параметризованными (обобщёнными) методами.

Обобщения добавили в язык безопасность типов.

  1. Параметризованные классы
  2. Ограниченные типы
  3. Применение метасимвольных аргументов
  4. Параметризованные методы и конструкторы
  5. Параметризованные интерфейсы
  6. Иерархии параметризованных классов
  7. Использование оператора instanceof c параметризованными классами
  8. Ограничения присущие обобщениям

1. Параметризованные классы

Следующий пример демонстрирует использование параметризованного класса, который описывает матрицу:

public class Matrix  < private T[] array; public Matrix(T[] array) < this.array = array.clone(); >public static void main(String[] args) < MatrixdoubleMatrix = new Matrix<>(new Double[2]); Matrix integerMatrix = new Matrix<>(new Integer[4]); Matrix byteMatrix = new Matrix<>(new Byte[7]); > > 

В объявлении Matrix integerMatrix Integer является аргументом типа.

Java не создает разные версии класса Matrix или любого другого параметризованного класса. Имеется только одна версия класса Matrix , которая существует в прикладной программе.

Дженерики работают только с объектами! Следующий код является неправильным:

Gen strOb = new Gen (53); // Ошибка, нельзя использовать примитивные типы 

Т обозначает имя параметра типа. Это имя используется в качестве заполнителя вместо которого в дальнейшем подставляется имя конкретного типа, передаваемого классу Matrix при создании объекта. Это означает, что обозначение Т применяется в классе Matrix всякий раз, когда требуется параметр типа. Всякий раз, когда объявляется параметр типа, он указывается в угловых скобках.

Обобщенные типы отличаются в зависимости от типов-аргументов. Следующий код не допустим:

doubleMatrix = integerMatrix; // Не верно! 

Несмотря на то, что doubleMatrix и integerMatrix имеют тип Matrix , они являются ссылками на разные типы, потому что типы их параметров отличаются.

Обобщенный класс может быть объявлен с любым количеством параметров типа. Например:

public class TwoGen  < private T obT; private V obV; public TwoGen(T obT, V obV) < this.obT = obT; this.obV = obV; >public void showTypes() < System.out.println("Тип T: " + obT.getClass().getName()); System.out.println("Тип V: " + obV.getClass().getName()); >public T getObT() < return obT; >public V getObV() < return obV; >> public class SimpleGen < public static void main(String[] args) < TwoGentwoGen = new TwoGen<>(88, "Generics"); twoGen.showTypes(); System.out.println("Значение T: " + twoGen.getObT()); System.out.println("Значение V: " + twoGen.getObV()); > > 

2. Ограниченные типы

Указывая параметр типа, можно наложить ограничение сверху в виде верхней границы, где объявляется супер класс, от которого должны быть унаследованы все аргументы типов. С этой целью вместе с параметром указывается ключевое слово extends :

class Gen

Параметр типа Т может быть заменен только указанным супер классом или его подклассами.

Рассмотрим пример использования ограниченного типа:

public class Average  < private T[] array; public Average(T[] array) < this.array = array; >public double average() < double sum = 0.0; for (T value : array) < sum += value.doubleValue(); >return sum / array.length; > > public class AverageDemo < public static void main(String[] args) < Integer[] intArray = ; Average integerAverage = new Average<>(intArray); System.out.println("Среднее значения для Integer " + integerAverage.average()); Double[] doubleArray = ; Average doubleAverage = new Average<>(doubleArray); System.out.println("Среднее значения для Double " + doubleAverage.average()); // Не откомпилируется, // потому что String не является наследником Number /* String[] strArray = ; Average strAverage = new Average<>(strArray); System.out.println("Среднее значения для String " + strAverage.average());*/ > >

В виде ограничения можно накладывать не только тип класса, но и тип интерфейса:

public class MyClass

Ограничение может включать в себя как тип класса, так и типы одного или нескольких интерфейсов:

class Gen

Тип класса должен быть задан первым. Накладывая на обобщенный тип ограничение, состоящее из класса и одного или нескольких интерфейсов, для их объединения следует воспользоваться логической операцией &: Таким образом, любой тип, передаваемый параметру Т , должен быть подклассом, производным от класса MyClass и реализующим интерфейсы MyInterface1 и MyInterface2 .

3. Применение метасимвольных аргументов

Представьте, что мы хотим добавить метод для сравнения средних значений массивов в класс Average из примера 3. Причем типы массивов могут быть разные:

Integer intArray[] = ; Double doubleArray[] = ; Average iob = new Average<>(intArray); Average dob = new Average<>(doubleArray); if (iob.sameAvg(dob)) < System.out.println("are the same.");>else

Так как Average параметризованный тип, какой тип параметра вы укажете для Average , когда создадите параметр метода типа Average ? Напрашивается следующий вариант:

boolean sameAvg(Average ob)

Но это не сработает, так как в этом случае метод sameAvg будет принимать аргументы только того же типа, что и существующий объект:

if (iob.sameAvg(iob)) < System.out.println("are the same.");>else

Чтобы создать обобщенную версию метода sameAvg() , следует воспользоваться другим средством обобщений Jаvа – метасимвольным аргументом. Метасимвольный аргумент обозначается знаком ? и представляет неизвестный тип.

boolean sameAvg(Average ob)

Мета символ не оказывает никакого влияния на тип создаваемых объектов класса Average . Это определяется оператором extends в объявлении класса Average. Мета символ просто совпадает с любым достоверным объектом класса Average .

Метасимвольные аргументы могут быть ограничены почти таким же образом, как и параметры типов. Ограничивать метасимвольный аргумент особенно важно при создании обобщенного типа, оперирующего иерархией классов. Например:

public class Average2  < private T[] array; public Average2(T[] array) < this.array = array.clone(); >public double average() < double sum = 0.0; for (T value : array) < sum += value.doubleValue(); >return sum / array.length; > boolean sameAvg(Average2 ob) < return average() == ob.average(); >> public class AverageDemo2 < public static void main(String[] args) < Integer[] intArray = ; Average2 iob = new Average2<>(intArray); System.out.println("Среднее значения для Integer " + iob.average()); Double[] doubleArray = ; Average2 dob = new Average2<>(doubleArray); System.out.println("Среднее значения для Double " + dob.average()); Float[] floatArray = ; Average2 fob = new Average2<>(floatArray); System.out.println("Среднее значения для Float " + fob.average()); System.out.print("Средние значения для iob и dob "); if (iob.sameAvg(dob)) < System.out.println("одинаковые."); >else < System.out.println("разные."); >System.out.print("Средние значения для iob и fob "); if (iob.sameAvg(fob)) < System.out.println("одинаковые."); >else < System.out.println("разные."); >> >

4. Параметризованные методы и конструкторы

В методах параметризованного класса можно использовать параметр типа, а следовательно, они становятся параметризованными относительно параметра типа.

Но можно объявить параметризованный метод, в котором непосредственно используется один или несколько параметров типа. Более того, можно объявить параметризованный метод, входящий в не параметризованный класс. Например:

public class GenMethodDemo < /** * Является ли объект x элементом массива array * * @param x * @param array * @param * @param * @return */ public static boolean isIn(T x, V[] array) < for (V element : array) < if (x.equals(element)) < return true; >> return false; > public static void main(String[] args) < Integer[] intArray = ; if (isIn(2, intArray)) < System.out.println("2 входит в массив intArray"); >if (!isIn(7, intArray)) < System.out.println("7 не входит в intArray"); >System.out.println(); String[] strArray = ; if (isIn("two", strArray)) < System.out.println("two входит в массив strArray"); >if (!isIn("seven", strArray)) < System.out.println("seven не входит в массив strArray"); >> >

Конструкторы также могут быть обобщенными, даже если их классы таковыми не являются. Например:

public class GenConstructor < private double value; public GenConstructor(T arg) < value = arg.doubleValue(); >public void showValue() < System.out.println("value: " + value); >> public class GenConstructorDemo < public static void main(String[] args) < GenConstructor genConstructor1 = new GenConstructor(100); GenConstructor genConstructor2 = new GenConstructor(123.5F); genConstructor1.showValue(); genConstructor2.showValue(); >>

5. Параметризованные интерфейсы

В дополнение к обобщенным классам и методам вы можете объявлять параметризованные интерфейсы. Параметризованные интерфейсы специфицируются так же, как и обобщенные классы:

public interface MyInterface  < T someMethod(T t); >public class MyClass implements MyInterface  < @Override public T someMethod(T t) < return t; >public static void main(String[] args) < MyInterfaceobject = new MyClass<>(); String str = object.someMethod("some string"); > >

6. Иерархии параметризованных классов

Параметризованные классы могут быть частью иерархии классов так же, как и любые другие не параметризованные классы. То есть параметризованный класс может выступать в качестве супер класса или подкласса.

Ключевое отличие между параметризованными и не параметризованными иерархиями состоит в том, что в параметризованной иерархии любые аргументы типов, необходимые параметризованному супер классу, всеми подклассами должны передаваться по иерархии вверх.

public class GenericSuper  < private T ob; public GenericSuper(T ob) < this.ob = ob; >private T getOb() < return ob; >> public class GenericSub extends GenericSuper  < public GenericSub(T ob) < super(ob); >>

Подкласс параметризованного супер класса необязательно должен быть параметризованным, но в нем все же должны быть, указаны параметры типа, требующиеся его параметризованному супер классу. Подкласс может, если требуется, быть, дополнен и своими параметрами типа. Супер классом для параметризованного класса может быть класс не параметризованный.

7. Использование оператора instanceof c параметризованными классами

public class HierarchyDemo < public static void main(String[] args) < GenericSuperobject = new GenericSub<>(88); if (object instanceof GenericSuper) < System.out.println("object is instance of GenericSuper"); >if (object instanceof GenericSub) < System.out.println("object is instance of GenericSub"); >// Ошибка компиляции - информация об обобщенном типе недоступна во время выполнения /* if (object instanceof GenericSub) < System.out.println("object is instance of GenericSub"); >*/ > >

8. Ограничения присущие обобщениям

Обобщениям присущи некоторые ограничения. Рассмотрим их:

1. Нельзя создавать экземпляр по параметру типа. Ни обычный объект, ни массив:

public class GenRestriction  < private T ob; private T[] array; public GenRestriction(T ob, T[] array) < // Недопустимо. //оb = new Т(); //array = new Т[10]; this.ob = ob; this.array = array; >>

2. Нельзя создать массив специфических для типа обобщенных ссылок:

public class GenArrays < public static void main(String[] args) < // Нельзя создать массив специфичных для типа обобщенных ссылок. // GenericSub[] gens = new GenericSub[10]; GenericSub[] gens = new GenericSub[10]; gens[0] = new GenericSub<>(34); > >

3. Нельзя создавать обобщенные статические переменные и методы. Но объявить статические обобщенные методы со своими параметрами типа все же можно:

public class GenericWrongStatic < // Неверно, нельзя создать статические переменные типа Т. //public static Т оb; // Неверно, ни один статический метод не может использовать Т. /* public static T getOb() < return оb; >*/ //Но объявить статические обобщенные методы со своими параметрами типа можно public static void getOb(V v) < System.out.println(v); >>
  • Вложенные классы
  • Задания

Обобщения (Generic)

Обобщения — это параметризованные типы. С их помощью можно объявлять классы, интерфейсы и методы, где тип данных указан в виде параметра. Обобщения добавили в язык безопасность типов, немного на тему говорится в статье про ArrayList.

Рассмотрим пример с обобщением.

 class Gen  < T ob; // объявление объекта типа T // Передать конструктору ссылку на объект типа T Gen(T o) < ob = o; >// Вернуть ob T getob() < return ob; >// Показать тип T void showType() < System.out.println("Тип T: " + ob.getClass().getName()); >> // Код для кнопки // Работаем с обобщённым классом // Создаём Gen-ссылку для Integer Gen iOb; // Создаём объект Gen iOb = new Gen(77); // Показать тип данных, используемый iOb iOb.showType(); // Получить значение iOb int value = iOb.getob(); System.out.println("Значение " + value); // Создадим объект Gen для String Gen strOb = new Gen("Обобщённый текст"); // Показать тип данных, используемый strOb strOb.showType(); // Получить значение strOb String str = strOb.getob(); System.out.println("Значение: " + str); 

В результате мы получим:

Типом T является java.lang.Integer Значение: 77 Типом T является java.lang.String Значение: Обобщённый текст

Изучим код. Мы объявили класс в следующей форме:

class Gen

В угловых скобках используется T — имя параметра типа. Это имя используется в качестве заполнителя, куда будет подставлено имя реального типа, переданного классу Gen при создании реальных типов. То есть параметр типа T применяется в классе всякий раз, когда требуется параметр типа. Угловые скобки указывают, что параметр может быть обобщён. Сам класс при этом называется обобщённым классом или параметризованным типом.

Далее тип T используется для объявления объекта по имени ob:

 T ob; // объявляет объект типа T 

Вместо T подставится реальный тип, который будет указан при создании объекта класса Gen. Объект ob будет объектом типа, переданного в параметре типа T. Если в параметре T передать тип String, то экземпляр ob будет иметь тип String.

Рассмотрим конструктор Gen().

 Get(T o)

Параметр o имеет тип T. Это значит, что реальный тип параметра o определяется типом, переданным параметром типа T при создании объекта класса Gen.

Параметр типа T также может быть использован для указания типа возвращаемого значения метода:

 T getob()

В именах переменных типа принято использовать заглавные буквы. Обычно для коллекций используется буква E, буквами K и V — типы ключей и значение (Key/Value), а буквой T (и при необходимости буквы S и U) — любой тип.

Как использовать обобщённый класс. Можно создать версию класса Gen для целых чисел:

 Gen iOb; 

В угловых скобках указан тип Integer, т.е. это аргумент типа, который передаётся в параметре типа T класса Gen. Фактически мы создаём версию класса Gen, в которой все ссылки на тип T становятся ссылками на тип Integer.

Когда мы присваиваем ссылку на экземпляр, то угловые скобки также требуется указывать.

 iOb = new Gen(77); 

Полная версия записи может быть такой:

 iOb = new Gen(new Integer(88)); 

Но такая запись избыточна, так как можно использовать автоматическую упаковку значения 77 в нужный формат.

Аналогично, можно было бы использовать вариант без автоупаковки для получения значения:

 int value = iOb.getob().intValue(); // избыточный код 

Обобщения работают только с объектами. Поэтому нельзя использовать в качестве параметра элементарные типы вроде int или char:

 Gen intOb = new Gen(44); // нельзя! 

Хотя объекты iOb и strOb имеют тип Gen , они являются ссылками на разные типы и их сравнивать нельзя.

 iOB = strOb; // нельзя! 

Использование обобщений автоматически гарантирует безопасность типов во всех операциях, где они задействованы. Это очень мощный механизм, широко используемый в Java.

Обобщённый класс с двумя параметрами

Можно указать два и более параметров типа через запятую.

 class TwoGen  < T ob1; V ob2; // Передать конструктору ссылки на объекты двух типов TwoGen(T o1, V o2) < ob1 = o1; ob2 = o2; >void showTypes() < System.out.println("Тип T: " + ob1.getClass().getName()); System.out.println("Тип V: " + ob2.getClass().getName()); >T getob1() < return ob1; >V getob2() < return ob2; >// Используем созданный класс TwoGen twogenObj = new TwoGen(77, "Обобщённый текст"); // Узнаем типы twogenObj.showTypes(); // Узнаем значения int value = twogenObj.getob1(); System.out.println("Значение: " + value); String str = twogenObj.getob2(); System.out.println("Значение: " + str); > 

Обобщённые методы

Никто не запрещает создавать и методы с параметрами и возвращаемыми значениями в виде обобщений.

 public static T getSomething(T. a)

Шаблоны аргументов

Шаблон аргументов указывается символом ? и представляет собой неизвестный тип.

 boolean sameAvg(Stats ob) . 

По сути, вопрос заменяет Object и мы можем использовать любой класс, который в любом случае будет происходить от Object.

Мы можем ограничить диапазон объектов, указав суперкласс.

 public void addItems(ArrayList list) 

В этом случае можно использовать классы, которые могут быть наследниками AnimalDog, Cat. А String или Integer вы уже не сможете использовать.

Пример обобщённого метода

 public static T getMiddle(T. a) < return a[a.length / 2]; >

Переменная типа вводится после модификаторов и перед возвращаемым типом.

Отдельно стоит упомянуть новинку JDK 7, позволяющую сократить код.

 MyClass mcObj = new MyClass(33, "Meow"); // старый способ в JDK 6 MyClass mcObj = new MyClass<>(33, "Meow"); // новый способ в JDK 7 

Во второй строке используются только угловые скобки, без указания типов.

Помните, что нельзя создать экземпляр типа параметра.

 class Gen  < T ob; Gen() < ob = new T(); // Недопустимо >> 

Под Android вам часто придётся использовать списочный массив, который использует обобщещние.

 ArrayList catNames = new ArrayList(); 

В Java 7, которую можно использовать в проектах для KitKat и выше, существует укороченная запись, когда справа не указывается тип (см. выше). Компилятор сам догадается по левой части выражения.

 ArrayList catNames = new ArrayList<>(); 

Generics

Дженерики (generics) в языке программирования Java — это сущности, которые могут хранить в себе данные только определенного типа. Например, список элементов, в котором могут быть одни числа. Но не только: дженерик — обобщенный термин для разных структур.

Освойте профессию «Java-разработчик»

Можно представить дженерик как папку для бумаг, куда нельзя положить ничего, кроме документов определенного формата. Это удобно: помогает разделить разные данные и не допустить ситуаций, когда в сущность передается что-то не то.

Дженерик-сущности еще иногда называют параметризованными, общими или обобщенными. Такая сущность создается со специальным параметром. Параметр позволяет указать, с каким типом данных она будет работать. Отсюда и название.

В разных источниках можно услышать про «тип-дженерик», «класс-дженерик» или «метод-дженерик». Это нормально, ведь обобщение и параметризация касаются всех этих сущностей, а generics — общий термин.

Для чего нужны дженерики

С дженериками работают программисты на Java. Без этой возможности писать код, который работает только с определенным видом данных, было сложнее. Существовало два способа, и оба неоптимальные:

  • указывать проверку типа вкоде. Например, получать данные — и сразу проверять, а если они не те, выдавать ошибку. Это помогло бы отсеять ненужные элементы. Но если бы класс понадобилось сделать более гибким, например, создать его вариацию для другого типа, его пришлось бы переписывать или копировать. Не получилось бы просто передать другой специальный параметр, чтобы тот же класс смог работать еще с каким-то типом;
  • полагаться на разработчиков. Например, оставлять в коде комментарий «Этот класс работает только с числами». Слишком велик риск, что кто-то не заметит комментарий и передаст в объект класса не те данные. И хорошо, если ошибка будет заметна сразу, а не уже на этапе тестирования.

Поэтому появились дженерики: они решают эту проблему, делают написание кода проще, а защиту от ошибок надежнее.

Профессия / 14 месяцев
Java-разработчик

Освойте востребованный язык

Group 1321314345 (4)

Как работают дженерики

Чтобы вернее понять принцип работы, нужно представлять, как устроены сущности в Java. Есть классы — это как бы «чертежи» будущих сущностей, описывающие, что они делают. И есть объекты — экземпляры классов, непосредственно существующие и работающие. Класс — как схема машины, объект — как машина.

Когда разработчик создает дженерик-класс, он приписывает к нему параметр в треугольных скобках — метку. К примеру, так:

Теперь при создании объекта этого класса нужно будет указать на месте T название типа, с которым будет работать объект. Например, myClass для целых чисел или myClass для строк. Сам класс остается универсальным, то есть общим. А вот каждый его объект специфичен для своего типа.

С помощью дженериков можно создать один класс, а потом на основе него — несколько объектов этого класса для разных типов. Не понадобится дублировать код и усложнять программу. Поэтому дженерики лучше и удобнее, чем проверка типа прямо в коде — тогда для каждого типа данных понадобился бы свой класс.

Что такое raw types

В Java есть понятие raw types. Так называют дженерик-классы, из которых удалили параметр. То есть изначально класс описали как дженерик, но при создании объекта этого класса тип ему не передали. То есть что-то вроде myClass<> — тип не указан.

Дословно это название переводится как «сырые типы». Пользоваться ими сейчас в коммерческой разработке — чаще всего плохая практика. Но в мире все еще много старого кода, который написали до появления дженериков. Если такой код еще не успели переписать, в нем может быть очень много «сырых типов». Это надо учитывать, чтобы не возникало проблем с совместимостью.

Дженерики-классы и дженерики-методы

Выше мы говорили, что дженериками могут быть разные сущности. Разберемся подробнее:

  • дженерик-классы (generic classes) это классы, «схемы» объектов с параметром. При создании объекта ему передается тип, с которым он будет работать;
  • дженерик-методы (generics methods) это методы, работающие по такому же принципу. Метод — это функция внутри объекта, то, что он может делать. Методу тип передается при вызове, сразу перед аргументами. Так можно создавать более универсальные функции и применять одну и ту же логику к данным разного типа.

Кстати, дженериками могут быть и встроенные классы или методы, и те, которые разработчик пишет самостоятельно. Например, встроенный ArrayList — список-массив — работает как дженерик.

Станьте Java-разработчиком
и создавайте сложные сервисы
на востребованном языке

Что будет, если передать дженерику не тот тип

Если объекту класса-дженерика передать не тот тип, который указали при его объявлении, он выдаст ошибку. Например, если в ходе работы экземпляра myClass в нем попытаются сохранить дробное число или даже строку, программа не скомпилируется. Вместо этого разработчик увидит ошибку: неверный тип.

Эта ошибка отличается от тех, которые возникнут, если не пользоваться дженериками. По ней сразу ясно, из-за чего она возникла и как можно ее исправить. Кроме того, она появляется сразу. Поэтому код становится легче отлаживать.

А если отправить «не тот» тип объекту без дженерика, действия с ним выполнятся с ошибкой. Но по этой ошибке не всегда очевидно, чем она вызвана. Худший вариант — код успешно запустится, но сработает неправильно: так ошибку будет найти еще сложнее.

Особенности дженериков

У дженериков есть несколько особенностей, о которых стоит знать при работе с ними. Если не учитывать эти детали, программировать будет как минимум менее удобно. А как максимум можно допустить ошибку и не понять, куда она закралась.

Выведение типа. Эта особенность касается объявления экземпляра класса, то есть создания объекта. Полная запись создания будет выглядеть так:

myClass objectForIntegers = new myClass();

objectForIntegers — это название объекта, оно может быть любым. То, что находится после знака «равно», — непосредственно команда «создать новый экземпляр класса».

Но полная запись очень громоздкая. Поэтому современные компиляторы Java способны на выведение типа — автоматическую его подстановку в записи после первого упоминания. То есть конструкцию myClass понадобится написать только один раз.

Запись, в которой программист пользуется возможностью выведения типа, будет выглядеть так:

myClass objectForIntegers = new myClass<>();

Повторное упоминание типа опускается. Запись становится короче. Кажется, что это мелочь, но таких конструкций в коде могут быть десятки и писать полную запись всегда было бы не очень удобно.

Стирание типов. Важная деталь, которая касается работы дженериков, — они существуют только на этапе компиляции. В этом их суть: «не пропускать» данные ненужного типа в объект, а такие вещи определяет компилятор.

После компиляции код на Java превращается в байт-код. И на этом уровне никаких дженериков нет. myClass и myClass в байт-коде будут идентичны, просто с разными данными внутри.

Это называется стиранием типов. Суть в том, что внутри дженерик-класса нет информации о его параметре и после компиляции эти сведения просто исчезают. Так сделали, потому что дженерики появились в Java не сразу. Если бы информацию о параметре добавили в байт-код, это сломало бы совместимость с более старыми версиями.

О стирании типов важно помнить. Для запущенной программы в байт-коде дженериков не существует, и это может вызвать ошибки. Например, при сравнении myClass и myClass программа скажет, что они одинаковые. А иногда в объект в запущенном коде и вовсе получается передать данные другого типа.

«Дикие карты». Еще одна интересная и полезная особенность дженериков — так называемые wildcards, или «дикие карты». Это термин из спорта, означающий особое приглашение спортсмена на соревнование в обход правил. А в карточных играх так называют карты, которые можно играть вместо других, например джокера.

В основе wildcards в Java лежит такая же идея: изменить предустановленное поведение и сделать что-то в обход установленных рамок. Когда объявляется «дикая карта», в треугольных скобках вместо названия типа ставится вопросительный знак. Это означает, что сюда можно подставить любой тип.

Подставить wildcard можно не везде. Например, при создании класса это сделать не получится, а при объявлении объекта этого класса — получится. Чаще всего «дикую карту» используют при работе с переменными и с коллекциями.

Ограниченные «дикие карты». Кроме стандартной wildcard, существует еще несколько типов — ограниченные «дикие карты». С их помощью можно передать в объект данные не только конкретного типа, но и унаследованных от него — «потомков». Или же «предков» — типов, от которых был унаследован упомянутый.

Ограниченный wildcard описывается как вопросительный знак, за которым следует правило.

Есть два вида ограничений:

  • upper bounding — ограничение сверху. За вопросительным знаком следует слово extends и название типа. В такой дженерик можно передавать названный тип и его потомков;
  • lower bounding — ограничение снизу. Ситуация наоборот: за вопросительным знаком слово super и тип, а подставлять можно элементы этого типа и его предков.

Скорее всего, впервые столкнуться с дженериками придется еще в начале изучения Java, просто новичку не сразу понятно, что это такое. Со временем появляется понимание, как работает эта конструкция, и становится легче решать более сложные задачи.

Java-разработчик

Java уже 20 лет в мировом топе языков программирования. На нем создают сложные финансовые сервисы, стриминги и маркетплейсы. Освойте технологии, которые нужны для backend-разработки, за 14 месяцев.

картинка (67)

Статьи по теме:

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *