Текстът отблизо
Проблем
Отчетът, който се бави
Вече знаеш, че 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()));Стъпка по стъпка, с думи
- report сочи празен текст.
- Първото += прави нов String с един ред: 19 знака.
- Второто += прави още един String. Копира в него стария ред (19 знака) и чак тогава долепва новия. Старият текст вече не трябва на никого.
- Третото копира 38 знака. С всеки ред копирането става повече: при 50 000 реда е над милион знака на завъртане.
- StringBuilder е един обект с буфер. Тук буферът е за 64 знака.
- append пише в буфера на място. Нищо не се копира, обектът е същият.
- Текстът е един и същ, 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**** **** **** 03660888123456ефак| Метод | Какво прави |
|---|---|
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);StringBuilder е обикновен обект. copy = note копира стрелката, двете променливи сочат един и същ обект, а append го променя на място. При String е различно: other += ... прави нов текст и мести само other към него. text си сочи стария.Идеята
По азбучен ред: compareTo
equals казва само „еднакви ли са“. За подреждане трябва повече: кой текст е преди другия. Това казва compareTo:
System.out.println("Андреев".compareTo("Борисов"));System.out.println("Борисов".compareTo("Борисов"));System.out.println("иванова".compareTo("Иванова"));System.out.println("иванова".compareToIgnoreCase("Иванова"));-10320- отрицателно число: първият е преди втория;
- 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)));Текстът се сравнява символ по символ. Първите четири символа са еднакви, после идват „1“ и „9“, а „1“ е преди „9“. Затова "INV-10" е преди "INV-9", точно както в речника „ба“ е преди „в“. Числата 10 и 9 се сравняват като числа едва след parseInt. Ако подреждаш номера като текст, допълвай с нули: INV-009, INV-010.
Идеята
Търсене, рязане, сглобяване
Ред от файл с контакти: име, имейл и град, разделени с „ ; “, с излишни интервали по краищата:
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.kolevafirma.bg18Елена Колева | 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 falsetrue trueisEmpty() е вярно само за текст с дължина 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.4Integer.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, ...).
Пробвай
Намери грешката
„Капак“ се чете еднакво и отзад напред, а програмата казва „капак не е палиндром“. На кой ред е грешката?
Избери реда с грешката.
reversed е StringBuilder, а word е String. equals на String връща true само за друг String със същите символи. За обект от друг клас е false, каквото и да има вътре. Поправката е word.equals(reversed.toString()).Упражнение
Текст, който сглобяваш
Реши задачите в редактора си. Не печатай въпроси.
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.703. Думите наобратно. Прочети изречение и отпечатай думите му в обратен ред. Между думите може да има повече от един интервал, а в резултата трябва да има точно един. Сглоби го със 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и пробвай с „иван“, „Борис“ и „ана“.
Тест