В предыдущей статье мы с вами поговорили о том, чем отличается язык Java от других языков программирования, а в этой статье речь пойдет о безопасности, которое предлагает данный язык, а также о инкрементной разработке, управлении памятью, обработке ошибок, потоках и масштабируемости. Итак, приступим. Вы, должно быть, много слышали о том, что язык Java — безопасный язык. Но что подразумевается под безопасностью? Безопасность от чего или кого? Средства безопасности, которые привлекают много внимания к Java, — это те свойства, которые делают возможными новые типы динамически портативного программного обеспечения. Язык Java обеспечивает несколько уровней защиты от опасно поврежденного кода, а также других вредных явлений, таких как вирусы и троянские кони. Далее мы посмотрим на то, как архитектура виртуальной машины определяет безопасность кода перед его запуском и как загрузчик классов (механизм загрузки интерпретатора Java в байт-коде) строит стену вокруг ненадежных классов. Эти средства составляют основу для формирования высокоуровневой политики безопасности, с помощью которой можно разрешать или запрещать разного рода действия для всех приложений в рамках виртуальной машины.
Однако в этой статье мы рассмотрим некоторые свойства языка программирования Java. Возможно более важной, чем средства безопасности Java, хотя часто незамеченной, является безопасность, которую предоставляет Java при решении распространенных проблем дизайна и программирования. Задача языка Java — быть как можно более защищенным как от простых ошибок, которые мы делаем сами, так и от тех, которые попадают к нам с унаследованным ПО. Целью Java было оставить язык простым, предоставить инструменты, которые продемонстрируют свою полезность и позволят пользователю создавать при необходимости более сложные объекты на высшем уровне языка.
Упрощение
В Java простота является правилом. Поскольку язык Java начинался с чистого листа, ему удалось избежать свойств, которые оказались запутанными или противоречивыми в других языках. Например, Java не допускает перегрузку операций, определенных программистом (которая в некоторых языках позволяет программистам переопределять значение базовых символов, таких, как + и -). У языка Java нет препроцессора исходного кода, так что в нем нет таких вещей, как макросы, инструкции #define или компиляция условного источника. Эти конструкции существуют в других языках в основном для поддержки зависимостей платформ, так что в этом смысле они не нужны в Java. Условная компиляция также широко используется для отладки, но сложная оптимизация рабочего цикла Java и такие свойства, как утверждения, решают проблему более подробно в других статьях.
Язык Java предоставляет четкую структуру пакетов для организации файлов классов. Система пакетов позволяет компилятору управлять некоторой функциональностью стандартной утилиты make (инструмент для создания выполняемого кода из исходного кода). Компилятор также может работать непосредственно с компилированными файлами Java, потому что сохраняется вся информация типа; нет необходимости во внешних «заголовочных» файлах, как в С/С++. Все это означает, что Java-коду нужно читать меньше контекста. В действительности иногда вы можете обнаружить, что быстрее будет посмотреть на исходный код Java, чем обращаться к документации класса.
Язык Java также по-своему подходит к некоторым структурным свойствам, которые были проблемными в других языках. Например, Java поддерживает только иерархию классов единичного наследования (у каждого класса может быть только один «родительский» класс), но позволяет многочисленное наследование интерфейсов. Интерфейс как абстрактный класс в С++ устанавливает поведение объекта без определения его выполнения. Это очень мощный механизм, позволяющий разработчику определить «контракт» поведения объекта, который может использоваться и упоминаться независимо от частной реализации объекта. Интерфейсы в Java исключают потребность в множественном наследовании классов и связанные с этим проблемы.
Java достаточно простой язык программирования, и в этом в первую очередь заключается его притягательная сила.
Безопасность типов и связывание методов
Один из атрибутов языка — это вид проверки типов, который он использует. Как правило, языки распределяются по категориям как статические и динамические, что относится к количеству информации о переменных, известных во время компиляции, против того, что известно во время работы приложения.
В строго статически типизированных языках, таких как С и С++, типы данных словно выбиты на камне, когда компилируется исходный код. Компилятор Java получает преимущество от этого, поскольку имеет достаточно информации для отлова многих типов ошибок перед выполнением кода. Например, компилятор не позволит вам хранить значение с плавающей точкой в переменной целого типа. Код тогда не нуждается в проверке типов во время выполнения, поэтому он может быть компилирован, чтобы стать компактным и быстрым. Но языки со статическими типами являются негибкими. Они не поддерживают библиотеки так же естественно, как языки с динамической проверкой типов, и делают невозможным для приложения безопасный импорт новых типов данных, когда оно выполняется.
Для сравнения: такие динамические языки, как Smalltalk или Lisp, имеют исполняемую систему, которая управляет типами объектов и производит необходимую проверку типов, пока приложение выполняется. У этих типов языков более сложное поведение и во многих аспектах более мощное. Однако в основном они медленнее, менее безопасны и тяжелы в отладке.
Разница языков похожа на разницу между автомобилями. Языки со статическими типами, такие как С++, аналогичны спортивным машинам: довольно безопасны и быстры, но пригодны лишь для езды по хорошо асфальтированной дороге. Высокодинамические языки, такие как Smalltalk, похожи на внедорожники: они дают больше свободы, но могут быть немного неуклюжими. Может быть весело (и иногда быстрее) с грохотом проехать через лесную глушь, но вы также можете застрять в яме.
Другой атрибут языка — это то, как он связывает вызов методов с их определениями. В таких статических языках, как С или С++, определения методов обычно связываются во время компиляции, если программист не укажет иное. Языки, подобные Smalltalk, с другой стороны, называются языками с отложенным связыванием, поскольку они локализируют определения методов динамически во время прогона. Раннее связывание важно из соображений выполнения; приложение может работать без затрат на поиск методов во время прогона. Но отложенное связывание более гибкое. Оно также необходимо в объектно-ориентированном языке, где новые типы могут подгружаться динамически, и только система выполнения может определять, какой метод запускать.
Язык Java предоставляет некоторые преимущества С++ и Smalltalk; это язык со статическими типами и отложенным связыванием. Каждый объект в Java имеет четко определенный тип, который понятен во время компиляции. Это означает, что компилятор Java может выполнять такой же тип статических проверок типа и использования анализа, как С++. В результате вы не можете связать объект с неправильным типом переменной или несуществующие методы с объектом. Компилятор Java идет еще дальше и не дает вам использовать неинициализированные переменные и создавать недостижимые инструкции.
Однако Java в полной мере является языком с динамическим выполнением. Система выполнения Java следит за всеми объектами и делает возможным определение их типов и связей во время выполнения. Это означает, что вы можете инспектировать объект во время выполнения, чтобы определить, что это за объект. В отличие от языка С или С++ смена одного объекта на другой отмечается системой выполнения и возможно использование новых типов динамически загружаемых объектов со степенью типа безопасности. И поскольку Java — язык с отложенным связыванием, подкласс всегда может переопределить методы в его родительском классе, даже если этот подкласс загружен во время выполнения.
Инкрементная разработка
Язык Java переносит все типы данных и информацию о сигнатуре метода с исходного кода в форму компилированного байт-кода. Это означает, что классы Java могут разрабатываться инкрементно. Ваш собственный исходный код Java может также безопасно компилироваться классами из других источников, которые ваш компилятор никогда не видел. Другими словами, вы можете писать новый код, ссылающийся на файлы бинарных классов без потери типов безопасности, которые вы получаете от исходного кода.
Язык Java не страдает от проблем «нестабильных базовых классов». В таких языках, как С++, применение базовых классов может быть эффективно заморожено, поскольку у него много производных классов; изменение базовых классов может потребовать рекомпиляции всех производных. Это особенно сложная проблема для разработчиков библиотек классов. Пока класс управляет действующей формой его оригинальной структуры, он может эволюционировать без нарушения других классов, производных от него или использующих его.
Управление динамической памятью
Некоторые из самых важных отличий между Java и такими языками более низкого уровня, как С и С++, связаны с тем, как Java управляет памятью. Язык Java устраняет случайные «указатели», которые могут ссылаться на произвольные области памяти, и добавляет сборку мусора для объектов и массивы высокого уровня к языку. Эти свойства устраняют проблемы безопасности, портативности и оптимизации, непреодолимые другим путем.
Сборка мусора спасла бесчисленное количество программистов от одного большого источника ошибок программирования на языках С и С++: эксплицитного распределения и освобождения памяти. В дополнение к управлению объектами памяти системы выполнения, система выполнения Java отслеживает все ссылки на этот объект. Когда объект больше не используется, Java автоматически убирает объект из памяти. Вы можете по большей части просто игнорировать объекты, которые вы больше не используете, с уверенностью, что интерпретатор уберет их в нужное время.
Язык Java использует сложный сборщик мусора, который работает на фоне, то есть большая часть сборки мусора происходит в нерабочее время, между паузами ввода/вывода, щелчками мышью или нажатием клавиш. Более продвинутые системы выполнения, такие как HotSpot, имеют более развитую сборку мусора, которая умеет различать шаблоны использования объектов (например, кратковременные от долговременных) и оптимизировать их сборку. Время выполнения Java теперь может настраиваться автоматически для оптимального распределения памяти для разных типов приложения в зависимости от их поведения. С этим типом профилирования времени выполнения автоматическое управление памятью может осуществляться более быстро, чем наиболее тщательно управляемые программистами ресурсы, — то, во что программисты старой школы до сих пор слабо верят.
Мы сказали, что у Java нет указателей. Собственно говоря, это утверждение верно, но оно также и обманчиво. Ссылки — вот что предлагает Java — безопасный тип указателей. Ссылка — это остротипизированный идентификатор объекта. Все объекты в Java, за исключением простых числовых типов, доступны через ссылки. Вы можете использовать ссылки для всех обычных типов структур данных, которые программист на языке С привык создавать указателями, — таких как связанные ссылки, древовидные схемы и т. д. Единственное отличие в том, что со ссылками это делается безопасным путем.
Другое важное отличие между ссылкой и указателем — это то, что вы не можете играть в игры со ссылками (осуществлять арифметику указателей), чтобы изменить их значения; они могут указывать только на определенные объекты или элементы массива. Ссылка — это элементарная вещь; вы не можете управлять значением ссылки иначе, чем назначив ее объекту. Ссылки передаются значением, и вы не можете сослаться на объект более чем через один косвенный уровень. Защита ссылок — один из самых фундаментальных аспектов безопасности. Это означает, что код Java должен играть по правилам; он не может считываться в местах, где не следует, и обходить правила.
Ссылки Java могут указывать только на типы классов. Нет никаких указателей к методам. Некоторые жалуются на их отсутствие, но вы обнаружите, что многие задачи, которые требуют указателей к методам, могут выполняться более чисто при использовании интерфейсов и классов адаптеров. Мы также должны заметить, что у Java есть сложный интерфейс Reflection API, который в действительности позволяет вам ссылаться и запускать индивидуальные методы. Однако это не является стандартным путем.
В завершение мы должны отметить, что массивы в Java являются подлинными объектами первого класса. Они могут динамически назначаться и присваиваться как другие объекты. Массивы знают свои размеры и типы и, хотя вы не можете напрямую определить или создать подклассы для классов массива, у них есть четко определенные отношения наследования, основанные на отношениях их базовых типов. Наличие истинных массивов в языке уменьшает нужду в арифметике указателей, которая используется в С или С++.
Обработка ошибок
Корневые каталоги Java находятся в сетевых устройствах и встроенных системах. Для таких приложений очень важно иметь надежное и программируемое управление ошибками. У Java есть мощный механизм обработки исключений, что-то наподобие новых реализаций в языке С++. Исключения обеспечивают более естественный и элегантный метод управления ошибками. Исключения позволяют отделить код управления ошибками от обычного кода, что способствует появлению более чистых и надежных приложений.
Когда возникает исключение, оно вызывает переход выполнения программы с обычного потока на запланированный блок кода «отлова». Исключение влечет за собой объект, содержащий информацию о ситуации, которая вызвала исключение. Компилятор Java требует, чтобы метод объявлял исключения, которые он генерирует, или отлавливает и сам имел дело с ними. Это продвигает информацию об ошибке на тот же уровень важности, что и аргумент, и возвращает типы методов. Как программист Java, вы точно знаете, с какими условиями исключений вам необходимо иметь дело, и у вас есть помощь компилятора в написании правильного программного обеспечения, которое не оставит их необработанными.
Потоки
Современные приложения требуют высокой степени параллелизма. Даже очень целенаправленное приложение может иметь сложный интерфейс пользователя, что требует параллельных операций. В то время как машины становятся быстрее, пользователи становятся более требовательными ко времени, которое тратится на ожидание. Потоки обеспечивают эффективную многопроцессорную обработку и распределение задач между клиентом и серверными приложениями. Язык Java делает потоки легкими в использовании, потому что их поддержка встроена в язык.
Параллелизм хорош, но это в большей степени относится к программированию с потоками, чем просто к выполнению множества задач одновременно. В большинстве случаев потоки должны быть синхронизированы (координированы), что может быть сложно без ясной языковой поддержки. Язык Java поддерживает синхронизацию, основанную на модели наблюдения и условия — тип системы «замка и ключа» для доступа к ресурсам. Ключевое слово синхронизирует объявленные методы и блоки кода для безопасного упорядоченного доступа внутри объекта. Также есть простые, примитивные методы для ясного ожидания и передачи сигналов между потоками, заинтересованными в одном объекте.
У Java также есть пакет параллельной обработки, который предоставляет мощные утилиты, адресующиеся к общим шаблонам в многопоточном программировании, такие как пул потоков, координация задач и сложное связывание. С добавлением пакета параллельной обработки и связанных утилит язык Java предоставляет некоторые из наиболее передовых утилит любого языка, связанного с потоками.
Хотя некоторым разработчикам, возможно, никогда не доведется писать многопоточный код, обучение программированию потоками является важным слагаемым в мастерстве программирования на Java и тем, что все разработчики должны понять.
Масштабируемость
На низшем уровне программы Java состоят из классов. Классы были задуманы как маленькие модульные компоненты. Над классами Java предоставляет пакеты, слой структуры, которая группирует классы в функциональные единицы. Пакеты предоставляют соглашения о наименовании для организации классов и второй слой организации контроля над видимостью переменных и методов в приложениях Java.
Внутри пакета, класс является либо публично видимым, либо защищенным от внешнего доступа. Пакеты формируют другой тип границ, который близок к уровню приложения. Оно пригодно для создания компонентов многоразового использования, работающими вместе в системе. Пакеты также помогают в создании масштабируемого приложения, которое может расти, как снежный ком для сильно большого кода.
В следующей статье поговорим о безопасной реализации программирования на языке Java.
С Уважением, МониторБанк