Углубленный взгляд на C++ и Java

Опубликовано: 2022-07-22

В бесчисленных статьях сравниваются технические возможности C++ и Java, но какие различия важнее всего учитывать? Когда сравнение показывает, например, что Java не поддерживает множественное наследование, а C++ поддерживает, что это значит? И хорошо ли это? Одни утверждают, что это преимущество Java, другие объявляют это проблемой.

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

Изучение основ: языковые сборки и экосистемы

C++ был запущен в 1985 году как интерфейс для компиляторов C, подобно тому, как TypeScript компилируется в JavaScript. Современные компиляторы C++ обычно компилируют собственный машинный код. Хотя некоторые утверждают, что компиляторы C++ снижают его переносимость и требуют перестроений для новых целевых архитектур, код C++ работает практически на любой процессорной платформе.

Впервые выпущенная в 1995 году, Java не встраивается непосредственно в нативный код. Вместо этого Java создает байт-код, промежуточное двоичное представление, которое выполняется на виртуальной машине Java (JVM). Другими словами, вывод компилятора Java требует запуска собственного исполняемого файла для конкретной платформы.

И C++, и Java попадают в семейство C-подобных языков, поскольку они в целом напоминают C по своему синтаксису. Наиболее существенное различие заключается в их экосистемах: в то время как C++ может беспрепятственно вызывать библиотеки на основе C или C++ или API операционной системы, Java лучше всего подходит для библиотек на основе Java. Вы можете получить доступ к библиотекам C в Java с помощью API Java Native Interface (JNI), но он подвержен ошибкам и требует некоторого кода C или C++. C++ также легче взаимодействует с оборудованием, чем Java, поскольку C++ является языком более низкого уровня.

Подробные компромиссы: обобщения, память и многое другое

Мы можем сравнивать C++ с Java со многих точек зрения. В некоторых случаях выбор между C++ и Java очевиден. Нативные приложения для Android обычно должны использовать Java, если приложение не является игрой. Большинству разработчиков игр следует выбрать C++ или другой язык для максимально плавной анимации в реальном времени; Управление памятью в Java часто вызывает задержки во время игры.

Кроссплатформенные приложения, не являющиеся играми, выходят за рамки этого обсуждения. Ни C++, ни Java в данном случае не идеальны, потому что они слишком многословны для эффективной разработки графического интерфейса. Для высокопроизводительных приложений лучше всего создавать модули C++, чтобы выполнять тяжелую работу, и использовать язык, более продуктивный для разработчиков, для графического интерфейса.

Кроссплатформенные приложения, не являющиеся играми, выходят за рамки этого обсуждения. Ни C++, ни Java в данном случае не идеальны, потому что они слишком многословны для эффективной разработки графического интерфейса.

Твитнуть

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

Особенность С++ Ява
Подходит для начинающих Нет Да
Производительность во время выполнения Лучший Хороший
Задержка Предсказуемый Непредсказуемый
Интеллектуальные указатели с подсчетом ссылок Да Нет
Глобальная пометочная сборка мусора Нет Необходимый
Распределение памяти стека Да Нет
Компиляция в собственный исполняемый файл Да Нет
Компиляция в байт-код Java Нет Да
Прямое взаимодействие с низкоуровневыми API операционной системы Да Требуется код C
Прямое взаимодействие с библиотеками C Да Требуется код C
Прямое взаимодействие с библиотеками Java Через JNI Да
Стандартизированное управление сборкой и пакетами Нет Мавен


Помимо функций, сравниваемых в таблице, мы также сосредоточимся на функциях объектно-ориентированного программирования (ООП), таких как множественное наследование, обобщения/шаблоны и отражение. Обратите внимание, что оба языка поддерживают ООП: Java требует этого, а C++ поддерживает ООП наряду с глобальными функциями и статическими данными.

Множественное наследование

В ООП наследование — это когда дочерний класс наследует атрибуты и методы родительского класса. Одним из стандартных примеров является класс Rectangle , который наследуется от более общего класса Shape :

 // Note that we are in a C++ file class Shape { // Position int x, y; public: // The child class must override this pure virtual function virtual void draw() = 0; }; class Rectangle: public Shape { // Width and height int w, h; public: void draw(); };

Множественное наследование — это когда дочерний класс наследуется от нескольких родителей. Вот пример использования классов Rectangle и Shape и дополнительного класса Clickable :

 // Not recommended class Shape {...}; class Rectangle: public Shape {...}; class Clickable { int xClick, yClick; public: virtual void click() = 0; }; class ClickableRectangle: public Rectangle, public Clickable { void click(); };

В данном случае у нас есть два базовых типа: Shape (базовый тип Rectangle ) и Clickable . ClickableRectangle наследуется от обоих, чтобы составить два типа объектов.

C++ поддерживает множественное наследование; Ява нет. Множественное наследование полезно в некоторых пограничных случаях, таких как:

  • Создание расширенного предметно-ориентированного языка (DSL).
  • Выполнение сложных вычислений во время компиляции.
  • Повышение безопасности типов проектов способами, которые просто невозможны в Java.

Однако использование множественного наследования обычно не рекомендуется. Это может усложнить код и повлиять на производительность, если не сочетается с метапрограммированием шаблонов, что лучше всего делать только самым опытным программистам на C++.

Дженерики и шаблоны

Универсальные версии классов, которые работают с любым типом данных, удобны для повторного использования кода. Оба языка предлагают эту поддержку — Java через универсальные шаблоны, C++ через шаблоны, — но гибкость шаблонов C++ может сделать расширенное программирование более безопасным и надежным. Компиляторы C++ создают новые настраиваемые классы или функции каждый раз, когда вы используете разные типы с шаблоном. Кроме того, шаблоны C++ могут вызывать пользовательские функции на основе типов параметров функции верхнего уровня, что позволяет использовать специализированный код для конкретных типов данных. Это называется специализацией шаблона. Java не имеет эквивалентной функции.

Напротив, при использовании дженериков компиляторы Java создают общие объекты без типов посредством процесса, называемого стиранием типов. Java выполняет проверку типов во время компиляции, но программисты не могут изменить поведение универсального класса или метода на основе его параметров типа. Чтобы лучше понять это, давайте рассмотрим быстрый пример универсальной функции std::string format(std::string fmt, T1 item1, T2 item2) , которая использует шаблон template<class T1, class T2> из C++. библиотека, которую я создал:

 std::string firstParameter = "A string"; int secondParameter = 123; // Format printed output as an eight-character-wide string and a hexadecimal value format("%8s %x", firstParameter, secondParameter); // Format printed output as two eight-character-wide strings format("%8s %8s", firstParameter, secondParameter);

C++ будет создавать функцию format как std::string format(std::string fmt, std::string item1, int item2) , тогда как Java создаст ее без определенных типов объектов string и int для item1 и item2 . В этом случае наш шаблон C++ знает, что последним входящим параметром является int , и поэтому может выполнить необходимое преобразование std::to_string во втором вызове format . Без шаблонов оператор C++ printf , пытающийся напечатать число в виде строки, как во втором вызове format , имел бы неопределенное поведение и мог привести к сбою приложения или печатать мусор. Функция Java сможет обрабатывать число как строку только при первом вызове format и не будет напрямую форматировать его как шестнадцатеричное целое число. Это тривиальный пример, но он демонстрирует способность C++ выбирать специализированный шаблон для обработки любого произвольного объекта класса без изменения его класса или функции format . Мы можем правильно произвести вывод в Java, используя отражение вместо дженериков, хотя этот метод менее расширяем и более подвержен ошибкам.

Отражение

В Java можно узнать (во время выполнения) структурные детали, например, какие члены доступны в классе или типе класса. Эта функция называется отражением, предположительно потому, что это похоже на поднесение зеркала к объекту, чтобы увидеть, что внутри. (Дополнительную информацию можно найти в документации по отражению Oracle.)

C++ не имеет полного отражения, но современный C++ предлагает информацию о типах времени выполнения (RTTI). RTTI позволяет обнаруживать определенные типы объектов во время выполнения, но не может получить доступ к такой информации, как члены объекта.

Управление памятью

Еще одно важное различие между C++ и Java заключается в управлении памятью, которое имеет два основных подхода: ручной, когда разработчики должны отслеживать и освобождать память вручную; и автоматический, когда программное обеспечение отслеживает, какие объекты все еще используются, чтобы утилизировать неиспользуемую память. В Java примером является сборка мусора.

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

В то время как Java предлагает только выделение кучи, C++ поддерживает как выделение кучи (используя new и delete или старые функции malloc C), так и выделение стека. Выделение стека может быть быстрее и безопаснее, чем выделение кучи, потому что стек представляет собой линейную структуру данных, а куча основана на дереве, поэтому память стека гораздо проще выделять и освобождать.

Еще одним преимуществом C++, связанным с выделением стека, является метод программирования, известный как получение ресурсов как инициализация (RAII). В RAII такие ресурсы, как ссылки, связаны с жизненным циклом контролирующего их объекта; ресурсы будут уничтожены в конце жизненного цикла этого объекта. RAII — это то, как интеллектуальные указатели C++ работают без ручного разыменования — интеллектуальный указатель, указанный в начале функции, автоматически разыменовывается при выходе из функции. Подключенная память также освобождается, если это последняя ссылка на интеллектуальный указатель. Хотя Java предлагает аналогичный шаблон, он более неудобен, чем RAII в C++, особенно если вам нужно создать несколько ресурсов в одном и том же блоке кода.

Производительность во время выполнения

Java имеет хорошую производительность во время выполнения, но C++ по-прежнему удерживает корону, поскольку ручное управление памятью быстрее, чем сборка мусора для реальных приложений. Хотя Java может превзойти C++ в некоторых крайних случаях благодаря JIT-компиляции, C++ выигрывает в большинстве нетривиальных случаев.

В частности, стандартная библиотека памяти Java перегружает сборщик мусора своими выделениями по сравнению с сокращенным использованием памяти C++ в куче. Тем не менее, Java по-прежнему относительно быстр и должен быть приемлемым, если задержка не является главной проблемой — например, в играх или приложениях с ограничениями в реальном времени.

Управление сборкой и пакетами

То, чего Java не хватает в производительности, компенсируется простотой использования. Одним из компонентов, влияющих на эффективность разработчиков, является управление сборкой и пакетами — то, как мы создаем проекты и вносим внешние зависимости в приложение. В Java инструмент под названием Maven упрощает этот процесс до нескольких простых шагов и интегрируется со многими IDE, такими как IntelliJ IDEA.

Однако в C++ не существует стандартизированного репозитория пакетов. Не существует даже стандартизированного метода для создания кода C++ в приложениях: некоторые разработчики предпочитают Visual Studio, а другие используют CMake или другой пользовательский набор инструментов. Еще больше усложняет ситуацию то, что некоторые коммерческие библиотеки C++ имеют двоичный формат, и нет единого способа интегрировать эти библиотеки в процесс сборки. Кроме того, различия в настройках сборки или версиях компилятора могут вызвать проблемы с работой двоичных библиотек.

Удобство для начинающих

Проблемы со сборкой и управлением пакетами — не единственная причина, по которой C++ гораздо менее удобен для начинающих, чем Java. У программиста могут возникнуть трудности с отладкой и безопасным использованием C++, если он не знаком с C, языками ассемблера или низкоуровневыми работами компьютера. Думайте о C++ как о мощном инструменте: он может многое сделать, но опасен при неправильном использовании.

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

Время принятия решения: C++ или Java?

Блок-схема с темно-синим пузырем «Начало» в верхнем левом углу, который в конечном итоге соединяется с одним из семи голубых окончаний под ним через серию белых переходов решений с темно-синими ответвлениями для «Да» и другие варианты, и светло-голубые ветки для «Нет». Первый — «Кроссплатформенное приложение с графическим интерфейсом?» из которого «Да» указывает на вывод: «Выберите кроссплатформенную среду разработки и используйте ее основной язык». «Нет» указывает на «Родное приложение для Android?» из которого «Да» указывает на второстепенный вопрос: «Это игра?» Из второстепенного вопроса «Нет» указывает на вывод «Используйте Java (или Kotlin)», а «Да» указывает на другой вывод: «Выберите кроссплатформенный игровой движок и используйте его рекомендуемый язык». Из «Родного приложения для Android?» вопрос, «Нет» указывает на «Родное приложение Windows?» из которого «Да» указывает на второстепенный вопрос: «Это игра?» Из второстепенного вопроса «Да» указывает на вывод «Выберите кроссплатформенный игровой движок и используйте его рекомендуемый язык», а «Нет» указывает на другой вывод: «Выберите среду графического интерфейса Windows и используйте ее основной язык (обычно C++ или C#)». Из «Родного приложения Windows?» вопрос, «Нет» указывает на «Серверное приложение?» из которого «Да» указывает на второстепенный вопрос «Тип разработчика?» Из второстепенного вопроса решение «Средний уровень навыков» указывает на вывод «Используйте Java (или C# или TypeScript)», а решение «Навык» указывает на вопрос третичного уровня: «Наивысший приоритет?» Из третьего вопроса решение «Производительность разработчика» указывает на вывод «Использовать Java (или C# или TypeScript)», а решение «Производительность» указывает на другой вывод: «Использовать C++ (или Rust)». Из «Серверного приложения?» ответ «Нет» указывает на второстепенный вопрос «Разработка драйвера?» Из вторичного вопроса «Да» указывает на вывод «Использовать C++ (или Rust)», а «Нет» указывает на третичный вопрос «Разработка IoT?» От третьего вопроса «Да» указывает на заключение «Используйте C++ (или Rust)», а «Нет» указывает на четвертый вопрос «Высокоскоростной трейдинг?» Из четвертичного вопроса «Да» указывает на вывод «Используйте C++ (или Rust)», а «Нет» указывает на последний оставшийся вывод: «Спросите кого-нибудь, знакомого с вашим целевым доменом».
Расширенное руководство по выбору лучшего языка для различных типов проектов.

Теперь, когда мы подробно изучили различия между C++ и Java, мы возвращаемся к нашему первоначальному вопросу: C++ или Java? Даже при глубоком понимании двух языков не существует универсального ответа.

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

Однако технические различия между C++ и Java могут быть лишь небольшим фактором при принятии решения. Определенные типы продуктов требуют особого выбора. Если вы все еще не уверены, вы можете обратиться к блок-схеме, но имейте в виду, что в конечном итоге она может привести вас к третьему языку.