Особенности настраиваемых типов
Поможем в ✍️ написании учебной работы
Поможем с курсовой, контрольной, дипломной, рефератом, отчетом по практике, научно-исследовательской и любой другой работой

· Использовать примитивные типы в качестве параметров типов нельзя.

При объявлении экземпляра настраиваемого типа аргумент типа, передаваемый типу, объявленному как параметр, должен быть каким-либо классом. Вы не можете использовать для этой цели базовый тип, такой как int или char. Например, в класс Gen можно передать любой класс как тип в параметре Т, но нельзя заменить Т ни одним базовым типом. Следовательно, приведенная далее строка кода недопустима:

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

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

· Если одинаковые настраиваемые типы имеют различные аргументы, то это различные типы.

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

iOb - strOb; // Неправильно!

Несмотря на то, что у переменных iOb и strOb тип Gen<T>, они ссылаются на объекты разных типов потому, что их аргументы типа отличаются. Это подход используется для усиления типовой защиты и предупреждения ошибок.

• Обеспечивают более жесткий контроль типов на стадии компиляции.

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

Общий синтаксис

Объявление настраиваемого типа

class имяКласса<список-параметров-типа> {...}

 class Generic2<T, E> {...}

Создание ссылки и объекта настраиваемого типа

имяКласса<список-параметров-типа> имяПеременной = new имяКласса<список-параметров-типа>(список-аргументов);

Generic2<Integer, String> gObj = new Generic2<Integer, String>(10, “ok”);

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

В предыдущих примерах параметры типа могли заменяться классами любого типа. В большинстве случаев это замечательно, но иногда полезно ограничить количество типов, которые можно передавать параметру типа. Например, Вы хотите создать настраиваемый класс, который содержит метод, возвращающий среднее арифметическое элементов числового массива. Более того, хотите использовать этот класс для получения среднего арифметического элементов массивов разных числовых типов, включая целые, числа с плавающей запятой и двойной точности. Следовательно, следует задать тип элементов массива в общем виде с помощью параметра типа. Вам нужно также каким-либо способом обеспечить действительную передачу только числовых типов. Для обработки подобных ситуаций язык Java предлагает ограниченные типы (bounded types). При объявлении параметра типа Вы можете задать верхнюю границу, определяющую суперкласс, от которого должны быть унаследованы все аргументы типа. Такое ограничение устанавливается с помощью ключевого слова extends при описании параметра типа, как показано в следующей строке:

 <Т extends superclass>

Приведенное объявление указывает на то, что параметр T можно заменить только типом superclass или его подклассами (производными от него классами). Таким образом, superclass задает верхнюю границу включительно. Вы можете использовать суперкласс Number как верхнюю границу для настройки класса Stats.

class Stats < T extends Number > {

T[] nums;

Stats(T[] o) {nums = o;}

double average() {

double sum = 0.0;

for(int i = 0; i < nums.length; i++)

sum += nums[i].doubleValue();

return sum / nums.length;

}

}

// Демонстрирует применение класса Stats.

class BoundsDemo {

public static void main(String args[]) {

Integer inums[] = { 1, 2, 3, 4, 5 };

Stats<Integer> iob = new Stats<Integer>(inums);

double v = iob.average();

System.out.println("iob average is " + v);

 

Double dnums[] = { 1.1, 2.2, 3.3, 4.4, 5.5 };

Stats<Double> dob = new Stats<Double>(dnums);

double w = dob.average();

System.out.println("dob average is " + w);

// Эти строки не будут компилироваться,так как String

// не является подклассом суперкласса Number.

// String strs[] = { "1", "2", "3", "4", "5" };

// Stats<String> strob = new Stats<String>(strs);

// double x = strob.average() ;

// System.out.printlnf"strob average is " + v);

}

}

Далее приведен вывод результатов работы программы

Average is 3.0

Average is 3.3

Поскольку теперь тип т ограничен суперклассом Number, компилятор языка Java знает, что все объекты типа т могут вызывать метод doubleValue(), определенный в суперклассе Number. Это само по себе значительное преимущество. Но кроме этого, ограничение типа т препятствует созданию нечисловых объектов типа Stats. Если удалить символы комментария из заключительных строк листинга, а затем выполнить компиляцию, Вы получите ошибки на этапе компиляции, так как тип String не является подклассом суперкласса Number.

Как имя типа может быть указан интерфейс!!!

Как имя типа может быть указан ранее введенный параметр!!!

Метасимвольный аргумент

Как ни полезна безопасность типов, иногда она может мешать формированию вполне приемлемых конструкций. Предположим, что в имеющийся класс Stats, описанный в предыдущем разделе, Вы хотите добавить метод sameAvg() который определяет, содержатся ли в двух объектах Stats массивы с одинаковым значением среднего арифметического, независимо от типа числовых данных массивов. Например, если один объект содержит значения 1.0, 2.0 и 3.0 типа double, а второй целые числа 1, 2 и 3, средние арифметические массивов будут одинаковы. Один из способов реализации метода sameAvg( ) — передача в класс Stats аргумента, последующее сравнение среднего арифметического этого аргумента со средним арифметическим объекта, вызвавшего метод, и возврат значения true, если средние арифметические одинаковы.

Поскольку Stats — настраиваемый класс, его метод sameAvg() может обрабатывать любой объект типа Stats и кажется, что создать этот метод просто. К сожалению, возникнут проблемы, как только Вы попытаетесь объявить параметр типа для класса Stats. Класс Stats — это параметризованный тип и неясно, какой же тип объявлять для параметра типа класса Stats в списке параметров метода.

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

// Этот пример не будет работать!

// Определяет, равны ли средние арифметические.

boolean sameAvg(Stats<T> ob) {

if ((average) == ob.average())

return true;

return false;

}

К сожалению, приведенный пример будет обрабатывать только те объекты класса Stats, у которых тип такой же, как у объекта, вызвавшего метод. Например, если метод вызывает объект типа Stats<Integer>, параметр ob должен тоже быть типа Stats<Integer>. Такой метод нельзя использовать для сравнения среднего арифметического объекта Stats<Double> со средним арифметическим объекта типа Stats<Short>. Следовательно, предложенный подход не будет работать, за исключением нескольких ситуаций, и не даст общего (т. е. универсального) решения.

Для создания универсального метода sameAvg() Вы должны использовать другую функциональную возможность средств настройки типов — метасимвольный аргумент, или символьную маску (wildcard argument). Метасимвольный аргумент задается знаком ? и представляет неизвестный тип. Используя такую маску, можно описать метод sameAvg( ) так, как показано в следующем фрагменте кода:

// Определяет, равны ли средние арифметические.

// Обратите внимание на применение метасимвола.

boolean sameAvg(Stats<?> ob) {

if ((average) == ob.average())

return true;

return false;

}

Важно понять, что метасимвол не влияет на тип создаваемого объекта класса Stats. Тип определяется ключевым словом extends в объявлении класса Stats. Метасимвол, или маска, обеспечивает совместимость любых допустимых объектов типа Stats.

Метасимвол с ограничениями

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

Ограниченный метасимвол, или маска, задает как верхнюю, так и нижнюю границы аргумента типа. Это позволяет ограничить набор типов объектов, которые будут обрабатываться методом. Чаще всего ограниченный метасимвольный аргумент используется для указания верхней границы с помощью ключевого слова extends практически так же, как при создании ограниченного типа.

Ограничение сверху

<? extends super >

Оно констатирует, что маска ? соответствует типу super и любому производному от него классу. Тип super допускается

Ограничение снизу

<? super sub >

В этом случае, допустимыми аргументами считаются только суперклассы класса, заданного как sub. Тип sub не допускается

Настраиваемые методы

Как было показано ранее, методы в настраиваемых классах могут использовать параметр типа и таким образом автоматически становятся настраиваемыми по отношению к этому параметру типа. Однако можно и объявить настраиваемый метод (generic method) с одним или несколькими собственными параметрами типа. Более того, есть возможность создать настраиваемый метод внутри ненастраиваемого класса.

В следующем примере объявляется ненастраиваемый класс, названный GenMethDemo, и внутри класса статический настраиваемый метод с именем isIn (). Метод isIn ( ) определяет, является ли объект элементом массива. Он может использоваться с объектом любого типа и любым массивом, при условии, что массив содержит объекты, тип которых сопоставим с типом проверяемого объекта.

class GenMethDemo {

// Определяет, является ли объект элементом массива.

static <T, V extends T> boolean isIn(T x, V[] y) {

for(int i=0; i < y.length; i++)

if(x.equals(y[i])) return true;

return false;

}

 

public static void main(String args[]) {

 // Использует метод isin() для объектов типа Integer.

Integer nums[] = { 1, 2, 3, 4, 5 };

if(isIn(2, nums))

System.out.println("2 is in nums");

if(!isIn(7, nums))

System.out.println("7 is not in nums");

System.out.println();

 // Использует метод isIn() для объектов типа String.

 String strs[] = { "one", "two", "three",

                 "four", "five" };

if(isIn("two", strs))

System.out.println("two is in strs");

if(!isIn("seven", strs))

System.out.println("seven is not in strs");

// Opps! He откомпилирует, поскольку типы не совместимы.

// if(isln("two", nums})

// System.out.println("two is in strs”);

}

}

Далее приведен вывод результатов работы программы:

2 is in nums

7 is not in nums

two is in strs

seven is not in strs

Рассмотрим подробно метод isIn(). Во-первых, обратите внимание на его объявление, приведенное в следующей строке:

static <T, V extends T> boolean isIn(T х, V[] у) {

Параметры типа указаны перед типом значения, возвращаемого методом. Во-вторых, обратите внимание на то, что параметр типа v ограничен сверху параметром типа T. Следовательно, тип параметра v должен быть таким же, как у параметра T или классом, производным от T. Описанная взаимосвязь параметров обеспечивает вызов метода isIn( ) с аргументами, сопоставимыми друг с другом. Отметьте также то, что метод isIn( ) статический, т.е. может вызываться независимо от какого-либо объекта. Однако настраиваемые методы могут быть как статическими, так и нестатическими, на этот счет нет никаких ограничений.

Теперь рассмотрим как метод isIn () вызывается в методе main ( ) с использованием обычного синтаксиса вызова без необходимости указания аргументов типа. Это возможно благодаря тому, что типы аргументов распознаются автоматически и типы Т и V настраиваются соответственно. Например, в первом вызове:

if(isIn(2, nums))

тип первого аргумента — Integer (в результате автоупаковки), что вызывает замену типа T классом Integer, Базовый тип второго аргумента тоже Integer, что в свою очередь ведет к замене параметра V классом Integer.

Во втором вызове используется тип String, и параметры T и V заменяются классом String.

Теперь рассмотрим строки комментария приведенные далее:

// if(isIn("two", nums))

//    System.out.println("two is in strs");

Если Вы удалите знаки комментария и попробуете откомпилировать программу, то получите сообщение об ошибке. Причина заключается в том, что параметр типа v ограничен типом T с помощью ключевого слова extends при объявлении параметра V. Это означает, что тип параметра V должен быть таким же, как тип параметра T или классом, производным от типа T. В нашем случае у первого аргумента тип — String, заменяющий T классом String, а у второго аргумента тип Integer который не является подклассом типа String. Подобная ситуация приводит к появлению ошибки несовместимости типов на этапе компиляции. Описанная способность обеспечения типовой безопасности — одно из важнейших преимуществ настраиваемых методов.

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

<type-param-list> ret-type meth-name(param-list) { //...

type-param-list всегда представляет собой разделенный запятыми список параметров типа. Обратите внимание на то, что у настраиваемых методов этот список предшествует типу значения, возвращаемого методом.

Ряд особенностей

Конструкторы так же могут быть настраиваемыми (даже если сам класс – нет)

class GenCons {

private double val;

<T extends Number> GenCons(T arg) {

val = arg.doubleValue();

}

 

void showval() {

System.out.println("val: " + val);

}

}

 

class GenConsDemo {

public static void main(String args[]) {

GenCons test = new GenCons(100);

GenCons test2 = new GenCons(123.5F);

 

test . showval ();

test 2. showval ();

}

}

Далее приведен вывод программы:

val : 100.0

val : 123.5

Поскольку конструктор GenCons ( ) задает параметр настраиваемого типа, который должен быть производным классом от класса Number , его можно вызвать c любым числовым типом, включая Integer , Float или Double . Следовательно, хотя класс GenCons не является настраиваемым типом, его конструктор настраиваемый.

Интерфейсы так же могут быть настраиваемыми

Кроме настраиваемых классов и методов Вы можете создавать настраиваемые интерфейсы (generic interface). Они задаются так же, как настраиваемые классы. Далее приведен пример настраиваемого интерфейса MinMax, объявляющий методы min() и mах(), которые должны возвращать минимальное и максимальное значения некоторого множества объектов.

// Интерфейс Min/Max.

interface MinMax<T extends Comparable<T>> {

T min();

T max();

}

// Теперь реализуем MinMax

class MyClass<T extends Comparable<T>> implements MinMax<T> {

T[] vals;

 

MyClass(T[] o) { vals = o; }

// Возвращает минимальное значение из vals.

public T min() {

T v = vals[0];

for(int i=1; i < vals.length; i++)

if(vals[i].compareTo(v) < 0) v = vals[i];

return v;

}

// Возвращает максимальное значение из vals. public T max() {

T v = vals[0];

for(int i=1; i < vals.length; i++)

if(vals[i].compareTo(v) > 0) v = vals[i];

return v;

}

}

 class GenIFDemo {

public static void main(String args[]) {

Integer inums[] = {3, 6, 2, 8, 6 };

Character chs[] = {'b', 'r', 'p', 'w' };

MyClass<Integer> iob = new MyClass<Integer>(inums);

MyClass<Character> cob = new MyClass<Character>(chs);

System.out.println("Max value in inums: " + iob.max());

System.out.println("Min value in inums: " + iob.min());

System.out.println("Max value in chs: " + cob.max());

System.out.println("Min value in chs: " + cob.min());

}

}

Далее приведен вывод результатов работы программы:

Max value in inums: 8

Min value in inums: 2

Max value in chs: w

Min value in chs: b

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

interface MinMax<T extends Comparable<T>> {

Вообще настраиваемый интерфейс объявляется так же, как настраиваемый класс. В нашем случае параметр типа — T, и он должен расширять тип Comparable. Обратите внимание на то, что тип Comparable — тоже настраиваемый тип. Он принимает параметр типа, который задает тип сравниваемых объектов.

Далее класс MyClass реализует интерфейс MinMax. Рассмотрим объявление класса MyClass, приведенное в следующей строке:

class MyClass<T extends Comparable<T>> implements MinMax<T> {

Уделите особое внимание способу, которым параметр типа T объявляется в классе MyClass и затем передается в интерфейс MinMax. Поскольку интерфейсу MinMax требуется тип, расширяющий тип Comparable, в классе, реализующем интерфейс (в нашем случае MyClass), должна быть задана та же самая граница. Более того, как только эта граница установлена, нет необходимости задавать ее снова в той части объявления класса, которая начинается с ключевого слова implements, На самом деле подобное действие было бы ошибкой. Например, приведенная далее часть кода некорректна и не будет компилироваться.

// Это неправильно!

class MyClass<T extends Comparable<T>>

implements MinMax<T extends Comparable<T>> {

Уже заданный параметр типа просто передается в интерфейс без дальнейшей модификации.

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

class MyClass implements MinMax<T> { //Ошибка!

Поскольку в классе MyClass не объявлен параметр типа, не существует способа передачи его в интерфейс MinMax. В данном случае идентификатор T просто неизвестен, и компилятор сообщит об этой ошибке. Конечно, если класс реализует конкретную версию настраиваемого интерфейса, такую как приведенная в следующей строке:

class MyClass implements MinMax<Integer> { // OK

реализующему классу нет необходимости быть настраиваемым.

Применение настраиваемого интерфейса обладает двумя преимуществами. Во-первых, интерфейс можно реализовать для данных разных типов. Во-вторых, у вас появляется возможность наложить ограничения (т. е. установить границы) на типы данных, для которых может быть реализован интерфейс. В случае интерфейса MinMax, например, только типы, реализующие интерфейс Comparable, могут передаваться для замены параметра T.

Далее приведена обобщенная синтаксическая конструкция для описания настраиваемого интерфейса:

interface interface-name<type-param-list> {//...

В данной записи type-param-list — это разделенный запятыми список параметров типа. Когда настраиваемый интерфейс реализуется, Вы должны заменить его списком аргументов типа, как показано в следующей строке:

class class-name<type-param-list>

implements interface-name<type-param-list> {//...

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

Невозможно создать экземпляр класса, задавая его тип с помощью параметра типа. Рассмотрим пример

class Gen<T> {

T ob;

Gen() {

ob = new T(); // Illegal!!!

}

}

В примере сделана недопустимая попытка создания экземпляра типа T. Причину легко понять: поскольку параметра типа T во время выполнения не существует, как компилятор узнает объект какого типа нужно создать? Напоминаю о том, что в процессе компиляции происходит стирание всех параметров типа.

Статические члены класса не могут использовать его параметры типа

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

class Wrong<T> {

// Wrong, no static variables of type T.

static T ob;

// Wrong, no static method can use T.

static T getob() {

return ob;

}

 

// Wrong, no static method can access Object

// of type T.

static void showob() {

System.out.println(ob);

 }

}

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

Настраиваемый класс не может расширять класс Throwable

Это означает, что у Вас нет возможности создавать настраиваемые классы исключений.

От настраиваемых типов можно наследовать, есть ряд особенностей

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

class Generic<T> {

T[] vals; //OK

Generic(T[] nums) {

//vals = new T[10]; //Не есть правильно!

vals = nums; //OK

}

}

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

Т valsU; // OK

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

// vals = new T[10]; // не может создать массив из объектов типа Т

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

Тем не менее, можно передать ссылку на совместимый по типу массив в конструктор Gen. о при создании объекта и присвоить эту ссылку переменной vai, как показано в следующей строке:

vals = nums // можно присвоить ссылку существующему массиву

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

Нельзя создать массив из ссылок на объекты конкретной версии настраиваемого типа

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

//Generic<Integer>[] gens = new Generic<Integer>[10];//Not OK

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

Generic<?>[] gens = new Generic<?>[10];

И как же это работает?

• Механизм стирания:

– В реальном байт-коде никаких настраиваемых типов в целом-то и нет…

– Вся информация о настраиваемых типах удаляется на стадии компиляции

– Именно компилятор осуществляет контроль безопасности приведения типов

– А внутри после компиляции все те же «обобщенные» классы, явные приведения типов и прочее, и прочее…

• Возможны ошибки неоднозначности…

Введение настраиваемых типов породило новую категорию ошибок, которых Вы должны беречься, ошибки неоднозначности (ambiguity errors). Такая ошибка возникает, когда механизм стирания вызывает два на вид отличающихся объявления настраиваемых типов для удаления информации об одном типе, порождая тем самым конфликт. Далее приведен пример, содержащий переопределение метода.

class MyGenClass < T , V > {

T ob 1;

V ob 2;

// ...

// These two overloaded methods are ambiguous.

// and will not compile.

void set(T o) {

ob1 = o;

}

void set(V o) {

ob2 = o;

}

}

Обратите внимание на то, что класс MyGenClass содержит объявления двух настраиваемых типов: T и V. В классе MyGenClass делается попытка переопределения метода set(), основанного на параметрах типа T и V. Это выглядит разумным, так как кажется, что T и V — разные типы. Но при этом возникает два вида неоднозначности.

Во-первых (судя по описанию класса MyGenClass), не требуется, чтобы типы T и V всегда были разными. Например, приведенное далее создание объекта класса MyGenClass — совершенно правильно (в принципе):

MyGenClass<String, String> obj = new MyGenClass<String, String>()

В этом случае и T, и V замещаются типом String. Это делает обе версии метода set() одинаковыми, что, конечно же, является ошибкой.

Во-вторых, и это более существенно, стирание информации о типе превратит обе версии метода set () в следующую:

void set(Object о)

Таким образом, переопределение метода set (), которое делается в классе MyGenClass, — в основе своей неоднозначно.

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

MyGenClass<T, V extends String> { //почти хорошо!

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

MyGenClass<Integer, String> x = new MyGenClass<Integer, String>();

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

MyGenClass< String, String> х = new MyGenClass< String, String>();

В данном случае, поскольку и у T, и у V — тип String, какую версию метода set() вызывать?

Откровенно говоря, в приаеденном примере гораздо лучше использовать два метода с разными именами, чем пытаться переопределять метод set (). Часто разрешение неоднозначности приводит к переработке кода, поскольку неоднозначность или неопределенность зачастую свидетельствует о концептуальной ошибке в вашем проекте.

Дата: 2019-02-19, просмотров: 241.