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

Текстът отблизо

Проблем

Отчетът, който се бави

Вече знаеш, че title.toUpperCase(); сам не прави нищо: текстът е неизменим и методът връща нов текст. Това има цена, която се вижда в цикъл.

Програмата на онлайн магазина сглобява дневния отчет ред по ред:

String report = "";
for (int i = 1; i <= 3; i++) {
report += "Поръчка " + i + ": 12.50 €\n";
}
System.out.print(report);
Изход
Поръчка 1: 12.50 €
Поръчка 2: 12.50 €
Поръчка 3: 12.50 €

С три поръчки е мигновено. В събота поръчките са 50 000 и същият цикъл отнема около 2,5 секунди. Измерихме го на машината, на която проверяваме кода на уроците.

Защо? Всяко report += ... не добавя към стария текст. То прави нов текст, копира в него целия отчет досега и чак тогава долепва новия ред. Към края всяко завъртане копира над милион символа.

Идеята

StringBuilder: текст, който се променя

За сглобяване на много парчета има StringBuilder. Това е обект, който пази текста в свой буфер и добавя на място, без да копира всичко наново:

StringBuilder report = new StringBuilder();
for (int i = 1; i <= 3; i++) {
report.append("Поръчка ").append(i).append(": 12.50 €\n");
}
System.out.print(report.toString());
Изход
Поръчка 1: 12.50 €
Поръчка 2: 12.50 €
Поръчка 3: 12.50 €
  • new StringBuilder() създава празен „строител“.
  • append(...) добавя накрая. Приема текст, число, символ и връща същия обект, затова извикванията се нареждат едно след друго.
  • toString() прави от него обикновен String, веднъж, накрая.

Със 50 000 поръчки този вариант отнема около 7 милисекунди вместо 2,5 секунди.

Ето какво става в паметта при двата начина. За да се види, че буферът не се копира, тук той е създаден с място за 64 знака.

String report = "";
for (int i = 1; i <= 3; i++) {
report += "Поръчка " + i + ": 12.50 €\n";
}
StringBuilder builder = new StringBuilder(64);
for (int i = 1; i <= 3; i++) {
builder.append("Поръчка ").append(i).append(": 12.50 €\n");
}
System.out.println(report.length() + " " + builder.length());
System.out.println(report.equals(builder.toString()));
Стъпка по стъпка, с думи
  1. report сочи празен текст.
  2. Първото += прави нов String с един ред: 19 знака.
  3. Второто += прави още един String. Копира в него стария ред (19 знака) и чак тогава долепва новия. Старият текст вече не трябва на никого.
  4. Третото копира 38 знака. С всеки ред копирането става повече: при 50 000 реда е над милион знака на завъртане.
  5. StringBuilder е един обект с буфер. Тук буферът е за 64 знака.
  6. append пише в буфера на място. Нищо не се копира, обектът е същият.
  7. Текстът е един и същ, 57 знака. С += бяха копирани още 57, а със StringBuilder нито един.
По-точно, за любопитните
  • Буферът расте сам: когато се напълни, StringBuilder заделя около два пъти по-голям и копира веднъж. Затова копирането е рядко.
  • Един израз като "Поръчка " + i + ": " + price компилаторът сам превръща в ефективен код. Проблемът е само в += в цикъл, където всяко завъртане е отделен нов текст.
  • Има и StringBuffer със същите методи. Той е безопасен, когато няколко нишки пишат в един обект едновременно, и затова е малко по-бавен. Нишките идват в модул „Програми в истинския свят“. Дотогава ползвай StringBuilder.

Виж го

Промяна на място

StringBuilder може не само да добавя. Номер на карта за печат на касовата бележка:

StringBuilder card = new StringBuilder("4532015112830366");
card.insert(12, ' ').insert(8, ' ').insert(4, ' ');
System.out.println(card);
card.replace(0, 14, "**** **** ****");
System.out.println(card);
StringBuilder phone = new StringBuilder("0888-123-456");
phone.deleteCharAt(8).deleteCharAt(4);
System.out.println(phone);
System.out.println(new StringBuilder("кафе").reverse());
Изход
4532 0151 1283 0366
**** **** **** 0366
0888123456
ефак
Метод Какво прави
insert(i, x) вмъква x преди символа с индекс i
replace(a, b, x) заменя символите от a до b (без b) с x
deleteCharAt(i) маха символа с индекс i
delete(a, b) маха символите от a до b (без b)
reverse() обръща текста отзад напред
length(), charAt(i) както при String

Интервалите в номера на картата се вмъкват отзад напред: първо на 12, после на 8, после на 4. Ако започнеш от 4, вмъкнатият интервал измества всичко след него и 8 вече не е мястото, което искаш. Тиретата в телефона се махат по същата причина първо отзад.

Пробвай

Две имена, един обект

Какво ще отпечата програмата?

StringBuilder note = new StringBuilder("Маса 4");
StringBuilder copy = note;
copy.append(", 2 кафета");
System.out.println(note);
String text = "Маса 4";
String other = text;
other += ", 2 кафета";
System.out.println(text);
Отговор

Идеята

По азбучен ред: compareTo

equals казва само „еднакви ли са“. За подреждане трябва повече: кой текст е преди другия. Това казва compareTo:

System.out.println("Андреев".compareTo("Борисов"));
System.out.println("Борисов".compareTo("Борисов"));
System.out.println("иванова".compareTo("Иванова"));
System.out.println("иванова".compareToIgnoreCase("Иванова"));
Изход
-1
0
32
0
  • отрицателно число: първият е преди втория;
  • 0: еднакви са;
  • положително: първият е след втория.

Знакът е важен, не самото число. С него подреждаш списък с имена, например с метода на мехурчето от урока за масивите:

String[] names = { "Петрова", "Андреев", "Иванова", "Борисов" };
for (int i = 0; i < names.length - 1; i++) {
for (int j = 0; j < names.length - 1 - i; j++) {
if (names[j].compareTo(names[j + 1]) > 0) {
String tmp = names[j];
names[j] = names[j + 1];
names[j + 1] = tmp;
}
}
}
System.out.println(String.join(", ", names));
Изход
Андреев, Борисов, Иванова, Петрова

String.join(", ", names) долепва елементите на масива с разделител между тях.

По-точно, за любопитните
  • compareTo сравнява символ по символ по кода им в таблицата със символи. Главните букви са преди малките, затова "иванова".compareTo("Иванова") е положително. compareToIgnoreCase не гледа главни и малки.
  • Числото е разликата между кодовете на първите различни символи (тук „и“ и „И“ се различават с 32) или между дължините, ако единият текст е начало на другия.
  • За подреждане по правилата на българския език, например с „ѝ“ и думи с тире, има java.text.Collator.getInstance(Locale.of("bg")).
  • java.util.Arrays.sort(names) подрежда масив с текстове по същото правило, без да пишеш мехурчето.

Пробвай

Фактура 10 или 9?

Номерата на фактурите са текст. Коя е по-ранна?

Какво ще отпечата програмата?

String a = "INV-10";
String b = "INV-9";
System.out.println(a.compareTo(b) < 0);
System.out.println(Integer.parseInt(a.substring(4)) < Integer.parseInt(b.substring(4)));
Отговор

Идеята

Търсене, рязане, сглобяване

Ред от файл с контакти: име, имейл и град, разделени с „ ; “, с излишни интервали по краищата:

String line = " Елена Колева ; elena.koleva@firma.bg ; Пловдив ";
String[] parts = line.strip().split(" ; ");
String email = parts[1];
int at = email.indexOf('@');
System.out.println(email.substring(0, at));
System.out.println(email.substring(at + 1));
System.out.println(email.indexOf('.', at));
System.out.println(String.join(" | ", parts));
System.out.println("=".repeat(20));
Изход
elena.koleva
firma.bg
18
Елена Колева | elena.koleva@firma.bg | Пловдив
====================
  • strip() маха интервалите отпред и отзад, като trim(), но познава и „широките“ интервали от другите езици. В нов код ползвай него.
  • indexOf('@') дава индекса на първата поява, а indexOf('.', at) търси чак от индекс at нататък. lastIndexOf търси отзад напред.
  • substring и split вече познаваш. String.join е обратното на split.
  • "=".repeat(20) повтаря текста 20 пъти.

Празно и „празно“ не е едно и също:

String empty = "";
String spaces = " ";
System.out.println(empty.isEmpty() + " " + spaces.isEmpty());
System.out.println(empty.isBlank() + " " + spaces.isBlank());
Изход
true false
true true

isEmpty() е вярно само за текст с дължина 0. isBlank() е вярно и когато има само интервали. За проверка на въведено поле обикновено искаш isBlank().

Идеята

Числа и текст

Числата влизат в текст и излизат от текст:

int code = Integer.parseInt("0042");
double price = Double.parseDouble("2.40");
String label = String.valueOf(code) + "-" + String.valueOf(price);
System.out.println(label);
Изход
42-2.4

Integer.parseInt и Double.parseDouble вече познаваш. String.valueOf е обратното: прави текст от число, символ или boolean.

За таблица с колони ти трябва String.format от урока за входа, с ширина на всяка колона. Бележка от кафене:

String[] names = { "Кафе", "Кроасан", "Минерална вода" };
double[] prices = { 2.40, 3.10, 1.50 };
int[] qty = { 2, 1, 3 };
double total = 0;
for (int i = 0; i < names.length; i++) {
double sum = prices[i] * qty[i];
total += sum;
System.out.println(String.format("%-16s%2d x %5.2f = %6.2f", names[i], qty[i], prices[i], sum));
}
System.out.println("-".repeat(35));
System.out.println("%-26s%9.2f €".formatted("Общо", total));
Изход
Кафе 2 x 2.40 = 4.80
Кроасан 1 x 3.10 = 3.10
Минерална вода 3 x 1.50 = 4.50
-----------------------------------
Общо 12.40 €
  • %-16s е текст в 16 знака, подравнен вляво. %5.2f е число в 5 знака с 2 след точката, подравнено вдясно. %2d е цяло число в 2 знака.
  • "шаблон".formatted(...) прави същото като String.format("шаблон", ...). Вика се направо върху шаблона.
По-точно, за любопитните
  • a.concat(b) е същото като a + b. Ще го срещнеш в по-стар код.
  • Ширината е най-малко толкова знака. По-дълъг текст не се реже, а избутва колоната вдясно.
  • Double.parseDouble("2,40") хвърля NumberFormatException: тук десетичният знак е винаги точка. А String.format("%.2f", 2.4) на компютър с български настройки дава 2,40. За текст, който друга програма ще чете, ползвай String.format(Locale.ROOT, ...).

Пробвай

Намери грешката

„Капак“ се чете еднакво и отзад напред, а програмата казва „капак не е палиндром“. На кой ред е грешката?

Избери реда с грешката.

Упражнение

Текст, който сглобяваш

Реши задачите в редактора си. Не печатай въпроси.

1. Палиндром. Прочети ред и кажи дали е палиндром, без значение от главните букви, интервалите, запетаите и тиретата. Този път обърни текста със StringBuilder.reverse() вместо с цикъл.

Вход
Дебел лебед
Изход
Палиндром
Вход
Дебел гълъб
Изход
Не е палиндром

2. Бележка с колони. На първия ред е броят на артикулите. На всеки следващ ред има име, цена и брой, разделени с ;. Отпечатай бележка с подравнени колони и сумата накрая.

Вход
3
Хляб;1.20;2
Сирене;9.80;1
Яйца;0.35;10
Изход
Хляб 2 x 1.20 = 2.40
Сирене 1 x 9.80 = 9.80
Яйца 10 x 0.35 = 3.50
------------------------------------
Общо 15.70

3. Думите наобратно. Прочети изречение и отпечатай думите му в обратен ред. Между думите може да има повече от един интервал, а в резултата трябва да има точно един. Сглоби го със StringBuilder.

Вход
Утре в 9 ще има среща
Изход
среща има ще 9 в Утре

Още задачи

  • Инициали. Прочети пълно име („Мария Иванова Петрова“) и отпечатай инициалите с точки: „М. И. П.“ Сглоби ги със StringBuilder.
  • Скрит имейл. От elena.koleva@firma.bg направи e***********@firma.bg: първата буква, после толкова звездички, колкото са останалите символи преди @. Ползвай indexOf и repeat.
  • Обиколка на String. Създай текст с кавички ("Примерен текст") и друг с new String("Примерен текст"). Сравни ги с == и с equals, вземи substring(0, 8), раздели втория по интервала и отпечатай частите, превърни "100" в число и го отпечатай със String.format("Стойност: %d", ...), и накрая долепи " и още" към StringBuilder с първия текст. Предвиди всеки ред, преди да пуснеш програмата.
  • Подреди без значение от главните букви. Промени мехурчето от картата за compareTo да ползва compareToIgnoreCase и пробвай с „иван“, „Борис“ и „ана“.

Тест

Провери се

1. Защо report += line в цикъл с 50 000 завъртания е бавно?
2. Какво връща "Борисов".compareTo("Андреев")?
3. StringBuilder a = new StringBuilder("12"); StringBuilder b = a; b.append("3"); Какво има в a?
4. Как превръщаш StringBuilder в String?
5. Каква е разликата между String.format(...) и "...".formatted(...)?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·