→ Пошук по сайту       Увійти / Зареєструватися
Знання Програмування на Java Навчальний курс по JAVA

Перетворення типів

 Java

Введення

Як вже говорилося, Java є строго типізованою мовою, а це означає, що кожен вираз і кожна змінна має строго певний тип вже на момент компіляції. Тип встановлюється на основі структури застосовуваних виразів і типів літералів, змінних і методів, використовуваних у цих виразах.

Наприклад:

 long a = 3;
a = 5 + 'A' + a;
print ("a =" + Math.round (a/2F));

Розглянемо, як у цьому прикладі компілятор встановлює тип кожного виразу і які перетворення (conversion) типів необхідно здійснити при кожній дії.

  • У першому рядку літерал 3 має тип за замовчуванням, тобто int. При присвоєнні цього значення змінної типу long необхідно провести перетворення.
  • У другому рядку спочатку здійснюється складання значень типу int і char. Другий аргумент буде перетворений так, щоб операція проводилася з точністю в 32 біта. Другий оператор складання знову зажадає перетворення, так як наявність змінної a збільшує точність до 64 біт.
  • У третьому рядку спочатку буде виконана операція ділення, для чого значення long треба буде привести до типу float, тому що другий операнд - дробовий літерал. Результат буде переданий в метод Math.round, який зробить математичне округлення і поверне цілочисельний результат типу int. Це значення необхідно перетворити в текст, щоб здійснити подальшу конкатенацію рядків. Як буде показано нижче, ця операція проводиться в два етапи - спочатку простий тип приводиться до об'єктному класу-"обгортці" (в даному випадку int до Integer), а потім у отриманого об'єкту викликається метод toString (), що дає перетворення до рядка.

Даний приклад показує, що навіть прості рядки можуть містити численні перетворення, часто непомітні для розробника. Часто бувають і такі випадки, коли програмісту необхідно явно змінити тип деякого виразу або змінної, наприклад, щоб скористатися придатним методом або конструктором.

Згадаймо вже розглянутий приклад:

 int b = 1;
byte c = (byte)-b;
int i = c;

Тут у другому рядку необхідно провести явне перетворення, щоб привласнити значення типу int змінній типу byte. У третьому ж рядку зворотнє приведення проводиться автоматично, неявним для розробника чином.

Розглянемо спочатку, які переходи між різними типами можна здійснити.

Види привидів

У Java передбачено сім видів привидів:

  • тотожне (identity);
  • розширення примітивного типу (widening primitive);
  • звуження примітивного типу (narrowing primitive);
  • розширення об'єктного типу (widening reference);
  • звуження об'єктного типу (narrowing reference);
  • перетворення до рядка (String);
  • заборонені перетворення (forbidden).

Розглянемо їх окремо.

Тотожне перетворення

Найпростішим є тотожне перетворення. У Java перетворення виразу будь-якого типу до точно такого ж типу завжди допустимо і успішно виконується.

Навіщо потрібно тотожне приведення? Є дві причини для того, щоб виділити таке перетворення в особливий вид.

По-перше, з теоретичної точки зору тепер можна стверджувати, що будь-який тип в Java може брати участь у перетворенні, хоча б в тотожній. Наприклад, примітивний тип boolean не можна привести ні до якого іншого типу, крім нього самого.

По-друге, іноді в Java можуть зустрічатися такі вирази, як довгий послідовний виклик методів:

 print (getCity (). getStreet (). getHouse (). getFlat (). getRoom ());

При виконанні такого виразу спочатку викликається перший метод getCity (). Можна припустити, що повертається значенням буде об'єкт класу City. У цього об'єкті далі буде викликаний наступний метод getStreet (). Щоб дізнатися, значення якого типу він поверне, необхідно подивитися опис класу City. У цього значення буде викликаний наступний метод (getHouse ()), і так далі. Щоб дізнатися результуючий тип всього висловлювання, необхідно переглянути опис кожного методу та класу.

Компілятор без складнощів впорається з таким завданням, проте розробнику буде нелегко простежити весь ланцюжок. У цьому випадку можна скористатися тотожним перетворенням, виконавши приведення до точно такого ж типу. Це нічого не змінить у структурі програми, але значно полегшить читання коду:

 print ((MyFlatImpl) (getCity (). getStreet (). getHouse (). getFlat ()));

Перетворення примітивних типів (розширення та звуження)

Очевидно, що наступні чотири види приведень легко представляються у вигляді таблиці 7.1.

Таблиця 7.1. Види зведень

Простий тип, розширений

Вказівний тип, розширений

Простий тип, звужений

Вказівний тип, звужений

 

Що все це означає? Почнемо по порядку. Для простих типів розширення означає, що здійснюється перехід від менш ємкісного типу до більш місткого. Наприклад, від типу byte (довжина 1 байт) до типу int (довжина 4 байти). Такі перетворення безпечні в тому сенсі, що новий тип завжди гарантовано вміщає в собі всі дані, які зберігалися в старому типі, і таким чином не відбувається втрати даних. Саме тому компілятор здійснює його сам, непомітно для розробника:

 byte b = 3;
int a = b; 

В останньому рядку значення змінної b типу byte буде перетворено до типу змінної a (тобто, int) автоматично, ніяких спеціальних дій для цього робити не потрібно.

Наступні 19 перетворень є такими, що розширяються

 від byte до short, int, long, float, double
від short до int, long, float, double
від char до int, long, float, double
від int до long, float, double
від long до float, double
від float до double

Зверніть увагу, що не можна провести перетворення до типу char від типів меншої або рівної довжини (byte, short), або, навпаки, до short від char без втрати даних. Це пов'язано з тим, що char, на відміну від інших цілочисельних типів, є беззнаковим.

Тим не менш, слід пам'ятати, що навіть при розширені дані все-таки можуть бути в особливих випадках спотворені. Вони вже розглядалися в попередній лекції, це приведення значень int до типу float і приведення значень типу long до типу float або double. Хоча ці дробові типи вміщають набагато більші числа, ніж відповідні цілі, але у них менше значущих розрядів.

Повторимо цей приклад:

 long a = 111111111111L;
float f = a;
a = (long) f;
print (a);

Результатом буде:

 111111110656

Зворотне перетворення - звуження - означає, що перехід здійснюється від більш ємного типу до менш ємного. При такому перетворенні є ризик втратити дані. Наприклад, якщо число типу int було більше 127, то при приведенні його до byte значення бітів старше восьмого будуть втрачені. У Java таке перетворення повинно відбуватися явним чином, тобто програміст в коді повинен явно вказати, що він має намір здійснити таке перетворення і готовий втратити дані.

Наступні 23 перетворення є звужуючими:

 від byte до char
від short до byte, char
від char до byte, short
від int до byte, short, char
від long до byte, short, char, int
від float до byte, short, char, int, long
від double до byte, short, char, int, long, float

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

 print ((byte) 383);
print ((byte) 384);
print ((byte) -384);

Результатом буде:

 1 27
-128
-128

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

Це вірно і для типу char:

char c = 40000;
print ((short) c);

Результатом буде:

-25536 

Звуження дробового типу до цілочисельного є більш складною процедурою. Вона проводиться в два етапи.

На першому кроці дробове значення перетворюється на long, якщо цільовим типом є long, або в int - у противному випадку (цільовий тип byte, short, char або int). Для цього вихідне дробове число спочатку математично округлюється в бік нуля, тобто дробова частина просто відкидається.

Наприклад, число 3,84 буде округлене до 3, а -3,84 перетвориться на -3. При цьому можуть виникнути особливі випадки:

  • якщо вихідне дробове значення є NaN, то результатом першого кроку буде 0 обраного типу (тобто int або long);
  • якщо вихідне дробове значення є позитивною чи негативною нескінченністю, то результатом першого кроку буде, відповідно, максимально чи мінімально можливе значення для обраного типу (тобто для int або long);
  • нарешті, якщо дробове значення було кінцевою величиною, але в результаті округлення вийшло занадто велике за модулем число для обраного типу (тобто для int або long), то, як і в попередньому пункті, результатом першого кроку буде, відповідно, максимально або мінімально можливе значення цього типу. Якщо ж результат округлення вкладається в діапазон значень вибраного типу, то він і буде результатом першого кроку.

На другому кроці проводиться подальше звуження від обраного цілочисельного типу до цільового, якщо таке потрібно, тобто може мати місце додаткове перетворення від int до byte, short або char.

Проілюструємо описаний алгоритм перетворенням від нескінченності до всіх цілочисловим типами:

float fmin = Float.NEGATIVE_INFINITY;
float fmax = Float.POSITIVE_INFINITY;
print ("long:" + (long) fmin + ".." + (Long) fmax);
print ("int:" + (int) fmin + ".." + (Int) fmax);
print ("short:" + (short) fmin + ".." + (Short) fmax);
print ("char:" + (int) (char) fmin + ".." + (Int) (char) fmax);
print ("byte:" + (byte) fmin + ".." + (Byte) fmax);

 Результатом буде: 

 long: -9223372036854775808 .. 9223372036854775807
int: -2147483648 .. 2147483647
short: 0 ..- 1
char: 0 .. 65535
byte: 0 ..- 1

Значення long і int цілком очевидні - дробові нескінченності перетворилися у, відповідно, мінімально та максимально можливі значення цих типів. Результат для наступних трьох типів (short, char, byte) є, по суті, подальше звуження значень, отриманих для int, згідно другого кроку процедури перетворення. А робиться це, як було описано, просто за рахунок відкидання старших бітів. Згадаймо, що мінімально можливе значення у бітовому вигляді представляється як 1000 .. 000 (всього 32 біта для int, тобто одиниця і 31 нуль). Максимально можливе - 1111 .. 111 (32 одиниці). Відкидаючи старші біти, отримуємо для негативної нескінченності результат 0, однаковий для всіх трьох типів. Для позитивної ж нескінченності отримуємо результат, всі біти якого дорівнюють 1. Для знакових типів byte і short така комбінація розглядається як -1, а для беззнакового char - як максимально можливе значення, тобто 65535.

Може скластися враження, що для char приведення дає точне значення. Однак це був окремий випадок - відкидання бітів в більшості випадків все-таки дає спотворення. Наприклад, звуження дробового значення 2 мільярди:

 float f = 2e9f;
print ((int) (char) f);
print ((int) (char)-f);

Результатом буде:

 37888
27648

Зверніть увагу на подвійне приведення для значень типу char в двох останніх прикладах. Зрозуміло, що перетворення від char до int не призводить до втрати точності, але дозволяє роздруковувати не символ, а його числовий код, що більш зручно для аналізу.

На завершення ще раз звернемо увагу на те, що примітивні значення типу boolean можуть брати участь тільки в тотожних перетвореннях.

Перетворення вказівних типів (розширення та звуження)

Переходимо до вказівнихтипів. Перетворення об'єктних типів найкраще ілюструється за допомогою дерева наслідування. Розглянемо невеликий приклад наслідування:

 // Оголошуємо клас Parent
class Parent {
   int x;
}
// Оголошуємо клас Child і успадковуємо
// Його від класу Parent
class Child extends Parent {
   int y;
}
// Оголошуємо другого спадкоємця
// Класу Parent - клас Child2
class Child2 extends Parent {
   int z;
} 

У кожному класі оголошено поле з унікальним ім'ям. Будемо розглядати це поле як приклад набору унікальних властивостей, притаманних деякого об'єктному типу.

Три оголошених класи можуть породжувати три види об'єктів. Об'єкти класу Parent володіють тільки одним полем x, а значить, тільки вказівники типу Parent можуть посилатися на такі об'єкти. Об'єкти класу Child володіють полем y і полем x, отриманим у спадок від класу Parent. Стало бути, на такі об'єкти можуть вказувати вказівник типу Child або Parent. Другий випадок вже ілюструвався таким прикладом:

 Parent p = new Child ();

Зверніть увагу, що за допомогою такого вказівника p можна звертатися лише до поля x створеного об'єкту. Поле y недоступне, тому що компілятор, перевіряючи коректність висловлювання p.y, не можна передбачити, що посилання p буде вказувати на об'єкт типу Child під час виконання програми. Він аналізує лише тип самої змінної, а вона оголошена як Parent, але в цьому класі немає поля y, що й викличе помилку компіляції.

Аналогічно, об'єкти класу Child2 володіють полем z і полем x, отриманим у спадок від класу Parent. Значить, на такі об'єкти можуть вказувати посилання типу Child2 або Parent.

Таким чином, посилання типу Parent можуть вказувати на об'єкт будь-якого з трьох розглянутих типів, а посилання типу Child і Child2 - тільки на об'єкти точно такого ж типу. Тепер можна перейти до перетворення вказівних типів на основі такого дерева наслідування.

Розширення означає перехід від більш конкретного типу до менш конкретного, тобто перехід від дітей до батьків. У нашому прикладі перетворення від будь-якого спадкоємця (Child, Child2) до батьків (Parent) є розширення, перехід до більш загального типу. Подібно до випадку з примітивними типами, цей перехід здійснюється самою JVM при необхідності і непомітно для розробника, тобто не вимагає ніяких додаткових зусиль, оскільки він завжди проходить успішно: завжди можна звертатися до об'єкта, породженому від спадкоємця, за типом його батька.

 Parent p1 = new Child ();
Parent p2 = new Child2 ();

В обох рядках змінним типу Parent присвоюється значення іншого типу, а значить, відбувається перетворення. Оскільки це розширення, воно виконується автоматично і завжди успішно.

Зверніть увагу, що при подібному перетворенні з самим об'єктом нічого не відбувається. Незважаючи на те, що, наприклад, поле y класу Child тепер недоступне, це не означає, що воно зникло. Така істотна зміна структури об'єкта неможлива. Вона була породжена від класу Child і зберігає всі його властивості. Змінився лише тип посилання, через яке йде звернення до об'єкта. Цю ситуацію можна умовно порівняти з розгляданням якогось предмета через підзорну трубу. Якщо перейти від труби з великим збільшенням до більш слабкої, то видимих деталей стане менше, але сам предмет, звичайно, ніяк від цього не зміниться.

Наступні перетворення є розширюючі:

  • від класу A до класу B, якщо A успадковується від B (важливим окремим випадком є перетворення від будь-якого посилального типу до Object);
  • від null-типу до будь-якого об'єктному типу.

Другий випадок ілюструється таким прикладом:

 Parent p = null;

Порожнє посилання null не має будь-якого конкретного вказівного типу, тому іноді говорять про спеціальний null-тип. Однак на практиці важливо, що таке значення можна прозоро перетворити до будь-якого об'єктному типу.

З вивченням інших вказівних типів (інтерфейсів і масивів) цей список буде розширюватися.

Зворотний перехід, тобто рух по дереву успадкування вниз, до спадкоємців, є звуженням. Наприклад, для розглянутого випадку, перехід від вказівного типу Parent, який може посилатися на об'єкти трьох класів, до посилання типу Child, яке може посилатися на об'єкти лише одного з трьох класів, очевидно, є звуженням. Такий перехід може виявитися неможливим. Якщо посилання типу Parent посилається на об'єкт типу Parent або Child2, то перехід до Child неможливий, адже в обох випадках об'єкт не має поле y, яке оголошено в класі Child. Тому при звуженні розробнику необхідно явним чином вказувати на те, що необхідно спробувати провести таке перетворення. JVM під час виконання перевірить коректність переходу. Якщо він можливий, перетворення буде проведено. Якщо ж ні - виникне помилка.

 Parent p = new Child ();
Child c = (Child) p;
   // Перетворення буде успішним.
Parent p2 = new Child2 ();
Child c2 = (Child) p2;
   // Під час виконання виникне помилка!

Щоб перевірити, чи можливий бажаний перехід, можна скористатися оператором instanceof:

 Parent p = new Child ();
if (p instanceof Child) {
   Child c = (Child) p;
}
Parent p2 = new Child2 ();
if (p2 instanceof Child) {
   Child c = (Child) p2;
}
Parent p3 = new Parent ();
if (p3 instanceof Child) {
   Child c = (Child) p3;
}

У даному прикладі помилок не виникне. Перше перетворення можливе, і воно буде здійснено. У другому і третьому випадках умови операторів if не спрацюють і спроб некоректного переходу не буде.

На даний момент можна назвати лише одне звужуюче перетворення: від класу A до класу B, якщо B успадковується від A (важливим окремим випадком є звуження типу Object до будь-якого іншого посилального типу). З вивченням інших документів, які типів (інтерфейсів і масивів) цей список буде розширюватися.

Перетворення до рядку

Це перетворення вже не раз згадувалося. Будь-який тип може бути приведений до рядка, тобто до примірника класу String. Таке перетворення є винятковим в силу того, що охоплює абсолютно всі типи, в тому числі і boolean, про який говорилося, що він не може брати участь ні в якому іншому приведення, крім тотожного.

Нагадаємо, як перетворюються різні типи.

  • Числові типи записуються в текстовому вигляді без втрати точності представлення. Формально таке перетворення відбувається в два етапи. Спочатку на основі примітивного значення породжується примірник відповідного класу-"обгортки", а потім у нього викликається метод toString (). Але оскільки ці дії зовні непомітні, багато JVM оптимізують їх і перетворять примітивні значення в текст безпосередньо.
  • Булевська величина приводиться до рядку "true" або "false" залежно від значення.
  • Для об'єктних величин викликається метод toString (). Якщо метод повертає null, то результатом буде рядок "null".
  • Для null-значення генерується рядок "null".

Заборонені перетворення

Не всі переходи між довільними типами допустимі. Наприклад, до заборонених перетворень відносяться: переходи від будь-якого вказівного типу до примітивного, від примітивного - до вказівного (крім перетворень до рядку). Вже згадуваний приклад - тип boolean - не можна привести ні до якого іншого типу, крім boolean (як завжди - за винятком приведення до рядка). Потім, неможливо привести один до одного типи, що знаходяться не на одній, а на сусідніх гілках дерева наслідування. У прикладі, який розглядався для ілюстрації перетворень посилальних типів, перехід від Child до Child2 заборонений. Справді, посилання типу Child може вказувати на об'єкти, породжені тільки від класу Child або його спадкоємців. Це виключає вірогідність того, що об'єкт буде сумісний з типом Child2.

Цим список заборонених перетворень не вичерпується. Він досить великий, і в той же час всі варіанти досить очевидні, тому докладно розглядатися не будуть. Бажаючі можуть отримати повну інформацію з специфікації.

Зрозуміло, спроба здійснити заборонене перетворення викличе помилку компіляції.

Застосування приведення

Тепер, коли розглянуті всі види перетворень, перейдемо до ситуацій в коді, де можуть зустрітися або знадобитися приведення.

Такі ситуації можуть бути згруповані таким чином.

  • Присвоєння значень змінним (assignment). Не всі переходи допустимі при такому перетворенні - обмеження обрані таким чином, щоб не могла виникнути хибна ситуація.
  • Виклик методу. Це перетворення застосовується до аргументів викликаємого методу або конструктору. Допускаються майже ті ж переходи, що і для присвоєння значень. Таке приведення ніколи не породжують помилок. Так само приведення здійснюється при поверненні значення з методу.
  • Явне приведення. У цьому випадку явно вказується, до якого типу потрібно привести початкове значення. Допускаються всі види перетворень, крім приведень до рядка і заборонених. Може виникати помилка часу виконання програми.
  • Оператор конкатенації виробляє перетворення до рядка своїх аргументів.
  • Числове розширення (numeric promotion). Числові операції можуть вимагати зміни типу аргументу(ів). Це перетворення має особливу назву - розширення (promotion), так як вибір цільового типу може залежати не тільки від початкового значення, але і від другого аргументу операції.

Розглянемо всі випадки більш докладно.

Присвоєння значень

Такі ситуації неодноразово застосовувалися в цій лекції для ілюстрації видів перетворення. Приведення може знадобитися, якщо змінній одного типу присвоюється значення іншого типу. Можливі наступні комбінації.

Якщо поєднання цих двох типів утворює заборонене приведення, виникне помилка. Наприклад, примітивні значення не можна присвоювати об'єктним змінним, включаючи такі приклади:

 // Приклад викличе помилку компіляції
// Примітивне значення не можна
// Привласнити об'єктній змінній
Parent p = 3;
// Приведення до класу-"обгортки"
// Також заборонено
Long a = 5L;
// Універсальне приведення до рядка
// Можливо тільки для оператора +
String s = "true";
Далі, якщо поєднання цих двох типів утворює розширення (примітивних або посилальних типів), то воно буде здійснено автоматично, неявним для розробника чином:
 int i = 10;
long a = i;
Child c = new Child ();
Parent p = c;

Якщо ж поєднання виявляється звуженням, то виникає помилка компіляції, такий перехід не може бути проведений неявно:

 // Приклад викличе помилку компіляції
int i = 10;
short s = i; // помилка! звуження!
Parent p = new Child ();
Child c = p; // помилка! звуження!
Як вже згадувалося, в подібних випадках необхідно виконувати перетворення явно:
 int i = 10;
short s = (short) i;
Parent p = new Child ();
Child c = (Child) p;

Більш докладно явне звуження розглядається нижче.

Тут може викликати здивування наступна ситуація, яка не породжує помилок компіляції:

 byte b = 1;
short s = 2 +3;
char c = (byte) 5 + 'a';

У першому рядку змінної типу byte присвоюється значення цілочисельного літерала типу int, що є звуженням. У другому рядку змінної типу short присвоюється результат складання двох літералів типу int, а тип цієї суми також int. Нарешті, в третьому рядку змінної типу char присвоюється результат складання числа 5, наведеного до типу byte, і символьного літерала.

Проте всі ці приклади коректні. Для зручності розробника компілятор проводить додатковий аналіз при присвоєнні значень змінним типу byte, short і char. Якщо таким змінним присвоюється величина типу byte, short, char або int, причому її значення може бути отримано вже на момент компіляції, і виявляється, що це значення вкладається в діапазон типу змінної, то явного приведення не потрібно. Якби такої можливості не було, довелося б писати так:

 byte b = (byte) 1;
    // Перетворення необов'язково
short s = (short) (2 +3);
   // Перетворення необов'язково
char c = (char) ((byte) 5 + 'a');
   // Перетворення необов'язково перетворення необхідно, так як число 200 не вкладається в тип byte
byte b2 = (byte) 200;

Виклик методу

Це приведення виникає у разі, коли викликається метод з оголошеними параметрами одних типів, а при виклику передаються аргументи інших типів. Оголошення методів розглядається в наступних лекціях курсу, однак такий простий приклад цілком зрозумілий:

 // Оголошення методу з параметром типу long
void calculate (long l) {
   ...
}
void main () {
   calculate (5);
}

Як видно, при виклику методу передається значення типу int, а не long, як визначено в оголошенні цього методу.

Тут компілятор робить ті ж кроки, що і при приведенні в процесі присвоєння значень змінним. Якщо типи утворюють заборонене перетворення, виникне помилка.

 // Приклад викличе помилку компіляції
void calculate (long a) {
   ...
}
void main () {
   calculate (new Long (5));
   // Тут буде помилка
}

Якщо звуження, то компілятор не зможе здійснити приведення і будуть потрібні явні вказівки.

 void calculate (int a) {
   ...
}

void main () {
   long a = 5;
   // Calculate (a);
   // Звуження! тут буде помилка.
   calculate ((int) a); / / коректний виклик
}

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

Треба відзначити, що, на відміну від ситуації присвоєння, при виклику методів компілятор не виробляє перетворень примітивних значень від byte, short, char або int до byte, short або char. Це призвело б до ускладнення роботи з перевантаженими методами. Наприклад:

 // Приклад викличе помилку компіляції
// Оголошуємо перевантажені методи
// З аргументами (byte, int) і (short, short)
int m (byte a, int b) {return a + b;}
int m (short a, short b) {return a-b;}
void main () {
   print (m (12, 2)); / / помилка компіляції!
}

У цьому прикладі компілятор видасть помилку, так як при виклику аргументи мають тип (int, int), а методу з такими параметрами немає. Якби компілятор проводив перетворення для цілих величин, подібно до ситуації з присвоєнням значень, то приклад став би коректним, але довелося б докладати додаткові зусилля, щоб вказати, який із двох можливих перевантажених методів хотілося б викликати.

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

 long get () {
   return 5;
}

Хоча у вираженні return вказаний цілочисельний літерал типу int, у всіх місцях, де буде викликаний цей метод, буде отримано значення типу long. Для такого перетворення діють ті ж правила, що і для присвоєння значення.

На закінчення розглянемо приклад, що включає в себе всі розглянуті випадки перетворення:

  short get (Parent p) {
   return 5 + 'A';
   // Приведення при поверненні значення
}
void main () {
   long a = 5L;
   // Приведення при присвоєнні значення
   get (new Child ());
   // Приведення при виклику методу
} 

 Явне приведення 

Явне приведення вже багаторазово використовувалося в прикладах. При такому перетворенні ліворуч від вираження, тип значення якого необхідно перетворити, в круглих дужках вказується цільової тип. Якщо перетворення пройде успішно, то результат буде точно зазначеного типу. Приклади:

 (Byte) 5
(Parent) new Child ()
(Flat) getCity (). GetStreet (
      ). GetHouse (). GetFlat ()

Якщо комбінація типів утворює заборонене перетворення, виникає помилка компіляції. Допускаються тотожні перетворення, розширення простих і об'єктних типів, звуження простих і об'єктних типів. Перші три завжди виконуються успішно. Останні два можуть стати причиною помилки виконання, якщо значення виявилися несумісними. Як наслідок, вираз null завжди може бути успішно перетворено до будь-якого вказівного типу. Але можна знайти спосіб все-таки закодувати заборонене перетворення.

 Child c = new Child ();
// Child2 c2 = (Child2) c;
// Заборонене перетворення
Parent p = c; // розширення
Child2 c2 = (Child2) p; // звуження

Такий код буде успішно скомпільований, однак, зрозуміло, при виконанні він завжди буде генерувати помилку в останньому рядку. "Обманювати" компілятор сенсу немає.

Оператор конкатенації рядків

Цей оператор уже розглядався досить докладно. Якщо обома його аргументами є рядки, то відбувається звичайна конкатенація. Якщо ж тип String має лише один з аргументів, то другий необхідно перетворити на текст. Це єдина операція, при якій проводиться універсальне приведення будь-якого значення до типу String.

Це одна з властивостей, що виділяють клас String із загального ряду.

Правила перетворення вже були докладно описані в цій лекції, а оператор конкатенації розглядався в лекції "Типи даних".

Невеликий приклад:

 int i = 1;
double d = i / 2.;
String s = "text";
print ("i =" + i + ", d =" + d + ", s =" + s);

Результатом буде:

 i = 1, d = 0.5, s = text

Числове розширення

Нарешті, останній вид перетворень застосовується при числових операціях, коли потрібно привести аргумент(и) до типу довжиною в 32 або 64 біта для проведення обчислень. Таким чином, при числовому розширенні здійснюється тільки розширення примітивних типів.

Розрізняють унарний і бінарне числове розширення.

Унарне числове розширення

Це перетворення розширює примітивні типи byte, short або char до типів int за правилами розширення примітивних типів.

Унарні числове розширення може виконуватися при наступних операціях:

  • унарні операції + і -;
  • бітове заперечення ~;
  • операції бітового зсуву <<,>>,>>>.

Оператори зсуву мають два аргументи, але вони розширюються незалежно один від одного, тому дане перетворення є унарним. Таким чином, результат виразу 5 <<3L має тип int. Взагалі, результат операторів зсуву завжди має тип int або long.

Приклади роботи всіх цих операторів з урахуванням розширення докладно розглядалися в попередніх лекціях.

Бінарне числове розширення

Це перетворення розширює всі примітивні числові типи, крім double, до типів int, long, float, double за правилами розширення примітивних типів. Бінарне числове розширення відбувається при числових операторах, що мають два аргументи, за такими правилами:

  • якщо будь-який з аргументів має тип double, то і другий приводиться до double;
  • інакше, якщо будь-який з аргументів має тип float, то і другий приводиться до float;
  • інакше, якщо будь-який з аргументів має тип long, то і другий приводиться до long;
  • інакше обидва аргументи наводяться до int.

Бінарне числове розширення може виконуватися при наступних операціях:

  • арифметичні операції +, -, *, /,%;
  • операції порівняння <, <=,>,> =, ==,! =;
  • бітові операції &, |, ^;
  • в деяких випадках для операції з умовою? :.

Приклади роботи всіх цих операторів з урахуванням розширення докладно розглядалися в попередніх лекціях.

Тип змінної та тип її значення

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

Як вже говорилося, змінна визначається трьома базовими характеристиками: ім'я, тип, значення. Ім'я дається довільним чином і ніяк не позначається на властивостях змінної. А от значення завжди має деякий тип, не обов'язково збігається з типом самої змінної. Тому необхідно розглянути всі можливі типи змінних і з'ясувати, значення яких типів вони можуть мати.

Почнемо з змінних примітивних типів. Оскільки ці змінні дійсно зберігають саме значення, то їх тип завжди точно збігається з типом значення.

Проілюструємо це правило на прикладі:

 byte b = 3;
char c = 'A' +3; long m = b + c;
double d = m-3F;

Тут змінна b буде зберігати значення типу byte після звуження цілочисельного літерала типу int. Змінна c буде зберігати тип char після того, як компілятор здійснить звужуюче перетворення результату підсумовування, який буде мати тип int. Для змінної m виконається розширення результату підсумовування типу від int до типу long. Нарешті, змінна d буде зберігати значення типу double, що вийшло в результаті розширення результату різниці, який має тип float.

Переходимо до вказівних типів. По-перше, значення будь-якої змінної такого типу - посилання, яке може вказувати лише на об'єкти, породжені від тих чи інших класів, і далі обговорюються тільки властивості даних класів. (Також об'єкти можуть породжуватися від масивів, ця тема розглядається в окремій лекції.)

Крім того, вказівна змінна будь-якого типу може мати значення null. Більшість дій над такої змінної, наприклад, звернення до полів чи методів, призведе до помилки.

Отже, який зв'язок між типом посилальної змінної та її значенням? Тут головне обмеження - перевірка компілятора, який стежить, щоб усі дії, які виконуються над об'єктом, були коректні. Компілятор не може передбачити, на об'єкт якого класу буде реально посилатися та чи інша змінна. Все, що вона має, - тип самої змінної. Саме його і використовує компілятор для перевірок. А значить, всі допустимі значення змінної повинні гарантовано мати властивості, визначеними у класі-типі цієї змінної. Таку гарантію дає тільки спадкування. Звідси отримуємо правило: вказівна змінна типу A може вказувати на об'єкти, породжені від самого типу A або його спадкоємців.

 Point p = new Point ();

У цьому прикладі змінна і її значення однакового типу, тому над об'єктом можна здійснювати всі можливі для даного класу дії.

 Parent p = new Child ();

Таке привласнення коректно, так як клас Child породжений від Parent. Однак тепер допустимі дії над змінної p, а значить, над об'єктом, тільки що створеним на основі класу Child, обмежені можливостями класу Parent. Наприклад, якщо в класі Child визначений якийсь новий метод newChildMethod (), то спроба його викликати p.newChildMethod () буде породжувати помилку компіляції. Необхідно підкреслити, що ніяких змін з самим об'єктом не відбувається, обмеження породжується використовуваним способом доступу до цього об'єкта - змінної типу Parent.

Щоб показати, що об'єкт не втратив ніяких властивостей, зробимо наступне звернення:

 ((Child) p). NewChildMethod ();

Тут на початку проводиться явне звуження до типу Child. Під час виконання програми JVM перевірить, чи сумісний тип об'єкта, на який посилається змінна p, з типом Child. У нашому випадку це саме так. У результаті виходить посилання типу Child, тому стає допустимим виклик методу newChildMethod (), який викликається в об'єкта, створеного в попередньому рядку.

Звернемо увагу на важливий окремий випадок - змінна типу Object може посилатися на об'єкти будь-якого типу.

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

Таблиця 4.1. Цілочисельні типи даних.

Тип змінної

Допустимі типи її значень

Примітивний

В точності співпадає з типом змінної

Вказівний

  • null
  • в точності співпадають з типом змінної
  • класи-наслідники від типу змінної

Object

  • null
  • будь-який вказівний

Висновок

У цій лекції були розглянуті правила роботи з типами даних в строго типізованій мові Java. Оскільки компілятор суворо відстежує тип кожної змінної і кожного виразу, в разі зміни цього типу необхідно чітко розуміти, які дії припустимі, а які ні, з точки зору компілятора і віртуальної машини.

Були розглянуті всі види приведення типів в Java, тобто перехід від одного типу до іншого. Вони розбиваються на 7 груп, починаючи з тотожного і закінчуючи забороненими. Основні 4 виду визначаються звужуючими або розширюють переходами між простими або вказівними типами. Важливо пам'ятати, що при явному звуженні числових типів старші біти просто відкидаються, що деколи призводить до несподіваного результату. Що стосується перетворення вказівних значень, то тут діє правило - перетворення ніколи не породжує нових і не змінює існуючих об'єктів. Змінюється лише спосіб роботи з ними.

Особливим у Java є перетворення до рядка.

Потім були розглянуті всі ситуації в програмі, де можуть відбуватися перетворення типів. Перш за все, це присвоєння значень, коли перетворення часто відбувається непомітно для програміста. Виклик методу багато в чому схожий на ініціалізацію. Явне приведення дозволяє здійснити бажаний перехід в тому випадку, коли компілятор не дозволяє зробити це неявно. Перетворення при виконанні числових операцій істотно впливає на результат.

На закінчення була розглянута зв'язок між типом змінної і типом її значення.

загрузка...
Теми розділу
Сторінки, близькі за змістом