Към съдържанието

Тема 2 — Референтни типове. Масиви. String. Базов клас Object.

Един от ключовите концептуални преходи при усвояването на Java е разбирането на разликата между прости и референтни типове. В C++ масивите и низовете са синтактична захар около прости указатели; в Java те са пълноправни обекти с методи, гаранции за безопасност и управление от GC (Garbage Collector). Тази глава разглежда масивите, системните класове за работа с низове и базовия клас Object, от който наследява всеки клас в езика.

1. Масиви (Array)

1.1 Организация и деклариране

В Java декларацията на масив създава само референция — паметта за елементите се заделя с new:

// C++:
int A[10]; // алокира памет веднага
// Java:
int[] A; // само референция (стойност null)
A = new int[10]; // алокира масив в heap
A[0] = 5;

Масивите са референтен тип, наследник на Object. При алокация елементите получават нулеви стойности автоматично:

Тип на елементитеНулева стойност
числови типове0
booleanfalse
char' '
референцииnull

1.2 Инициализация

int[] primes = { 2, 3, 5, 7, 11 }; // инициализатор
byte[] bar = new byte[] { 1, 2, 3, 4, 5 }; // алтернативен синтаксис

1.3 Граничен контрол и length

Java проверява всеки индекс автоматично. При излизане извън границите се хвърля ArrayIndexOutOfBoundsException. Полето length е достъпно за всеки масив без допълнителна декларация:

int[] A = new int[10];
System.out.println(A.length); // 10
A = new int[20];
System.out.println(A.length); // 20 — length следва масива, не е константа

1.4 Многомерни и нееднородни масиви

int[][] матрица = new int[4][3]; // редовни 4×3
// Нееднородни редове (jagged arrays):
int[][] jagged = new int[4][];
jagged[1] = new int[4];
jagged[3] = new int[2];
// Двумерен масив от обекти:
Object[][] board = new Object[2][3];
board[0][0] = new Rectangle(0, 1, 2, 3);

1.5 Копиране — System.arraycopy

Простото присвояване B = A копира само референцията, не данните. За реално копиране се ползва:

System.arraycopy(src, srcPos, dst, dstPos, count);
// Пример — копира 5 елемента от A[0..4] в B[0..4]:
int[] A = new int[10];
int[] B = new int[5];
System.arraycopy(A, 0, B, 0, 5);

Особености:

  • При прости типове — задължително типово съответствие между src и dst;
  • При референтни типове — позволено само ако присвояването B[i] = A[i] е валидно; при несъответствие — ArrayStoreException по времена на изпълнение;
  • Работи коректно при съвпадение на source и destination (вътрешно копиране).

2. Капани с референции на масиви

Тъй като масивите са референтни типове, присвояването копира референцията — двете имена сочат към един и същ обект в heap:

int[] A = new int[]{5, 0, 0};
int[] B = A; // B и A сочат едно и също място
B[0] = 2;
// A[0] == 2 !

По-деликатен проблем — ArrayStoreException:

String[] strings = new String[10];
Object[] objects = strings; // String[] е подтип на Object[] — допустимо
objects[0] = new Date(); // ArrayStoreException при изпълнение!
// реалният масив е String[], Date не пасва

3. Низове — String, StringBuilder, StringBuffer

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

КласХарактеристикаКога се използва
StringНепроменяем (immutable)По подразбиране; безопасно предаване
StringBuilderПроменяем, несинхронизиранИзграждане в рамките на метод
StringBufferПроменяем, синхронизиранСподелен между нишки

3.1 Клас String

String е final клас — не може да се наследява. Вътрешно представяне: char[] с final маркировка, което прави обектите неизменяеми след създаването им.

Конструиране:

String quote = "constant string"; // от литерал (string pool)
String characters = new String(charArray); // от char[]
String encoded = new String(data, "ISO8859_1");

Основни операции:

quote.length() // брой символи
"John " + "Smith" // конкатенация с +
"John ".concat("Smith") // еквивалентно
s.charAt(index) // символ на позиция
url.startsWith("http:") // проверка на префикс
abcs.indexOf('p') // индекс на символ (-1 ако не е намерен)
abcs.indexOf("def") // индекс на поднизка
log.contains("emergency") // (Java 5.0+)
str.trim() // премахване на whitespace
"FOO".toLowerCase()
"abcde".substring(2, 5) // "cde"
"xxooxxxoo".replace("xx", "X")

Сравняване — задължително с equals(), никога с ==:

String one = "FOO";
String two = "foo";
one.equals(two); // false — правилно!
one.equalsIgnoreCase(two); // true
one == two; // ГРЕШНО — сравнява референции, не стойности

== проверява дали двете референции сочат един и същ обект в паметта. Два отделни new String("FOO") извиквания създават различни обекти с == == false, дори ако съдържанието е идентично.

Автоматичен StringBuilder при конкатенация:

Компилаторът оптимизира верига от + операции до StringBuilder:

String foo = "To " + "be " + "or";
// Еквивалентно на:
String foo = new StringBuilder().append("To ").append("be ").append("or").toString();

3.2 StringBuilder и StringBuffer

За многократно изграждане на низ в цикъл задължително се ползва StringBuilderString + в цикъл реалокира нов обект при всяка итерация:

// ГРЕШНО — O(n²) реалокации:
String text = "";
while ((line = readLine()) != null) text += line;
// ПРАВИЛНО — единична алокация с нарастване:
StringBuilder sb = new StringBuilder();
while ((line = readLine()) != null) sb.append(line);
String text = sb.toString();

Специфични методи: append(), insert(), reverse() — предефинирани за различни типове.

4. Базов клас Object

Всеки клас в Java неявно наследява Object. Дефинираните методи образуват минимален договор, задължителен за всеки обект:

МетодОписание
protected Object clone()Плитко копие на обекта
boolean equals(Object o)Сравнение по стойност
protected void finalize()Извиква се от GC при нулиране на всички референции
final Class getClass()Клас по времена на изпълнение
int hashCode()Хеш код за HashMap, HashSet
String toString()Низово представяне

4.1 clone() — плитко и дълбоко копиране

  • Плитко (shallow clone) — копира само полетата; ако поле е референция, двата обекта сочат един и същ вътрешен обект;
  • Дълбоко (deep clone) — рекурсивно копира и съдържанието на референтните полета.

4.2 getClass()

Object obj = new ArrayList();
obj.getClass().getName(); // "java.util.ArrayList"

4.3 hashCode()

Гарантирано е консистентен по времена на изпълнение на програмата. Използва се от HashMap и HashSet за намиране на кофата (bucket). Правило: ако equals() е предефиниран, hashCode() трябва да е предефиниран по съвместим начин — равни обекти трябва да дават равни хеш кодове.

public int hashCode() {
final int PRIME = 31;
int result = 1;
result = PRIME * result + ((strName == null) ? 0 : strName.hashCode());
result = PRIME * result + ((strTitle == null) ? 0 : strTitle.hashCode());
return result;
}

4.4 equals()

Еквивалент на operator== на C++ при сравнение на съдържание. Трябва да отговаря на:

  • Рефлексивност: x.equals(x) == true;
  • Симетричност: x.equals(y)y.equals(x);
  • Транзитивност: x.equals(y) && y.equals(z)x.equals(z);
  • Консистентност при непроменени обекти;
  • x.equals(null) == false винаги.
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
Student other = (Student) obj;
return strFacNumer != null && strFacNumer.equals(other.strFacNumer);
}

4.5 toString()

Предефинирането позволява конзолен изход, сериализация и предаване в поток:

public String toString() {
return strTitle + " " + strName + "\tUpdated: " + lastDate;
}

5. Видове класове по обхват

  • На ниво файл — стандартен публичен клас;
  • На ниво пакет — без public, видим само в пакета;
  • Вградени (Inner Classes) — в обхват {}:
    • в клас (Inner class) — достъп до полетата на обхващащия;
    • в метод (локален) — видим само в метода;
    • анонимен — без явно зададено име, деклариран inline.

Резюме

  • Масивите в Java са референтни типове; декларацията не алокира памет — нужно е new. При излизане от индекс — ArrayIndexOutOfBoundsException.
  • Присвояването B = A копира само референцията; за реално копиране се ползва System.arraycopy().
  • String е непроменяем; за изграждане в цикъл задължително се ползва StringBuilder.
  • Сравняването на String обекти се прави с equals(), а не с ==.
  • Базовият клас Object дефинира clone(), equals(), hashCode(), toString(), getClass() — при предефиниране на equals() трябва да се предефинира и hashCode().