Символни низове. Динамични стрингове. Регулярни изрази.

Както вече споменахме символните низове в Java не са нищо друго от поредица от символи. Познати от C, това е т.нар. char[] – масив от символи. В Java съществува клас String, който репрезентира типа данни символен низ.  Тъй като String е клас, то за да запишем в дадена променлива низ, то ни трябва обект от тип класа String. Както при всички референтни типове, така и при String, стойностите се пазят в Heap-a.

Създаване на стрингове:

Има няколко начина да създадете стринг.

  1. Създавате променлива от тип стринг и я инициализирате с т.нар. string literal:
String str = “hello”;

String srting = “Hi! How are you?”;

Важна особеност на String е, че са неизменни (immutable ). Това означава, че ако един път е създаден обект от тип String с конкретна стойност в Heap-a, то той никога не може да бъде модифициран. Дори и да искаме да реинициализираме променливата с друга стойност, автоматично се създава нов обект, ако такъв (със същата стойност)вече не съществува. Последното е много важно – ако в паметта съществува обект със стойност, с която искаме да създаваме новия String, то нов не се създава, а променливата започва да реферира него. Това означава, че може да имаме 3 променливи от тип String, които сочат едновременно към един и същи обект в динамичната памет.Какво да направим, за да запазваме всеки стринг в нов обект

2. Създаване на променлива от тип стринг и инициализиране чрез

String str = new String(“hello”);

String string = new String(“Hi! How are you?”);

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

Ако конкатенирате стрингове, то както се досещате ще се случва по първия начин, тоест след всяка конкатенация ще се създава нов обект от тип стринг. Това нещо би забавило програмата, ще се получи излишно създаване на обекти. Нека погледнем следния пример:

String str = “a”

for(int i = 0; i<1000000;i++){

str = str+”a”;

}

Горният код, колкото и да е безсмислен, би могъл да се репродуцира в сериозен проблем в реална програма. На всяка итерация от общо 1 милион, ще се създава нов обект, а стария такъв ще се унищожава, само за да му се добави един символ отзад. Такива операции биха натоварили излишно паметта, защото ще имаме създадени 1 милион обекта. Няма ли вариант, при който да оперираме върху стринга, но това да става в рамките на  1 обект? По надолу ще разберете повече за динамичните низове.

Основни операции над стрингове

Основните операции над стрингове са свързани с добавяне, премахване на символи, обхождане символ по символ и претърсване на символния масив. За целта класът String разполага с множество готови методи за работа със стрингове.

Няколко метода на класа String:

  1. char charAt(int index): Връща символ на определена позиция.
  2. int codePointAt(int index): Подобно на горния метод, само че връща Unicode кода, отговарящ на символа на определената позиция
  3. void getChars(int srcBegin, int srcEnd, char[] dest, int destBegin):
  4. Копира символите от src в dest масива, но само в точно определен обхват – от srcBegin до SrcEnd. В новия масив се записват от позиция destBegin.
  5. boolean equals(Object obj): Сравнява два стринга и връща true, ако и двата са еднакви.
  6. boolean contentEquals(StringBuffer sb): Сравнява стринг с определен стрингбуфер.
  7. boolean equalsIgnoreCase(String string): Сравнява два стринга, без да има предвид главни и малки букви.
  8. int compareTo(String string): Сравнява два стринга, като сравнява Unicode кода на всеки два по два символа от двата стринга.
  9. int compareToIgnoreCase(String string): Същия като горния, но няма предвид главни и малки букви.
  10. boolean regionMatches(int srcoffset, String dest, int destoffset, int len):
  11. Сравнява част от текущия стринг с част от друг стринг(dest), с определена дължина(len) на частите, които се сравняват.
  12. boolean regionMatches(boolean ignoreCase, int srcoffset, String dest, int destoffset, int len): Същият като горния, но е добавен булев параметър, посочващ дали да се интересува от малки и големи букви.
  13. boolean startsWith(String prefix, int offset): Проверява дали част от стринга(от offset нататък) започва с определена представка.
  14. boolean startsWith(String prefix): Проверява дали стринга започва с определена представка.
  15. boolean endsWith(String suffix): Проверява дали стринга завършва с определена наставка.
  16. int hashCode(): Връща хеш кода на стринга.
  17. int indexOf(String ch): Връща индекса на първия символ, който е същия като подадения.
  18. int indexOf(String ch, int fromIndex): Същият като горния, но започва да търси символа от определена позицияint lastIndexOf(int ch): Връща индекса на последния символ в текущия стринг, който е като подадения ch.
  19. int lastIndexOf(int ch, int fromIndex): Същият като горния, но започва да търси съвпадение от определена позиция
  20. int indexOf(String str): Връща индекса на мястото, на което за първи път е срещнал даден подниз(str) в текущия низ.
  21. int lastindexOf(String str): Връща индекса на мястото, на което за последен път е срещнал даден подниз (str) в текущия низ.
  22. String substring(int beginIndex):Връща подниз, съставен от текущия низ, след определена позиция (beginIndex).
  23. String concat(String str): Конкатенира два стринга.
  24. String replace(char oldChar, char newChar): Замества стар с нов символ в текущия символен низ.
  25. boolean contains(CharSequence s): Проверява дали текущият стринг съдържа подадената последователност от символи.
  26. String replaceFirst(String regex, String replacement): Замества първия срещнат подниз, отговарящ на подадения regex-регулярен израз, с подаден нов низ(replacement)
  27. String replaceAll(String regex, String replacement): Работи като горния, но замества всички открити поднизове отговарящи на рег. Израз.
  28. String[] split(String regex, int limit): Отрязва частите от текущия стринг, която отговаря на определен регулярен израз. и ги връща в масив от стрингове, като се съобразява с посочения limit.
  29. String[] split(String regex): Същият като горния, но няма лимит.
  30. String toLowerCase(Locale locale): Връща текущия низ с малки букви, съобразявайки се с подадената локация.
  31. String toLowerCase(): Същият като горния, но не зависи от локация.Еквивалентен е на toLowerCase(Locale. getDefault()).
  32. String toUpperCase(Locale locale): Връща низа с главни букви, съобразено с локацията.
  33. String toUpperCase():Еквивалентно на toUpperCase(Locale.getDefault()).
  34. String trim(): Връща стринг, еквивалентен на текущия, но с премахнати първи и последен интервал, ако такива има.
  35. char[] toCharArray(): Конвертира стринга до масив от символи.
  36. static String copyValueOf(char[] data):Обратно на горния, връща стринг по подаден масив от символи.
  37. static String copyValueOf(char[] data, int offset, int count): Същият като горния, но с добавени 2 аргумента – offset – отместване, от което да започне и count – брой символи, които да запише.
  38. static String valueOf(data type): Връща стрингово представяне на опредлен тип данни.
  39. byte[] getBytes(String charsetName): Конвертира подаден стринг до масив от байтове, използвайки специфицирана кодировка.
  40. byte[] getBytes(): Същият като горния, но с кодировка по подразбиране
  41. int length(): Връща дължината на текущия стринг.
  42. boolean matches(String regex): Проверява дали текущият стринг отговаря на определен регулярен израз.

Примери:

  1. Конвертиране на String  към int:

Начин 1:

String strNum=”1234″;

int num = Integer.parseInt(strNum);

За да използваме метод  parseInt(), важно е да се каже, че всички символи трябва да са цифри, а само първия може да е символ, различен от цифра – символът минус („-“). Във всички останали случаи, при които се опитваме да парснем число, но вътре има и други символи ще предизвикат изключение от типа NumberFormatException.

public class Example{

public static void main(String args[]){

String str="123";

int firstNumber = 100;

int secondNumber = Integer.parseInt(str);

int sum= firstNumber + secondNumber;

System.out.println("Result is: "+sum);//result is 223

}

}

Начин 2:

Използваме метод Integer.valueOf();

String str="-10"; int num = Integer.valueOf(str);  //result is -10.

Методът работи по същия начин като parseInt() и отново се предизвиква изключение, в случай, че се подаде неразрешен символ.

Аналогично на горните преобразувания, можем да конвертираме и други типове . Например:

String str="203456";long num = Long.parseLong(str); //the result is: 2034556

2.  Конвертиране на int към String:

Начин 1: Подобно на горните примери можем да използваме String.valueOf(int i).

int intVar = 111;String str = String.valueOf(intVar);//the result is 111

Начин 2: С използване на  Integer.toString(int i):

int intVar = 200;String str = Integer.toString(intVar);//the result is 200.

Аналогично е ако искаме да конвертираме друг тип към стринг. Например long или double.

long longVar = 200;String str = Long.toString(longVar);

Дори и boolean:

boolean boolVar = false;String str = Boolean.toString(boolVar);

3. Премахване на крайни ненужни празни интервали от стринг (trailing spaces), но без премахване на такива в началото(leading spaces).:

public class Example {

public static void main(String[] args) {

System.out.println("#"+trimTrailingSpaces(" How are you??")+"@");

System.out.println("#"+trimTrailingSpaces(" I'm Fine. ")+"@");

}

public static String trimTrailingSpaces( String str){

if( str == null)   return null;

int len = str.length();

for( ; len > 0; len--) {

if( ! Character.isWhitespace( str.charAt( len - 1)))

break;

}

return str.substring( 0, len);

}

}

Резултатът е :

#  How are you??@#    I'm Fine.@

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

  1. Проверяване на стринг, дали е “palindrome” чрез рекурсия. Това е такъв стринг, който прочете отпред назад и отзад напред е един и същ. Например „apeepa“.
public class PalindromStringCheckingExample{

public static boolean isPalindrom(String str){

// if length is 0 or 1 then String is palindrome

if(str.length() == 0 || str.length() == 1)

return true;

/* check if the first and the last symbols are equal.         and  continue with the next internal couple of symbols.              */

if(str.charAt(0) == str.charAt(str.length()-1))

return isPalindrom(str.substring(1, str.length()-1));

return false;

}

public static void main(String[]args){

Scanner scanner = new Scanner(System.in); // read the string

System.out.println("Enter the String for check:");

String string = scanner.nextLine();

if(isPal(string)){

System.out.println(string + " is a palindrome");

}

else{

System.out.println(string + " is not a palindrome");

}

}

}

Друг вариант е чрез следния метод, на който подаваме масив от символи;

public static boolean istPalindrom(char[] word){

int firstindex = 0;

int lastIndex = word.length - 1;

while (lastIndex > firstindex) {

if (word[firstindex] != word[lastIndex]) {

return false;

}

++firstindex;

--lastIndex;

}

return true;

}

Динамични низове: По-горе поставихме един проблем при работа със стрингове- излишното създаване на обекти при всеки опит за промяна върху стойността им. В Java съществуват обаче  и т.нар. динамични низове. При тях низът представлява динамичен масив от символи, който може да бъде изменян по всякакъв начин. За реализиране на такъв динамичен  масив ни помага класът StringBuilder, както и неговият аналог StringBuffer. Разликата между двата класа е, че StringBuffer представлява синхронизирана имплементация на StringBuilder-а, той е thread safe. Тоест, ако динамичният масив е споделен ресурс и се налага синхронизация, то е редно да използваме синхронизираната имплементация. Използването на StringBuffer и изобщо на всички подобни обекти, грижещи се за синхронизация, е изключително ресурсоемко и влияе на производителността. По-често срещаният вариант е да не се налага синхронизиране, затова се използва StringBuilder. Работата с двата класа е абсолютно сходна – имат едни и същи методи.

Някои по-важни методи на класа StringBuilder/StringBuffer:

  1. public StringBuffer append(String s):Добавя нов стринг към дадения
  2. public StringBuffer reverse(): Обръща наобратно стринга.
  3. public delete(int start, int end) Изтрива подниз от дадения, между индекс start и индекс
  4. public insert(int offset, int i) Вмъква на опредлено отстояние (offset) определено число (i). Съществуват още множество предефинирани методи на този – с втори параметър boolean, char, char[],float, double, String  и тн.
  5. replace(int start, int end, String str) Замества символи от определена позиция(start) на настоящия низ до определена позиция(end) с нов низ (str).

Пример:

public class Example{

public static void main(String args[]){

StringBuffer strBuffer = new StringBuffer("George");

strBuffer.append(" is from Bulgaria.");

System.out.println(strBuffer); // George is from Bulgaria.

}

}

Регулярни  изрази:

Често се налага в дадена програма да търсим  израз в даден стринг, или да изследваме даден стринг дали отговаря на даден шаблон.  В този момент регулярните изрази вършат перфектна работа.  Регулярен израз  е такъв израз, съдържащ специални символи,  чрез който се проверява за съвпадение  на стринг с даден шаблон.  Пише се, когато искаме да проверим дали даден стринг „прилича“  на даден шаблон, или по-просто казано, дали отговаря на него.  Например, искаме да проверим дали имейла на потребителя попада в шаблона ххххх@ххх.хх.  Най-важното при  тази задача е да си дефинираме правилно шаблона, с който ще правим проверка. За целта трябва да знаем кои символи са разрешени и какъв е техният брой.

Java предлага пакет, в който са дефинирани класове и техни методи за работа с регулярни изрази- java.util.regex.

  • Клас Pattern – репрезентира един регулярен израз. Не предоставя публични конструктори, така че, за да създадете шаблон, ще трябва да извикате първо един от статичните му методи compile() с подаден като аргумент регулярен израз в кавички, които ще върнат създаден обект от тип Pattern.
  • Клас Matcher – този клас всъщност има функционалност да преценява дали даден низ покрива шаблона. Той претърсва стринга, за да открие дали отговаря на шаблона, който му е зададен. Не предоставя публични конструктори, така че, за да създадете инстанция, използвате извикване на метод matcher() върху обект от тип Pattern.
  • PatternSyntaxException – от типа на т.нар. „unchecked exceptions“ , който сигнализира за синтактична грешка в експрешън шаблона.Нека разгледаме основните правила за създаване на изрази.

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

Нека разгледаме основните правила за създаване на изрази:

Regular Expression Описание:
. Всеки символ
^[regex] Открива regex, който трябва да се намира в началото на реда. Например  „^\w+“ – започва с 1 или повече букви, цифри или долна черта.
[regex]$ Открива regex, който трябва да се намира в края на реда. Например  „[a-z]$“ – означава завършва с малка буква.
[abc] Сет дефиниция (дефиниция на група)– може да открие символите a ИЛИ b ИЛИ c
[abc][vz] Сет дефиниция – търси всички a ИЛИ b ИЛИ c , последвани от всички от V или Z.
[^abc] Всичко освен a, b или c. Със символа „^“ се означава търсене на всичко, освен …
[a-d1-7] Всички символи, попадащи в обхвата от а до d и от 1 до 7, но не d1.
X|Z Намира Х или Z.
XZ Намира Х, директно последвано от Z.
$ Проверява дали следва край на реда.

Общо взето всяка поредица от символи, заградени в квадратните скоби представлява дефиниция на група от символа, като всяка група отговаря за  1 символ.

Примери:

  • word – търсим думата “word”.
  • [a-z] – малки букви;
  • [^a-z] – всички символи БЕЗ малките букви;
  • [A-Z] – главни букви;
  • [^A-Z] – всички символи БЕЗ главните букви;
  • [0-9] – цифри от 0 до 9;
  • [^0-9] – всички символи БЕЗ цифри;
  • [a-zA-Z] – малки и големи букви;
  • [0-9a-zA-Z] – малки и големи букви и цифри;
  • [a-z-[mnp]] – малки букви от a до z, но без буквите m, n и p;
  • [a-z-[m-p]] – малки букви от a до z, но без буквите от m до p;
  • \s – интервал, табулация, нов ред, връщане в начало на ред (еквивалентно на [ \t\n\r]);
  • \w – букви, цифри и долна черта (еквивалентно на [a-zA-Z0-9_]);
  • \W – всички символи БЕЗ букви, цифри и долна черта ([^a-zA-Z0-9_]).

Мета символи: – специални символи, които са предефинирани да се ползват вместо по-често срещаните изрази.

Regular Expression Description
\d Всяка цифра, екв. на:  [0-9]
\D Никоя цифра, екв. на: [^0-9]
\s Интервал, табулация, нове ред, връщане на нов ред  (whitespace character) – празни символи.
\S Не празни символи-  тоест всички без горните, или екв. на: [^\s]
\w Дума, съставена от букви, цифри и долна черта– или екв. на: [a-zA-Z_0-9]
\W Всички символи без букви, символи и долна черта, екв. на:  [^\w]
\S+ Поредица от няколко не празни символа.

 

Специални знаци:

Знаците, които може да се поставят в регулярни изрази са дадени в таблицата по-долу със съответните им значения:

Regular Expression Description Examples
* Даден символ се среща се 0 или повече пъти. X* намира 0 или няколко пъти срещаща се буква. .* намира всякаква последователност от знаци.
+ Даден символ се появява 1 или повече пъти. X+ – Намира 1 или много пъти срещаща се буква Х.
? Даден символ се появява – 0 или 1 път. X? Открива никаква, или точно 1 буква Х
{X} Появява се Х брой пъти Occurs X number of times, {} \d{3} търси 3 букви

.{10} Всяка поредица от символи, с дължина 10 знака.

{X,Y} Даден символ се появява Х или У пъти \d{1,4} означава, че символът d трябва да се среща от 1 до 4 пъти.

Задача 1: Да се напише клас, който валидира потребителски имена:

Изисквания:

  • Може да съдържа букви от a-z малки;
  • Може да съдържа и цифрите от 0-9;
  • Може да съдържа долна черта или тире;
  • Може да бъде с дължина от 3 до 15 символа.
public class UsernameValidator{

private Pattern pattern;

private Matcher matcher;

//declare a string pattern for username.

private static final String USERNAME_PATTERN = "^[a-z0-9_-]{3,15}$";

public UsernameValidator(){

pattern = Pattern.compile(USERNAME_PATTERN);

}

public boolean validateUserName(final String username){

matcher = pattern.matcher(username);

return matcher.matches();

}

}

Задача 2: Валидирайте мейл адрес:

public class MailValidator {

private Pattern pattern;

private Matcher matcher;

private static final String EMAIL_PATTERN =

"^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@"

+ "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$";

public EmailValidator() {

pattern = Pattern.compile(EMAIL_PATTERN);

}

public boolean validateMail(final String checkedMail) {

matcher = pattern.matcher(checkedMail);

return matcher.matches();

}

}

Явор Томов, Даниел Джолев

Leave a Reply

Your email address will not be published. Required fields are marked *