язык Java глазами плюсовика.

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

Рис.1. Краткое содержание материала.

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

Но язык этот очень прост, знаниями этого языка никого не удивишь. К тому же на его синтаксисе построено много других языков. Но даже знать полностью Си, и успешно справиться, например с программой ВУЗа используя этот язык, вовсе не означает что Вы хороший специалист (или вообще специалист) языка Си. Это означает только то, что Вы просто знаете как на нём писать, не более. Наверное это сложно объяснить, но попробую:

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

Забавно то, что как улучшение сортировки пузырьком, привело к созданию одного из самых эффективных алгоритмов сортировки, на подобие и создание (на основе Си) языка Си++, привело к созданию языка, считающегося языком с самым долгим временем вхождения. Существует мнение что поместить в голове одного человека все особенности этого языка (именно языка) невозможно. Уж очень он не компактен. Конечно же под поместить в голову, имеется в виду вовсе не синтаксис.
Это естественно как-бы намекает на то что такая особенность Си++ даёт очень сильные возможности по написанию крайне эффективно работающих решений, но написание этих решений требует более высокой квалификации от разработчика, большего внимания к деталям, чем это требуется в труде на других распространенных языках.

В любом случае, Си+Си++ это очень хорошо, но, язык формирует мышление, а мышление язык, а уметь хорошо мыслить очень важно, особенно для программиста, поэтому при всей любви к одному конкретному языку, не стоит обходить вниманием и другие. Хотя бы потому что знание других языков, позволит ещё более смаковать их критику, и критику айти индустрии в общем, чем я могу позволить себе сейчас :)

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

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

Прежде всего о книге, коллеги сказали начать с Эккеля.

Рис.2. Книга по которой планирую ознакомиться с Ява.

Эккель вкратце занимался сначала Си++, в том числе преподавал и писал книги по этому языку ("Философия С++"), потом перешел на Джаву, на момент написания данной книги он боготворил язык Питон (в этой книге по Яве). Хотя книга написана в 2006 году, до сих пор трудов по Питону у него нет. Не смотря на 2006 год, данную книгу рекомендуют к изучению и поныне (2016).

Почему я привел рисунок. В нём видно что я подрисовал. Вконтакте я оставил такой комментарий "Надеюсь, это синтаксическое упрощение для изучающих Яву, но не его филосовия на тему как пишут на ++ :)" т.к. если писать в подобном ключе сравнительные тесты производительности Явы, и Си++, не удивительно что Ява выйдет в лидеры. Хотя обычно делают более хитроумные препоны, основанные либо на не знании Си++, либо с некоторой политикой, как это было с бенчем от Оракла. Естественно Оракл заинтересован.

Поехали

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

Одним из первых бросающихся в глаза отличий является то что в Ява все даже библиотечные классы имеют названия, начинающиеся называются с большой буквы. В мире Си++ это принято только для пользовательских типов, библиотечные же типы наследуют философию именования, унаследованную из Си, например unordered_map.

Java

  1. // Объявление ссылки на строку

С++

  1. // Обявление автоматической переменной типа Строка
  2. string s;

Java

  1. // Объявляется ссылка, привязанная к выделенному в куче объекту типа String
  2. String s = "str"
  3. // Вообще это "синтаксический сахар", и не все классы поддерживают такое.
  4. // Более общепринято писать так:
  5. String s = new String("str"); // Да, у привыкшего к синтаксису Си++ тут может немножко "подгарать"

C++

  1. // Определяется автоматическая переменная типа string
  2. // вызывается конструктор преобразования, который сам выделяет
  3. // в куче память, и вносит туда константный массив содержащий в себе "str".
  4. string s = "str";

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

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


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

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

Всё это конечно же можно получить и в Си++, причем возможно потратив усилий не более чем в Джава, подсказка - boost.


Встроенные в язык Джава символьные вычисления (поддержка языком типа BigInteger). Ну, почему бы и нет. ++way для этого подключение библиотеки. Например такая, и конечно же это есть и в бусте.

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

Java

  1. // Java way, оопшно, но не красиво
  2. f = d.multiply(e);

C++

  1. // C++ way, красиво, удобно
  2. f = d * e;

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

  1. Variable<int> x;
  2. std::count_if ( vct.begin(), vct.end(), x >= 0 && x <= 25 );

В Джава системе типов нет unsigned вариаций. А нативные типы не могут храниться в куче. Поэтому char ch = 'x' - это не ссылка на символ, а непосредственно объект символа, хранящийся на стеке. Если необходимо разместить объект нативного типа в куче - необходимо использовать класс обёртку
  1. Character ch = new Character('x');

Данные типы обёртки имеют определенные над собой (или в себе? Как там в Яве? в ++ можно и так и так, и причем по поведению опеределение над классом и в классе операции - серьезно различаются в деталях (и влияет на использование), но схожи в общем) операцию присваивания соответствующих нативных типов и операцию присваивания типа обертки к нативному типу, определенную над нативным типом.

Такие ограничения проистекают из-за Ява архитектуры сборщика мусора. Все типы, которые могут быть обработаны сборщиком мусора, в Джава наследуются от одного общего класса Object. Все методы по-умолчанию в Джава виртуальны, поэтому, осмысливая это на языке Си++ - все Ява классы являются полиморфными, это позволяет сборщику мусора, по ссылке типа Object используя наверное какую-то свою внутреннюю механику (возможно почти похожую на механику классового полиморфизма Си++ (vfptr, а в особо сложных случаях, невозможных в Ява и vbptr+vfptr, подробнее здесь) понимать какой по факту, в этой ссылке базового типа на самом деле находится тип, и вызывать правильное удаление объекта.

Но виртуальность, служащая полиморфизму классовой иерархии значительно снижает производительность, вероятно поэтому нативные типы не наследуются от Object. А по скольку я Ява принципиально нет деструкторов, то получаетя все что не Object, в кучу не поместить.

Т.к. в Яве нет деструкторов, то, при программировании необходимо учитывать то, что если оперативной памяти в системе достаточно много, то сборщик мусора, может вообще не уничтожать объект, который давно лишен всех ссылок на себя, но при этом держит в себе открытым некоторых ресурс, предоставляемый операционной системой.


В компьютерной литературе понятие ссылки более соответствует понятию "указателя" языков Си и Си++. Ссылка в Си++ (немного грубо обобщая) есть указатель который всегда автоматически разыменовывается (поэтому при работе выглядит как настоящий объект, а не косвенная связь к этому объекту), а это ("всегда разыменовывается") накладывает важное ограничение на ссылку - она не может быть пустой.

Ссылки же Явы могут быть пустыми, для этого используется (не сишное) ключевое слово null.

(что же собой представляет null мне не известно, статический ли константный объект это как nullptr в Си++, или просто последовательность нульбайтов NULL, как си, которую компилятор транслирует в "ничего не указывающий адрес"), возможно разработчикам на Яве не нужно думать о том что же есть null, а просто знать его свойства.

Тем не менее, массив в Яве создается так char ch[] = new char[n]; против char * ch = new char[n]; в Си++. Так же массив формируется не из указателей а из набора ссылок, проинициализированных null. В Cи++ же, инициализируется либо конструкторами по умолчанию, либо ничем (рандомные значения) - это есть UnspecifedBehaviour, для пользовательских типов в С++ обязателен вызов конструкторов по-умолчанию для всех элементов массива.

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

В итоге Си++ даже контейнеры (служащие не только для более удобной но и защищенной работы) будут работать быстрее, т.к. в них можно переключаться между свободным обращением к адресу ct.[index] или к защищенному обращению ct.at(index), в Яве же похоже все операции защищены, что тратит время выполнения.

при этом объявление массива элементов хранящихся на куче выглядит так:

  1. char ch[] = new char[n];

а так выглядит объявление массива элементов, хранящихся на стеке

  1. char[] arr = { 1, 2 };

Учитывая что в Яве все пользовательские классы автоматически наследованны от общего класса Object - наталкивает на мысль что все ненативные типы могут хранится в одном общем массиве, методы же будут вызываться именно те которые необходимо, в виду того что все методы по умолчанию в Яве виртуальны. Здесь тоже нет ничего невозможного или сложнореализуемого в рамках Си++.

Массивы в Ява (напомню в ява - все классы) обладают полем, хранящим данное о размере массива. Поэтому классический обход массива будет выглядить так:

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

напомню что для статически существующего массива в си++ пришлось бы вычислять в объявлении цикла его размер

  1. for (int i=0; i<sizeof(args)/sizeof(args[0]); i++);

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

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

Контейнеры же (гарантирующие непрерывное линейное следование элементов в мнимом линейном банке памяти) в Си++ должны обходиться так:

  1. for (int i=0; i<args.size(); i++);

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

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

По_ссылке можно видеть что gcc все еще поддерживает отмену константности, вероятно это оставлено намеренно для компиляции некоторой кодовой си базы, которую приходится компилировать и поныне. (хотя clang не позволяет этого для си)

Методы же возвращают копию значения, это полностью обезопасиВАЕТ) внутренне поле от несанкционированного изменения.

В массивах же Явы поле length объявлено с ключевым словом final, в отличие от современного C++, данное слово в Ява может применяться еще и к членам-переменным в классе, работая как const (кстати в Ява нет слова const, его аналог здесь это final, а так же нет возможности защитить передаваемую ссылку в метод константностью). Грубо говоря в Си++ так же можно было бы написать const length, (для статических контейнеров) но, в Си++ есть понятие указатель, и указанные выше явные преобразования типов...

Кстати то, что в Яве массив (есть класс) с final length - говорит о том что такие массивы не могут быть расширены. Это статические экземляры. В Си++ (и Си) расширение статических массивов хоть и моветон, но если очень хочется то возможно, при этом границы расширения зависят от платформы, в которой исполняется программа.

В контейнерах же Явы, используется тот же подход что и в ++, в виду того что поле length там не может быть final. А защитить можно через возвращение копии значения методом size().

Говорят Ява менее многословна чем Си++. Вот пример, на моё имхо, лишних различий. Обходить массив и контейнер (поддерживающий операцию []) придется немного разным кодом.

В Си++ же код может быть одинаков, в не зависимости от типа контейнера (контейнер ли это или сырой массив)


Любым примитивным типам в Ява автоматически присваиваются значения по-умолчанию.

Удобно, но не всегда оправдано в плане производительности.
Т.к. в таком случае:

  1. int val; // здесь произойдут лишние действия.
  2. val = obj.foo();

хотя вполне себе возможно что компиляторы Ява умеют отключать в таком случае лишние действия (оптимизирующие компиляторы).

В Си и Си++, напомню, что только статически определённым объектам нативных типов присваиваются значения по-умолчанию, и то косвенно, из-за того что эти объекты размещены в области памяти называемой BSS, которая перед стартом процесса операционной системой затириется нулями.

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

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

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

Эккель пишет что такие необъявленные переенные вызовут ошибку копмиляции, у меня не вызвало (java-7-openjdk).


В Ява символы занимают 2байта (сделано для юникода) интересно, хм, тогда получается в местностях, где используют английский язык, ява строки (подозреваю, состоящие из Ява символов) будут кушать в два раза больше памяти чем нужно. Дела) Хотя может быть они юзают тип byte для латинских символов? Это предстоит узнать.
Ну да, так и есть. Здесь в исходных тестах, работающих с английскими словами, используют byte тип.

Забавно то, что в Си++ есть тип wchar_t, но он совершенно не подходит для распространенной нынче UTF-8 кодировки, и приходится идти на некоторые хитрости, или же..

Ну конечно, опять буст! ) там всё есть для полной поддержки UTF8)


Расширение кода (использование стронних библиотек) в Си++ выглядит так:
#include <имя библиотеки>, так же, все серьезные библиотеки поддерживают такое множество сущностей (живущее отдельно, вне пространства заголовочных файлов, т.е. некоторая именнованная область видимости, открытая к расширению) - как пространство имен. Пространоство имен, это как бы некий фильтр. Помогающий справляться с неоднозначностью при совпадении имен ф-й, глобальных объектов, классов.

Подключение пространства имени:

  1. using namespace std;

Частичное подключение объектов (или классов) из пространства имен:

  1. using std::cout; //включение в область видимости глобального объекта
  2. using std::string; // и класса синтаксические не отличаются

В Яве нет отдельных объектов (глобальных переменных) и отдельных ф-й. Все что есть всё инкапсулировано в классы, библиотеки же классов называются пакетами, пакеты подключаются так:

  1. import java.io.*; // аналог using namespace std;

или так:

  1. import java.util.ArrayList; // aналог using std::vector;

Понятия заголовочных файлов в Яве нет, разрешение области видимости в отличие от Си++ (::) оформлено в виде точек, а структура пакетов хранится в виде обратной доменной записи.


в Яве статические поля и методы похожи на плюсовые, но разнятся синтаксисом (слева Ява, справа СИ++)
  1. ClassName.StaticMethod(); // ClassName::StaticMethod()
  2. ClassName RefToClass = new ClassName(); // ClassName Obj;
  3. RefToClass.StaticMethod() // Obj.StaticMethod();

Так же, значения неконстантных статических полей можно описать прямо в классе, в Си++ в таком случае нужно отдельное описание, через обращение к оператору разрешения области видимости.

Java

  1. class UsingStaticClass
  2. {
  3. // статические переменные
  4. static int a = 2;
  5. static int b;
  6.  
  7. // в Ява существует понятие статический блок
  8. // который вызывается разово при "загрузке класса"
  9. // в данном блоке можно произвести вычисления.
  10. static
  11. {
  12. b = a * 2;
  13. }
  14. }

C++

  1. class B
  2. {
  3. public:
  4. static const int a = 2;
  5. static const int b = a * 2;
  6. };
  7.  
  8. class D
  9. {
  10. public:
  11. static int a;
  12. static int b;
  13. };
  14. int D::a = 2;
  15. int D::b = a * 2;

Так же статик методы Явы не имеют доступа к указателю (в Яве) "члену" this, и super.

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


System.out, является статическим объектом (заранее созданным), как и std::cout в Си++. Отличие в том что out статический член класса (определенный со словом static), а cout статически существующий объект в пространстве имён std, примечательно что если бы данный объект был объявлен со словом static - это бы ограничило его область видимости до единицы трансляции в которой он определен. В Ява слово static более однозначно, чем в Си++.

Но и в Яве у static так же есть смысловая перегрузка, вот такая:

  1. import static java.lang.Math.*;

Данное использование static позволяет "подключить" (Джависты наверное говорят "импортировать" или "имплементировать") (в примерах которые я видел подключают не в класс, а в единицу трансляции (выражаясь терминами Си)) статические методы другого класса, это используется для того чтобы можно было не писать длинный доменный путь к нужному статическому методу.

В Си++ все эти трюки естественно возможны:

  1. // например через указатель в общей области видимости единицы трансляции
  2. using stat_fu = int(*)(void);
  3. stat_fu fu = &B::fu;
  4. // или указатель на метод внутри класса
  5. class D
  6. {
  7. public:
  8. ...
  9. static int (*pm)(void);
  10. };
  11. int (*D::pm)(void) = &B::fu;

комментарии в стиле
  1. /**
  2. */

служат для утилиты javadoc, которая генерирует документацию к классу, элемменту класса, или методу класса. В Си++ такой инфрастукруктуры не существует, но тем не менее, это не мешает пользовать стронние библиотеки, например doxygen.

Так под конец первичного ознакомления приведу это 12 минутное видео.

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


Операторы в Ява применяются только к нативным типам (на языке ява правильнее говорить примитивным) в Си++ же, мне кажется слово "нативные" весьма хорошо отражает суть вещей, т.к и пользовательские типы могут быть примитивны (std::is_trivial+std::is_standard_layout), но не являться нативными.

При этом в Ява только несколько операторов могут применяться не к примитивным типам, это =, !=, ==, и +, += для строк.

В отличии от std::string (где потребуется ручной выбор нужных методов конвертации, или ручная перегрузка операторов), к Ява строкам можно прибавлять примитивные типы непосредственно из оператора +.

В Ява логические операторы &&, ||, ! применимы только к boolean. Например if (iVal), в отличие от Си++ не сработает. Так же нельзя неявно преобразовать бул тип в никакой другой, и никакой другой в бул (даже явно).


При сравнении генерации случайных чисел, прежде всего бросилось в глаза поддержка в Яве генераций не только целочисленных типов. В Си++ для этого нужно сделать свою ф-ии (или методы, как угодно), к счастью это просто. Так же в Си++ можно пользоваться как классическим Си генератором, так и более современным генератором, вернее даже несколькими разными классами генераторов, экземпляры которых можно настроить на свои диапазоны.
Плюсовику в Ява стоит быть внимательным к сравнению объектов.
  1. Integer n1 = new Integer (1);
  2. Integer n2 = new Integer (1);
  3. if (n1==n2) { } // не войдет в if блок

Т.к. в Ява ссылки (которые глазами плюсовика выглядят не как ссылки а как переменные) это обрезанные указатели - то, понятно что не войдет в if блок в виду разницы указателей.

Поэтому сравненивать все кроме нативных типов хранимых на стеке принято таким очень "удобным" спосбом:

  1. if ( n1.equals(n2) ) { }

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


Еще одно особенность:
  1. class Data
  2. {
  3. String str;
  4. }

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


В Яве нет неявных преобразований нативных типов*, в отличие от Си++, где нативные типы могут быть преобразованы и из меньшего в больший и из большего в меньший, неявно. Чтобы запретить такое неявное преобразование - придётся обернуть нативные типы в классы-обёртки, и использовать уже их вместо нативных типов.

* - /* на самом деле есть, но только при определении переменных, например float f = 1f; - здесь непосредсвенно вносится константа типа float в объект_переменную f, а здесь float f = 1; - в f вносится константа типа int, с выполнением неявного преобразования к типу float. Так же расширяющее преобразование при объявлении переменной не требует явного преобразования long lng = i/*i - have int type*/, при сужающем же преобразовании нужно явное приведение. Ровно так же как и при любом (как сужающим, так и расширяющим) преобразовании при передаче переменных в методы нужно указывать явные преобразования. */

Причем они должны не быть в отношении иерархии. Т.к. например типы, находящиеся в отношении иерархии так же подлежат неявному преобразованию в направлении от производных к базовому. При этом пользовательские типы в иерархии невозможно защитить от неявного преобразования - explicit операторы преобразования не помогут в данном случае. (т.к. компилятор просто их не будет использовать, мнемоническое правило: если классы в иерархии, то для их приведения к друг другу - компилятор использует свой низкоуровневый механизм приведения, а не определенные пользователем методы) Хотя всё же и здесь есть способ защитить метод от попадания в него объекта производного класса, неявно преобразованного к типу формального параметра, например такой:

  1. template <typename T> void foo (T a)
  2. {
  3. static_assert(std::is_same<T, SomeUpperClass>::value,
  4. "foo can only take SomeUpperClass type");
  5. }

Т.к. в Ява невозможно передавать производные классы (находящихся в отношении иерархии) по значению, а только по ссылкам, то можно говорить что и здесь нет неявных преобразований (они просто невозможны в виду того что при передачи всегда будет использоваться механизм косвенной передачи+иерархический_полиморфизм). В Си++ в случае передачи по сслыке (или указателю) объектов находящихся в иерархии - поведение будет идетично Явавскому, но конечно же методы должны быть виртуальными (в том числе для возможного последующего явного "понижающего преобразования" времени выполнения из указателя или ссылки базового типа в производный - требование хотя бы одного виртуального метода является обязательным, а вот для преобразования на этапе компиляции (раннее связывание) для понижающего преобразования вовсе не критично наличие хотя бы одного виртуального метода).

  1. #include <typeinfo>
  2. #include <iostream>
  3.  
  4. class A
  5. {
  6. public:
  7. int a;
  8. int b;
  9. A(int _a = 1, int _b = 1): a(_a), b(_b) { }
  10. virtual void printA() { std::cout << "A: " << a << std::endl; }
  11. void printB() { std::cout << "A: " << b << std::endl; }
  12. };
  13.  
  14. class B : public A
  15. {
  16. public:
  17. B():A(2,2) { }
  18. void printA() { std::cout << "B: " << a << std::endl; }
  19. void printB() { std::cout << "B: " << b << std::endl; }
  20. };
  21.  
  22. void foo (A &a)
  23. {
  24.  
  25. a.printA();
  26. a.printB();
  27. }
  28.  
  29. void foo2 (A &a)
  30. {
  31. try
  32. {
  33. B & b = dynamic_cast<B&>( a );
  34. b.printA();
  35. b.printB();
  36. }
  37. catch (std::bad_cast & e)
  38. {
  39. std::cerr << e.what() << std::endl;
  40. }
  41. }
  42.  
  43. int main(int argc, char *argv[])
  44. {
  45. A a;
  46. B b;
  47.  
  48. std::cout << "используем ссылку на базовый тип для передачи объекта производного типа \n";
  49. foo(a);
  50. foo(b);
  51. std::cout << "вызываем с последующим явным понижающим преобразованием: \n";
  52. foo2(a);
  53. foo2(b);
  54.  
  55. return 0;
  56. }
  57. // вывыедет:
  58. //
  59. // используем ссылку на базовый тип для передачи объекта производного типа
  60. // A: 1
  61. // A: 1
  62. // B: 2
  63. // A: 2 <-- неполиморфный (невиртуальный метод), для него используется раннее связывание.
  64. //
  65. // вызываем с последующим явным понижающим преобразованием:
  66. //
  67. // std::bad_cast
  68. // B: 2
  69. // B: 2 <-- т.к. всего один виртуальный метод делает класс полиморфным, то из ссылки на базовый тип
  70. // можно получить ссылку на производный, и работь уже как с производным.

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

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

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

При этом от переполнения размера типа Ява не защищена, и например в таком коде: int bigger = big * 4, где big равен максимально возможной величине типа int, произойдет переполнение и в bigger будет находиться число -4.

Если нужно что-то преобразовать - программист должен указать это явно.

Это я действительно оценил, в отличие от сборщика мусора, который я считаю ненужным инструментом (мягко говоря), на фоне средств работы с памятью (и идиомы RAII), которые появились в 11-м Си++.

Если интересно, эти две статьи дают почувствовать мир Си++, в плане возможных неявных преобразований пользовательских типов, которые могут нарушить правильную работу программы, и способов борьбы (причем элегантной и красивой, но не малословной) с этим: статья 1, из неё можно узнать про опасность приведения пользовательского типа к бул, статья 2, в ней рассматриваются шаблоны (независимые от типов параметров функции/методы), и то как указать жесткий перечень соответствия типов, которые могут инстанцировать шаблон, исходя из анализа входящих типов, причем используя универсальный интроспективный подход, не требующий перегрузок, Т.е. условие ограничение будет уметь работать и с новыми типами, без необходимости что-то переписывать.

Java

  1.  

C++

  1.  

Java

  1.  

C++

  1.  

Java

  1.  

C++

  1.  

Java

  1.  

C++

  1.  

Java

  1.  

C++

  1.  

Java

  1.  

C++

  1.  

Java

  1.  

C++

  1.  

Java

  1.  

C++

  1.  
  1.  
  1.  
  1.  
  1.  
  1.  
  1.  
  1.