C, PHP, VB, .NET

Дневникът на Филип Петров


* Обект String

Публикувано на 26 септември 2009 в раздел ПИК3 Java.

Досега нееднократно използвахме символни низове. Сега обаче ще се спрем по-подробно на тях. Символните низове (String) съдържат поредица от символи от unicode таблицата. От самото начало трябва специално да отбележим, че символните низове се записват в паметта heap, т.е. те са обекти (именно затова първата буква на String е главна - това е конвенция за различаване на обекти). Можете да си представите един String като масив, в който всеки елемент е unicode символ. За разлика от масивите обаче, към определен елемент (буква) от String трябва да се обръщаме чрез метод:

String s = "Hello World";
System.out.println(s.charAt(6));

Конструкцията String s = "Hello World" често се приема за съкратен запис на String s = new String("Hello World"). Те обаче имат съществена разлика - ще бъде разгледана по-надолу при разглеждането на метод "equals".

За да определим дължината на String използваме метод length():

String s = "Hello World";
System.out.println("The String \""+s+"\" contains "+s.length()+" letters");

При символните низове важат всички споменати преди "escape" символи (\n, \t, \r, ...). Сега обаче ще се фокусираме върху същината на факта, че символните низове са обекти. Нека разгледаме следния пример:

String s = "Hello World";
String s2 = s;

Променливата "s" е референтен тип към обект от тип String. С други думи в нея е записан адреса в heap паметта, където е записан символния низ. Когато направим инициализацията "String s2 = s", то на s2 предаваме стойността на s - това е адреса в heap паметта. Така и двете променливи s и s2 сочат към един и същи символен низ.

Има и още нещо, което трябва да знаете е, че символните низове са непроменими (immutable). Разгледайте следния пример:

String s = "Hello World";
s = "abcd";

Тук първоначално се създава низ "Hello World", който се записва в heap. Променливата s "сочи" към този символен низ. После правим присвояване "s = "abcd"". Това означава, че създаваме нов символен низ "abcd" и го записваме на ново място в heap паметта - оригиналният символен низ "Hello World" не се променя. Тогава какво става с него?

За подобни случаи в Java е измислен т.нар. "garbage collector". Той се грижи да "почиства" обекти в heap паметта, към които никой не сочи. В Java НЕ е възможно вие собственоръчно да изтривате обекти, както това ставаше в C++. Ще разгледаме проблеми свързани с garbage collector в по-нататъшна статия.

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

1. Сравнение за еднаквост: методът equals() връща true или false в зависимост дали подадения низ е същия:

     String s1 = "Hello World";
     String s2 = "Hello World";
     if (s1.equals(s2)) System.out.println("The strings are equal");
     else System.out.println("The strings are NOT equal");

String е immutable, т.е. непроменим обект. Поради тази причина JVM си прави оптимизация - използва т.нар. "String pool". Чрез него се спазва принципът "interning" от компютърните науки - ако при два обекта имаме едни и същи данни, пазим само едно тяхно копие.

Този принцип често кара програмистите да сравняват текстови низове с == вместо с .equals(). Това обаче е погрешно! String pool не винаги се използва. Той се използва само при създаване на низ чрез съкратения запис, но не и при създаване на низ чрез new String(). Например:

String s1 = "Hello";
String s2 = "Hello";
String s3 = new String("Hello");
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s1.equals(s3));

ще даде

true
false
true

Нормално ние създаваме String чрез съкратения запис, а много хора свикват със сравнение чрез ==. Това понякога може да ви "подхлъзне".

2. Сравнение за еднаквост без взимане под внимание на малки и големи букви:

     String s1 = "Hello World";
     String s2 = "HELLO WoRlD";
     if (s1.equalsIgnoreCase(s2)) System.out.println("The strings are equal");
     else System.out.println("The strings are NOT equal");

3. Лексикографска подредба: има два метода - compareTo() сравнява лексикографски като се отчитат позициите на буквите в кодовата таблица, а compareToIgnoreCase() сравнява без да отчита разлика между малки и големи букви. И двете функции връщат отрицателно число ако низа, с който сравняваме е по-голям (лексикографски), 0 ако са идентични и положително число ако низа, с който сравняваме е по-малък:

     String s1 = "Zhivko";
     String s2 = "Petar";
     if (s1.compareToIgnoreCase(s2)<0){
       System.out.println(s1);
       System.out.println(s2);
     }
     else{
       System.out.println(s2);
       System.out.println(s1);
     }

Трябва обаче да се внимава с метод compareTo(). Главните букви са преди малките в кодовата таблица. Така може да се получи следното:

     String s1 = "petar";
     String s2 = "ZHIVSKO";
     if (s1.compareTo(s2)<0){
       System.out.println(s1);
       System.out.println(s2);
     }
     else{
       System.out.println(s2);
       System.out.println(s1);
     }

Тук резултатът ще бъде, че ZHIVKO ще излезе подреден преди petar. Затова трябва добре да преценявате точно как ще подреждате низовете и дали главните букви трябва да имат значение.

4. Конкатенация на низове: вече се запознахме с операторът "+" за низове, когато демонстрирахме System.out.print(). Възможно е да използвате и метод concat(), но едва ли ще ви се стори по-удобен от простото "събиране на низове".

5. Извличане на част от низ: усещането от програмистите на C, че всеки символен низ е масив от символи от тип char тук може да помогне много за усвояването на "извличане на подниз". За целта се използва метод "substring", който приема за параметър две числа - индекс на първата буква и индекс на последната буква от подниза. Ето един пример:

     String str = "Hello World";
     String substr = str.substring(2,9);
     System.out.println(substr);

Резултатът ще бъде "llo Wor".

6. Търсене в символен низ: понякога ни се налага да работим с низове с променлива дължина. Затова е нужно да имаме функционалност за търсене в низове. Основно се използват два метода - indexOf() за търсене на първо срещане и lastIndexOf() за търсене на последно срещане (отзад напред):

     String file = "c:\\Document and Settings\\philip\\My Documents\\file.doc";
     String filename = file.substring(file.lastIndexOf("\\")+1, file.length());
     String diskdrive = file.substring(0, file.indexOf("\\"));
     System.out.println("The file "+filename+" is on drive "+diskdrive);

Когато обаче търсим не първото, а поредно срещане на дума, то ни е нужна допълнителна функционалност. За щастие това не е проблем - единствено добавяме втори параметър на метод indexOf():

    String userinfo = "Users:\nuser:petar;pass:1234\nuser:misho;pass:password";
    int indexofuser = userinfo.indexOf("user:");
    int indexofpass = userinfo.indexOf(";pass:");
    System.out.println("List of users:");
    while(indexofuser!=-1 && indexofpass!=-1){
      System.out.println(userinfo.substring(indexofuser+5, indexofpass));
 indexofuser = userinfo.indexOf("user:", indexofuser+1); indexofpass = userinfo.indexOf(";pass:", indexofpass+1);
    }

7. Промяна на подниз: наистина рядко използвана функционалност, но все пак налична. Ако желаем да подменим част от символите в низ с други, то записваме:

    String credits = "Philip Petrov, e-mail: philip@abv.bh, Tu-Sofia 1206";
    System.out.println(credits);
    credits = credits.replace("philip@abv.bh", "philip@abv.bg");
    System.out.println(credits);

В горния пример имайте предвид, че присвояването "credits = credits.replace..." НЕ променя оригиналния обект. На практика първоначалния низ остава в паметта, а credits започва да "сочи" към новосъздаден обект (този с променения текст). По принцип това не е проблем, но все пак трябва да се внимава за т.нар. "утечки в паметта". В Java можем да разчитаме само и единствено на Garbage Collector да почиства паразитните обекти, към които нито една променлива не сочи.

8. Превръщане на малки в големи букви и обратно:

    String name = "Philip Petrov";
    System.out.println(name.toLowerCase());
    System.out.println(name.toUpperCase());

9. Премахване на празни символи: това е изключително полезен метод при взимане на информация от потребител. Например ако правим форма за автентикация потребителят може да въведе своето потребителско име като "pesho1987 " с празни символи накрая просто защото го е копирал от e-mail. При изпращане на такъв низ към базата данни обаче ще бъде върнато съобщение, че такъв потребител не съществува. Обикновено ние не позволяваме празни интервали в потребителските имена, затова можем да ги премахваме:

    String username = "     pesho1987   ";
    System.out.println("["+username+"]");
    System.out.println("["+username.trim()+"]");

Ще видите, че се премахват празните символи както в началото, така и в края на низа. Също така трябва да знаете, че се премахват и табулациите (\t) и новите редове (\n).

10. Превръщане на низ в число и обратно: това е операция, която обикновено се опитваме да избягваме. Всъщност този метод не е от класа String, а е от клас Integer:

    String num = "-98";
    try{
      int i = Integer.parseInt(num);
      System.out.println(i);
    }
    catch(java.lang.NumberFormatException e){
      System.out.println(num+" is not a number");
    }

Аналогично класовете Boolean, Double, Short и т.н. имат методи parseBoolean(), parseDouble(), parseShort() и т.н.

Обратната операция (превръщане на число в низ) не е проблем. Много от обектите в Java притежават метод "toString()":

    int i = -98;
    String num = Integer.toString(i);

11. Още за непроменимост на низове: от казаното дотук трябва да запомните нещо основно - низовете са непроменими. Това, което споменахме в пример 8 е изключително важно - когато направите присвояване на съществуващ низ към друг, то оригиналният низ остава в паметта, създава се нов и променливата се насочва към него. Тези оставащи паразитни низове в паметта могат да бъдат много вредни за производителността на програмата. Следния пример демонстрира ужасно неправилно управление на паметта:

    // DO NOT DO THIS
    String str = "sum = ";
    String strsum = "0";
    int n = 10000;
    for (int i=1; i<=n; i++){
      strsum = Long.toString((Long.parseLong(strsum)+(long)i));
    }
    System.out.println(strsum);

Удебеленият текст означава, че при този оператор ще бъдат създадени 10000 обекта от тип String. При по-сложни примери, с по-големи обеми от данни - можете да се досетите какво ще се получи с производителността на програмата. Също не добра практика е и непрекъснатото превръщане на String в Long и обратно.

 



Добави коментар

Адресът на електронната поща няма да се публикува


*