Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Скачиваний:
33
Добавлен:
14.04.2015
Размер:
592.9 Кб
Скачать

1.7. Недостатки ооп

Документирование классов - задача более трудная, чем это было в случае процедур и

модулей. Поскольку любой метод может быть переопределен, в документации должно

говориться не только о том, что делает данный метод, но также и о том, в каком контексте

он вызывается. Ведь переопределенные методы обычно вызываются не клиентом, а самим

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

вызывается данный метод. Для абстрактных методов, которые пусты, в документации

должно даже говориться о том, для каких целей предполагается использовать

переопределяемый метод.

над данными. Зато количество методов намного выше. Короткие методы обладают тем

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

обработки сообщения иногда "размазан" по многим маленьким методам.

Абстракцией данных не следует злоупотреблять. Чем больше данных скрыто в недрах

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

клиентам не разрешается знать о тех или иных данных, а то, что клиентам для работы с

классом этих данных знать не требуется.

Часто можно слышать, что ООП является неэффективным. Как же дело обстоит в

действительности? Мы должны четко проводить грань между неэффективностью на этапе

выполнения, неэффективностью в смысле распределения памяти и неэффективностью,

связанной с излишней универсализацией.

1. Неэффективность на этапе выполнения. В языках типа Smalltalk сообщения

интерпретируются во время выполнения программы путем осуществления поиска их

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

медленный процесс. И даже при использовании наилучших методов оптимизации

Smalltalk-программы в десять раз медленнее оптимизированных C-программ.

+ посылка сообщения приводит

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

сообщения выполняются лишь на 10% медленнее, чем обычные процедурные вызовы.

И поскольку сообщения встречаются в программе гораздо реже других операций, их

воздействие на время выполнения влияния практически не оказывает.

Однако существует другой фактор, который влияет на время выполнения: это

инкапсуляция данных. Рекомендуется не предоставлять прямой доступ к полям класса,

а выполнять каждую операцию над данными через методы. Такая схема приводит к

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

Однако, когда инкапсуляция используется только там, где она необходима (т.е. в

случаях, где это становится преимуществом), то замедление вполне приемлемое.

2. Неэффективность в смысле распределения памяти. Динамическое связывание и

проверка типа на этапе выполнения требуют по ходу работы информации о типе

объекта. Такая информация хранится в дескрипторе типа, и он выделяется один на

класс.

3. Излишняя универсальность. Неэффективность может также означать, что программа

имеет ненужные возможности. В библиотечном классе часто содержится больше

методов, чем это реально необходимо. А поскольку лишние методы не могут быть

удалены, то они становятся мертвым грузом. Это не воздействует на время выполнения,

но влияет на возрастание размера кода.

Одно из возможных решений - строить базовый класс с минимальным числом методов,

а затем уже реализовывать различные расширения этого класса, которые позволят

нарастить функциональность.

Другой подход - дать возможность компоновщику удалять лишние методы. Такі

интеллектуальные компоновщики уже доступны для различных языков и операционных

систем.

Но нельзя утверждать, что ООП неэффективно. Если классы используются лишь там, где

это действительно необходимо, то потеря эффективности из-за повышенного расхода

памяти и меньшей производительности незначительна. Кроме того, часто более важной

является надежность программного обеспечения и небольшое время его написания, а не

производительность.

.

2

ava є строго універсальна мова. Це означає, що будь-яка змінна і

будь-який вираз мають відомий тип ще на момент компіляції. Таке суворе правило

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

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

bugs) необхідно спочатку виявити тестуванням (що може зажадати вельми

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

розуміння моделі типів даних в Java сильно допомагає в написанні якісних

програм.

Всі типи даних поділяються на дві групи. Першу складають 8 простих або

примітивних (від англійського primitive) типів даних. Вони поділяються на три підгрупи:

• цілочисельні

- Byte

- Short

- Int

- Long

- Char (також є цілочисловим типом)

• дробові

- Float

- Double

• булевський

- Boolean

Другу групу складають об'єктні або посилальні (від англійського reference) типи

даних. Це всі класи, інтерфейси і масиви. У стандартних Бібліотекою перших

версій Java перебувало кілька сотень класів та інтерфейсів, зараз їх вже тисячі.

Крім стандартних, написані багато і багато класи та інтерфейси, що становлять

будь-яку Java програму.

Ілюструвати логіку роботи з типами даних найпростіше на прикладі змінних.

2. Змінні

Змінні використовуються в програмі для зберігання даних. Будь-яка змінна має

три базові характеристики:

• ім'я;

• тип;

• значення.

Ім'я унікально ідентифікує змінну і дозволяє до неї звертатися в програмі.

Тип описує, які величини може зберігати змінна. Значення - поточна величина,

зберігається у змінній на даний момент.

Робота зі змінною завжди починається з її оголошення (declaration). Звичайно, воно має

включати в себе ім'я оголошуваної змінної. Як було сказано, в Java будь-яка змінна

має строгий тип, який також задається при оголошенні і ніколи не змінюється. Значення

може бути зазначено відразу (це називається ініціалізацією), а в більшості випадків

завдання початкової величини можна і відкласти.

Деякі приклади оголошення змінних примітивного типу int з ініціалізатор

і без таких:

int a;

int b = 0, c = 3 +2;

int d = b + c;

int e = a = 5;

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

арифметичне вираз. Іноді цей вислів може бути обчислено під час

компіляції (таке як 3 +2), тоді компілятор відразу записує результат. Іноді це

дія відкладається на момент виконання програми (наприклад, b + c). В останньому

випадку кільком змінним присвоюється одне і те ж значення, однак оголошується

лише перша з них (в даному прикладі е), інші вже повинні існувати.

Резюмуємо: оголошення змінних і можлива ініціалізація при оголошенні

описуються наступним чином. Спочатку вказується тип змінної, потім її ім'я

і, якщо необхідно, ініціалізатор, який може бути константою або виразом,

обчислюваним під час компіляції або виконанні програми. Зокрема, можна

користуватися і вже оголошеними змінними. Далі можна поставити кому і

оголосити нову змінну точно такого ж типу.

Після оголошення змінна може бути використана в різних виразах, в

яких буде братися її поточне значення. Також в будь-який момент можна змінити

значення, використовуючи оператор присвоювання, приблизно так само, як це робилося в

ініціалізатор.

Крім того, при оголошенні змінної може бути використано ключове слово final.

Його вказують перед типом змінної, і тоді її необхідно відразу ініціалізувати

і вже більше ніколи не міняти її значення. Таким чином, final-змінні стають

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

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

Найпростіший приклад оголошення final-змінної:

final double pi = 3.1415;

3. Примітивні і посилальні типи даних

Тепер на прикладі змінних можна проілюструвати різницю між примітивними

і посилальними типами даних. Розглянемо приклад, коли оголошуються 2 змінні

одного типу, прирівнюються друг другу, а потім значення однієї з них змінюється. Що

відбудеться з другої змінної?

Візьмемо простий тип int:

int a = 5; / / оголошуємо першу змінну і инициализируем її

int b = a; / / оголошуємо другу змінну, і прирівнюємо її до першої

a = 3; / / змінюємо значення першої

print (b); / / перевіряємо значення другої

Тут і далі ми вважаємо, що функція print (...) дозволяє нам деяким (не важливо, яким

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

функцію зі стандартної бібліотеки System.out.println (...), яка виводить значення на

системну консоль).

В результаті ми побачимо, що значення змінної b не змінилося, воно залишилося рівним

5. Це означає, що змінні простого типу зберігають безпосередньо свої значення,

і при прирівнювання двох змінних відбувається копіювання цього значення. Щоб

ще раз підкреслити цю особливість наведемо ще один приклад:

byte b = 3;

int a = b;

У даному прикладі відбувається перетворення типів (перетворення докладно

розглядається в спеціальному розділі). Для нас зараз важливо констатувати, що

змінна b зберігає значення 3 типи byte, а змінна a - значення 3 типи int. Це два

різних значення, і у другому рядку при присвоєнні сталося копіювання.

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

на деякі об'єкти. Розглянемо для прикладу клас, що описує точку на

координатної площини з цілочисельними координатами. Опис класу - це окрема

тема, але в нашому простому випадку воно тривіально:

class Point {

int x, y;

}

Тепер складемо приклад, аналогічний наведеному вище для int-змінних, вважаючи,

що вираз new Point (3,5) створює новий об'єкт-точку з координатами (3,5).

Point p1 = new Point (3,5);

Point p2 = p1;

p1.x = 7;

print (p2.x);

У третьому рядку ми змінили горизонтальну координату точки, на яку посилалася

змінна p1, і тепер нас цікавить, як це позначилося на точці, на яку посилається

змінна p2. Провівши такий експеримент, можна переконатися, що цього разу ми побачимо

оновлене значення. Тобто об'єктні змінні після прирівнювання залишаються

"Пов'язаними" один з одним, зміни однієї позначаються на інший.

Таким чином, примітивні змінні є дійсними сховищами

даних. Кожна змінна має значення, не залежне від інших. Посилальні ж

змінні зберігають лише посилання на об'єкти, причому різні змінні можуть

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

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

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

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

об'єктом:

Point p1 = new Point (3,5);

Point p2 = p1;

p1 = new Point (7,9);

print (p2.x);

У цьому прикладі ми отримаємо 3, тобто після третього рядка змінні p1 і p2 посилаються

на різні об'єкти і тому мають різні значення.

Тепер легко зрозуміти сенс літерала null. Таке значення може прийняти мінлива

будь-якого посилального типу. Це означає, що її посилання нікуди не вказує, об'єкт

відсутня. Відповідно, будь-яка спроба звернутися до об'єкта через таку змінну

(Наприклад, викликати метод або взяти значення поля) призведе до помилки.

Так само значення null можна передати в якості будь-якого об'єктного аргументу при виклику

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

Пам'ять у Java з точки зору програміста представляється не нулями і одиницями або

набором байтів, а як якесь віртуальний простір, в якому існують об'єкти.

І доступ до пам'яті здійснюється не з фізичного адресою або вказівником, а лише через

посилання на об'єкти. Посилання повертається при створенні об'єкта і далі може бути

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

допускається наявність декількох посилань на один об'єкт. Можлива і протилежна

ситуація - коли на якийсь об'єкт не існує жодного посилання. Такий об'єкт вже не

доступний програмі і є "сміттям", тобто марно займає апаратні

ресурси. Для їх звільнення не потрібно ніяких зусиль. До складу будь віртуальної

машини обов'язково входить автоматичний збирач сміття garbage collector - фоновий

процес, який якраз і займається знищенням непотрібних об'єктів.

Дуже важливо пам'ятати, що об'єктна змінна, на відміну від примітивної, може мати

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

змінної - якийсь клас, то змінна може посилатися на об'єкт, породжений від

спадкоємця цього класу. Всі випадки подібного розбіжності будуть розглянуті в

наступних розділах курсу.

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

3.1. Примітивні типи

Як уже говорилося, існує 8 простих типів даних, які діляться на

цілочисельні (integer), дробові (floating-point) і Булевського (boolean).

3.2. Цілочисельні типи

Цілочисельні типи - це byte, short, int, long, також до них відносять і char. Перші чотири

типу мають довжину 1, 2, 4 і 8 байт відповідно, довжина char - 2 байта, що безпосередньо

випливає з того, що всі символи Java описуються стандартом Unicode. Довжини типів

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

представляється віртуальною, і вирахувати, скільки фізичних ресурсів займе та чи

інша змінна так прямолінійно не вийде.

4 основних типи є знаковими. char доданий до цілочисловим типам даних, так

як з точки зору JVM символ і його код - взаімооднозначное поняття. Звичайно, код

символу завжди позитивний, тому char - єдиний беззнаковий тип.

Ініціалізувати його можна як символьним, так і цілочисловим літералів. У всьому

іншому char - повноцінний числовий тип даних, який може брати участь, наприклад,

в арифметичних діях, операціях порівняння і т.п. Нижче у таблиці зведені дані

по всіх розібраним типами:

Назва типу

Довжина (байти)

Область значень

byte

1

-128 .. 127

short

2

-32.768 .. 32.767

int

4

-2.147.483.648 .. 2.147.483.647

long

8

-9.223.372.036.854.775.808 ..

9.223.372.036.854.775.807 (приблизно

1019)

char

2

'\ U0000' .. '\ Uffff', або 0 .. 65.535

Зверніть увагу, що int вміщує приблизно 2 мільярди, а тому підходить у багатьох

випадках, коли не потрібні надвеликі числа. Щоб уявити собі розміри типу

long, зазначимо, що саме він використовується в Java для відліку часу. Як і в багатьох

мовах, час відраховується від 1 січня 1970 року в мілісекундах. Так от, місткість

long дозволяє відраховувати час протягом мільйонів століть (!), причому як в майбутнє,

так і в минуле.

Чому було звернуто увагу саме на ці два типи, int і long? Справа в тому, що

цілочисельні літерали мають тип int за замовчуванням або тип long, якщо стоїть буква L

або l. Саме тому коректним літералів вважається тільки таке число, яке

укладається в 4 або 8 байт відповідно. Інакше компілятор вважає це помилкою. Таким

чином, наступні літерали є коректними:

1

-2147483648

2147483648L

0L

111111111111111111L

Над цілочисельними аргументами можна робити наступні операції:

• операції порівняння (повертають булеве значення)

- <, <=,>,> =

- ==,! =

• числові операції (повертають числове значення)

- Унарні операції + і -

- Арифметичні операції +, -, *, /,%

- Операції інкремента і декремента (в префіксной і постфіксной формі): + + і -

- Операції бітового зсуву <<, >>, >>>

- Бітові операції, &, |, ^

• оператор з умовою? :

• оператор приведення типів

• оператор конкатенації з рядком +

Оператори порівняння цілком очевидні і особливого розгляду не вимагають. Їх результат

завжди булевского типу (true або false).

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

Єдине уточнення можна зробити щодо операторів + і -, які можуть

бути як бінарними (мати два операнда), так і унарний (мати один операнд). Бінарні

операнди є операторами додавання і віднімання відповідно. Унарний оператор

+ Повертає значення, рівне аргументу (+ x завжди дорівнює x). Унарний оператор -,

застосований до значення x, повертає результат, рівний 0-x. Несподіваний ефект

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

примітивного типу.

int x = -2147483648; / / Найменше можливе значення типу int

int y =-x;

Тепер значення змінної y насправді одно не 2147483648, оскільки таке число

не вкладається в область значень типу int, а в точності дорівнює значенню x! Іншими

словами в цьому прикладі вираз-x == x істинно!

Справа в тому, що якщо при виконанні числових операцій над цілими числами виникає

переповнення, і результат не може бути збережений в даному примітивному типі, то Java

не створює ніяких помилок. Замість цього всі старші біти, які перевищують

місткість типу, просто відкидаються. Це може привести не тільки до втрати точної

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

біта виявиться протилежне значення.

int x = 300000;

print (x * x);

Результатом такого прикладу буде:

-194313216

Повертаючись до інвертування числа -2147483648, стає ясно, що математичний

результат дорівнює в точності +231 або, в двійковому форматі, 10 .... 0 (одиниця і 31 нуль).

Але тип int розглядає першу одиницю як знаковий біт, і результат виходить рівним

-2147483648.

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

використовуваних типів, призводить до помилки компіляції (див. розділ "Лексика"). Якщо ж

переповнення виникає в результаті виконання операції, "зайві" біти просто

відкидаються.

Підкреслимо, що вираз типу -5 не є цілочисловим літералів. На самому

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

(Наприклад, 2147483648) можуть зустрічатися тільки в поєднанні з унарні оператором -.

Крім цього, числові операції в Java володіють ще однією особливістю. Хоча

цілочисельні типи володіють довжиною в 8, 16, 32 і 64 біта, обчислення проводяться лише

з 32-х і 64-х бітної точністю. А це означає, що перед обчисленнями може знадобитися

перетворити тип одного або декількох операндів.

Якщо хоча б один аргумент операції має тип long, то всі аргументи наводяться до цього

типу, і результат операції також буде типу long. Обчислення буде вироблено з

точністю в 64 біта, а більш старші біти, якщо такі з'являються в результаті,

відкидаються.

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

всі аргументи перетворюються в int (це відноситься до byte, short, char). Результат також має

тип int. Всі біти старше 32-го ігноруються.

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

приклад:

int i = 300000;

print (i * i); / / множення з точністю 32 біта

long m = i;

print (m * m); / / множення з точністю 64 біта

print (1 / (mi)); / / спробуємо отримати різницю значень int і long

Результатом такого прикладу буде:

-194313216

90000000000

потім ми отримаємо помилку ділення на нуль, оскільки змінні i і m хоч і різних

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

множення проводилося з точністю в 32 біта, більш старші біти були відкинуті.

Друге - з точністю в 64 бита, відповідь не спотворився.

Питання приведення типів, і в тому числі спеціальний оператор для такої дії,

докладно розглядається в наступних розділах. Однак тут хотілося б відзначити

кілька прикладів, які не настільки очевидні, і можуть створити проблеми при написанні

програм. По-перше, підкреслимо, що результатом операції з цілочисельними

аргументами завжди є ціле число. А значить в наступному прикладі

double x = 1/2;

змінної x буде присвоєно значення 0, а не 0.5, як можна було б очікувати. Детально

операції з дробовими аргументами розглядаються нижче, але щоб отримати значення

0.5 достатньо написати 1. / 2 (тепер перший аргумент дробовий, і результат не буде

заокруглений).

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

скільки мілісекунд міститься в тижні і у місяці:

print (1000 * 60 * 60 * 24 * 7); / / обчислення для тижні

print (1000 * 60 * 60 * 24 * 30); / / обчислення для місяця

Необхідно перемножити кількість мілісекунд в одній секунді (1000), секунд - в

хвилині (60), хвилин - в годині (60), годин - в дні (24), і днів в тижні і місяці (7 і 30

відповідно). Отримуємо:

604800000

-1702967296

Очевидно, у другому обчисленні сталося переповнення. Достаточно сделать последний

аргумент величиной типа long:

print(1000*60*60*24*30L); // вычисление для месяца

Получаем правильный результат:

2592000000

Подобные вычисления разумно переводить на 64-битную точность не на последней

операции, а заранее, чтобы избежать переполнения.

Понятно, что типы большей длины могут хранить больший спектр значений, а потому Java

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

такие строки вызовут ошибку компиляции:

// пример вызовет ошибку компиляции

int x=1;

byte b=x;

Хотя программисту и очевидно, что переменная b должна получить значение 1, что легко

укладывается в тип byte, однако компилятор не может вычислять значение переменной x

при обработке второй строки, он знает лишь, что ее тип int.

А вот менее очевидный пример:

// пример вызовет ошибку компиляции

byte b=1;

byte c=b+1;

И здесь компилятор не сможет успешно завершить работу. При операции сложения

значение переменной b будет преобразовано в тип int, и таким же будет результат сложения,

а значит, его нельзя так просто присвоить переменной типа byte.

Аналогично:

// пример вызовет ошибку компиляции

int x=2;

long y=3;

int z=x+y;

Здесь результат сложения будет уже типа long. Точно также некорректна такая

инициализация:

// пример вызовет ошибку компиляции

byte b=5;

byte c=-5;

Даже унарный оператор - проводит вычисления с точностью не меньше 32 бит.

byte b=1;

byte c=(byte)-b;

Итак, все числовые операторы возвращают результат типа int или long. Однако есть два

исключения.

Во-первых, это операторы инкрементации и декрементации. Их действие заключается в

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

сохраняется в этой переменной, и значение всей операции равно значению переменной

(до или после изменения в зависимости от того, является оператор префиксным или

постфиксным). А значит, и тип значения совпадает с типом переменной. (На самом деле

вычисления все равно производятся с точностью минимум 32 бита, однако при присвоении

результата переменной его тип понижается.)

byte x=5;

byte y1=x++; // на момент начала исполнения x равен 5

byte y2=x--; // на момент начала исполнения x равен 6

byte y3=++x; // на момент начала исполнения x равен 5

byte y4=--x; // на момент начала исполнения x равен 6

print(y1);

print(y2);

print(y3);

print(y4);

В результате получаем:

5

6

6

5

Никаких проблем с присвоением результата операторов ++ и -- переменным типа byte.

Завершая рассмотрение этих операторов, приведем еще один пример:

byte x=-128;

print(-x);

byte y=127;

print(++y);

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

128

-128

Этот пример иллюстрирует вопросы преобразования типов при вычислениях и случаи

переполнения.

Вторым исключением является оператор с условием ? :. Если второй и третий операнды

имеют одинаковый тип, то и результат операции будет такого же типа.

byte x=2;

byte y=3;

byte z=(x>y) ? x : y; // верно, x и y одинакового типа

byte abs=(x>0) ? x : -x; // неверно!

Последняя строка неверна, так как третий аргумент содержит числовую операцию, стало

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

если второй аргумент имеет тип byte, а третий - short, значение будет типа int.

Наконец, рассмотрим оператор конкатенации со строкой. Оператор + может принимать в

качестве аргумента строковые величины. Если одним из аргументов является строка, а

вторым - целое число, то число будет преобразовано в текст, и строки объединятся.

int x=1;

print("x="+x);

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

x=1

Обратите внимание на следующий пример:

print(1+2+"text");

print("text"+1+2);

Его результатом будет:

3text

text12

Отдельно рассмотрим работу с типом char. Значения этого типа могут полноценно

участвовать в числовых операциях:

char c1=10;

char c2='A'; // латинская буква A (\u0041, код 65)

int i=c1+c2-'B';

Переменная i получит значение 9.

Рассмотрим следующий пример:

char c='A';

print(c);

print(c+1);

print("c="+c);

print('c'+'='+с);

Его результатом будет:

A

66

c=A

225

В первом случае в метод print было передано значение типа char, поэтому отобразился

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

появилось на экране. Далее при сложении со строкой тип char был преобразован в текст

в виде символа. Наконец в последней строке произошло сложение трех чисел: 'c' (код 99),

'=' (код 61) и переменной c (т.е. код 'A' - 65).

Для каждого примитивного типа существуют специальные вспомогательные классы-обертки

(wrapper classes). Для типов byte, short, int, long, char это Byte, Short, Integer, Long, Character.

Эти классы содержат многие полезные методы для работы с целочисленными значениями.

Например, преобразование из текста в число. Кроме этого, есть класс Math, который хоть

и предназначен в основном для работы с дробными числами, но также предоставляет

некоторые возможности и для целых.

В заключение подчеркнем, что единственные операции с целыми числами, при которых

Java генерирует ошибки - это деление на ноль (операторы / и %).

4. Дробные типы

Дробные типы - это float и double. Их длины - 4 и 8 байт соответственно. Оба типа знаковые.

Ниже в таблице сведены их характеристики:

Название типа

Длина (байты)

Область значений

float

4

3.40282347e+38f ; 1.40239846e-45f

double

8

1.79769313486231570e+308 ;

4.94065645841246544e-324

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

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

насколько можно приблизиться к нулю, другими словами - каково наименьшее

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

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

нельзя задать литерал, значение которого по модулю слишком мало для данного типа,

компилятор сгенерирует ошибку underflow.

// пример вызовет ошибку компиляции

float f = 1e40f; // значение слишком велико, overflow

double d = 1e-350; // значение слишком мало, underflow

Напомним, что если в конце литерала стоит буква F или f, то литерал рассматривается

как значение типа float. По умолчанию дробный литерал имеет тип double, при желании

это можно подчеркнуть буквой D или d.

Над дробными аргументами можно производить следующие операции:

• операции сравнения (возвращают булевское значение)

- <, <=, >, >=

- ==, !=

• числовые операции (возвращают числовое значение)

- унарные операции + и -

- арифметические операции +, -, *, /, %

- операции инкремента и декремента (в префиксной и постфиксной форме): ++ и --

• оператор с условием ? :

• оператор приведения типов

• оператор конкатенации со строкой +

Практически все операторы действуют по тем же принципам, что и для целочисленных

операторов (оператор деления с остатком % рассматривался в предыдущей главе, а

операторы ++ и -- также увеличивают или уменьшают значение переменной на единицу).

Уточним лишь, что операторы сравнения корректно работают и в случае сравнения

целочисленных значений с дробными. Таким образом, в основном необходимо рассмотреть

вопросы переполнения и преобразования типов при вычислениях.

Для дробных вычислений появляется уже два типа переполнения - overflow и underflow.

Тем не менее, Java и здесь никак не сообщает о возникновении подобных ситуаций. Немає

ни ошибок, ни других способов обнаружить их. Более того, даже деление на ноль не

приводит к некорректной ситуации. А значит, дробные вычисления вообще не порождают

никаких ошибок.

Такая свобода связана с наличием специальных значений дробного типа. Они определяются

спецификацией IEEE 754 и уже перечислялись в разделе "Лексика":

• положительная и отрицательная бесконечности (positive/negative infinity);

• значение "не число", Not-a-Number, или сокращенно NaN;

• положительный и отрицательный нули.

Все эти значения представлены как для типа float, так и для double.

Положительную и отрицательную бесконечности можно получить следующим образом:

1f/0f // положительная бесконечность, тип float

-1d/0d // отрицательная бесконечность, тип double

Также в классах Float и Double определены константы POSITIVE_INFINITY и NEGA-

TIVE_INFINITY. Как видно из примера, такие величины получаются при делении конечных

величин на ноль.

Значение NaN можно получить, например, в результате следующих действий:

0.0/0.0 // деление ноль на ноль

(1.0/0.0)*0.0 // умножение бесконечности на ноль

Эта величина также представлена константами NaN в классах Float и Double.

Величины положительный и отрицательный ноль записываются очевидным образом:

0.0 // дробный литерал со значением положительного нуля

+0.0 // унарная операция +, ее значение - положительный ноль

-0.0 // унарная операция -, ее значение - отрицательный ноль

Все дробные значения строго упорядочены. Отрицательная бесконечность меньше любого

другого дробного значения, положительная - больше. Значения +0.0 и -0.0 считаются

равными, то есть выражение 0.0==-0.0 истинно, а 0.0>-0.0 - ложно. Однако другие операторы

различают их, например, выражение 1.0/0.0 дает положительную бесконечность, а 1.0/-

0.0 - отрицательную.

Единственное исключение - значение NaN. Если хотя бы один из аргументов операции

сравнения равняется NaN, то результат заведомо будет false (для оператора !=

соответственно всегда true). Таким образом, единственное значение x, при котором

выражение x!=x истинно, именно NaN.

Возвращаемся к вопросу возникновения переполнения в числовых операциях. Якщо

получаемое значение слишком велико по модулю (overflow), то результатом будет

бесконечность соответствующего знака.

print(1e20f*1e20f);

print(-1e200*1e200);

В результате получаем:

Infinity

-Infinity

Если результат, напротив, получается слишком мал (underflow), то он просто округляется

до нуля. Также поступают и в случае, когда количество десятичных знаков превышает

допустимое количество:

print(1e-40f/1e10f); // underflow для float

print(-1e-300/1e100); // underflow для double

float f=1e-6f;

print(f);

f+=0.002f;

print(f);

f+=3;

print(f);

f+=4000;

print(f);

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

0.0

-0.0

1.0E-6

0.002001

3.002001

4003.002

Как видно, в последней строке был утрачен 6-й разряд после десятичной точки.

Другой пример (из спецификации языка Java):

double d = 1e-305 * Math.PI;

print(d);

for (int i = 0; i < 4; i++)

print(d /= 100000);

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

3.141592653589793E-305

3.1415926535898E-310

3.141592653E-315

3.142E-320

0.0

Таким образом, как и для целочисленных значений, явное выписывание в коде литералов,

которые слишком велики (overflow) или слишком малы (underflow) для используемых типов,

приводит к ошибке компиляции (см. главу "Лексика"). Если же переполнение возникает в

результате выполнения операции, то возвращается одно из специальных значений.

Теперь перейдем к преобразованию типов. Если хотя бы один аргумент имеет тип double,

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

иметь тип double. Вычисление будет произведено с точностью в 64 бита.

Если же аргументов типа double нет, а хотя бы один аргумент имеет тип float, то все

аргументы приводятся к float, вычисление производится с точностью в 32 бита, и результат

имеет тип float.

Эти утверждения верны и в случае, если один из аргументов целочисленный. Если хотя

бы один из аргументов имеет значение NaN, то и результатом операции будет NaN.

Еще раз рассмотрим простой пример:

print(1/2);

print(1/2.);

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

0

0.5

Достаточно одного дробного аргумента, чтобы результат операции также имел дробный

тип.

Более сложный пример:

int x=3;

int y=5;

print (x/y);

print((double)x/y);

print(1.0*x/y);

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

0

0.6

0.6

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

поскольку оба операнда представлены переменными, в этом примере нельзя просто

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

Необходимо либо преобразовать один из аргументов (второй вывод на экран), либо вставить

еще одну фиктивную операцию с дробным аргументом (последняя строка).

Приведения типов подробно рассматриваются в другой главе, однако обратим здесь

внимание на несколько моментов.

Во-первых, при приведении дробных значений к целым типам, дробная часть просто

отбрасывается. Например, число 3.84 будет преобразовано в целое 3, а -3.84 превратится

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

Math.round(…).

Во-вторых, при приведении значений int к типу float и при приведении значений типа long

к типу float и double возможны потери точности, не смотря на то, что эти дробные типы

вмещают гораздо большие числа, чем соответствующие целые. Рассмотрим следующий

приклад:

long l=111111111111L;

float f = l;

l = (long) f;

print(l);

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

111111110656

Тип float не смог сохранить все значащие разряды, хотя преобразование от long к float

произошло без специального оператора в отличие от обратного перехода.

Для каждого примитивного типа существуют специальные вспомогательные классы-обертки

(wrapper classes). Для типов float и double это Float и Double. Эти классы содержат многие

полезные методы для работы с дробными значениями. Например, преобразование из

текста в число.

Кроме этого, класс Math предоставляет большое количество методов для операций над

дробными значениями, например, извлечение квадратного корня, возведение в любую

степень, тригонометрические и другие. Также в этом классе определены константы PI и

основание натурального логарифма E

2

Імена (names) використовуються в програмі для доступу до оголошеним (declared) раніше

"Об'єктах", "елементів", "конструкціям" мови (всі ці слова-синоніми були використані

тут в їх загальному сенсі, а не як терміни ООП, наприклад). Конкретніше, в Java мають

імена:

• пакети;

• класи;

• інтерфейси;

• елементи (member) посилальних типів:

- Поля;

- Методи;

- Внутрішні класи та інтерфейси;

• аргументи:

- Методів;

- Конструкторів;

- Обробників помилок;

• локальні змінні.

Відповідно, всі вони повинні бути оголошені спеціальним чином, що буде

поступово розглядатися по ходу курсу. Крім цього, також оголошуються конструктори,

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

Нагадаємо, що пакети (packages) в Java - це спосіб логічно групувати класи, що

необхідно, оскільки часто кількість класів у системі становить кілька

тисяч або навіть десятків тисяч. Крім класів та інтерфейсів в пакетах можуть перебувати

вкладені пакети. Синонімами цього слова в інших мовах є бібліотека або

модуль.

2. Імена

2.1. Прості і складені імена. Елементи.

Імена бувають простими (simple), що складаються з одного ідентифікатора (вони

визначаються

під

час

оголошення),

і

складовими

(Qualified),

складаються

з

послідовності ідентифікаторів, розділених крапкою. Для пояснення цих термінів

необхідно розглянути ще одне поняття.

У пакетів і посилальних типів (класів, інтерфейсів, масивів) є елементи (members).

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

пакета і класу, розділених крапкою.

Далі класи та інтерфейси будуть називатися об'єднуючим терміном тип (type).

Елементами пакета є класи та інтерфейси, що містяться в цьому пакеті, а також

вкладені пакети. Щоб отримати складене ім'я пакета, необхідно до повного імені

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

Наприклад, складене ім'я основного пакета мови Java - java.lang (тобто, просте ім'я

цього пакета lang, і він знаходиться в осяжний пакеті java). Усередині нього є вкладений

пакет, призначений для типів технології reflection, яка згадувалася в попередніх

главах. Просте назву пакета reflect, а, значить, складене - java.lang.reflect.

Просте ім'я класів та інтерфейсів дається при оголошенні, наприклад Object, String, Point.

Щоб отримати складене ім'я таких типів треба до складеного імені пакета, в якому

знаходиться тип, через точку додати просте ім'я типу. Наприклад, java.lang.Object,

java.lang.reflect.Method або com.myfirm.MainClass. Сенс останнього виразу такий:

спочатку йде звернення до пакету com, потім до його елементу - вкладеному пакету myfirm,

а потім до елемента пакета myfirm - класу MainClass. Тут com.myfirm - складене ім'я

пакета, де лежить клас MainClass, а MainClass-просте ім'я цього класу. Складаємо їх

і поділяємо точкою - виходить повне ім'я класу com.myfirm.MainClass.

Для посилальних типів елементами є поля і методи, а також внутрішні типи

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

так і отримані у спадок від батьківських класів та інтерфейсів, якщо такі

є. Просте ім'я елементів також дається при ініціалізації. Наприклад, toString (),

PI, InnerClass. Складене ім'я виходить шляхом об'єднання простого або складеного

імені типу або змінної об'єктного типу з ім'ям елемента. Наприклад, ref.toString (),

java.lang.Math.PI, OuterClass.InnerClass. Інші звернення до елементів посилальних типів

вже неодноразово застосовувалися в прикладах у попередніх розділах.

Соседние файлы в папке Програмне_забезпечення_ОС_ИНФ_5_сем