что было до дженериков java
Основы дженериков Java
Краткое введение в основы Java-дженериков.
1. введение
Дженерики Java были введены в JDK 5.0 с целью уменьшения ошибок и добавления дополнительного уровня абстракции над типами.
Эта статья представляет собой краткое введение в дженерики в Java, цель, стоящую за ними, и как их можно использовать для улучшения качества нашего кода.
Дальнейшее чтение:
Ссылки на методы в Java
Получение полей из класса Java с помощью отражения
2. Потребность в дженериках
Давайте представим себе сценарий, в котором мы хотим создать список на Java для хранения Integer ; у нас может возникнуть соблазн написать:
Удивительно, но компилятор будет жаловаться на последнюю строку. Он не знает, какой тип данных возвращается. Компилятор потребует явного приведения:
Было бы намного проще, если бы программисты могли выразить свое намерение использовать определенные типы, а компилятор мог бы обеспечить правильность такого типа. Это основная идея, лежащая в основе дженериков.
Давайте изменим первую строку предыдущего фрагмента кода на:
Добавляя оператор diamond<>, содержащий тип, мы сужаем специализацию этого списка только до Integer type, т. е. указываем тип, который будет храниться внутри списка. Компилятор может принудительно применить тип во время компиляции.
В небольших программах это может показаться тривиальным дополнением, однако в больших программах это может значительно повысить надежность и облегчить чтение программы.
3. Общие методы
Универсальные методы-это те методы, которые написаны с одним объявлением метода и могут вызываться с аргументами разных типов. Компилятор обеспечит правильность любого используемого типа. Вот некоторые свойства универсальных методов:
Пример определения универсального метода преобразования массива в список:
Мы передаем функцию, которая преобразует массив с элементами типа T в список с элементами типа G. Примером может быть преобразование Целого числа в его строковое представление:
Стоит отметить, что Oracle рекомендует использовать заглавную букву для представления общего типа и выбирать более описательную букву для представления формальных типов, например, в коллекциях Java T используется для типа, K для ключа, V для значения.
3.1. Ограниченные дженерики
Как упоминалось ранее, параметры типа могут быть ограничены. Ограниченный означает ” ограниченный “, мы можем ограничить типы, которые могут быть приняты методом.
Например, мы можем указать, что метод принимает тип и все его подклассы (верхняя граница) или тип все его суперклассы (нижняя граница).
Чтобы объявить верхний ограниченный тип, мы используем ключевое слово extends после типа, за которым следует верхняя граница, которую мы хотим использовать. Например:
Ключевое слово extends используется здесь для обозначения того, что тип T расширяет верхнюю границу в случае класса или реализует верхнюю границу в случае интерфейса.
3.2. Множественные границы
Тип также может иметь несколько верхних границ следующим образом:
4. Использование Подстановочных Знаков С Дженериками
Подстановочные знаки представлены знаком вопроса в Java ” ? ” и они используются для обозначения неизвестного типа. Подстановочные знаки особенно полезны при использовании дженериков и могут использоваться в качестве типа параметра, но сначала следует рассмотреть важное примечание.
Известно, что Объект является ли супертип всех классов Java, однако, коллекцией Объект не является супертипом какой-либо коллекции.
Например, List не является супертипом List и назначение переменной типа List переменной типа List приведет к ошибке компилятора. Это делается для предотвращения возможных конфликтов, которые могут возникнуть, если мы добавим разнородные типы в одну и ту же коллекцию.
То же правило применяется к любой коллекции типа и его подтипов. Рассмотрим этот пример:
Теперь этот метод будет работать с типом Building и всеми его подтипами. Это называется верхним ограниченным подстановочным знаком, где тип Building является верхней границей.
5. Тип Стирания
Дженерики были добавлены в Java для обеспечения безопасности типов и для того, чтобы дженерики не вызывали накладных расходов во время выполнения, компилятор применяет процесс, называемый стирание типов для дженериков во время компиляции.
Это пример стирания типов:
При стирании типа неограниченный тип T заменяется на Object следующим образом:
Если тип ограничен, то во время компиляции он будет заменен на связанный:
изменится после компиляции:
6. Универсальные и примитивные типы данных
Ограничение универсалий в Java заключается в том, что параметр типа не может быть примитивным типом.
Например, следующее не компилируется:
В качестве примера давайте рассмотрим метод add списка:
Подпись метода add является:
И будет скомпилирован для:
Однако Java предоставляет упакованные типы для примитивов, а также автобоксы и распаковку, чтобы развернуть их:
Итак, если мы хотим создать список, который может содержать целые числа, мы можем использовать оболочку:
Скомпилированный код будет эквивалентен:
7. Заключение
Дженерики Java являются мощным дополнением к языку Java, поскольку они облегчают работу программиста и менее подвержены ошибкам. Универсальные алгоритмы обеспечивают корректность типов во время компиляции и, самое главное, позволяют реализовывать универсальные алгоритмы, не вызывая дополнительных накладных расходов для наших приложений.
Пришел, увидел, обобщил: погружаемся в Java Generics
Java Generics — это одно из самых значительных изменений за всю историю языка Java. «Дженерики», доступные с Java 5, сделали использование Java Collection Framework проще, удобнее и безопаснее. Ошибки, связанные с некорректным использованием типов, теперь обнаруживаются на этапе компиляции. Да и сам язык Java стал еще безопаснее. Несмотря на кажущуюся простоту обобщенных типов, многие разработчики сталкиваются с трудностями при их использовании. В этом посте я расскажу об особенностях работы с Java Generics, чтобы этих трудностей у вас было поменьше. Пригодится, если вы не гуру в дженериках, и поможет избежать много трудностей при погружении в тему.
Работа с коллекциями
Предположим, банку нужно подсчитать сумму сбережений на счетах клиентов. До появления «дженериков» метод вычисления суммы выглядел так:
С появлением Generics необходимость в проверке и приведении типа отпала:
Во второй строчке проверки необходимость тоже отпадала. Если потребуется, приведение типов ( casting ) будет сделано на этапе компиляции.
Принцип подстановки
Тип | Подтип |
Number | Integer |
List | ArrayList |
Collection | List |
Iterable | Collection |
Примеры отношения тип/подтип
Вот пример использования принципа подстановки в Java:
Ковариантность, контравариантность и инвариантность
Но если мы попытаемся изменить содержимое массива через переменную arr и запишем туда число 42, то получим ArrayStoreException на этапе выполнения программы, поскольку 42 является не строкой, а числом. В этом недостаток ковариантности массивов Java: мы не можем выполнить проверки на этапе компиляции, и что-то может сломаться уже в рантайме.
«Дженерики» инвариантны. Приведем пример:
Wildcards
Всегда ли Generics инварианты? Нет. Приведу примеры:
Это ковариантность. List — подтип List
extends B — символ подстановки с указанием верхней границы super B — символ подстановки с указанием нижней границы где B — представляет собой границу 2. Почему нельзя получить элемент из списка ниже? The Get and Put Principle или PECS (Producer Extends Consumer Super)Особенность wildcard с верхней и нижней границей дает дополнительные фичи, связанные с безопасным использованием типов. Из одного типа переменных можно только читать, в другой — только вписывать (исключением является возможность записать null для extends и прочитать Object для super ). Чтобы было легче запомнить, когда какой wildcard использовать, существует принцип PECS — Producer Extends Consumer Super. и Raw типыЕсли мы опустим указание типа, например, как здесь: Если мы попытаемся вызвать параметризованный метода у Raw типа, то компилятор выдаст нам предупреждение «Unchecked call». Если мы попытаемся выполнить присваивание ссылки на параметризованный тип Raw типу, то компилятор выдаст предупреждение «Unchecked assignment». Игнорирование этих предупреждений, как мы увидим позже, может привести к ошибкам во время выполнения нашего приложения. Wildcard CaptureПопробуем теперь реализовать метод, выполняющий перестановку элементов списка в обратном порядке. Более подробно о Wildcard Capture можно прочитать здесь и здесь. ВыводПеременные типаВот еще пример из класса Enum: Multiple bounds (множественные ограничения)ВыводПеременная типа может быть ограничена только сверху одним или несколькими типами. В случае множественного ограничения левая граница (первое ограничение) используется в процессе затирания (Type Erasure). Type ErasureНа скриншоте ниже два примера программы: Разница между ними в том, что слева происходит compile-time error, а справа все компилируется без ошибок. Почему? Reifiable типыПочему информация об одних типах доступна, а о других нет? Дело в том, что из-за процесса затирания типов компилятором информация о некоторых типах может быть потеряна. Если она потерялась, то такой тип будет уже не reifiable. То есть она во время выполнения недоступна. Если доступна – соответственно, reifiable. Решение не делать все обобщенные типы доступными во время выполнения — это одно из наиболее важных и противоречивых проектных решений в системе типов Java. Так сделали, в первую очередь, для совместимости с существующим кодом. За миграционную совместимость пришлось платить — полная доступность системы обобщенных типов во время выполнения невозможна. И еще одна задачка. Почему в примере ниже нельзя создать параметризованный Exception? Каждое catch выражение в try-catch проверяет тип полученного исключения во время выполнения программы (что равносильно instanceof), соответственно, тип должен быть Reifiable. Поэтому Throwable и его подтипы не могут быть параметризованы. Unchecked WarningsКомпиляция нашего приложения может выдать так называемый Unchecked Warning — предупреждение о том, что компилятор не смог корректно определить уровень безопасности использования наших типов. Это не ошибка, а предупреждение, так что его можно пропустить. Но желательно все-так исправить, чтобы избежать проблем в будущем. Heap PollutionКак мы упомянули ранее, присваивание ссылки на Raw тип переменной параметризованного типа, приводит к предупреждению «Unchecked assignment». Если мы проигнорируем его, то возможна ситуация под названием » Heap Pollution » (загрязнение кучи). Вот пример: В строке (1) компилятор предупреждает об «Unchecked assignment». Рассмотрим еще один пример: Java разрешает выполнить присваивание в строке (1). Это необходимо для обеспечения обратной совместимости. Но если мы попытаемся выполнить метод add в строке (2), то получим предупреждение Unchecked call — компилятор предупреждает нас о возможной ошибке. В самом деле, мы же пытаемся в список строк добавить целое число. ReflectionХотя при компиляции параметризованные типы подвергаются процедуре стирания (type erasure), кое-какую информацию мы можем получить с помощью Reflection. С появлением Generics класс java.lang.Class стал параметризованным. Рассмотрим вот этот код: ВыводЕсли информация о типе доступна во время выполнения программы, то такой тип называется Reifiable. К Reifiable типам относятся: примитивные типы, непараметризованные типы, параметризованные типы с неограниченным символом подстановки, Raw типы и массивы, элементы которых являются reifiable. Игнорирование Unchecked Warnings может привести к «загрязнению кучи» и ошибкам во время выполнения программы. Reflection не позволяет получить информацию о типе объекта, если он не Reifiable. Но Reflection позволяет получить информацию о типе возвращаемого методом значения, о типе аргументов метода и о типе полей класса. Type InferenceТермин можно перевести как «Вывод типа». Это возможность компилятора определять (выводить) тип из контекста. Вот пример кода: С появлением даймонд-оператора в Java 7 мы можем не указывать тип у ArrayList : Предположим у нас есть вот такой класс, который описывает связный список: Результат обобщенного метода List.nil() может быть выведен из правой части: Механизм выбора типа компилятором показывает, что аргумент типа для вызова List.nil() действительно String — это работает в JDK 7, все хорошо. Выглядит разумно, что компилятор также должен иметь возможность вывести тип, когда результат такого вызова обобщенного метода передается другому методу в качестве аргумента, например: В JDK 7 мы получили бы compile-time error. А в JDK 8 скомпилируется. Это и есть первая часть JEP-101, его первая цель — вывод типа в позиции аргумента. Единственная возможность осуществить это в версиях до JDK 8 — использовать явный аргумент типа при вызове обобщенного метода: Вторая часть JEP-101 говорит о том, что неплохо бы выводить тип в цепочке вызовов обобщенных методов, например: Но данная задача не решена до сих пор, и вряд ли в ближайшее время появится такая функция. Возможно, в будущих версиях JDK необходимость в этом исчезнет, но пока нужно указывать аргументы вручную: После выхода JEP 101 на StackOverflow появилось множество вопросов по теме. Программисты спрашивают, почему код, который выполнялся на 7-й версии, на 8-й выполняется иначе – или вообще не компилируется? Вот пример такого кода: Посмотрим на байт-код после компиляции на JDK1.8: А теперь байт-код после компиляции на JDK1.7 – то есть на Java 7: Чтобы избежать таких проблем, Oracle выпустил руководство по переходу с JDK1.7 на JDK 1.8 в котором описаны проблемы, которые могут возникнуть при переходе на новую версию Java, и то, как эти проблемы можно решить. Например если вы хотите, чтобы в коде выше после компиляции на Java 8 все работало так же, как и на Java 7, сделайте приведение типа вручную: ЗаключениеНа этом мой рассказ о Java Generics подходит к концу. Вот другие источники, которые помогут вам в освоении темы: Вопросы собеседования с Java Generics (Ответы)Популярные вопросы, связанные с дженерикой Java интервью и, конечно, ответы. 1. ВведениеВ этой статье мы пройдемся по некоторым примерам Java дженериков интервью вопросы и ответы. Общие сведения являются основной концепцией Java, впервые представленной в Java 5. Из-за этого, почти все Java-базы кода будут использовать их, почти гарантируя, что разработчик будет работать в них в какой-то момент. Вот почему очень важно, чтобы понять их правильно, и именно поэтому они, скорее всего, будет предложено о во время интервью процесса. 2. ВопросыNo 1. Что такое общий параметр типа?Начнем с простого примера, без дженериков, чтобы продемонстрировать это: В этом случае тип параметра метода потреблять () метод струна. Он не параметризирован и не настраивается. Теперь давайте заменим наш Струнные тип с общим типом, который мы будем называть Т. Он назван так по конвенции: Когда мы реализуем нашего потребителя, мы можем предоставить тип что мы хотим, чтобы потреблять в качестве аргумента. Это общий параметр типа: В этом случае, теперь мы можем потреблять integers. Мы можем поменять эту тип за все, что мы требуем. No 2. Каковы некоторые преимущества использования общих типов?Одним из преимуществ использования дженериков является избегание слепок и обеспечение безопасности типа. Это особенно полезно при работе с коллекциями. Давайте продемонстрируем это: В нашем примере тип элемента в нашем списке неизвестен компилятору. Это означает, что единственное, что может быть гарантировано, это то, что это объект. Поэтому, когда мы извлекаем наш элемент, Объект это то, что мы получаем обратно. Как авторы кода, мы знаем, что это Струна, но мы должны бросить наш объект на один, чтобы исправить проблему явно. Это производит много шума и шаблонов. Далее, если мы начинаем думать о возможности ручной ошибки, проблема литья становится все хуже. Что, если бы у нас случайно Интегер в нашем списке? В этом случае, мы хотели бы получить ClassCastException во время выполнения, как Интегер не могут быть брошены на струна. Теперь давайте попробуем повториться, на этот раз с помощью дженериков: Как мы видим, с помощью дженериков у нас есть проверка типа компиляции, которая предотвращает КлассCastExceptions и устраняет необходимость литья. В3. Что такое стирание типа?Аргументация, лежащая в основе основного выбора реализации, проста – сохранение обратной совместимости с более старыми версиями Java. Когда общий код компилируется в карт-код, это будет, как если бы общий тип никогда не существовало. Это означает, что компиляция будет: Важно понимать стирание типа. В противном случае разработчик может запутаться и думать, что он сможет получить тип во время выполнения: Вышеупомянутый пример является псевдокод эквивалент того, что вещи могут выглядеть без стирания типа, но, к сожалению, это невозможно. Еще раз, общая информация о типе недоступна во время выполнения. No 4. Если общий тип опущен при мгновенной дифференциации объекта, будет ли код по-прежнему компилироваться?Поскольку дженериков не существовало до Java 5, их можно вообще не использовать. Например, дженерики были переоборудованы под большинство стандартных классов Java, таких как коллекции. Если мы посмотрим на наш список из вопроса один, то мы увидим, что у нас уже есть пример опуская общий тип: Несмотря на возможность компиляции, все еще вероятно, что будет предупреждение от компилятора. Это связано с тем, что мы теряем дополнительную проверку времени компиляции, которую мы получаем от использования дженериков. Важно помнить, что в то время как обратная совместимость и стирание типов делают возможным опустить общие типы, это плохая практика. В5 евро. Чем общий метод отличается от общего типа?Общий метод, где параметр типа вводится в метод, в рамках этого метода. Давайте попробуем это на примере: Мы использовали статический метод, но могли бы также использовать не статичных один, если мы хотим. Используя вывод типа (охватываемый в следующем вопросе), мы можем вызвать это как любой обычный метод, без необходимости указывать какие-либо аргументы типа, когда мы делаем это. No 6. Что такое вывод типа?Как мы видим, нет необходимости в гипсе, и нет необходимости проходить в какой-либо общий аргумент типа. Тип аргумента только высовывовыв возвращает тип. No7. Что такое параметр связанного типа?До сих пор все наши вопросы охватывали общие типы аргументов, которые не связаны. Это означает, что наши общие аргументы типа может быть любой тип, который мы хотим. Когда мы используем ограничивающие параметры, мы ограничиваем типы, которые могут быть использованы в качестве общих аргументов типа. Например, предположим, что мы хотим, чтобы наш общий тип всегда был подклассом животных: С помощью расширений , мы заставляем T быть подклассом животных . Мы могли бы иметь клетку кошек: Но мы не могли иметь клетку объектов, так как объект не является подклассом животного: Одним из преимуществ этого является то, что все методы животного доступны для компилятора. Мы знаем, что наш тип расширяет его, чтобы мы могли написать общий алгоритм, который работает на любом животном. Это означает, что мы не должны воспроизводить наш метод для различных подклассов животных: No 8. Можно ли задекларировать параметр множественного связанного типа?Возможно объявление нескольких границ для наших общих типов. В нашем предыдущем примере мы указали одну границу, но мы могли бы также указать больше, если мы хотим: В нашем примере животное является классом и сравнимым является интерфейсом. Теперь наш тип должен уважать обе эти верхние границы. Если бы наш тип был подклассом животных, но не реампилировать сопоставимый, то код не будет компилироваться. Также стоит помнить, что если одной из верхних границ является класс, то это должен быть первый аргумент. No9. Что такое тип wildcard?No10. Что такое верхний связанный wildcard?Давайте попробуем продемонстрировать это с фермы класса, который будет хранить животных, во-первых, без подстановочного знака типа: Если бы у нас было несколько подклассов животных , такие как кошки и собаки , мы можем сделать неверное предположение, что мы можем добавить их все в нашу ферму: Это потому, что компилятор ожидает коллекцию конкретного типа животных , не один он подклассов. Теперь давайте введем верхнюю границу подстановочный знак для нашего метода добавления животных: Теперь, если мы попробуем еще раз, наш код будет компилироваться. Это потому, что мы сейчас говорим компилятор принять коллекцию любого подтипа животных. No11. Что такое неограниченный Wildcard?Неограниченный подстановочный знак – это подстановочный знак без верхней или нижней границы, который может представлять любой тип. Важно также знать, что тип подстановочного знака не является синонимом объекта. Это связано с тем, что подстановочный знак может быть любого типа, в то время как тип объекта является конкретно объектом (и не может быть подклассом объекта). Давайте продемонстрируем это на примере: Опять же, причина, по которой вторая строка не компилируется, заключается в том, что требуется список объектов, а не список строк. Первая строка компилирует, потому что список любого неизвестного типа является приемлемым. No12. Что такое нижняя ограниченная wildcard?Используя супер, мы могли бы назвать addDogs в списке объектов: Это имеет смысл, так как объект является суперклассом животного. Если бы мы не использовали нижнюю ограниченную подстановочный знак, код бы не компилировать, так как список объектов не является списком животных. Если мы подумаем об этом, мы не сможем добавить собаку в список любого подкласса животных, таких как кошки, или даже собаки. Только суперкласс животных. Например, это не будет компиляция: No13. Когда вы решите использовать нижний связанный тип по сравнению с верхним связанным типом?При работе с коллекциями общим правилом выбора между верхними или нижними подстановочные знаки является PECS. PECS означает производитель расширяет, потребительские супер. Это можно легко продемонстрировать с помощью некоторых стандартных интерфейсов и классов Java. Производитель расширяет просто означает, что если вы создаете производителя общего типа, а затем использовать расширяет ключевое слово. Давайте попробуем применить этот принцип к коллекции, чтобы понять, почему это имеет смысл: Потребительские супер означает обратное производитель расширяется. Все это означает, что если мы имеем дело с чем-то, что потребляет элементы, то мы должны использовать супер ключевое слово. Мы можем продемонстрировать это, повторив наш предыдущий пример: Мы только добавляем к нашему списку животных, так что наш список животных является потребителем. Вот почему мы используем супер ключевое слово. Это означает, что мы могли бы пройти в список любого суперкласса животных, но не подкласса. Например, если бы мы попытались пройти в список собак или кошек, то код не будет компилировать. Последнее, что нужно рассмотреть, что делать, если коллекция является одновременно потребителем и производителем. Примером этого может быть коллекция, в которой элементы добавляются и удаляются. В этом случае следует использовать неохведенный подстановочный знак. No14. Существуют ли ситуации, когда общая информация о типе доступна во время выполнения?Существует одна ситуация, когда общий тип доступен во время выполнения. Это когда общий тип является частью подписи класса, как это: Используя отражение, мы получаем параметр этого типа: Этот код несколько хрупкий. Например, это зависит от параметра типа, определяемого на немедленном суперклассе. Но, это показывает, ЧТО JVM имеет информацию такого типа.
|